feat(layout): add Rect::clamp() method (#749)

* feat(layout): add a Rect::clamp() method

This ensures a rectangle does not end up outside an area. This is useful
when you want to be able to dynamically move a rectangle around, but
keep it constrained to a certain area.

For example, this can be used to implement a draggable window that can
be moved around, but not outside the terminal window.

```rust
let window_area = Rect::new(state.x, state.y, 20, 20).clamp(area);
state.x = rect.x;
state.y = rect.y;
```

* refactor: use rstest to simplify clamp test

* fix: use rstest description instead of string

test layout::rect::tests:🗜️:case_01_inside ... ok
test layout::rect::tests:🗜️:case_02_up_left ... ok
test layout::rect::tests:🗜️:case_04_up_right ... ok
test layout::rect::tests:🗜️:case_05_left ... ok
test layout::rect::tests:🗜️:case_03_up ... ok
test layout::rect::tests:🗜️:case_06_right ... ok
test layout::rect::tests:🗜️:case_07_down_left ... ok
test layout::rect::tests:🗜️:case_08_down ... ok
test layout::rect::tests:🗜️:case_09_down_right ... ok
test layout::rect::tests:🗜️:case_10_too_wide ... ok
test layout::rect::tests:🗜️:case_11_too_tall ... ok
test layout::rect::tests:🗜️:case_12_too_large ... ok

* fix: less ambiguous docs for this / other rect

* fix: move rstest to dev deps
This commit is contained in:
Josh McKinney 2024-01-05 11:38:30 -08:00 committed by GitHub
parent fe84141119
commit f13fd73d9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 0 deletions

View file

@ -54,6 +54,7 @@ fakeit = "1.1"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rstest = "0.18.2"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.

View file

@ -198,10 +198,44 @@ impl Rect {
.try_into()
.expect("invalid number of rects")
}
/// Clamp this rect to fit inside the other rect.
///
/// If the width or height of this rect is larger than the other rect, it will be clamped to the
/// other rect's width or height.
///
/// If the left or top coordinate of this rect is smaller than the other rect, it will be
/// clamped to the other rect's left or top coordinate.
///
/// If the right or bottom coordinate of this rect is larger than the other rect, it will be
/// clamped to the other rect's right or bottom coordinate.
///
/// This is different from [`Rect::intersection`] because it will move this rect to fit inside
/// the other rect, while [`Rect::intersection`] instead would keep this rect's position and
/// truncate its size to only that which is inside the other rect.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```
pub fn clamp(self, other: Rect) -> Rect {
let width = self.width.min(other.width);
let height = self.height.min(other.height);
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
Rect::new(x, y, width, height)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
@ -399,4 +433,22 @@ mod tests {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [_a, _b, _c] = Rect::new(0, 0, 2, 1).split(&layout);
}
#[rstest]
#[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
#[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
#[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
#[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
#[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
#[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
#[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
#[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
#[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
#[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
#[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
#[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
let other = Rect::new(10, 10, 100, 100);
assert_eq!(rect.clamp(other), expected);
}
}