diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 012a5592..b541bc07 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -65,6 +65,13 @@ This is a quick summary of the sections below: - MSRV is now 1.63.0 - `List` no longer ignores empty strings +## v0.29.0 + +### `Rect::area()` now returns u32 instead of u16 ([#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). + ## v0.28.0 ### `Backend::size` returns `Size` instead of `Rect` ([#1254]) @@ -136,8 +143,6 @@ This change simplifies the trait and makes it easier to implement. ### `Frame::size` is deprecated and renamed to `Frame::area` -[#1293]: https://github.com/ratatui/ratatui/pull/1293 - `Frame::size` is renamed to `Frame::area` as it's the more correct name. ## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0) diff --git a/benches/main/buffer.rs b/benches/main/buffer.rs index 2f7801e5..9bcee7e2 100644 --- a/benches/main/buffer.rs +++ b/benches/main/buffer.rs @@ -8,12 +8,7 @@ use ratatui::{ criterion::criterion_group!(benches, empty, filled, with_lines); const fn rect(size: u16) -> Rect { - Rect { - x: 0, - y: 0, - width: size, - height: size, - } + Rect::new(0, 0, size, size) } fn empty(c: &mut Criterion) { diff --git a/src/buffer/buffer.rs b/src/buffer/buffer.rs index 2a509cd1..e93b2e92 100644 --- a/src/buffer/buffer.rs +++ b/src/buffer/buffer.rs @@ -254,9 +254,10 @@ impl Buffer { return None; } // remove offset - let y = position.y - self.area.y; - let x = position.x - self.area.x; - Some((y * self.area.width + x) as usize) + let y = (position.y - self.area.y) as usize; + let x = (position.x - self.area.x) as usize; + let width = self.area.width as usize; + Some(y * width + x) } /// Returns the (global) coordinates of a cell given its index diff --git a/src/layout/rect.rs b/src/layout/rect.rs index 97497117..e42e42e6 100644 --- a/src/layout/rect.rs +++ b/src/layout/rect.rs @@ -56,32 +56,41 @@ impl Rect { height: 0, }; - /// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If - /// clipped, aspect ratio will be preserved. - pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self { - let max_area = u16::MAX; - let (clipped_width, clipped_height) = - if u32::from(width) * u32::from(height) > u32::from(max_area) { - let aspect_ratio = f64::from(width) / f64::from(height); - let max_area_f = f64::from(max_area); - let height_f = (max_area_f / aspect_ratio).sqrt(); - let width_f = height_f * aspect_ratio; - (width_f as u16, height_f as u16) - } else { - (width, height) - }; + /// 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 + /// maximum value of `u16`, the width or height will be clamped to keep the right or bottom + /// coordinate within `u16`. + /// + /// # Examples + /// + /// ``` + /// use ratatui::layout::Rect; + /// + /// let rect = Rect::new(1, 2, 3, 4); + /// ``` + 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 { x, y, - width: clipped_width, - height: clipped_height, + width, + height, } } /// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be /// clamped to `u16::MAX`. - pub const fn area(self) -> u16 { - self.width.saturating_mul(self.height) + pub const fn area(self) -> u32 { + (self.width as u32) * (self.height as u32) } /// Returns true if the `Rect` has no area. @@ -349,6 +358,8 @@ impl From<(Position, Size)> for Rect { #[cfg(test)] mod tests { + use std::u16; + use rstest::rstest; use super::*; @@ -496,46 +507,28 @@ mod tests { #[test] fn size_truncation() { - for width in 256u16..300u16 { - for height in 256u16..300u16 { - let rect = Rect::new(0, 0, width, height); - rect.area(); // Should not panic. - assert!(rect.width < width || rect.height < height); - // The target dimensions are rounded down so the math will not be too precise - // but let's make sure the ratios don't diverge crazily. - assert!( - (f64::from(rect.width) / f64::from(rect.height) - - f64::from(width) / f64::from(height)) - .abs() - < 1.0 - ); + assert_eq!( + Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000), + Rect { + x: u16::MAX - 100, + y: u16::MAX - 1000, + width: 100, + height: 1000 } - } - - // 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] fn size_preservation() { - for width in 0..256u16 { - for height in 0..256u16 { - let rect = Rect::new(0, 0, width, height); - rect.area(); // Should not panic. - assert_eq!(rect.width, width); - assert_eq!(rect.height, height); + assert_eq!( + Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000), + Rect { + x: u16::MAX - 100, + y: u16::MAX - 1000, + 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] @@ -546,7 +539,7 @@ mod tests { width: 10, height: 10, }; - const _AREA: u16 = RECT.area(); + const _AREA: u32 = RECT.area(); const _LEFT: u16 = RECT.left(); const _RIGHT: u16 = RECT.right(); const _TOP: u16 = RECT.top(); diff --git a/tests/terminal.rs b/tests/terminal.rs index 6315637e..cf799b5a 100644 --- a/tests/terminal.rs +++ b/tests/terminal.rs @@ -1,21 +1,12 @@ use std::error::Error; use ratatui::{ - backend::{Backend, TestBackend}, + backend::TestBackend, layout::Rect, widgets::{Block, Paragraph, Widget}, 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] fn swap_buffer_clears_prev_buffer() { let backend = TestBackend::new(100, 50);