Skip to content

Commit

Permalink
feat(backend): backend provides window_size, add Size struct (#276)
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 29, 2023
1 parent 51fdcbe commit d077903
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 28 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()?;
Ok(Rect::new(0, 0, width, height))
}

fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}

fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
}
Expand Down
23 changes: 21 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,18 @@ pub enum ClearType {
UntilNewLine,
}

/// The window sizes in columns,rows and optionally pixel width,height.
pub struct WindowSize {
/// Size in character/cell columents,rows.
pub columns_rows: Size,
/// Size in pixel width,height.
///
/// The `pixels` fields may not be implemented by all terminals and return `0,0`.
/// See https://man7.org/linux/man-pages/man4/tty_ioctl.4.html under section
/// "Get and set window size" / TIOCGWINSZ where the fields are commented as "unused".
pub 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 +123,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(),
pixels: termion::terminal_size_pixels()?.into(),
})
}

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)))
}

fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let ScreenSize {
cols,
rows,
xpixel,
ypixel,
} = self
.buffered_terminal
.terminal()
.get_screen_size()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
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
pixels: Size {
width: u16_max(xpixel),
height: u16_max(ypixel),
},
))
})
}

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)
}
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(),
pixels: WINDOW_PIXEL_SIZE,
})
}

fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
Expand Down
13 changes: 13 additions & 0 deletions src/layout.rs
Expand Up @@ -647,6 +647,19 @@ 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 }
}
}

#[cfg(test)]
mod tests {
use strum::ParseError;
Expand Down

0 comments on commit d077903

Please sign in to comment.