Skip to content

Commit

Permalink
feat(backend): backend provides window_size, add Size struct
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
benjajaja committed Aug 26, 2023
1 parent ad3413e commit 5001d14
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 44 deletions.
25 changes: 20 additions & 5 deletions src/backend/crossterm.rs
Expand Up @@ -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},
};

Expand Down Expand Up @@ -169,12 +170,26 @@ where
}

fn size(&self) -> io::Result<Rect> {
let (width, height) =
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;

let (width, height) = terminal::size()?;

Check warning on line 173 in src/backend/crossterm.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/crossterm.rs#L173

Added line #L173 was not covered by tests
Ok(Rect::new(0, 0, width, height))
}

fn window_size(&mut self) -> Result<WindowSize, io::Error> {

Check warning on line 177 in src/backend/crossterm.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/crossterm.rs#L177

Added line #L177 was not covered by tests
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 },
})
}

Check warning on line 191 in src/backend/crossterm.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/crossterm.rs#L179-L191

Added lines #L179 - L191 were not covered by tests

fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
}
Expand Down
16 changes: 14 additions & 2 deletions src/backend/mod.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Rect, io::Error>;

/// 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<WindowSize, io::Error>;

/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> Result<(), io::Error>;
}
Expand Down
11 changes: 9 additions & 2 deletions src/backend/termion.rs
Expand Up @@ -10,9 +10,9 @@ use std::{
};

use crate::{
backend::{Backend, ClearType},
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::Rect,
prelude::Rect,
style::{Color, Modifier},
};

Expand Down Expand Up @@ -160,6 +160,13 @@ where
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}

fn window_size(&mut self) -> Result<WindowSize, io::Error> {
Ok(WindowSize {
columns_rows: termion::terminal_size()?.into(),
window_pixels: termion::terminal_size_pixels()?.into(),

Check warning on line 166 in src/backend/termion.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termion.rs#L163-L166

Added lines #L163 - L166 were not covered by tests
})
}

Check warning on line 168 in src/backend/termion.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termion.rs#L168

Added line #L168 was not covered by tests

fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
}
Expand Down
49 changes: 32 additions & 17 deletions src/backend/termwiz.rs
Expand Up @@ -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},
};

Expand Down Expand Up @@ -169,22 +170,31 @@ impl Backend for TermwizBackend {
}

fn size(&self) -> Result<Rect, io::Error> {
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)))
}

Check warning on line 175 in src/backend/termwiz.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termwiz.rs#L173-L175

Added lines #L173 - L175 were not covered by tests

fn window_size(&mut self) -> Result<WindowSize, io::Error> {

Check warning on line 177 in src/backend/termwiz.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termwiz.rs#L177

Added line #L177 was not covered by tests
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),

Check warning on line 191 in src/backend/termwiz.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termwiz.rs#L179-L191

Added lines #L179 - L191 were not covered by tests
},
if term_height > usize::from(max) {
max
} else {
term_height as u16
window_pixels: Size {
width: u16_max(xpixel),
height: u16_max(ypixel),

Check warning on line 195 in src/backend/termwiz.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termwiz.rs#L193-L195

Added lines #L193 - L195 were not covered by tests
},
))
})

Check warning on line 197 in src/backend/termwiz.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termwiz.rs#L197

Added line #L197 was not covered by tests
}

fn flush(&mut self) -> Result<(), io::Error> {
Expand Down Expand Up @@ -221,3 +231,8 @@ impl From<Color> for ColorAttribute {
}
}
}

#[inline]
fn u16_max(i: usize) -> u16 {
u16::try_from(i).unwrap_or(u16::MAX)
}

Check warning on line 238 in src/backend/termwiz.rs

View check run for this annotation

Codecov / codecov/patch

src/backend/termwiz.rs#L236-L238

Added lines #L236 - L238 were not covered by tests
16 changes: 14 additions & 2 deletions src/backend/test.rs
Expand Up @@ -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.
Expand Down Expand Up @@ -179,6 +179,18 @@ impl Backend for TestBackend {
Ok(Rect::new(0, 0, self.width, self.height))
}

fn window_size(&mut self) -> Result<WindowSize, io::Error> {
// 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(())
}
Expand Down
24 changes: 24 additions & 0 deletions src/layout.rs
Expand Up @@ -647,6 +647,30 @@ fn try_split(area: Rect, layout: &Layout) -> Result<Rc<[Rect]>, 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<Size> 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;
Expand Down
29 changes: 16 additions & 13 deletions src/terminal.rs
Expand Up @@ -3,7 +3,7 @@ use std::{fmt, io};
use crate::{
backend::{Backend, ClearType},
buffer::Buffer,
layout::Rect,
layout::{Rect, Size},
widgets::{StatefulWidget, Widget},
};

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<B> Drop for Terminal<B>
Expand Down Expand Up @@ -185,12 +185,15 @@ where
}

pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
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())),
};
Expand Down Expand Up @@ -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
Expand All @@ -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)?;
}
Expand Down Expand Up @@ -313,7 +316,7 @@ where

Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
size: self.last_known_size,
})
}

Expand Down Expand Up @@ -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());

Check warning on line 436 in src/terminal.rs

View check run for this annotation

Codecov / codecov/patch

src/terminal.rs#L436

Added line #L436 was not covered by tests
let area = Rect {
x: self.viewport_area.left(),
y: self.viewport_area.top().saturating_sub(missing_lines),
Expand Down Expand Up @@ -466,7 +469,7 @@ where
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
size: Size,

Check warning on line 472 in src/terminal.rs

View check run for this annotation

Codecov / codecov/patch

src/terminal.rs#L472

Added line #L472 was not covered by tests
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
Expand Down
5 changes: 2 additions & 3 deletions tests/terminal.rs
Expand Up @@ -2,7 +2,6 @@ use std::error::Error;

use ratatui::{
backend::{Backend, TestBackend},
layout::Rect,
widgets::Paragraph,
Terminal,
};
Expand Down Expand Up @@ -37,13 +36,13 @@ fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
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(())
}

0 comments on commit 5001d14

Please sign in to comment.