From c7faa78931a9a30227c003b5432c7c780088185f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Gro=C3=9Fe?= Date: Sat, 12 Aug 2023 10:42:56 +0100 Subject: [PATCH] feat(backend): add Size struct for when a Rect is too much Add a `Size` struct for the cases where a `Rect`'s `x`/`y` is unused (always zero). A `Size` can be made into a `Rect` but not the opposite. `Size::new` is "clipped" so that the area is under max u16, just like `Rect`. However, just like `Rect`, a `Size` literal can be constructed without this constraint. For example, the `window_size()` pixel size is not constrained by the area. --- src/backend/crossterm.rs | 14 ++++++-------- src/backend/mod.rs | 8 ++++---- src/backend/termion.rs | 14 +++++++++----- src/backend/termwiz.rs | 13 ++++++++----- src/backend/test.rs | 13 ++++++++----- src/layout.rs | 35 +++++++++++++++++++++++++++++++++++ src/terminal.rs | 22 +++++++++++----------- tests/terminal.rs | 6 +++--- 8 files changed, 84 insertions(+), 41 deletions(-) diff --git a/src/backend/crossterm.rs b/src/backend/crossterm.rs index 2d400b7c2..e2cdf5510 100644 --- a/src/backend/crossterm.rs +++ b/src/backend/crossterm.rs @@ -20,7 +20,7 @@ use crossterm::{ use crate::{ backend::{Backend, ClearType, WindowSize}, buffer::Cell, - layout::Rect, + layout::Size, style::{Color, Modifier}, }; @@ -168,11 +168,9 @@ where self.buffer.flush() } - fn size(&self) -> io::Result { - let (width, height) = - terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - - Ok(Rect::new(0, 0, width, height)) + fn size(&self) -> io::Result { + let (width, height) = terminal::size()?; + Ok(Size::new(width, height)) } fn window_size(&mut self) -> Result { @@ -183,8 +181,8 @@ where height, } = terminal::window_size()?; Ok(WindowSize { - columns_rows: (columns, rows), - window_pixels: (width, height), + columns_rows: Size::new(columns, rows), + window_pixels: Size { width, height }, }) } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 2d5dd58f2..dd47f2c7f 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -27,7 +27,7 @@ use std::io; -use crate::{buffer::Cell, layout::Rect}; +use crate::{buffer::Cell, layout::Size}; #[cfg(feature = "termion")] mod termion; @@ -59,8 +59,8 @@ pub enum ClearType { } pub struct WindowSize { - pub columns_rows: (u16, u16), - pub window_pixels: (u16, u16), + pub columns_rows: Size, + pub window_pixels: Size, } /// The `Backend` trait provides an abstraction over different terminal libraries. @@ -115,7 +115,7 @@ pub trait Backend { } /// Get the size of the terminal screen in columns/rows as a [`Rect`]. - fn size(&self) -> Result; + fn size(&self) -> Result; /// Get the size of the terminal screen in columns/rows and pixels as [`(Rect, (width, /// height))`]. diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 1c3c7de26..83f5a5880 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -12,7 +12,7 @@ use std::{ use crate::{ backend::{Backend, ClearType, WindowSize}, buffer::Cell, - layout::Rect, + layout::Size, style::{Color, Modifier}, }; @@ -155,15 +155,19 @@ where ) } - fn size(&self) -> io::Result { + fn size(&self) -> io::Result { let terminal = termion::terminal_size()?; - Ok(Rect::new(0, 0, terminal.0, terminal.1)) + Ok(Size::new(terminal.0, terminal.1)) } fn window_size(&mut self) -> Result { + let (pixel_width, pixel_height) = termion::terminal_size_pixels()?; Ok(WindowSize { - columns_rows: termion::terminal_size()?, - window_pixels: termion::terminal_size_pixels()?, + columns_rows: self.size()?, + window_pixels: Size { + width: pixel_width, + height: pixel_height, + }, }) } diff --git a/src/backend/termwiz.rs b/src/backend/termwiz.rs index f7cf38958..b3ceb7970 100644 --- a/src/backend/termwiz.rs +++ b/src/backend/termwiz.rs @@ -17,7 +17,7 @@ use termwiz::{ use crate::{ backend::{Backend, WindowSize}, buffer::Cell, - layout::Rect, + layout::Size, style::{Color, Modifier}, }; @@ -168,9 +168,9 @@ impl Backend for TermwizBackend { Ok(()) } - fn size(&self) -> Result { + fn size(&self) -> Result { let (cols, rows) = self.buffered_terminal.dimensions(); - Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows))) + Ok(Size::new(u16_max(cols), u16_max(rows))) } fn window_size(&mut self) -> Result { @@ -185,8 +185,11 @@ impl Backend for TermwizBackend { .get_screen_size() .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; Ok(WindowSize { - columns_rows: (u16_max(cols), u16_max(rows)), - window_pixels: (u16_max(xpixel), u16_max(ypixel)), + columns_rows: Size::new(u16_max(cols), u16_max(rows)), + window_pixels: Size { + width: u16_max(xpixel), + height: u16_max(ypixel), + }, }) } diff --git a/src/backend/test.rs b/src/backend/test.rs index f1b6cea1b..81128278b 100644 --- a/src/backend/test.rs +++ b/src/backend/test.rs @@ -11,7 +11,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ backend::{Backend, WindowSize}, buffer::{Buffer, Cell}, - layout::Rect, + layout::{Rect, Size}, }; /// A backend used for the integration tests. @@ -174,15 +174,18 @@ impl Backend for TestBackend { Ok(()) } - fn size(&self) -> Result { - Ok(Rect::new(0, 0, self.width, self.height)) + fn size(&self) -> Result { + Ok(Size::new(self.width, self.height)) } fn window_size(&mut self) -> Result { // Some arbitrary window pixel size, probably doesn't need much testing. - static WINDOW_PIXEL_SIZE: (u16, u16) = (640, 480); + static WINDOW_PIXEL_SIZE: Size = Size { + width: 640, + height: 480, + }; Ok(WindowSize { - columns_rows: (self.width, self.height), + columns_rows: self.size()?, window_pixels: WINDOW_PIXEL_SIZE, }) } diff --git a/src/layout.rs b/src/layout.rs index 3608d69ca..8672a8b3a 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -587,6 +587,41 @@ fn try_split(area: Rect, layout: &Layout) -> Result, AddConstraintErr Ok(results) } +/// A simple size struct +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] +pub struct Size { + pub width: u16, + pub height: u16, +} + +impl Size { + /// Creates a new size, with width and height limited to keep the area under max u16. + /// If clipped, aspect ratio will be preserved. + pub fn new(width: u16, height: u16) -> Size { + let max_area = u16::max_value(); + 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) + }; + Size { + width: clipped_width, + height: clipped_height, + } + } +} + +impl From for Rect { + fn from(size: Size) -> Self { + Rect::new(0, 0, size.width, size.height) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/terminal.rs b/src/terminal.rs index 18c3d570c..cb14c769b 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -3,7 +3,7 @@ use std::io; use crate::{ backend::{Backend, ClearType}, buffer::Buffer, - layout::Rect, + layout::{Rect, Size}, widgets::{StatefulWidget, Widget}, }; @@ -40,7 +40,7 @@ where viewport: Viewport, viewport_area: Rect, /// Last known size of the terminal. Used to detect if the internal buffers have to be resized. - last_known_size: Rect, + last_known_size: Size, /// Last known position of the cursor. Used to find the new area when the viewport is inlined /// and the terminal resized. last_known_cursor_pos: (u16, u16), @@ -142,7 +142,7 @@ where #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct CompletedFrame<'a> { pub buffer: &'a Buffer, - pub area: Rect, + pub size: Size, } impl Drop for Terminal @@ -177,10 +177,10 @@ where pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result> { let size = match options.viewport { Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?, - Viewport::Fixed(area) => area, + Viewport::Fixed(area) => Size::new(area.width, area.height), }; let (viewport_area, cursor_pos) = match options.viewport { - Viewport::Fullscreen => (size, (0, 0)), + Viewport::Fullscreen => (size.into(), (0, 0)), Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?, Viewport::Fixed(area) => (area, (area.left(), area.top())), }; @@ -231,9 +231,9 @@ where /// Updates the Terminal so that internal buffers match the requested size. Requested size will /// be saved so the size can remain consistent when rendering. /// This leads to a full clear of the screen. - pub fn resize(&mut self, size: Rect) -> io::Result<()> { + pub fn resize(&mut self, size: Size) -> io::Result<()> { let next_area = match self.viewport { - Viewport::Fullscreen => size, + Viewport::Fullscreen => size.into(), Viewport::Inline(height) => { let offset_in_previous_viewport = self .last_known_cursor_pos @@ -303,7 +303,7 @@ where Ok(CompletedFrame { buffer: &self.buffers[1 - self.current], - area: self.last_known_size, + size: self.last_known_size, }) } @@ -357,7 +357,7 @@ where } /// Queries the real size of the backend. - pub fn size(&self) -> io::Result { + pub fn size(&self) -> io::Result { self.backend.size() } @@ -420,7 +420,7 @@ where let height = height.min(self.last_known_size.height); self.backend.append_lines(height)?; let missing_lines = - height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top()); + height.saturating_sub(self.last_known_size.height - self.viewport_area.top()); let area = Rect { x: self.viewport_area.left(), y: self.viewport_area.top().saturating_sub(missing_lines), @@ -456,7 +456,7 @@ where fn compute_inline_size( backend: &mut B, height: u16, - size: Rect, + size: Size, offset_in_previous_viewport: u16, ) -> io::Result<(Rect, (u16, u16))> { let pos = backend.get_cursor()?; diff --git a/tests/terminal.rs b/tests/terminal.rs index a090dec2e..82abb9822 100644 --- a/tests/terminal.rs +++ b/tests/terminal.rs @@ -2,7 +2,7 @@ use std::error::Error; use ratatui::{ backend::{Backend, TestBackend}, - layout::Rect, + layout::Size, widgets::Paragraph, Terminal, }; @@ -37,13 +37,13 @@ fn terminal_draw_returns_the_completed_frame() -> Result<(), Box> { f.render_widget(paragraph, f.size()); })?; assert_eq!(frame.buffer.get(0, 0).symbol, "T"); - assert_eq!(frame.area, Rect::new(0, 0, 10, 10)); + assert_eq!(frame.size, Size::new(10, 10)); terminal.backend_mut().resize(8, 8); let frame = terminal.draw(|f| { let paragraph = Paragraph::new("test"); f.render_widget(paragraph, f.size()); })?; assert_eq!(frame.buffer.get(0, 0).symbol, "t"); - assert_eq!(frame.area, Rect::new(0, 0, 8, 8)); + assert_eq!(frame.size, Size::new(8, 8)); Ok(()) }