From 5001d140852d79bc4eac391bb4206866ecae3513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Gro=C3=9Fe?= Date: Tue, 13 Jun 2023 07:44:25 +0100 Subject: [PATCH] feat(backend): backend provides window_size, add Size struct For image (sixel, iTerm2, Kitty...) support that handles graphics in terms of `Rect` so that the image area can be included in layouts. For example: an image is loaded with a known pixel-size, and drawn, but the image protocol has no mechanism of knowing the actual cell/character area that been drawn on. It is then impossible to skip overdrawing the area. Returning the window size in pixel-width / pixel-height, together with colums / rows, it can be possible to account the pixel size of each cell / character, and then known the `Rect` of a given image, and also resize the image so that it fits exactly in a `Rect`. Crossterm and termwiz also both return both sizes from one syscall, while termion does two. Add a `Size` struct for the cases where a `Rect`'s `x`/`y` is unused (always zero). `Size` is not "clipped" for `area < u16::max_value()` like `Rect`. This is why there are `From` implementations between the two. --- src/backend/crossterm.rs | 25 ++++++++++++++++---- src/backend/mod.rs | 16 +++++++++++-- src/backend/termion.rs | 11 +++++++-- src/backend/termwiz.rs | 49 ++++++++++++++++++++++++++-------------- src/backend/test.rs | 16 +++++++++++-- src/layout.rs | 24 ++++++++++++++++++++ src/terminal.rs | 29 +++++++++++++----------- tests/terminal.rs | 5 ++-- 8 files changed, 131 insertions(+), 44 deletions(-) diff --git a/src/backend/crossterm.rs b/src/backend/crossterm.rs index aef71c9e4..511217f26 100644 --- a/src/backend/crossterm.rs +++ b/src/backend/crossterm.rs @@ -18,9 +18,10 @@ use crossterm::{ }; use crate::{ - backend::{Backend, ClearType}, + backend::{Backend, ClearType, WindowSize}, buffer::Cell, - layout::Rect, + layout::Size, + prelude::Rect, style::{Color, Modifier}, }; @@ -169,12 +170,26 @@ where } fn size(&self) -> io::Result { - let (width, height) = - terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - + let (width, height) = terminal::size()?; Ok(Rect::new(0, 0, width, height)) } + fn window_size(&mut self) -> Result { + let crossterm::terminal::WindowSize { + columns, + rows, + width, + height, + } = terminal::window_size()?; + Ok(WindowSize { + columns_rows: Size { + width: columns, + height: rows, + }, + window_pixels: Size { width, height }, + }) + } + fn flush(&mut self) -> io::Result<()> { self.buffer.flush() } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b8665278b..f3999d0a5 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -29,7 +29,7 @@ use std::io; use strum::{Display, EnumString}; -use crate::{buffer::Cell, layout::Rect}; +use crate::{buffer::Cell, layout::Size, prelude::Rect}; #[cfg(feature = "termion")] mod termion; @@ -60,6 +60,11 @@ pub enum ClearType { UntilNewLine, } +pub struct WindowSize { + pub columns_rows: Size, + pub window_pixels: Size, +} + /// The `Backend` trait provides an abstraction over different terminal libraries. /// It defines the methods required to draw content, manipulate the cursor, and /// clear the terminal screen. @@ -111,9 +116,16 @@ pub trait Backend { } } - /// Get the size of the terminal screen as a [`Rect`]. + /// Get the size of the terminal screen in columns/rows as a [`Rect`]. fn size(&self) -> Result; + /// Get the size of the terminal screen in columns/rows and pixels as [`WindowSize`]. + /// + /// The reason for this not returning only the pixel size, given the redundancy with the + /// `size()` method, is that the underlying backends most likely get both values with one + /// syscall, and the user is also most likely to need columns,rows together with pixel size. + fn window_size(&mut self) -> Result; + /// Flush any buffered content to the terminal screen. fn flush(&mut self) -> Result<(), io::Error>; } diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 7ef28bb74..10a73c928 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -10,9 +10,9 @@ use std::{ }; use crate::{ - backend::{Backend, ClearType}, + backend::{Backend, ClearType, WindowSize}, buffer::Cell, - layout::Rect, + prelude::Rect, style::{Color, Modifier}, }; @@ -160,6 +160,13 @@ where Ok(Rect::new(0, 0, terminal.0, terminal.1)) } + fn window_size(&mut self) -> Result { + Ok(WindowSize { + columns_rows: termion::terminal_size()?.into(), + window_pixels: termion::terminal_size_pixels()?.into(), + }) + } + fn flush(&mut self) -> io::Result<()> { self.stdout.flush() } diff --git a/src/backend/termwiz.rs b/src/backend/termwiz.rs index 18e3d2668..350cc358c 100644 --- a/src/backend/termwiz.rs +++ b/src/backend/termwiz.rs @@ -11,13 +11,14 @@ use termwiz::{ cell::{AttributeChange, Blink, Intensity, Underline}, color::{AnsiColor, ColorAttribute, SrgbaTuple}, surface::{Change, CursorVisibility, Position}, - terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal}, + terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal}, }; use crate::{ - backend::Backend, + backend::{Backend, WindowSize}, buffer::Cell, - layout::Rect, + layout::Size, + prelude::Rect, style::{Color, Modifier}, }; @@ -169,22 +170,31 @@ impl Backend for TermwizBackend { } fn size(&self) -> Result { - let (term_width, term_height) = self.buffered_terminal.dimensions(); - let max = u16::max_value(); - Ok(Rect::new( - 0, - 0, - if term_width > usize::from(max) { - max - } else { - term_width as u16 + let (cols, rows) = self.buffered_terminal.dimensions(); + Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows))) + } + + fn window_size(&mut self) -> Result { + let ScreenSize { + cols, + rows, + xpixel, + ypixel, + } = self + .buffered_terminal + .terminal() + .get_screen_size() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + Ok(WindowSize { + columns_rows: Size { + width: u16_max(cols), + height: u16_max(rows), }, - if term_height > usize::from(max) { - max - } else { - term_height as u16 + window_pixels: Size { + width: u16_max(xpixel), + height: u16_max(ypixel), }, - )) + }) } fn flush(&mut self) -> Result<(), io::Error> { @@ -221,3 +231,8 @@ impl From for ColorAttribute { } } } + +#[inline] +fn u16_max(i: usize) -> u16 { + u16::try_from(i).unwrap_or(u16::MAX) +} diff --git a/src/backend/test.rs b/src/backend/test.rs index 40ba14059..f174889d2 100644 --- a/src/backend/test.rs +++ b/src/backend/test.rs @@ -9,9 +9,9 @@ use std::{ use unicode_width::UnicodeWidthStr; use crate::{ - backend::Backend, + backend::{Backend, WindowSize}, buffer::{Buffer, Cell}, - layout::Rect, + layout::{Rect, Size}, }; /// A backend used for the integration tests. @@ -179,6 +179,18 @@ impl Backend for TestBackend { Ok(Rect::new(0, 0, 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: Size = Size { + width: 640, + height: 480, + }; + Ok(WindowSize { + columns_rows: (self.width, self.height).into(), + window_pixels: WINDOW_PIXEL_SIZE, + }) + } + fn flush(&mut self) -> Result<(), io::Error> { Ok(()) } diff --git a/src/layout.rs b/src/layout.rs index 97005ef72..a71cd1703 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -647,6 +647,30 @@ 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 From<(u16, u16)> for Size { + fn from((width, height): (u16, u16)) -> Self { + Size { width, height } + } +} + +// impl From for Rect { +// fn from(size: Size) -> Self { +// Rect { +// x: 0, +// y: 0, +// width: size.width, +// height: size.height, +// } +// } +// } + #[cfg(test)] mod tests { use strum::ParseError; diff --git a/src/terminal.rs b/src/terminal.rs index 14be202ad..4689b8fd5 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -3,7 +3,7 @@ use std::{fmt, io}; use crate::{ backend::{Backend, ClearType}, buffer::Buffer, - layout::Rect, + layout::{Rect, Size}, widgets::{StatefulWidget, Widget}, }; @@ -50,7 +50,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), @@ -152,7 +152,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 @@ -185,12 +185,15 @@ 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, + let size: Size = match options.viewport { + Viewport::Fullscreen | Viewport::Inline(_) => backend.window_size()?.columns_rows, + Viewport::Fixed(area) => Size { + width: area.width, + height: area.width, + }, }; let (viewport_area, cursor_pos) = match options.viewport { - Viewport::Fullscreen => (size, (0, 0)), + Viewport::Fullscreen => (Rect::new(0, 0, size.width, size.height), (0, 0)), Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?, Viewport::Fixed(area) => (area, (area.left(), area.top())), }; @@ -241,9 +244,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 => Rect::new(0, 0, size.width, size.height), Viewport::Inline(height) => { let offset_in_previous_viewport = self .last_known_cursor_pos @@ -270,7 +273,7 @@ where pub fn autoresize(&mut self) -> io::Result<()> { // fixed viewports do not get autoresized if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) { - let size = self.size()?; + let size: Size = self.backend.window_size()?.columns_rows; if size != self.last_known_size { self.resize(size)?; } @@ -313,7 +316,7 @@ where Ok(CompletedFrame { buffer: &self.buffers[1 - self.current], - area: self.last_known_size, + size: self.last_known_size, }) } @@ -430,7 +433,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), @@ -466,7 +469,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..ed12fbb7d 100644 --- a/tests/terminal.rs +++ b/tests/terminal.rs @@ -2,7 +2,6 @@ use std::error::Error; use ratatui::{ backend::{Backend, TestBackend}, - layout::Rect, widgets::Paragraph, Terminal, }; @@ -37,13 +36,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, (10, 10).into()); 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, (8, 8).into()); Ok(()) }