feat: implement Rect ops for moving (#1596)
feat: implement Rect ops for moving Implemented `Add`, `AddAssign`, `Sub`, and `SubAssign` on `Rect` for `Offset`. This makes it possible to move rects ```rust let rect = Rect::new(1, 2, 3, 4); let moved = rect + Offset(1, 2); let moved = rect - Offset(1, 2); let moved = rect + Offset(-1, -2); ``` Additionally Rect, Size, Offset, and Position now all have MIN and MAX consts.
This commit is contained in:
@@ -123,9 +123,9 @@ impl InputForm {
|
||||
frame.render_widget(&self.age, age_area);
|
||||
|
||||
let cursor_position = match self.focus {
|
||||
Focus::FirstName => first_name_area.offset(self.first_name.cursor_offset()),
|
||||
Focus::LastName => last_name_area.offset(self.last_name.cursor_offset()),
|
||||
Focus::Age => age_area.offset(self.age.cursor_offset()),
|
||||
Focus::FirstName => first_name_area + self.first_name.cursor_offset(),
|
||||
Focus::LastName => last_name_area + self.last_name.cursor_offset(),
|
||||
Focus::Age => age_area + self.age.cursor_offset(),
|
||||
};
|
||||
frame.set_cursor_position(cursor_position);
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ mod direction;
|
||||
mod flex;
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod offset;
|
||||
mod position;
|
||||
mod rect;
|
||||
mod size;
|
||||
@@ -326,6 +327,7 @@ pub use direction::Direction;
|
||||
pub use flex::Flex;
|
||||
pub use layout::{Layout, Spacing};
|
||||
pub use margin::Margin;
|
||||
pub use offset::Offset;
|
||||
pub use position::Position;
|
||||
pub use rect::{Columns, Offset, Positions, Rect, Rows};
|
||||
pub use rect::{Columns, Positions, Rect, Rows};
|
||||
pub use size::Size;
|
||||
|
||||
66
ratatui-core/src/layout/offset.rs
Normal file
66
ratatui-core/src/layout/offset.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::layout::Position;
|
||||
|
||||
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`](crate::layout::Rect::offset) for usage.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// A zero offset
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
/// The minimum offset
|
||||
pub const MIN: Self = Self::new(i32::MIN, i32::MIN);
|
||||
|
||||
/// The maximum offset
|
||||
pub const MAX: Self = Self::new(i32::MAX, i32::MAX);
|
||||
|
||||
/// Creates a new `Offset` with the given values.
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Position> for Offset {
|
||||
fn from(position: Position) -> Self {
|
||||
Self {
|
||||
x: i32::from(position.x),
|
||||
y: i32::from(position.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_sets_components() {
|
||||
assert_eq!(Offset::new(-3, 7), Offset { x: -3, y: 7 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constants_match_expected_values() {
|
||||
assert_eq!(Offset::ZERO, Offset::new(0, 0));
|
||||
assert_eq!(Offset::MIN, Offset::new(i32::MIN, i32::MIN));
|
||||
assert_eq!(Offset::MAX, Offset::new(i32::MAX, i32::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_position_converts_coordinates() {
|
||||
let position = Position::new(4, 9);
|
||||
let offset = Offset::from(position);
|
||||
|
||||
assert_eq!(offset, Offset::new(4, 9));
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,13 @@ pub struct Position {
|
||||
|
||||
impl Position {
|
||||
/// Position at the origin, the top left edge at 0,0
|
||||
pub const ORIGIN: Self = Self { x: 0, y: 0 };
|
||||
pub const ORIGIN: Self = Self::new(0, 0);
|
||||
|
||||
/// Position at the minimum x and y values
|
||||
pub const MIN: Self = Self::ORIGIN;
|
||||
|
||||
/// Position at the maximum x and y values
|
||||
pub const MAX: Self = Self::new(u16::MAX, u16::MAX);
|
||||
|
||||
/// Create a new position
|
||||
pub const fn new(x: u16, y: u16) -> Self {
|
||||
|
||||
@@ -3,10 +3,11 @@ use core::array::TryFromSliceError;
|
||||
use core::cmp::{max, min};
|
||||
use core::fmt;
|
||||
|
||||
use crate::layout::{Margin, Position, Size};
|
||||
pub use self::iter::{Columns, Positions, Rows};
|
||||
use crate::layout::{Margin, Offset, Position, Size};
|
||||
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
mod ops;
|
||||
|
||||
use super::{Constraint, Flex, Layout};
|
||||
|
||||
@@ -66,21 +67,57 @@ use super::{Constraint, Flex, Layout};
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To create a new `Rect`, use [`Rect::new`]. The size of the `Rect` will be clamped to keep the
|
||||
/// right and bottom coordinates within `u16`. Note that this clamping does not occur when creating
|
||||
/// a `Rect` directly.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// assert_eq!(
|
||||
/// rect,
|
||||
/// Rect {
|
||||
/// x: 1,
|
||||
/// y: 2,
|
||||
/// width: 3,
|
||||
/// height: 4
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// You can also create a `Rect` from a [`Position`] and a [`Size`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Position, Rect, Size};
|
||||
///
|
||||
/// // Create a rectangle manually
|
||||
/// let rect = Rect::new(10, 5, 80, 20);
|
||||
/// assert_eq!(rect.x, 10);
|
||||
/// assert_eq!(rect.y, 5);
|
||||
/// assert_eq!(rect.width, 80);
|
||||
/// assert_eq!(rect.height, 20);
|
||||
/// let position = Position::new(1, 2);
|
||||
/// let size = Size::new(3, 4);
|
||||
/// let rect = Rect::from((position, size));
|
||||
/// assert_eq!(
|
||||
/// rect,
|
||||
/// Rect {
|
||||
/// x: 1,
|
||||
/// y: 2,
|
||||
/// width: 3,
|
||||
/// height: 4
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// // Create from position and size
|
||||
/// let rect = Rect::from((Position::new(10, 5), Size::new(80, 20)));
|
||||
/// To move a `Rect` without modifying its size, add or subtract an [`Offset`] to it.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Offset, Rect};
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let offset = Offset::new(5, 6);
|
||||
/// let moved_rect = rect + offset;
|
||||
/// assert_eq!(moved_rect, Rect::new(6, 8, 3, 4));
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rect {
|
||||
@@ -94,30 +131,6 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// A zero offset
|
||||
pub const ZERO: Self = Self { x: 0, y: 0 };
|
||||
|
||||
/// Creates a new `Offset` with the given values.
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
|
||||
@@ -133,6 +146,12 @@ impl Rect {
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// The minimum possible Rect
|
||||
pub const MIN: Self = Self::ZERO;
|
||||
|
||||
/// The maximum possible Rect
|
||||
pub const MAX: Self = Self::new(0, 0, u16::MAX, u16::MAX);
|
||||
|
||||
/// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
|
||||
///
|
||||
/// If the width or height would cause the right or bottom coordinate to be larger than the
|
||||
@@ -253,15 +272,7 @@ impl Rect {
|
||||
/// See [`Offset`] for details.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
Self {
|
||||
x: i32::from(self.x)
|
||||
.saturating_add(offset.x)
|
||||
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
|
||||
y: i32::from(self.y)
|
||||
.saturating_add(offset.y)
|
||||
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
|
||||
..self
|
||||
}
|
||||
self + offset
|
||||
}
|
||||
|
||||
/// Returns a new `Rect` that contains both the current one and the given one.
|
||||
|
||||
136
ratatui-core/src/layout/rect/ops.rs
Normal file
136
ratatui-core/src/layout/rect/ops.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
||||
|
||||
use super::{Offset, Rect};
|
||||
|
||||
impl Neg for Offset {
|
||||
type Output = Self;
|
||||
|
||||
/// Negates the offset.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the negated value overflows (i.e. `x` or `y` is `i32::MIN`).
|
||||
fn neg(self) -> Self {
|
||||
Self {
|
||||
x: self.x.neg(),
|
||||
y: self.y.neg(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Rect {
|
||||
type Output = Self;
|
||||
|
||||
/// Moves the rect by an offset without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn add(self, offset: Offset) -> Self {
|
||||
let max_x = i32::from(u16::MAX - self.width);
|
||||
let max_y = i32::from(u16::MAX - self.height);
|
||||
let x = i32::from(self.x).saturating_add(offset.x).clamp(0, max_x) as u16;
|
||||
let y = i32::from(self.y).saturating_add(offset.y).clamp(0, max_y) as u16;
|
||||
Self { x, y, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Rect> for Offset {
|
||||
type Output = Rect;
|
||||
|
||||
/// Moves the rect by an offset without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn add(self, rect: Rect) -> Rect {
|
||||
rect + self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Rect {
|
||||
type Output = Self;
|
||||
|
||||
/// Subtracts an offset from the rect without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest
|
||||
fn sub(self, offset: Offset) -> Self {
|
||||
// Note this cannot be simplified to `self + -offset` because `Offset::MIN` would overflow
|
||||
let max_x = i32::from(u16::MAX - self.width);
|
||||
let max_y = i32::from(u16::MAX - self.height);
|
||||
let x = i32::from(self.x).saturating_sub(offset.x).clamp(0, max_x) as u16;
|
||||
let y = i32::from(self.y).saturating_sub(offset.y).clamp(0, max_y) as u16;
|
||||
Self { x, y, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<Offset> for Rect {
|
||||
/// Moves the rect by an offset in place without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn add_assign(&mut self, offset: Offset) {
|
||||
*self = *self + offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<Offset> for Rect {
|
||||
/// Moves the rect by an offset in place without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn sub_assign(&mut self, offset: Offset) {
|
||||
*self = *self - offset;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MIN, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(u16::MAX- 5, u16::MAX - 6, 5, 6))]
|
||||
fn add_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
assert_eq!(rect + offset, expected);
|
||||
assert_eq!(offset + rect, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), -Offset::MAX, Rect::new(u16::MAX - 5, u16::MAX - 6, 5, 6))]
|
||||
fn sub_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
assert_eq!(rect - offset, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MIN, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(u16::MAX - 5, u16::MAX - 6, 5, 6))]
|
||||
fn add_assign_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
let mut rect = rect;
|
||||
rect += offset;
|
||||
assert_eq!(rect, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), -Offset::MAX, Rect::new(u16::MAX - 5, u16::MAX - 6, 5, 6))]
|
||||
fn sub_assign_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
let mut rect = rect;
|
||||
rect -= offset;
|
||||
assert_eq!(rect, expected);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,12 @@ impl Size {
|
||||
/// A zero sized Size
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
/// The minimum possible Size
|
||||
pub const MIN: Self = Self::ZERO;
|
||||
|
||||
/// The maximum possible Size
|
||||
pub const MAX: Self = Self::new(u16::MAX, u16::MAX);
|
||||
|
||||
/// Create a new `Size` struct
|
||||
pub const fn new(width: u16, height: u16) -> Self {
|
||||
Self { width, height }
|
||||
|
||||
@@ -53,7 +53,7 @@ fn render(frame: &mut Frame, selected_tab: usize) {
|
||||
frame.render_widget(title.centered(), top);
|
||||
|
||||
render_content(frame, main, selected_tab);
|
||||
render_tabs(frame, main.offset(Offset { x: 1, y: 0 }), selected_tab);
|
||||
render_tabs(frame, main + Offset::new(1, 0), selected_tab);
|
||||
}
|
||||
|
||||
/// Render the tabs.
|
||||
|
||||
Reference in New Issue
Block a user