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:
Josh McKinney
2025-11-30 11:57:08 -08:00
committed by GitHub
parent a6356c157c
commit 96d097ef76
8 changed files with 276 additions and 49 deletions

View File

@@ -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);
}

View File

@@ -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;

View 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));
}
}

View File

@@ -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 {

View File

@@ -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.

View 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);
}
}

View File

@@ -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 }

View File

@@ -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.