fix(rect)!: Rect::area now returns u32 and Rect::new() no longer clamps area to u16::MAX (#1378)

This change fixes the unexpected behavior of the Rect::new() function to
be more intuitive. The Rect::new() function now clamps the width and
height of the rectangle to keep each bound within u16::MAX. The
Rect::area() function now returns a u32 instead of a u16 to allow for
larger areas to be calculated.

Previously, the Rect::new() function would clamp the total area of the
rectangle to u16::MAX, by preserving the aspect ratio of the rectangle.

BREAKING CHANGE: Rect::area() now returns a u32 instead of a u16.

Fixes: <https://github.com/ratatui/ratatui/issues/1375>

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
Josh McKinney 2024-10-14 15:04:56 -07:00 committed by GitHub
parent 4069aa8274
commit 3df685e114
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 58 additions and 73 deletions

View file

@ -71,6 +71,13 @@ This is a quick summary of the sections below:
## v0.29.0 (Unreleased) ## v0.29.0 (Unreleased)
### `Rect::area()` now returns u32 instead of u16 ([#1378])
[#1378]: https://github.com/ratatui/ratatui/pull/1378
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
### `Line` now implements `From<Cow<str>` ([#1373]) ### `Line` now implements `From<Cow<str>` ([#1373])
[#1373]: https://github.com/ratatui/ratatui/pull/1373 [#1373]: https://github.com/ratatui/ratatui/pull/1373

View file

@ -8,12 +8,7 @@ use ratatui::{
criterion::criterion_group!(benches, empty, filled, with_lines); criterion::criterion_group!(benches, empty, filled, with_lines);
const fn rect(size: u16) -> Rect { const fn rect(size: u16) -> Rect {
Rect { Rect::new(0, 0, size, size)
x: 0,
y: 0,
width: size,
height: size,
}
} }
fn empty(c: &mut Criterion) { fn empty(c: &mut Criterion) {

View file

@ -48,7 +48,7 @@ struct App {
} }
impl App { impl App {
fn new() -> Self { const fn new() -> Self {
Self { Self {
x: 0.0, x: 0.0,
y: 0.0, y: 0.0,

View file

@ -269,9 +269,10 @@ impl Buffer {
return None; return None;
} }
// remove offset // remove offset
let y = position.y - self.area.y; let y = (position.y - self.area.y) as usize;
let x = position.x - self.area.x; let x = (position.x - self.area.x) as usize;
Some((y * self.area.width + x) as usize) let width = self.area.width as usize;
Some(y * width + x)
} }
/// Returns the (global) coordinates of a cell given its index /// Returns the (global) coordinates of a cell given its index

View file

@ -55,32 +55,41 @@ impl Rect {
height: 0, height: 0,
}; };
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If /// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
/// clipped, aspect ratio will be preserved. ///
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self { /// If the width or height would cause the right or bottom coordinate to be larger than the
let max_area = u16::MAX; /// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
let (clipped_width, clipped_height) = /// coordinate within `u16`.
if u32::from(width) * u32::from(height) > u32::from(max_area) { ///
let aspect_ratio = f64::from(width) / f64::from(height); /// # Examples
let max_area_f = f64::from(max_area); ///
let height_f = (max_area_f / aspect_ratio).sqrt(); /// ```
let width_f = height_f * aspect_ratio; /// use ratatui::layout::Rect;
(width_f as u16, height_f as u16) ///
} else { /// let rect = Rect::new(1, 2, 3, 4);
(width, height) /// ```
}; pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
// these calculations avoid using min so that this function can be const
let max_width = u16::MAX - x;
let max_height = u16::MAX - y;
let width = if width > max_width { max_width } else { width };
let height = if height > max_height {
max_height
} else {
height
};
Self { Self {
x, x,
y, y,
width: clipped_width, width,
height: clipped_height, height,
} }
} }
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be /// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
/// clamped to `u16::MAX`. /// clamped to `u16::MAX`.
pub const fn area(self) -> u16 { pub const fn area(self) -> u32 {
self.width.saturating_mul(self.height) (self.width as u32) * (self.height as u32)
} }
/// Returns true if the `Rect` has no area. /// Returns true if the `Rect` has no area.
@ -505,46 +514,28 @@ mod tests {
#[test] #[test]
fn size_truncation() { fn size_truncation() {
for width in 256u16..300u16 { assert_eq!(
for height in 256u16..300u16 { Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
let rect = Rect::new(0, 0, width, height); Rect {
rect.area(); // Should not panic. x: u16::MAX - 100,
assert!(rect.width < width || rect.height < height); y: u16::MAX - 1000,
// The target dimensions are rounded down so the math will not be too precise width: 100,
// but let's make sure the ratios don't diverge crazily. height: 1000
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
} }
} );
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
} }
#[test] #[test]
fn size_preservation() { fn size_preservation() {
for width in 0..256u16 { assert_eq!(
for height in 0..256u16 { Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
let rect = Rect::new(0, 0, width, height); Rect {
rect.area(); // Should not panic. x: u16::MAX - 100,
assert_eq!(rect.width, width); y: u16::MAX - 1000,
assert_eq!(rect.height, height); width: 100,
height: 1000
} }
} );
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
} }
#[test] #[test]
@ -555,7 +546,7 @@ mod tests {
width: 10, width: 10,
height: 10, height: 10,
}; };
const _AREA: u16 = RECT.area(); const _AREA: u32 = RECT.area();
const _LEFT: u16 = RECT.left(); const _LEFT: u16 = RECT.left();
const _RIGHT: u16 = RECT.right(); const _RIGHT: u16 = RECT.right();
const _TOP: u16 = RECT.top(); const _TOP: u16 = RECT.top();

View file

@ -1,21 +1,12 @@
use std::error::Error; use std::error::Error;
use ratatui::{ use ratatui::{
backend::{Backend, TestBackend}, backend::TestBackend,
layout::Rect, layout::Rect,
widgets::{Block, Paragraph, Widget}, widgets::{Block, Paragraph, Widget},
Terminal, TerminalOptions, Viewport, Terminal, TerminalOptions, Viewport,
}; };
#[test]
fn terminal_buffer_size_should_be_limited() {
let backend = TestBackend::new(400, 400);
let terminal = Terminal::new(backend).unwrap();
let size = terminal.backend().size().unwrap();
assert_eq!(size.width, 255);
assert_eq!(size.height, 255);
}
#[test] #[test]
fn swap_buffer_clears_prev_buffer() { fn swap_buffer_clears_prev_buffer() {
let backend = TestBackend::new(100, 50); let backend = TestBackend::new(100, 50);