diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a89179 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.insertFinalNewline": true, + "rust-analyzer.cargo.features": "all", + "rust-analyzer.imports.granularity.enforce": true, + "rust-analyzer.imports.granularity.group": "module", + "rust-analyzer.imports.group.enable": true, + "evenBetterToml.formatter.columnWidth": 100, +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bef60bc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +All notable changes to this project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +Procedure when bumping the version number: +1. Update dependencies in a separate commit +2. Set version number in `Cargo.toml` +3. Add new section in this changelog +4. Commit with message `Bump version to X.Y.Z` +5. Create tag named `vX.Y.Z` +6. Push `master` and the new tag + +## Unreleased + +## v0.3.4 - 2025-03-8 + +### Added +- `Frame::set_bell` to print a bell character when the frame is displayed +- `widgets::bell` + +## v0.3.3 - 2025-02-28 + +### Fixed +- Rendering glitches in unicode-based with estimation + +## v0.3.2 - 2025-02-23 + +### Added +- Unicode-based grapheme width estimation method + +## v0.3.1 - 2025-02-21 + +### Fixed +- Rendering glitches, mainly related to emoji + +## v0.3.0 - 2024-11-06 + +### Added +- `Terminal::mark_dirty` + +### Changed +- **(breaking)** Updated dependencies + +## v0.2.3 - 2024-04-25 + +### Fixed +- Width measurements of ASCII control characters +- Toss messing up the terminal state + +## v0.2.2 - 2024-01-14 + +### Fixed +- Crash when drawing `widgets::Predrawn` with width 0 + +## v0.2.1 - 2024-01-05 + +### Added +- `Frame::set_title` +- `WidgetExt::title` +- `widgets::title` + +## v0.2.0 - 2023-08-31 + +### Changed +- **(breaking)** Updated dependencies + +## v0.1.0 - 2023-05-14 + +Initial release diff --git a/Cargo.toml b/Cargo.toml index ecf6494..22967fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "toss" -version = "0.1.0" +version = "0.3.4" edition = "2021" [dependencies] -crossterm = "0.23.2" -unicode-linebreak = "0.1.2" -unicode-segmentation = "1.9.0" -unicode-width = "0.1.9" +async-trait = "0.1.83" +crossterm = "0.28.1" +unicode-linebreak = "0.1.5" +unicode-segmentation = "1.12.0" +unicode-width = "0.2.0" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 57cc976..62c0c75 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,39 +1,30 @@ use crossterm::event::Event; -use crossterm::style::{ContentStyle, Stylize}; -use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use crossterm::style::Stylize; +use toss::{Frame, Pos, Style, Terminal}; fn draw(f: &mut Frame) { - f.write( - Pos::new(0, 0), - "Hello world!", - ContentStyle::default().green(), - ); + f.write(Pos::new(0, 0), ("Hello world!", Style::new().green())); f.write( Pos::new(0, 1), - "Press any key to exit", - ContentStyle::default().on_dark_blue(), + ("Press any key to exit", Style::new().on_dark_blue()), ); f.show_cursor(Pos::new(16, 0)); } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + let mut dirty = true; + while dirty { term.autoresize().unwrap(); - draw(term.frame()); - - if term.present().unwrap() == Redraw::NotRequired { - break; - } + term.present().unwrap(); + dirty = term.measure_widths().unwrap(); } } fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); + term.set_measuring(true); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs new file mode 100644 index 0000000..000cf91 --- /dev/null +++ b/examples/hello_world_widgets.rs @@ -0,0 +1,47 @@ +use std::io; + +use crossterm::event::Event; +use crossterm::style::Stylize; +use toss::widgets::{BorderLook, Text}; +use toss::{Style, Styled, Terminal, Widget, WidgetExt}; + +fn widget() -> impl Widget { + let styled = Styled::new("Hello world!", Style::new().dark_green()) + .then_plain("\n") + .then("Press any key to exit", Style::new().on_dark_blue()); + Text::new(styled) + .padding() + .with_horizontal(1) + .border() + .with_look(BorderLook::LINE_DOUBLE) + .with_style(Style::new().dark_red()) + .background() + .with_style(Style::new().on_yellow().opaque()) + .float() + .with_all(0.5) +} + +fn render_frame(term: &mut Terminal) { + let mut dirty = true; + while dirty { + term.present_widget(widget()).unwrap(); + dirty = term.measure_widths().unwrap(); + } +} + +fn main() { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new().unwrap(); + term.set_measuring(true); + + loop { + // Render and display a frame. A full frame is displayed on the terminal + // once this function exits. + render_frame(&mut term); + + // Exit if the user presses any buttons + if !matches!(crossterm::event::read().unwrap(), Event::Resize(_, _)) { + break; + } + } +} diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index db98bae..c90c4ae 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -1,79 +1,67 @@ use crossterm::event::Event; -use crossterm::style::{ContentStyle, Stylize}; -use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use crossterm::style::Stylize; +use toss::{Frame, Pos, Style, Terminal}; fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), "Writing over wide graphemes removes the entire overwritten grapheme.", - ContentStyle::default(), ); - let under = ContentStyle::default().white().on_dark_blue(); - let over = ContentStyle::default().black().on_dark_yellow(); + let under = Style::new().white().on_dark_blue(); + let over = Style::new().black().on_dark_yellow(); for i in 0..6 { let delta = i - 2; - f.write(Pos::new(2 + i * 7, 2), "a😀", under); - f.write(Pos::new(2 + i * 7, 3), "a😀", under); - f.write(Pos::new(2 + i * 7, 4), "a😀", under); - f.write(Pos::new(2 + i * 7 + delta, 3), "b", over); - f.write(Pos::new(2 + i * 7 + delta, 4), "😈", over); + f.write(Pos::new(2 + i * 7, 2), ("a😀", under)); + f.write(Pos::new(2 + i * 7, 3), ("a😀", under)); + f.write(Pos::new(2 + i * 7, 4), ("a😀", under)); + f.write(Pos::new(2 + i * 7 + delta, 3), ("b", over)); + f.write(Pos::new(2 + i * 7 + delta, 4), ("😈", over)); } f.write( Pos::new(0, 6), "Wide graphemes at the edges of the screen apply their style, but are not", - ContentStyle::default(), - ); - f.write( - Pos::new(0, 7), - "actually rendered.", - ContentStyle::default(), ); + f.write(Pos::new(0, 7), "actually rendered."); let x1 = -1; let x2 = f.size().width as i32 / 2 - 3; let x3 = f.size().width as i32 - 5; - f.write(Pos::new(x1, 9), "123456", under); - f.write(Pos::new(x1, 10), "😀😀😀", under); - f.write(Pos::new(x2, 9), "123456", under); - f.write(Pos::new(x2, 10), "😀😀😀", under); - f.write(Pos::new(x3, 9), "123456", under); - f.write(Pos::new(x3, 10), "😀😀😀", under); + f.write(Pos::new(x1, 9), ("123456", under)); + f.write(Pos::new(x1, 10), ("😀😀😀", under)); + f.write(Pos::new(x2, 9), ("123456", under)); + f.write(Pos::new(x2, 10), ("😀😀😀", under)); + f.write(Pos::new(x3, 9), ("123456", under)); + f.write(Pos::new(x3, 10), ("😀😀😀", under)); let scientist = "👩‍🔬"; f.write( Pos::new(0, 12), "Most terminals ignore the zero width joiner and display this female", - ContentStyle::default(), ); f.write( Pos::new(0, 13), "scientist emoji as a woman and a microscope: 👩‍🔬", - ContentStyle::default(), ); - for i in 0..(f.width(scientist) + 4) { - f.write(Pos::new(2, 15 + i as i32), scientist, under); - f.write(Pos::new(i as i32, 15 + i as i32), "x", over); + for i in 0..(f.widthdb().width(scientist) + 4) { + f.write(Pos::new(2, 15 + i as i32), (scientist, under)); + f.write(Pos::new(i as i32, 15 + i as i32), ("x", over)); } } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + let mut dirty = true; + while dirty { term.autoresize().unwrap(); - draw(term.frame()); - - if term.present().unwrap() == Redraw::NotRequired { - break; - } + term.present().unwrap(); + dirty = term.measure_widths().unwrap(); } } fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); + term.set_measuring(true); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index f150142..5292378 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -1,7 +1,5 @@ use crossterm::event::Event; -use crossterm::style::ContentStyle; -use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use toss::{Frame, Pos, Styled, Terminal}; fn draw(f: &mut Frame) { let text = concat!( @@ -14,37 +12,46 @@ fn draw(f: &mut Frame) { "This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}non-\u{2060}breaking\u{00a0}spaces.\n", "\n", "It can also properly handle wide graphemes (like emoji 🤔), ", - "including ones usually displayed incorrectly by terminal emulators, like 👩‍🔬 (a female scientist emoji).", + "including ones usually displayed incorrectly by terminal emulators, like 👩‍🔬 (a female scientist emoji).\n", + "\n", + "Finally, tabs are supported as well. ", + "The following text is rendered with a tab width of 4:\n", + "\tx\n", + "1\tx\n", + "12\tx\n", + "123\tx\n", + "1234\tx\n", + "12345\tx\n", + "123456\tx\n", + "1234567\tx\n", + "12345678\tx\n", + "123456789\tx\n", ); - let breaks = f.wrap(text, f.size().width.into()); - let lines = toss::split_at_indices(text, &breaks); - for (i, line) in lines.iter().enumerate() { - f.write( - Pos::new(0, i as i32), - line.trim_end(), - ContentStyle::default(), - ); + let width = f.size().width.into(); + let breaks = f.widthdb().wrap(text, width); + let lines = Styled::new_plain(text).split_at_indices(&breaks); + for (i, mut line) in lines.into_iter().enumerate() { + line.trim_end(); + f.write(Pos::new(0, i as i32), line); } } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + let mut dirty = true; + while dirty { term.autoresize().unwrap(); - draw(term.frame()); - - if term.present().unwrap() == Redraw::NotRequired { - break; - } + term.present().unwrap(); + dirty = term.measure_widths().unwrap(); } } fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); + term.set_measuring(true); + term.set_tab_width(4); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/src/buffer.rs b/src/buffer.rs index 4707b53..022145b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,34 +1,8 @@ +use std::ops::Range; + use crossterm::style::ContentStyle; -use unicode_segmentation::UnicodeSegmentation; -use crate::widthdb::WidthDB; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct Size { - pub width: u16, - pub height: u16, -} - -impl Size { - pub const ZERO: Self = Self { - width: 0, - height: 0, - }; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Pos { - pub x: i32, - pub y: i32, -} - -impl Pos { - pub const ZERO: Self = Self { x: 0, y: 0 }; - - pub fn new(x: i32, y: i32) -> Self { - Self { x, y } - } -} +use crate::{Pos, Size, Style, Styled, WidthDb}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { @@ -49,13 +23,93 @@ impl Default for Cell { } } -#[derive(Debug, Default)] +#[derive(Debug, Clone, Copy)] +struct StackFrame { + pub pos: Pos, + pub size: Size, + pub drawable_area: Option<(Pos, Size)>, +} + +impl StackFrame { + fn intersect_areas( + a_start: Pos, + a_size: Size, + b_start: Pos, + b_size: Size, + ) -> Option<(Pos, Size)> { + // The first row/column that is not part of the area any more + let a_end = a_start + a_size; + let b_end = b_start + b_size; + + let x_start = a_start.x.max(b_start.x); + let x_end = a_end.x.min(b_end.x); + let y_start = a_start.y.max(b_start.y); + let y_end = a_end.y.min(b_end.y); + + if x_start < x_end && y_start < y_end { + let start = Pos::new(x_start, y_start); + let size = Size::new((x_end - x_start) as u16, (y_end - y_start) as u16); + Some((start, size)) + } else { + None + } + } + + fn then(&self, pos: Pos, size: Size) -> Self { + let pos = self.local_to_global(pos); + + let drawable_area = self + .drawable_area + .and_then(|(da_pos, da_size)| Self::intersect_areas(da_pos, da_size, pos, size)); + + Self { + pos, + size, + drawable_area, + } + } + + fn local_to_global(&self, local_pos: Pos) -> Pos { + local_pos + self.pos + } + + fn global_to_local(&self, global_pos: Pos) -> Pos { + global_pos - self.pos + } + + /// Ranges along the x and y axis where drawing is allowed, in global + /// coordinates. + fn legal_ranges(&self) -> Option<(Range, Range)> { + if let Some((pos, size)) = self.drawable_area { + let xrange = pos.x..pos.x + size.width as i32; + let yrange = pos.y..pos.y + size.height as i32; + Some((xrange, yrange)) + } else { + None + } + } +} + +#[derive(Debug, Default, Clone)] pub struct Buffer { size: Size, data: Vec, + cursor: Option, + + /// A stack of rectangular drawing areas. + /// + /// When rendering to the buffer with a nonempty stack, it behaves as if it + /// was the size of the topmost stack element, and characters are translated + /// by the position of the topmost stack element. No characters can be + /// placed outside the area described by the topmost stack element. + stack: Vec, } impl Buffer { + /// Index in `data` of the cell at the given position. The position must + /// be inside the buffer. + /// + /// Ignores the stack. fn index(&self, x: u16, y: u16) -> usize { assert!(x < self.size.width); assert!(y < self.size.height); @@ -67,30 +121,65 @@ impl Buffer { y * width + x } - pub(crate) fn at(&self, x: u16, y: u16) -> &Cell { + /// A reference to the cell at the given position. The position must be + /// inside the buffer. + /// + /// Ignores the stack. + pub fn at(&self, x: u16, y: u16) -> &Cell { assert!(x < self.size.width); assert!(y < self.size.height); + let i = self.index(x, y); &self.data[i] } + /// A mutable reference to the cell at the given position. The position must + /// be inside the buffer. + /// + /// Ignores the stack. fn at_mut(&mut self, x: u16, y: u16) -> &mut Cell { assert!(x < self.size.width); assert!(y < self.size.height); + let i = self.index(x, y); &mut self.data[i] } + fn current_frame(&self) -> StackFrame { + self.stack.last().copied().unwrap_or(StackFrame { + pos: Pos::ZERO, + size: self.size, + drawable_area: Some((Pos::ZERO, self.size)), + }) + } + + pub fn push(&mut self, pos: Pos, size: Size) { + self.stack.push(self.current_frame().then(pos, size)); + } + + pub fn pop(&mut self) { + self.stack.pop(); + } + + /// Size of the current drawable area, respecting the stack. pub fn size(&self) -> Size { - self.size + self.current_frame().size + } + + pub fn cursor(&self) -> Option { + self.cursor.map(|p| self.current_frame().global_to_local(p)) + } + + pub fn set_cursor(&mut self, pos: Option) { + self.cursor = pos.map(|p| self.current_frame().local_to_global(p)); } /// Resize the buffer and reset its contents. /// /// The buffer's contents are reset even if the buffer is already the - /// correct size. + /// correct size. The stack is reset as well. pub fn resize(&mut self, size: Size) { - if size == self.size() { + if size == self.size { self.data.fill_with(Cell::default); } else { let width: usize = size.width.into(); @@ -101,58 +190,86 @@ impl Buffer { self.data.clear(); self.data.resize_with(len, Cell::default); } + + self.cursor = None; + + self.stack.clear(); } - /// Reset the contents of the buffer. + /// Reset the contents and stack of the buffer. /// /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. pub fn reset(&mut self) { - self.data.fill_with(Cell::default); + self.resize(self.size); } /// Remove the grapheme at the specified coordinates from the buffer. /// /// Removes the entire grapheme, not just the cell at the coordinates. - /// Preserves the style of the affected cells. Works even if the coordinates - /// don't point to the beginning of the grapheme. + /// Preserves the style of the affected cells. Preserves the cursor. Works + /// even if the coordinates don't point to the beginning of the grapheme. + /// + /// Ignores the stack. fn erase(&mut self, x: u16, y: u16) { let cell = self.at(x, y); let width: u16 = cell.width.into(); let offset: u16 = cell.offset.into(); + for x in (x - offset)..(x - offset + width) { let cell = self.at_mut(x, y); let style = cell.style; + *cell = Cell::default(); cell.style = style; } } - pub fn write( - &mut self, - widthdb: &mut WidthDB, - mut pos: Pos, - content: &str, - style: ContentStyle, - ) { - // If we're not even visible, there's nothing to do - if pos.y < 0 || pos.y >= self.size.height as i32 { - return; + /// Write styled text to the buffer, respecting the width of individual + /// graphemes. + /// + /// The initial x position is considered the first column for tab width + /// calculations. + pub fn write(&mut self, widthdb: &mut WidthDb, pos: Pos, styled: &Styled) { + let frame = self.current_frame(); + let (xrange, yrange) = match frame.legal_ranges() { + Some(ranges) => ranges, + None => return, // No drawable area + }; + let pos = frame.local_to_global(pos); + if !yrange.contains(&pos.y) { + return; // Outside of drawable area } let y = pos.y as u16; - for grapheme in content.graphemes(true) { - let width = widthdb.grapheme_width(grapheme); - if width > 0 { - self.write_grapheme(pos.x, y, width, grapheme, style); + let mut col: usize = 0; + for (_, style, grapheme) in styled.styled_grapheme_indices() { + let x = pos.x + col as i32; + let width = widthdb.grapheme_width(grapheme, col); + col += width as usize; + if grapheme == "\t" { + for dx in 0..width { + self.write_grapheme(&xrange, x + dx as i32, y, 1, " ", style); + } + } else if width > 0 { + self.write_grapheme(&xrange, x, y, width, grapheme, style); } - pos.x += width as i32; } } + /// Write a single grapheme to the buffer, respecting its width. + /// /// Assumes that `pos.y` is in range. - fn write_grapheme(&mut self, x: i32, y: u16, width: u8, grapheme: &str, style: ContentStyle) { - let min_x = 0; - let max_x = self.size.width as i32 - 1; // Last possible cell + fn write_grapheme( + &mut self, + xrange: &Range, + x: i32, + y: u16, + width: u8, + grapheme: &str, + style: Style, + ) { + let min_x = xrange.start; + let max_x = xrange.end - 1; // Last possible cell let start_x = x; let end_x = x + width as i32 - 1; // Coordinate of last cell @@ -163,12 +280,13 @@ impl Buffer { if start_x >= min_x && end_x <= max_x { // Fully visible, write actual grapheme + let base_style = self.at(start_x as u16, y).style; for offset in 0..width { let x = start_x as u16 + offset as u16; self.erase(x, y); *self.at_mut(x, y) = Cell { content: grapheme.to_string().into_boxed_str(), - style, + style: style.cover(base_style), width, offset, }; @@ -178,13 +296,21 @@ impl Buffer { let start_x = start_x.max(0) as u16; let end_x = end_x.min(max_x) as u16; for x in start_x..=end_x { + let base_style = self.at(x, y).style; self.erase(x, y); *self.at_mut(x, y) = Cell { - style, + style: style.cover(base_style), ..Default::default() }; } } + + if let Some(pos) = self.cursor { + if pos.y == y as i32 && start_x <= pos.x && pos.x <= end_x { + // The cursor lies within the bounds of the current grapheme and + self.cursor = None; + } + } } pub fn cells(&self) -> Cells<'_> { @@ -206,6 +332,9 @@ impl<'a> Iterator for Cells<'a> { type Item = (u16, u16, &'a Cell); fn next(&mut self) -> Option { + if self.x >= self.buffer.size.width { + return None; + } if self.y >= self.buffer.size.height { return None; } diff --git a/src/coords.rs b/src/coords.rs new file mode 100644 index 0000000..1735746 --- /dev/null +++ b/src/coords.rs @@ -0,0 +1,153 @@ +use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; + +/// Size in screen cells. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Size { + pub width: u16, + pub height: u16, +} + +impl Size { + pub const ZERO: Self = Self::new(0, 0); + + pub const fn new(width: u16, height: u16) -> Self { + Self { width, height } + } + + /// Add two [`Size`]s using [`u16::saturating_add`]. + pub const fn saturating_add(self, rhs: Self) -> Self { + Self::new( + self.width.saturating_add(rhs.width), + self.height.saturating_add(rhs.height), + ) + } + + /// Subtract two [`Size`]s using [`u16::saturating_sub`]. + pub const fn saturating_sub(self, rhs: Self) -> Self { + Self::new( + self.width.saturating_sub(rhs.width), + self.height.saturating_sub(rhs.height), + ) + } +} + +impl Add for Size { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::new(self.width + rhs.width, self.height + rhs.height) + } +} + +impl AddAssign for Size { + fn add_assign(&mut self, rhs: Self) { + self.width += rhs.width; + self.height += rhs.height; + } +} + +impl Sub for Size { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self::new(self.width - rhs.width, self.height - rhs.height) + } +} + +impl SubAssign for Size { + fn sub_assign(&mut self, rhs: Self) { + self.width -= rhs.width; + self.height -= rhs.height; + } +} + +/// Position in screen cell coordinates. +/// +/// The x axis points to the right. The y axis points down. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pos { + pub x: i32, + pub y: i32, +} + +impl Pos { + pub const ZERO: Self = Self::new(0, 0); + + pub const fn new(x: i32, y: i32) -> Self { + Self { x, y } + } +} + +impl From for Pos { + fn from(s: Size) -> Self { + Self::new(s.width.into(), s.height.into()) + } +} + +impl Add for Pos { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +impl Add for Pos { + type Output = Self; + + fn add(self, rhs: Size) -> Self { + Self::new(self.x + rhs.width as i32, self.y + rhs.height as i32) + } +} + +impl AddAssign for Pos { + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl AddAssign for Pos { + fn add_assign(&mut self, rhs: Size) { + self.x += rhs.width as i32; + self.y += rhs.height as i32; + } +} + +impl Sub for Pos { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self::new(self.x - rhs.x, self.y - rhs.y) + } +} + +impl Sub for Pos { + type Output = Self; + + fn sub(self, rhs: Size) -> Self { + Self::new(self.x - rhs.width as i32, self.y - rhs.height as i32) + } +} + +impl SubAssign for Pos { + fn sub_assign(&mut self, rhs: Self) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +impl SubAssign for Pos { + fn sub_assign(&mut self, rhs: Size) { + self.x -= rhs.width as i32; + self.y -= rhs.height as i32; + } +} + +impl Neg for Pos { + type Output = Self; + + fn neg(self) -> Self { + Self::new(-self.x, -self.y) + } +} diff --git a/src/frame.rs b/src/frame.rs index f8c46af..e42ba6b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,35 +1,40 @@ //! Rendering the next frame. -use crossterm::style::ContentStyle; - use crate::buffer::Buffer; -pub use crate::buffer::{Pos, Size}; -use crate::widthdb::WidthDB; -use crate::wrap; +use crate::{Pos, Size, Styled, WidthDb}; #[derive(Debug, Default)] pub struct Frame { - pub(crate) widthdb: WidthDB, + pub(crate) widthdb: WidthDb, pub(crate) buffer: Buffer, - cursor: Option, + pub(crate) title: Option, + pub(crate) bell: bool, } impl Frame { + pub fn push(&mut self, pos: Pos, size: Size) { + self.buffer.push(pos, size); + } + + pub fn pop(&mut self) { + self.buffer.pop(); + } + pub fn size(&self) -> Size { self.buffer.size() } pub fn reset(&mut self) { self.buffer.reset(); - self.cursor = None; + self.title = None; } pub fn cursor(&self) -> Option { - self.cursor + self.buffer.cursor() } pub fn set_cursor(&mut self, pos: Option) { - self.cursor = pos; + self.buffer.set_cursor(pos); } pub fn show_cursor(&mut self, pos: Pos) { @@ -40,27 +45,19 @@ impl Frame { self.set_cursor(None); } - /// Determine the width of a grapheme. - /// - /// If the width has not been measured yet, it is estimated using the - /// Unicode Standard Annex #11. - pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { - self.widthdb.grapheme_width(grapheme) + pub fn set_title(&mut self, title: Option) { + self.title = title; } - /// Determine the width of a string based on its graphemes. - /// - /// If the width of a grapheme has not been measured yet, it is estimated - /// using the Unicode Standard Annex #11. - pub fn width(&mut self, s: &str) -> usize { - self.widthdb.width(s) + pub fn set_bell(&mut self, bell: bool) { + self.bell = bell; } - pub fn wrap(&mut self, text: &str, width: usize) -> Vec { - wrap::wrap(text, width, &mut self.widthdb) + pub fn widthdb(&mut self) -> &mut WidthDb { + &mut self.widthdb } - pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) { - self.buffer.write(&mut self.widthdb, pos, content, style); + pub fn write>(&mut self, pos: Pos, styled: S) { + self.buffer.write(&mut self.widthdb, pos, &styled.into()); } } diff --git a/src/lib.rs b/src/lib.rs index 9cbdd9f..a204e8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,29 @@ +#![forbid(unsafe_code)] +// Rustc lint groups +#![warn(future_incompatible)] +#![warn(rust_2018_idioms)] +#![warn(unused)] +// Rustc lints +#![warn(noop_method_call)] +#![warn(single_use_lifetimes)] +// Clippy lints +#![warn(clippy::use_self)] + mod buffer; -pub mod frame; -pub mod terminal; +mod coords; +mod frame; +mod style; +mod styled; +mod terminal; +mod widget; +pub mod widgets; mod widthdb; mod wrap; -pub use wrap::split_at_indices; +pub use coords::*; +pub use frame::*; +pub use style::*; +pub use styled::*; +pub use terminal::*; +pub use widget::*; +pub use widthdb::*; diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..56c66ce --- /dev/null +++ b/src/style.rs @@ -0,0 +1,60 @@ +use crossterm::style::{ContentStyle, Stylize}; + +fn merge_cs(base: ContentStyle, cover: ContentStyle) -> ContentStyle { + ContentStyle { + foreground_color: cover.foreground_color.or(base.foreground_color), + background_color: cover.background_color.or(base.background_color), + underline_color: cover.underline_color.or(base.underline_color), + attributes: cover.attributes, + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Style { + pub content_style: ContentStyle, + pub opaque: bool, +} + +impl Style { + pub fn new() -> Self { + Self::default() + } + + pub fn transparent(mut self) -> Self { + self.opaque = false; + self + } + + pub fn opaque(mut self) -> Self { + self.opaque = true; + self + } + + pub fn cover(self, base: ContentStyle) -> ContentStyle { + if self.opaque { + return self.content_style; + } + + merge_cs(base, self.content_style) + } +} + +impl AsRef for Style { + fn as_ref(&self) -> &ContentStyle { + &self.content_style + } +} + +impl AsMut for Style { + fn as_mut(&mut self) -> &mut ContentStyle { + &mut self.content_style + } +} + +impl Stylize for Style { + type Styled = Self; + + fn stylize(self) -> Self::Styled { + self + } +} diff --git a/src/styled.rs b/src/styled.rs new file mode 100644 index 0000000..33a8285 --- /dev/null +++ b/src/styled.rs @@ -0,0 +1,195 @@ +use std::iter::Peekable; +use std::slice; + +use unicode_segmentation::{GraphemeIndices, Graphemes, UnicodeSegmentation}; + +use crate::Style; + +#[derive(Debug, Default, Clone)] +pub struct Styled { + text: String, + /// List of `(style, until)` tuples. The style should be applied to all + /// chars in the range `prev_until..until`. + styles: Vec<(Style, usize)>, +} + +impl Styled { + pub fn new>(text: S, style: Style) -> Self { + Self::default().then(text, style) + } + + pub fn new_plain>(text: S) -> Self { + Self::default().then_plain(text) + } + + pub fn then>(mut self, text: S, style: Style) -> Self { + let text = text.as_ref(); + if !text.is_empty() { + self.text.push_str(text); + self.styles.push((style, self.text.len())); + } + self + } + + pub fn then_plain>(self, text: S) -> Self { + self.then(text, Style::new()) + } + + pub fn and_then(mut self, mut other: Self) -> Self { + let delta = self.text.len(); + for (_, until) in &mut other.styles { + *until += delta; + } + + self.text.push_str(&other.text); + self.styles.extend(other.styles); + self + } + + pub fn text(&self) -> &str { + &self.text + } + + pub fn split_at(self, mid: usize) -> (Self, Self) { + let (left_text, right_text) = self.text.split_at(mid); + + let mut left_styles = vec![]; + let mut right_styles = vec![]; + let mut from = 0; + for (style, until) in self.styles { + if from < mid { + left_styles.push((style, until.min(mid))); + } + if mid < until { + right_styles.push((style, until.saturating_sub(mid))); + } + from = until; + } + + let left = Self { + text: left_text.to_string(), + styles: left_styles, + }; + + let right = Self { + text: right_text.to_string(), + styles: right_styles, + }; + + (left, right) + } + + pub fn split_at_indices(self, indices: &[usize]) -> Vec { + let mut lines = vec![]; + + let mut rest = self; + let mut offset = 0; + + for i in indices { + let (left, right) = rest.split_at(i - offset); + lines.push(left); + rest = right; + offset = *i; + } + + lines.push(rest); + + lines + } + + pub fn trim_end(&mut self) { + self.text = self.text.trim_end().to_string(); + + let text_len = self.text.len(); + let mut styles_len = 0; + for (_, until) in &mut self.styles { + styles_len += 1; + if *until >= text_len { + *until = text_len; + break; + } + } + + while self.styles.len() > styles_len { + self.styles.pop(); + } + } +} + +////////////////////////////// +// Iterating over graphemes // +////////////////////////////// + +pub struct StyledGraphemeIndices<'a> { + text: GraphemeIndices<'a>, + styles: Peekable>, +} + +impl<'a> Iterator for StyledGraphemeIndices<'a> { + type Item = (usize, Style, &'a str); + + fn next(&mut self) -> Option { + let (gi, grapheme) = self.text.next()?; + let (mut style, mut until) = **self.styles.peek().expect("styles cover entire text"); + while gi >= until { + self.styles.next(); + (style, until) = **self.styles.peek().expect("styles cover entire text"); + } + Some((gi, style, grapheme)) + } +} + +impl Styled { + pub fn graphemes(&self) -> Graphemes<'_> { + self.text.graphemes(true) + } + + pub fn grapheme_indices(&self) -> GraphemeIndices<'_> { + self.text.grapheme_indices(true) + } + + pub fn styled_grapheme_indices(&self) -> StyledGraphemeIndices<'_> { + StyledGraphemeIndices { + text: self.grapheme_indices(), + styles: self.styles.iter().peekable(), + } + } +} + +////////////////////////// +// Converting to Styled // +////////////////////////// + +impl From<&str> for Styled { + fn from(text: &str) -> Self { + Self::new_plain(text) + } +} + +impl From for Styled { + fn from(text: String) -> Self { + Self::new_plain(text) + } +} + +impl> From<(S,)> for Styled { + fn from((text,): (S,)) -> Self { + Self::new_plain(text) + } +} + +impl> From<(S, Style)> for Styled { + fn from((text, style): (S, Style)) -> Self { + Self::new(text, style) + } +} + +impl> From<&[(S, Style)]> for Styled { + fn from(segments: &[(S, Style)]) -> Self { + let mut result = Self::default(); + for (text, style) in segments { + result = result.then(text, *style); + } + result + } +} diff --git a/src/terminal.rs b/src/terminal.rs index eb5645e..07fe686 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,22 +1,28 @@ //! Displaying frames on a terminal. -use std::io::Write; -use std::{io, mem}; +use std::io::{self, Write}; +use std::mem; use crossterm::cursor::{Hide, MoveTo, Show}; -use crossterm::style::{PrintStyledContent, StyledContent}; -use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::event::{ + DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, + PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; +use crossterm::style::{Print, PrintStyledContent, StyledContent}; +use crossterm::terminal::{ + BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, + LeaveAlternateScreen, SetTitle, +}; use crossterm::{ExecutableCommand, QueueableCommand}; -use crate::buffer::{Buffer, Size}; -use crate::frame::Frame; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Redraw { - Required, - NotRequired, -} +use crate::buffer::Buffer; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod}; +/// Wrapper that manages terminal output. +/// +/// This struct (usually) wraps around stdout and handles showing things on the +/// terminal. It cleans up after itself when droppped, so it shouldn't leave the +/// terminal in a weird state even if your program crashes. pub struct Terminal { /// Render target. out: Box, @@ -31,17 +37,17 @@ pub struct Terminal { impl Drop for Terminal { fn drop(&mut self) { - let _ = crossterm::terminal::disable_raw_mode(); - let _ = self.out.execute(LeaveAlternateScreen); - let _ = self.out.execute(Show); + let _ = self.suspend(); } } impl Terminal { + /// Create a new [`Terminal`] that wraps stdout. pub fn new() -> io::Result { Self::with_target(Box::new(io::stdout())) } + /// Create a new terminal wrapping a custom output. pub fn with_target(out: Box) -> io::Result { let mut result = Self { out, @@ -49,13 +55,136 @@ impl Terminal { prev_frame_buffer: Buffer::default(), full_redraw: true, }; - crossterm::terminal::enable_raw_mode()?; - result.out.execute(EnterAlternateScreen)?; + result.unsuspend()?; Ok(result) } + /// Temporarily restore the terminal state to normal. + /// + /// This is useful when running external programs the user should interact + /// with directly, for example a text editor. + /// + /// Call [`Self::unsuspend`] to return the terminal state before drawing and + /// presenting the next frame. + pub fn suspend(&mut self) -> io::Result<()> { + crossterm::terminal::disable_raw_mode()?; + #[cfg(not(windows))] + { + self.out.execute(PopKeyboardEnhancementFlags)?; + self.out.execute(DisableBracketedPaste)?; + } + self.out.execute(LeaveAlternateScreen)?; + self.out.execute(Show)?; + Ok(()) + } + + /// Restore the terminal state after calling [`Self::suspend`]. + /// + /// After calling this function, a new frame needs to be drawn and presented + /// by the application. The previous screen contents are **not** restored. + pub fn unsuspend(&mut self) -> io::Result<()> { + crossterm::terminal::enable_raw_mode()?; + self.out.execute(EnterAlternateScreen)?; + #[cfg(not(windows))] + { + self.out.execute(EnableBracketedPaste)?; + self.out.execute(PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES, + ))?; + } + self.full_redraw = true; + Ok(()) + } + + /// Set the tab width in columns. + /// + /// For more details, see [`Self::tab_width`]. + pub fn set_tab_width(&mut self, tab_width: u8) { + self.frame.widthdb.tab_width = tab_width; + } + + /// The tab width in columns. + /// + /// For accurate width calculations and consistency across terminals, tabs + /// are not printed to the terminal directly, but instead converted into + /// spaces. + pub fn tab_width(&self) -> u8 { + self.frame.widthdb.tab_width + } + + /// Set the grapheme width estimation method. + /// + /// For more details, see [`WidthEstimationMethod`]. + pub fn set_width_estimation_method(&mut self, method: WidthEstimationMethod) { + self.frame.widthdb.estimate = method; + } + + /// The grapheme width estimation method. + /// + /// For more details, see [`WidthEstimationMethod`]. + pub fn width_estimation_method(&mut self) -> WidthEstimationMethod { + self.frame.widthdb.estimate + } + + /// Enable or disable grapheme width measurements. + /// + /// For more details, see [`Self::measuring`]. + pub fn set_measuring(&mut self, active: bool) { + self.frame.widthdb.measure = active; + } + + /// Whether grapheme widths should be measured or estimated. + /// + /// Handling of wide characters is inconsistent from terminal emulator to + /// terminal emulator, and may even depend on the font the user is using. + /// + /// When enabled, any newly encountered graphemes are measured whenever + /// [`Self::measure_widths`] is called. This is done by clearing the screen, + /// printing the grapheme and measuring the resulting cursor position. + /// Because of this, the screen will flicker occasionally. However, grapheme + /// widths will always be accurate independent of the terminal + /// configuration. + /// + /// When disabled, the width of graphemes is estimated using the Unicode + /// Standard Annex #11. This usually works fine, but may break on some emoji + /// or other less commonly used character sequences. + pub fn measuring(&self) -> bool { + self.frame.widthdb.measure + } + + /// Whether any unmeasured graphemes were seen since the last call to + /// [`Self::measure_widths`]. + /// + /// Returns `true` whenever [`Self::measure_widths`] would return `true`. + pub fn measuring_required(&self) -> bool { + self.frame.widthdb.measuring_required() + } + + /// Measure widths of all unmeasured graphemes. + /// + /// If width measurements are disabled, this function does nothing. For more + /// info, see [`Self::measuring`]. + /// + /// Returns `true` if any new graphemes were measured and the screen must be + /// redrawn. Keep in mind that after redrawing the screen, graphemes may + /// have become visible that have not yet been measured. You should keep + /// re-measuring and re-drawing until this function returns `false`. + pub fn measure_widths(&mut self) -> io::Result { + if self.frame.widthdb.measuring_required() { + self.full_redraw = true; + self.frame.widthdb.measure_widths(&mut self.out)?; + Ok(true) + } else { + Ok(false) + } + } + /// Resize the frame and other internal buffers if the terminal size has /// changed. + /// + /// Should be called before drawing a frame and presenting it with + /// [`Self::present`]. It is not necessary to call this when using + /// [`Self::present_widget`] or [`Self::present_async_widget`]. pub fn autoresize(&mut self) -> io::Result<()> { let (width, height) = crossterm::terminal::size()?; let size = Size { width, height }; @@ -68,42 +197,86 @@ impl Terminal { Ok(()) } + /// The current frame. pub fn frame(&mut self) -> &mut Frame { &mut self.frame } + /// A database of grapheme widths. + pub fn widthdb(&mut self) -> &mut WidthDb { + &mut self.frame.widthdb + } + + /// Mark the terminal as dirty, forcing a full redraw whenever any variant + /// of [`Self::present`] is called. + pub fn mark_dirty(&mut self) { + self.full_redraw = true; + } + /// Display the current frame on the screen and prepare the next frame. - /// Returns `true` if an immediate redraw is required. + /// + /// Before drawing and presenting a frame, [`Self::measure_widths`] and + /// [`Self::autoresize`] should be called. /// /// After calling this function, the frame returned by [`Self::frame`] will /// be empty again and have no cursor position. - pub fn present(&mut self) -> io::Result { - if self.frame.widthdb.measuring_required() { - self.frame.widthdb.measure_widths(&mut self.out)?; - // Since we messed up the screen by measuring widths, we'll need to - // do a full redraw the next time around. - self.full_redraw = true; - // Throwing away the current frame because its content were rendered - // with unconfirmed width data. Also, this function guarantees that - // after it is called, the frame is empty. - self.frame.reset(); - return Ok(Redraw::Required); - } + pub fn present(&mut self) -> io::Result<()> { + self.out.queue(BeginSynchronizedUpdate)?; + let result = self.draw_to_screen(); + self.out.queue(EndSynchronizedUpdate)?; + result?; + self.out.flush()?; + + mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); + self.frame.reset(); + + Ok(()) + } + + /// Display a [`Widget`] on the screen. + /// + /// Before creating and presenting a widget, [`Self::measure_widths`] should + /// be called. There is no need to call [`Self::autoresize`]. + pub fn present_widget(&mut self, widget: W) -> Result<(), E> + where + E: From, + W: Widget, + { + self.autoresize()?; + widget.draw(self.frame())?; + self.present()?; + Ok(()) + } + + /// Display an [`AsyncWidget`] on the screen. + /// + /// Before creating and presenting a widget, [`Self::measure_widths`] should + /// be called. There is no need to call [`Self::autoresize`]. + pub async fn present_async_widget(&mut self, widget: W) -> Result<(), E> + where + E: From, + W: AsyncWidget, + { + self.autoresize()?; + widget.draw(self.frame()).await?; + self.present()?; + Ok(()) + } + + fn draw_to_screen(&mut self) -> io::Result<()> { if self.full_redraw { - io::stdout().queue(Clear(ClearType::All))?; + self.out.queue(Clear(ClearType::All))?; self.prev_frame_buffer.reset(); // Because the screen is now empty self.full_redraw = false; } self.draw_differences()?; self.update_cursor()?; - self.out.flush()?; + self.update_title()?; + self.ring_bell()?; - mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); - self.frame.reset(); - - Ok(Redraw::NotRequired) + Ok(()) } fn draw_differences(&mut self) -> io::Result<()> { @@ -136,4 +309,19 @@ impl Terminal { self.out.queue(Hide)?; Ok(()) } + + fn update_title(&mut self) -> io::Result<()> { + if let Some(title) = &self.frame.title { + self.out.queue(SetTitle(title.clone()))?; + } + Ok(()) + } + + fn ring_bell(&mut self) -> io::Result<()> { + if self.frame.bell { + self.out.queue(Print('\x07'))?; + } + self.frame.bell = false; + Ok(()) + } } diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 0000000..356a047 --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,127 @@ +use async_trait::async_trait; + +use crate::widgets::{ + Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float, + JoinSegment, Layer2, Padding, Resize, Title, +}; +use crate::{Frame, Size, WidthDb}; + +// TODO Feature-gate these traits + +pub trait Widget { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result; + + fn draw(self, frame: &mut Frame) -> Result<(), E>; +} + +#[async_trait] +pub trait AsyncWidget { + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result; + + async fn draw(self, frame: &mut Frame) -> Result<(), E>; +} + +pub trait WidgetExt: Sized { + fn background(self) -> Background { + Background::new(self) + } + + fn border(self) -> Border { + Border::new(self) + } + + fn boxed<'a, E>(self) -> Boxed<'a, E> + where + Self: Widget + 'a, + { + Boxed::new(self) + } + + fn boxed_send_sync<'a, E>(self) -> BoxedSendSync<'a, E> + where + Self: Widget + Send + Sync + 'a, + { + BoxedSendSync::new(self) + } + + fn boxed_async<'a, E>(self) -> BoxedAsync<'a, E> + where + Self: AsyncWidget + Send + Sync + 'a, + { + BoxedAsync::new(self) + } + + fn desync(self) -> Desync { + Desync(self) + } + + fn first2(self) -> Either2 { + Either2::First(self) + } + + fn second2(self) -> Either2 { + Either2::Second(self) + } + + fn first3(self) -> Either3 { + Either3::First(self) + } + + fn second3(self) -> Either3 { + Either3::Second(self) + } + + fn third3(self) -> Either3 { + Either3::Third(self) + } + + fn float(self) -> Float { + Float::new(self) + } + + fn segment(self) -> JoinSegment { + JoinSegment::new(self) + } + + fn below(self, above: W) -> Layer2 { + Layer2::new(self, above) + } + + fn above(self, below: W) -> Layer2 { + Layer2::new(below, self) + } + + fn padding(self) -> Padding { + Padding::new(self) + } + + fn resize(self) -> Resize { + Resize::new(self) + } + + fn title(self, title: S) -> Title { + Title::new(self, title) + } +} + +// It would be nice if this could be restricted to types implementing Widget. +// However, Widget (and AsyncWidget) have the E type parameter, which WidgetExt +// doesn't have. We sadly can't have unconstrained type parameters like that in +// impl blocks. +// +// If WidgetExt had a type parameter E, we'd need to specify that parameter +// everywhere we use the trait. This is less ergonomic than just constructing +// the types manually. +// +// Blanket-implementing this trait is not great, but usually works fine. +impl WidgetExt for W {} diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..cbbff7c --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,35 @@ +pub mod background; +pub mod bell; +pub mod border; +pub mod boxed; +pub mod cursor; +pub mod desync; +pub mod editor; +pub mod either; +pub mod empty; +pub mod float; +pub mod join; +pub mod layer; +pub mod padding; +pub mod predrawn; +pub mod resize; +pub mod text; +pub mod title; + +pub use background::*; +pub use bell::*; +pub use border::*; +pub use boxed::*; +pub use cursor::*; +pub use desync::*; +pub use editor::*; +pub use either::*; +pub use empty::*; +pub use float::*; +pub use join::*; +pub use layer::*; +pub use padding::*; +pub use predrawn::*; +pub use resize::*; +pub use text::*; +pub use title::*; diff --git a/src/widgets/background.rs b/src/widgets/background.rs new file mode 100644 index 0000000..d0ba530 --- /dev/null +++ b/src/widgets/background.rs @@ -0,0 +1,71 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget, WidthDb}; + +#[derive(Debug, Clone, Copy)] +pub struct Background { + pub inner: I, + pub style: Style, +} + +impl Background { + pub fn new(inner: I) -> Self { + Self { + inner, + style: Style::new().opaque(), + } + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + + fn fill(&self, frame: &mut Frame) { + let size = frame.size(); + for dy in 0..size.height { + for dx in 0..size.width { + frame.write(Pos::new(dx.into(), dy.into()), (" ", self.style)); + } + } + } +} + +impl Widget for Background +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.fill(frame); + self.inner.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for Background +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.fill(frame); + self.inner.draw(frame).await + } +} diff --git a/src/widgets/bell.rs b/src/widgets/bell.rs new file mode 100644 index 0000000..b37fb67 --- /dev/null +++ b/src/widgets/bell.rs @@ -0,0 +1,55 @@ +use crate::{Frame, Size, Widget, WidthDb}; + +/////////// +// State // +/////////// + +#[derive(Debug, Default, Clone)] +pub struct BellState { + // Whether the bell should be rung the next time the widget is displayed. + pub ring: bool, +} + +impl BellState { + pub fn new() -> Self { + Self::default() + } + + pub fn widget(&mut self) -> Bell<'_> { + Bell { state: self } + } +} + +//////////// +// Widget // +//////////// + +#[derive(Debug)] +pub struct Bell<'a> { + state: &'a mut BellState, +} + +impl Bell<'_> { + pub fn state(&mut self) -> &mut BellState { + self.state + } +} + +impl Widget for Bell<'_> { + fn size( + &self, + _widthdb: &mut WidthDb, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(Size::ZERO) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + if self.state.ring { + frame.set_bell(true); + self.state.ring = false + } + Ok(()) + } +} diff --git a/src/widgets/border.rs b/src/widgets/border.rs new file mode 100644 index 0000000..062cd8f --- /dev/null +++ b/src/widgets/border.rs @@ -0,0 +1,201 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget, WidthDb}; + +#[derive(Debug, Clone, Copy)] +pub struct BorderLook { + pub top_left: &'static str, + pub top_right: &'static str, + pub bottom_left: &'static str, + pub bottom_right: &'static str, + pub top: &'static str, + pub bottom: &'static str, + pub left: &'static str, + pub right: &'static str, +} + +impl BorderLook { + /// ```text + /// +-------+ + /// | Hello | + /// +-------+ + /// ``` + pub const ASCII: Self = Self { + top_left: "+", + top_right: "+", + bottom_left: "+", + bottom_right: "+", + top: "-", + bottom: "-", + left: "|", + right: "|", + }; + + /// ```text + /// ┌───────┐ + /// │ Hello │ + /// └───────┘ + /// ``` + pub const LINE: Self = Self { + top_left: "┌", + top_right: "┐", + bottom_left: "└", + bottom_right: "┘", + top: "─", + bottom: "─", + left: "│", + right: "│", + }; + + /// ```text + /// ┏━━━━━━━┓ + /// ┃ Hello ┃ + /// ┗━━━━━━━┛ + /// ``` + pub const LINE_HEAVY: Self = Self { + top_left: "┏", + top_right: "┓", + bottom_left: "┗", + bottom_right: "┛", + top: "━", + bottom: "━", + left: "┃", + right: "┃", + }; + + /// ```text + /// ╔═══════╗ + /// ║ Hello ║ + /// ╚═══════╝ + /// ``` + pub const LINE_DOUBLE: Self = Self { + top_left: "╔", + top_right: "╗", + bottom_left: "╚", + bottom_right: "╝", + top: "═", + bottom: "═", + left: "║", + right: "║", + }; +} + +impl Default for BorderLook { + fn default() -> Self { + Self::LINE + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Border { + pub inner: I, + pub look: BorderLook, + pub style: Style, +} + +impl Border { + pub fn new(inner: I) -> Self { + Self { + inner, + look: BorderLook::default(), + style: Style::default(), + } + } + + pub fn with_look(mut self, look: BorderLook) -> Self { + self.look = look; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + + fn draw_border(&self, frame: &mut Frame) { + let size = frame.size(); + let right = size.width.saturating_sub(1).into(); + let bottom = size.height.saturating_sub(1).into(); + + for y in 1..bottom { + frame.write(Pos::new(right, y), (self.look.right, self.style)); + frame.write(Pos::new(0, y), (self.look.left, self.style)); + } + + for x in 1..right { + frame.write(Pos::new(x, bottom), (self.look.bottom, self.style)); + frame.write(Pos::new(x, 0), (self.look.top, self.style)); + } + + frame.write( + Pos::new(right, bottom), + (self.look.bottom_right, self.style), + ); + frame.write(Pos::new(0, bottom), (self.look.bottom_left, self.style)); + frame.write(Pos::new(right, 0), (self.look.top_right, self.style)); + frame.write(Pos::new(0, 0), (self.look.top_left, self.style)); + } + + fn push_inner(&self, frame: &mut Frame) { + let mut size = frame.size(); + size.width = size.width.saturating_sub(2); + size.height = size.height.saturating_sub(2); + + frame.push(Pos::new(1, 1), size); + } +} + +impl Widget for Border +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let max_width = max_width.map(|w| w.saturating_sub(2)); + let max_height = max_height.map(|h| h.saturating_sub(2)); + let size = self.inner.size(widthdb, max_width, max_height)?; + Ok(size + Size::new(2, 2)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.draw_border(frame); + + self.push_inner(frame); + self.inner.draw(frame)?; + frame.pop(); + + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Border +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let max_width = max_width.map(|w| w.saturating_sub(2)); + let max_height = max_height.map(|h| h.saturating_sub(2)); + let size = self.inner.size(widthdb, max_width, max_height).await?; + Ok(size + Size::new(2, 2)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.draw_border(frame); + + self.push_inner(frame); + self.inner.draw(frame).await?; + frame.pop(); + + Ok(()) + } +} diff --git a/src/widgets/boxed.rs b/src/widgets/boxed.rs new file mode 100644 index 0000000..3d9713f --- /dev/null +++ b/src/widgets/boxed.rs @@ -0,0 +1,142 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +pub struct Boxed<'a, E>(Box + 'a>); + +impl<'a, E> Boxed<'a, E> { + pub fn new(inner: I) -> Self + where + I: Widget + 'a, + { + Self(Box::new(inner)) + } +} + +impl Widget for Boxed<'_, E> { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.wrap_size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.wrap_draw(frame) + } +} + +pub struct BoxedSendSync<'a, E>(Box + Send + Sync + 'a>); + +impl<'a, E> BoxedSendSync<'a, E> { + pub fn new(inner: I) -> Self + where + I: Widget + Send + Sync + 'a, + { + Self(Box::new(inner)) + } +} + +impl Widget for BoxedSendSync<'_, E> { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.wrap_size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.wrap_draw(frame) + } +} + +pub struct BoxedAsync<'a, E>(Box + Send + Sync + 'a>); + +impl<'a, E> BoxedAsync<'a, E> { + pub fn new(inner: I) -> Self + where + I: AsyncWidget + Send + Sync + 'a, + { + Self(Box::new(inner)) + } +} + +#[async_trait] +impl AsyncWidget for BoxedAsync<'_, E> { + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.wrap_size(widthdb, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.wrap_draw(frame).await + } +} + +trait WidgetWrapper { + fn wrap_size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result; + + fn wrap_draw(self: Box, frame: &mut Frame) -> Result<(), E>; +} + +impl WidgetWrapper for W +where + W: Widget, +{ + fn wrap_size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.size(widthdb, max_width, max_height) + } + + fn wrap_draw(self: Box, frame: &mut Frame) -> Result<(), E> { + (*self).draw(frame) + } +} + +#[async_trait] +trait AsyncWidgetWrapper { + async fn wrap_size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result; + + async fn wrap_draw(self: Box, frame: &mut Frame) -> Result<(), E>; +} + +#[async_trait] +impl AsyncWidgetWrapper for W +where + W: AsyncWidget + Send + Sync, +{ + async fn wrap_size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.size(widthdb, max_width, max_height).await + } + + async fn wrap_draw(self: Box, frame: &mut Frame) -> Result<(), E> { + (*self).draw(frame).await + } +} diff --git a/src/widgets/cursor.rs b/src/widgets/cursor.rs new file mode 100644 index 0000000..2bb8199 --- /dev/null +++ b/src/widgets/cursor.rs @@ -0,0 +1,68 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; + +#[derive(Debug, Clone, Copy)] +pub struct Cursor { + pub inner: I, + pub position: Pos, +} + +impl Cursor { + pub fn new(inner: I) -> Self { + Self { + inner, + position: Pos::ZERO, + } + } + + pub fn with_position(mut self, position: Pos) -> Self { + self.position = position; + self + } + + pub fn with_position_xy(self, x: i32, y: i32) -> Self { + self.with_position(Pos::new(x, y)) + } +} + +impl Widget for Cursor +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame)?; + frame.show_cursor(self.position); + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Cursor +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame).await?; + frame.show_cursor(self.position); + Ok(()) + } +} diff --git a/src/widgets/desync.rs b/src/widgets/desync.rs new file mode 100644 index 0000000..67e7488 --- /dev/null +++ b/src/widgets/desync.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Widget}; + +pub struct Desync(pub I); + +impl Widget for Desync +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut crate::WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut crate::Frame) -> Result<(), E> { + self.0.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for Desync +where + I: Widget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut crate::WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(widthdb, max_width, max_height) + } + + async fn draw(self, frame: &mut crate::Frame) -> Result<(), E> { + self.0.draw(frame) + } +} diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs new file mode 100644 index 0000000..aa36e8e --- /dev/null +++ b/src/widgets/editor.rs @@ -0,0 +1,485 @@ +use std::iter; + +use crossterm::style::Stylize; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{Frame, Pos, Size, Style, Styled, Widget, WidthDb}; + +/// Like [`WidthDb::wrap`] but includes a final break index if the text ends +/// with a newline. +fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { + let mut breaks = widthdb.wrap(text, width); + if text.ends_with('\n') { + breaks.push(text.len()) + } + breaks +} + +/////////// +// State // +/////////// + +#[derive(Debug, Clone)] +pub struct EditorState { + text: String, + + /// Index of the cursor in the text. + /// + /// Must point to a valid grapheme boundary. + cursor_idx: usize, + + /// Column of the cursor on the screen just after it was last moved + /// horizontally. + cursor_col: usize, + + /// Position of the cursor when the editor was last rendered. + last_cursor_pos: Pos, +} + +impl EditorState { + pub fn new() -> Self { + Self::with_initial_text(String::new()) + } + + pub fn with_initial_text(text: String) -> Self { + Self { + cursor_idx: text.len(), + cursor_col: 0, + last_cursor_pos: Pos::ZERO, + text, + } + } + + /////////////////////////////// + // Grapheme helper functions // + /////////////////////////////// + + fn grapheme_boundaries(&self) -> Vec { + self.text + .grapheme_indices(true) + .map(|(i, _)| i) + .chain(iter::once(self.text.len())) + .collect() + } + + /// Ensure the cursor index lies on a grapheme boundary. If it doesn't, it + /// is moved to the next grapheme boundary. + /// + /// Can handle arbitrary cursor index. + fn move_cursor_to_grapheme_boundary(&mut self) { + for i in self.grapheme_boundaries() { + #[allow(clippy::comparison_chain)] + if i == self.cursor_idx { + // We're at a valid grapheme boundary already + return; + } else if i > self.cursor_idx { + // There was no valid grapheme boundary at our cursor index, so + // we'll take the next one we can get. + self.cursor_idx = i; + return; + } + } + + // The cursor was out of bounds, so move it to the last valid index. + self.cursor_idx = self.text.len(); + } + + /////////////////////////////// + // Line/col helper functions // + /////////////////////////////// + + /// Like [`Self::grapheme_boundaries`] but for lines. + /// + /// Note that the last line can have a length of 0 if the text ends with a + /// newline. + fn line_boundaries(&self) -> Vec { + let newlines = self + .text + .char_indices() + .filter(|(_, c)| *c == '\n') + .map(|(i, _)| i + 1); // utf-8 encodes '\n' as a single byte + iter::once(0) + .chain(newlines) + .chain(iter::once(self.text.len())) + .collect() + } + + /// Find the cursor's current line. + /// + /// Returns `(line_nr, start_idx, end_idx)`. + fn cursor_line(&self, boundaries: &[usize]) -> (usize, usize, usize) { + let mut result = (0, 0, 0); + for (i, (start, end)) in boundaries.iter().zip(boundaries.iter().skip(1)).enumerate() { + if self.cursor_idx >= *start { + result = (i, *start, *end); + } else { + break; + } + } + result + } + + fn cursor_col(&self, widthdb: &mut WidthDb, line_start: usize) -> usize { + widthdb.width(&self.text[line_start..self.cursor_idx]) + } + + fn line(&self, line: usize) -> (usize, usize) { + let boundaries = self.line_boundaries(); + boundaries + .iter() + .copied() + .zip(boundaries.iter().copied().skip(1)) + .nth(line) + .expect("line exists") + } + + fn move_cursor_to_line_col(&mut self, widthdb: &mut WidthDb, line: usize, col: usize) { + let (start, end) = self.line(line); + let line = &self.text[start..end]; + + let mut width = 0; + for (gi, g) in line.grapheme_indices(true) { + self.cursor_idx = start + gi; + if col > width { + width += widthdb.grapheme_width(g, width) as usize; + } else { + return; + } + } + + if !line.ends_with('\n') { + self.cursor_idx = end; + } + } + + fn record_cursor_col(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.line_boundaries(); + let (_, start, _) = self.cursor_line(&boundaries); + self.cursor_col = self.cursor_col(widthdb, start); + } + + ///////////// + // Editing // + ///////////// + + pub fn text(&self) -> &str { + &self.text + } + + pub fn set_text(&mut self, widthdb: &mut WidthDb, text: String) { + self.text = text; + self.move_cursor_to_grapheme_boundary(); + self.record_cursor_col(widthdb); + } + + pub fn clear(&mut self) { + self.text = String::new(); + self.cursor_idx = 0; + self.cursor_col = 0; + } + + /// Insert a character at the current cursor position and move the cursor + /// accordingly. + pub fn insert_char(&mut self, widthdb: &mut WidthDb, ch: char) { + self.text.insert(self.cursor_idx, ch); + self.cursor_idx += ch.len_utf8(); + self.record_cursor_col(widthdb); + } + + /// Insert a string at the current cursor position and move the cursor + /// accordingly. + pub fn insert_str(&mut self, widthdb: &mut WidthDb, str: &str) { + self.text.insert_str(self.cursor_idx, str); + self.cursor_idx += str.len(); + self.record_cursor_col(widthdb); + } + + /// Delete the grapheme before the cursor position. + pub fn backspace(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.grapheme_boundaries(); + for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { + if *end == self.cursor_idx { + self.text.replace_range(start..end, ""); + self.cursor_idx = *start; + self.record_cursor_col(widthdb); + break; + } + } + } + + /// Delete the grapheme after the cursor position. + pub fn delete(&mut self) { + let boundaries = self.grapheme_boundaries(); + for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { + if *start == self.cursor_idx { + self.text.replace_range(start..end, ""); + break; + } + } + } + + ///////////////////// + // Cursor movement // + ///////////////////// + + pub fn move_cursor_left(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.grapheme_boundaries(); + for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { + if *end == self.cursor_idx { + self.cursor_idx = *start; + self.record_cursor_col(widthdb); + break; + } + } + } + + pub fn move_cursor_right(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.grapheme_boundaries(); + for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { + if *start == self.cursor_idx { + self.cursor_idx = *end; + self.record_cursor_col(widthdb); + break; + } + } + } + + pub fn move_cursor_left_a_word(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.grapheme_boundaries(); + let mut encountered_word = false; + for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)).rev() { + if *end == self.cursor_idx { + let g = &self.text[*start..*end]; + let whitespace = g.chars().all(|c| c.is_whitespace()); + if encountered_word && whitespace { + break; + } else if !whitespace { + encountered_word = true; + } + self.cursor_idx = *start; + } + } + self.record_cursor_col(widthdb); + } + + pub fn move_cursor_right_a_word(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.grapheme_boundaries(); + let mut encountered_word = false; + for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { + if *start == self.cursor_idx { + let g = &self.text[*start..*end]; + let whitespace = g.chars().all(|c| c.is_whitespace()); + if encountered_word && whitespace { + break; + } else if !whitespace { + encountered_word = true; + } + self.cursor_idx = *end; + } + } + self.record_cursor_col(widthdb); + } + + pub fn move_cursor_to_start_of_line(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.line_boundaries(); + let (line, _, _) = self.cursor_line(&boundaries); + self.move_cursor_to_line_col(widthdb, line, 0); + self.record_cursor_col(widthdb); + } + + pub fn move_cursor_to_end_of_line(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.line_boundaries(); + let (line, _, _) = self.cursor_line(&boundaries); + self.move_cursor_to_line_col(widthdb, line, usize::MAX); + self.record_cursor_col(widthdb); + } + + pub fn move_cursor_up(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.line_boundaries(); + let (line, _, _) = self.cursor_line(&boundaries); + if line > 0 { + self.move_cursor_to_line_col(widthdb, line - 1, self.cursor_col); + } + } + + pub fn move_cursor_down(&mut self, widthdb: &mut WidthDb) { + let boundaries = self.line_boundaries(); + + // There's always at least one line, and always at least two line + // boundaries at 0 and self.text.len(). + let amount_of_lines = boundaries.len() - 1; + + let (line, _, _) = self.cursor_line(&boundaries); + if line + 1 < amount_of_lines { + self.move_cursor_to_line_col(widthdb, line + 1, self.cursor_col); + } + } + + pub fn last_cursor_pos(&self) -> Pos { + self.last_cursor_pos + } + + pub fn widget(&mut self) -> Editor<'_> { + Editor { + highlighted: Styled::new_plain(&self.text), + hidden: None, + focus: true, + state: self, + } + } +} + +impl Default for EditorState { + fn default() -> Self { + Self::new() + } +} + +//////////// +// Widget // +//////////// + +#[derive(Debug)] +pub struct Editor<'a> { + state: &'a mut EditorState, + highlighted: Styled, + pub hidden: Option, + pub focus: bool, +} + +impl Editor<'_> { + pub fn state(&mut self) -> &mut EditorState { + self.state + } + + pub fn text(&self) -> &Styled { + &self.highlighted + } + + pub fn highlight(&mut self, highlight: F) + where + F: FnOnce(&str) -> Styled, + { + self.highlighted = highlight(&self.state.text); + assert_eq!(self.state.text, self.highlighted.text()); + } + + pub fn with_highlight(mut self, highlight: F) -> Self + where + F: FnOnce(&str) -> Styled, + { + self.highlight(highlight); + self + } + + pub fn with_visible(mut self) -> Self { + self.hidden = None; + self + } + + pub fn with_hidden>(mut self, placeholder: S) -> Self { + self.hidden = Some(placeholder.into()); + self + } + + pub fn with_hidden_default_placeholder(self) -> Self { + self.with_hidden(("", Style::new().grey().italic())) + } + + pub fn with_focus(mut self, active: bool) -> Self { + self.focus = active; + self + } + + fn wrapped_cursor(cursor_idx: usize, break_indices: &[usize]) -> (usize, usize) { + let mut row = 0; + let mut line_idx = cursor_idx; + + for break_idx in break_indices { + if cursor_idx < *break_idx { + break; + } else { + row += 1; + line_idx = cursor_idx - break_idx; + } + } + + (row, line_idx) + } + + fn indices(&self, widthdb: &mut WidthDb, max_width: Option) -> Vec { + let max_width = max_width + // One extra column for cursor + .map(|w| w.saturating_sub(1) as usize) + .unwrap_or(usize::MAX); + let text = self.hidden.as_ref().unwrap_or(&self.highlighted); + wrap(widthdb, text.text(), max_width) + } + + fn rows(&self, indices: &[usize]) -> Vec { + let text = match self.hidden.as_ref() { + Some(hidden) if !self.highlighted.text().is_empty() => hidden, + _ => &self.highlighted, + }; + text.clone().split_at_indices(indices) + } + + fn cursor(&self, widthdb: &mut WidthDb, width: u16, indices: &[usize], rows: &[Styled]) -> Pos { + if self.hidden.is_some() { + return Pos::new(0, 0); + } + + let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.state.cursor_idx, indices); + let cursor_col = widthdb.width(rows[cursor_row].text().split_at(cursor_line_idx).0); + + // Ensure the cursor is always visible + let cursor_col = cursor_col.min(width.saturating_sub(1).into()); + + let cursor_row: i32 = cursor_row.try_into().unwrap_or(i32::MAX); + let cursor_col: i32 = cursor_col.try_into().unwrap_or(i32::MAX); + Pos::new(cursor_col, cursor_row) + } +} + +impl Widget for Editor<'_> { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + _max_height: Option, + ) -> Result { + let indices = self.indices(widthdb, max_width); + let rows = self.rows(&indices); + + let width = rows + .iter() + .map(|row| widthdb.width(row.text())) + .max() + .unwrap_or(0) + // One extra column for cursor + .saturating_add(1); + let height = rows.len(); + + let width: u16 = width.try_into().unwrap_or(u16::MAX); + let height: u16 = height.try_into().unwrap_or(u16::MAX); + Ok(Size::new(width, height)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let indices = self.indices(frame.widthdb(), Some(size.width)); + let rows = self.rows(&indices); + let cursor = self.cursor(frame.widthdb(), size.width, &indices, &rows); + + for (i, row) in rows.into_iter().enumerate() { + frame.write(Pos::new(0, i as i32), row); + } + + if self.focus { + frame.set_cursor(Some(cursor)); + } + self.state.last_cursor_pos = cursor; + + Ok(()) + } +} diff --git a/src/widgets/either.rs b/src/widgets/either.rs new file mode 100644 index 0000000..cb9a55d --- /dev/null +++ b/src/widgets/either.rs @@ -0,0 +1,118 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +macro_rules! mk_either { + ( + pub enum $name:ident { + $( $constr:ident($ty:ident), )+ + } + ) => { + #[derive(Debug, Clone, Copy)] + pub enum $name< $( $ty ),+ > { + $( $constr($ty), )+ + } + + impl Widget for $name< $( $ty ),+ > + where + $( $ty: Widget, )+ + { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + $( Self::$constr(w) => w.size(widthdb, max_width, max_height), )+ + } + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + $( Self::$constr(w) => w.draw(frame), )+ + } + } + } + + #[async_trait] + impl AsyncWidget for $name< $( $ty ),+ > + where + $( $ty: AsyncWidget + Send + Sync, )+ + { + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + $( Self::$constr(w) => w.size(widthdb, max_width, max_height).await, )+ + } + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + $( Self::$constr(w) => w.draw(frame).await, )+ + } + } + } + }; +} + +mk_either! { + pub enum Either2 { + First(I1), + Second(I2), + } +} + +mk_either! { + pub enum Either3 { + First(I1), + Second(I2), + Third(I3), + } +} + +mk_either! { + pub enum Either4 { + First(I1), + Second(I2), + Third(I3), + Fourth(I4), + } +} + +mk_either! { + pub enum Either5 { + First(I1), + Second(I2), + Third(I3), + Fourth(I4), + Fifth(I5), + } +} + +mk_either! { + pub enum Either6 { + First(I1), + Second(I2), + Third(I3), + Fourth(I4), + Fifth(I5), + Sixth(I6), + } +} + +mk_either! { + pub enum Either7 { + First(I1), + Second(I2), + Third(I3), + Fourth(I4), + Fifth(I5), + Sixth(I6), + Seventh(I7), + } +} diff --git a/src/widgets/empty.rs b/src/widgets/empty.rs new file mode 100644 index 0000000..5de4fdf --- /dev/null +++ b/src/widgets/empty.rs @@ -0,0 +1,42 @@ +use crate::{Frame, Size, Widget, WidthDb}; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Empty { + pub size: Size, +} + +impl Empty { + pub fn new() -> Self { + Self { size: Size::ZERO } + } + + pub fn with_width(mut self, width: u16) -> Self { + self.size.width = width; + self + } + + pub fn with_height(mut self, height: u16) -> Self { + self.size.height = height; + self + } + + pub fn with_size(mut self, size: Size) -> Self { + self.size = size; + self + } +} + +impl Widget for Empty { + fn size( + &self, + _widthdb: &mut WidthDb, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.size) + } + + fn draw(self, _frame: &mut Frame) -> Result<(), E> { + Ok(()) + } +} diff --git a/src/widgets/float.rs b/src/widgets/float.rs new file mode 100644 index 0000000..5cfb349 --- /dev/null +++ b/src/widgets/float.rs @@ -0,0 +1,166 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; + +#[derive(Debug, Clone, Copy)] +pub struct Float { + pub inner: I, + horizontal: Option, + vertical: Option, +} + +impl Float { + pub fn new(inner: I) -> Self { + Self { + inner, + horizontal: None, + vertical: None, + } + } + + pub fn horizontal(&self) -> Option { + self.horizontal + } + + pub fn set_horizontal(&mut self, position: Option) { + if let Some(position) = position { + assert!((0.0..=1.0).contains(&position)); + } + self.horizontal = position; + } + + pub fn vertical(&self) -> Option { + self.vertical + } + + pub fn set_vertical(&mut self, position: Option) { + if let Some(position) = position { + assert!((0.0..=1.0).contains(&position)); + } + self.vertical = position; + } + + pub fn with_horizontal(mut self, position: f32) -> Self { + self.set_horizontal(Some(position)); + self + } + + pub fn with_vertical(mut self, position: f32) -> Self { + self.set_vertical(Some(position)); + self + } + + pub fn with_all(self, position: f32) -> Self { + self.with_horizontal(position).with_vertical(position) + } + + pub fn with_left(self) -> Self { + self.with_horizontal(0.0) + } + + pub fn with_right(self) -> Self { + self.with_horizontal(1.0) + } + + pub fn with_top(self) -> Self { + self.with_vertical(0.0) + } + + pub fn with_bottom(self) -> Self { + self.with_vertical(1.0) + } + + pub fn with_center_h(self) -> Self { + self.with_horizontal(0.5) + } + + pub fn with_center_v(self) -> Self { + self.with_vertical(0.5) + } + + pub fn with_center(self) -> Self { + self.with_all(0.5) + } + + fn push_inner(&self, frame: &mut Frame, size: Size, mut inner_size: Size) { + let mut inner_pos = Pos::ZERO; + + if let Some(horizontal) = self.horizontal { + let available = size.width.saturating_sub(inner_size.width) as f32; + // Biased towards the left if horizontal lands exactly on the + // boundary between two cells + inner_pos.x = (horizontal * available).floor().min(available) as i32; + inner_size.width = inner_size.width.min(size.width); + } else { + inner_size.width = size.width; + } + + if let Some(vertical) = self.vertical { + let available = size.height.saturating_sub(inner_size.height) as f32; + // Biased towards the top if vertical lands exactly on the boundary + // between two cells + inner_pos.y = (vertical * available).floor().min(available) as i32; + inner_size.height = inner_size.height.min(size.height); + } else { + inner_size.height = size.height; + } + + frame.push(inner_pos, inner_size); + } +} + +impl Widget for Float +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let inner_size = self + .inner + .size(frame.widthdb(), Some(size.width), Some(size.height))?; + + self.push_inner(frame, size, inner_size); + self.inner.draw(frame)?; + frame.pop(); + + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Float +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let inner_size = self + .inner + .size(frame.widthdb(), Some(size.width), Some(size.height)) + .await?; + + self.push_inner(frame, size, inner_size); + self.inner.draw(frame).await?; + frame.pop(); + + Ok(()) + } +} diff --git a/src/widgets/join.rs b/src/widgets/join.rs new file mode 100644 index 0000000..20cd413 --- /dev/null +++ b/src/widgets/join.rs @@ -0,0 +1,721 @@ +use std::cmp::Ordering; + +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; + +// The following algorithm has three goals, listed in order of importance: +// +// 1. Use the available space +// 2. Avoid shrinking segments where possible +// 3. Match the given weights as closely as possible +// +// Its input is a list of weighted segments where each segment wants to use a +// certain amount of space. The weights signify how the available space would be +// assigned if goal 2 was irrelevant. +// +// First, the algorithm must decide whether it must grow or shrink segments. +// Because goal 2 has a higher priority than goal 3, it never makes sense to +// shrink a segment in order to make another larger. In both cases, a segment's +// actual size is compared to its allotment, i. e. what size it should be based +// on its weight. +// +// Growth +// ====== +// +// If segments must be grown, an important observation can be made: If all +// segments are smaller than their allotment, then each segment can be assigned +// its allotment without violating goal 2, thereby fulfilling goal 3. +// +// Another important observation can be made: If a segment is at least as large +// as its allotment, it must never be grown as that would violate goal 3. +// +// Based on these two observations, the growth algorithm first repeatedly +// removes all segments that are at least as large as their allotment. It then +// resizes the remaining segments to their allotments. +// +// Shrinkage +// ========= +// +// If segments must be shrunk, an important observation can be made: If all +// segments are larger than their allotment, then each segment can be assigned +// its allotment, thereby fulfilling goal 3. Since goal 1 is more important than +// goal 2, we know that some elements must be shrunk. +// +// Another important observation can be made: If a segment is at least as small +// as its allotment, it must never be shrunk as that would violate goal 3. +// +// Based on these two observations, the shrinkage algorithm first repeatedly +// removes all segments that are at least as small as their allotment. It then +// resizes the remaining segments to their allotments. + +#[derive(Debug)] +struct Segment { + major: u16, + minor: u16, + weight: f32, + growing: bool, + shrinking: bool, +} + +impl Segment { + fn new(major_minor: (u16, u16), segment: &JoinSegment) -> Self { + Self { + major: major_minor.0, + minor: major_minor.1, + weight: segment.weight, + growing: segment.growing, + shrinking: segment.shrinking, + } + } +} + +fn total_size(segments: &[&mut Segment]) -> u16 { + let mut total = 0_u16; + for segment in segments { + total = total.saturating_add(segment.major); + } + total +} + +fn total_weight(segments: &[&mut Segment]) -> f32 { + segments.iter().map(|s| s.weight).sum() +} + +fn balance(segments: &mut [Segment], available: u16) { + let segments = segments.iter_mut().collect::>(); + match total_size(&segments).cmp(&available) { + Ordering::Less => grow(segments, available), + Ordering::Greater => shrink(segments, available), + Ordering::Equal => {} + } +} + +fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { + assert!(available >= total_size(&segments)); + + // Only grow segments that can be grown. + segments.retain(|s| { + if s.growing { + return true; + } + available = available.saturating_sub(s.major); + false + }); + + // Repeatedly remove all segments that do not need to grow, i. e. that are + // at least as large as their allotment. + loop { + let mut total_weight = total_weight(&segments); + + // If there are no segments with a weight > 0, space is distributed + // evenly among all remaining segments. + if total_weight <= 0.0 { + for segment in &mut segments { + segment.weight = 1.0; + } + total_weight = segments.len() as f32; + } + + let mut removed = 0; + segments.retain(|s| { + let allotment = s.weight / total_weight * available as f32; + if (s.major as f32) < allotment { + return true; // May need to grow + } + removed += s.major; + false + }); + available -= removed; + + if removed == 0 { + break; // All remaining segments are smaller than their allotments + } + } + + let total_weight = segments.iter().map(|s| s.weight).sum::(); + if total_weight <= 0.0 { + return; // No more segments left + } + + // Size each remaining segment according to its allotment. + let mut used = 0; + for segment in &mut segments { + let allotment = segment.weight / total_weight * available as f32; + segment.major = allotment.floor() as u16; + used += segment.major; + } + + // Distribute remaining unused space from left to right. + // + // The rounding error on each segment is at most 1, so we only need to loop + // over the segments once. + let remaining = available - used; + assert!(remaining as usize <= segments.len()); + for segment in segments.into_iter().take(remaining.into()) { + segment.major += 1; + } +} + +fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { + assert!(available <= total_size(&segments)); + + // Only shrink segments that can be shrunk. + segments.retain(|s| { + if s.shrinking { + return true; + } + available = available.saturating_sub(s.major); + false + }); + + // Repeatedly remove all segments that do not need to shrink, i. e. that are + // at least as small as their allotment. + loop { + let mut total_weight = total_weight(&segments); + + // If there are no segments with a weight > 0, space is distributed + // evenly among all remaining segments. + if total_weight <= 0.0 { + for segment in &mut segments { + segment.weight = 1.0; + } + total_weight = segments.len() as f32; + } + + let mut removed = 0; + segments.retain(|s| { + let allotment = s.weight / total_weight * available as f32; + if (s.major as f32) > allotment { + return true; // May need to shrink + } + + // The segment size subtracted from `available` is always smaller + // than or equal to its allotment. Since `available` is the sum of + // all allotments, it can never go below 0. + assert!(s.major <= available); + + removed += s.major; + false + }); + available -= removed; + + if removed == 0 { + break; // All segments want more than their weight allows. + } + } + + let total_weight = segments.iter().map(|s| s.weight).sum::(); + if total_weight <= 0.0 { + return; // No more segments left + } + + // Size each remaining segment according to its allotment. + let mut used = 0; + for segment in &mut segments { + let allotment = segment.weight / total_weight * available as f32; + segment.major = allotment.floor() as u16; + used += segment.major; + } + + // Distribute remaining unused space from left to right. + // + // The rounding error on each segment is at most 1, so we only need to loop + // over the segments once. + let remaining = available - used; + assert!(remaining as usize <= segments.len()); + for segment in segments.into_iter().take(remaining.into()) { + segment.major += 1; + } +} + +#[derive(Debug, Clone, Copy)] +pub struct JoinSegment { + pub inner: I, + weight: f32, + pub growing: bool, + pub shrinking: bool, +} + +impl JoinSegment { + pub fn new(inner: I) -> Self { + Self { + inner, + weight: 1.0, + growing: true, + shrinking: true, + } + } + + pub fn weight(&self) -> f32 { + self.weight + } + + pub fn set_weight(&mut self, weight: f32) { + assert!(weight >= 0.0); + self.weight = weight; + } + + pub fn with_weight(mut self, weight: f32) -> Self { + self.set_weight(weight); + self + } + + pub fn with_growing(mut self, enabled: bool) -> Self { + self.growing = enabled; + self + } + + pub fn with_shrinking(mut self, enabled: bool) -> Self { + self.shrinking = enabled; + self + } + + pub fn with_fixed(self, fixed: bool) -> Self { + self.with_growing(!fixed).with_shrinking(!fixed) + } +} + +fn to_mm(horizontal: bool, w: T, h: T) -> (T, T) { + if horizontal { + (w, h) + } else { + (h, w) + } +} + +fn from_mm(horizontal: bool, major: T, minor: T) -> (T, T) { + if horizontal { + (major, minor) + } else { + (minor, major) + } +} + +fn size>( + horizontal: bool, + widthdb: &mut WidthDb, + segment: &JoinSegment, + major: Option, + minor: Option, +) -> Result<(u16, u16), E> { + if horizontal { + let size = segment.inner.size(widthdb, major, minor)?; + Ok((size.width, size.height)) + } else { + let size = segment.inner.size(widthdb, minor, major)?; + Ok((size.height, size.width)) + } +} + +fn size_with_balanced>( + horizontal: bool, + widthdb: &mut WidthDb, + segment: &JoinSegment, + balanced: &Segment, + minor: Option, +) -> Result<(u16, u16), E> { + size(horizontal, widthdb, segment, Some(balanced.major), minor) +} + +async fn size_async>( + horizontal: bool, + widthdb: &mut WidthDb, + segment: &JoinSegment, + major: Option, + minor: Option, +) -> Result<(u16, u16), E> { + if horizontal { + let size = segment.inner.size(widthdb, major, minor).await?; + Ok((size.width, size.height)) + } else { + let size = segment.inner.size(widthdb, minor, major).await?; + Ok((size.height, size.width)) + } +} + +async fn size_async_with_balanced>( + horizontal: bool, + widthdb: &mut WidthDb, + segment: &JoinSegment, + balanced: &Segment, + minor: Option, +) -> Result<(u16, u16), E> { + size_async(horizontal, widthdb, segment, Some(balanced.major), minor).await +} + +fn sum_major_max_minor(segments: &[Segment]) -> (u16, u16) { + let mut major = 0_u16; + let mut minor = 0_u16; + for segment in segments { + major = major.saturating_add(segment.major); + minor = minor.max(segment.minor); + } + (major, minor) +} + +#[derive(Debug, Clone)] +pub struct Join { + horizontal: bool, + segments: Vec>, +} + +impl Join { + pub fn horizontal(segments: Vec>) -> Self { + Self { + horizontal: true, + segments, + } + } + + pub fn vertical(segments: Vec>) -> Self { + Self { + horizontal: false, + segments, + } + } +} + +impl Widget for Join +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height); + + let mut segments = Vec::with_capacity(self.segments.len()); + for segment in &self.segments { + let major_minor = size(self.horizontal, widthdb, segment, None, max_minor)?; + segments.push(Segment::new(major_minor, segment)); + } + + if let Some(available) = max_major { + balance(&mut segments, available); + + let mut new_segments = Vec::with_capacity(self.segments.len()); + for (segment, balanced) in self.segments.iter().zip(segments.into_iter()) { + let major_minor = + size_with_balanced(self.horizontal, widthdb, segment, &balanced, max_minor)?; + new_segments.push(Segment::new(major_minor, segment)); + } + segments = new_segments; + } + + let (major, minor) = sum_major_max_minor(&segments); + let (width, height) = from_mm(self.horizontal, major, minor); + Ok(Size::new(width, height)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let frame_size = frame.size(); + let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height); + + let widthdb = frame.widthdb(); + let mut segments = Vec::with_capacity(self.segments.len()); + for segment in &self.segments { + let major_minor = size(self.horizontal, widthdb, segment, None, Some(max_minor))?; + segments.push(Segment::new(major_minor, segment)); + } + balance(&mut segments, max_major); + + let mut major = 0_i32; + for (segment, balanced) in self.segments.into_iter().zip(segments.into_iter()) { + let (x, y) = from_mm(self.horizontal, major, 0); + let (w, h) = from_mm(self.horizontal, balanced.major, max_minor); + frame.push(Pos::new(x, y), Size::new(w, h)); + segment.inner.draw(frame)?; + frame.pop(); + major += balanced.major as i32; + } + + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Join +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height); + + let mut segments = Vec::with_capacity(self.segments.len()); + for segment in &self.segments { + let major_minor = + size_async(self.horizontal, widthdb, segment, None, max_minor).await?; + segments.push(Segment::new(major_minor, segment)); + } + + if let Some(available) = max_major { + balance(&mut segments, available); + + let mut new_segments = Vec::with_capacity(self.segments.len()); + for (segment, balanced) in self.segments.iter().zip(segments.into_iter()) { + let major_minor = size_async_with_balanced( + self.horizontal, + widthdb, + segment, + &balanced, + max_minor, + ) + .await?; + new_segments.push(Segment::new(major_minor, segment)); + } + segments = new_segments; + } + + let (major, minor) = sum_major_max_minor(&segments); + let (width, height) = from_mm(self.horizontal, major, minor); + Ok(Size::new(width, height)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + let frame_size = frame.size(); + let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height); + + let widthdb = frame.widthdb(); + let mut segments = Vec::with_capacity(self.segments.len()); + for segment in &self.segments { + let major_minor = + size_async(self.horizontal, widthdb, segment, None, Some(max_minor)).await?; + segments.push(Segment::new(major_minor, segment)); + } + balance(&mut segments, max_major); + + let mut major = 0_i32; + for (segment, balanced) in self.segments.into_iter().zip(segments.into_iter()) { + let (x, y) = from_mm(self.horizontal, major, 0); + let (w, h) = from_mm(self.horizontal, balanced.major, max_minor); + frame.push(Pos::new(x, y), Size::new(w, h)); + segment.inner.draw(frame).await?; + frame.pop(); + major += balanced.major as i32; + } + + Ok(()) + } +} + +macro_rules! mk_join { + ( + pub struct $name:ident { + $( pub $arg:ident: $type:ident [$n:expr], )+ + } + ) => { + #[derive(Debug, Clone, Copy)] + pub struct $name< $($type),+ >{ + horizontal: bool, + $( pub $arg: JoinSegment<$type>, )+ + } + + impl< $($type),+ > $name< $($type),+ >{ + pub fn horizontal( $($arg: JoinSegment<$type>),+ ) -> Self { + Self { horizontal: true, $( $arg, )+ } + } + + pub fn vertical( $($arg: JoinSegment<$type>),+ ) -> Self { + Self { horizontal: false, $( $arg, )+ } + } + } + + impl Widget for $name< $($type),+ > + where + $( $type: Widget, )+ + { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height); + + let mut segments = [ $( + Segment::new( + size(self.horizontal, widthdb, &self.$arg, None, max_minor)?, + &self.$arg, + ), + )+ ]; + + if let Some(available) = max_major { + balance(&mut segments, available); + + let new_segments = [ $( + Segment::new( + size_with_balanced(self.horizontal, widthdb, &self.$arg, &segments[$n], max_minor)?, + &self.$arg, + ), + )+ ]; + segments = new_segments; + } + + let (major, minor) = sum_major_max_minor(&segments); + let (width, height) = from_mm(self.horizontal, major, minor); + Ok(Size::new(width, height)) + } + + #[allow(unused_assignments)] + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let frame_size = frame.size(); + let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height); + + let widthdb = frame.widthdb(); + let mut segments = [ $( + Segment::new( + size(self.horizontal, widthdb, &self.$arg, None, Some(max_minor))?, + &self.$arg, + ), + )+ ]; + balance(&mut segments, max_major); + + let mut major = 0_i32; + $( { + let balanced = &segments[$n]; + let (x, y) = from_mm(self.horizontal, major, 0); + let (w, h) = from_mm(self.horizontal, balanced.major, max_minor); + frame.push(Pos::new(x, y), Size::new(w, h)); + self.$arg.inner.draw(frame)?; + frame.pop(); + major += balanced.major as i32; + } )* + + Ok(()) + } + } + + #[async_trait] + impl AsyncWidget for $name< $($type),+ > + where + E: Send, + $( $type: AsyncWidget + Send + Sync, )+ + { + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height); + + let mut segments = [ $( + Segment::new( + size_async(self.horizontal, widthdb, &self.$arg, None, max_minor).await?, + &self.$arg, + ), + )+ ]; + + if let Some(available) = max_major { + balance(&mut segments, available); + + let new_segments = [ $( + Segment::new( + size_async_with_balanced(self.horizontal, widthdb, &self.$arg, &segments[$n], max_minor).await?, + &self.$arg, + ), + )+ ]; + segments = new_segments; + } + + let (major, minor) = sum_major_max_minor(&segments); + let (width, height) = from_mm(self.horizontal, major, minor); + Ok(Size::new(width, height)) + } + + #[allow(unused_assignments)] + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + let frame_size = frame.size(); + let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height); + + let widthdb = frame.widthdb(); + let mut segments = [ $( + Segment::new( + size_async(self.horizontal, widthdb, &self.$arg, None, Some(max_minor)).await?, + &self.$arg, + ), + )+ ]; + balance(&mut segments, max_major); + + let mut major = 0_i32; + $( { + let balanced = &segments[$n]; + let (x, y) = from_mm(self.horizontal, major, 0); + let (w, h) = from_mm(self.horizontal, balanced.major, max_minor); + frame.push(Pos::new(x, y), Size::new(w, h)); + self.$arg.inner.draw(frame).await?; + frame.pop(); + major += balanced.major as i32; + } )* + + Ok(()) + } + } + }; +} + +mk_join! { + pub struct Join2 { + pub first: I1 [0], + pub second: I2 [1], + } +} + +mk_join! { + pub struct Join3 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], + } +} + +mk_join! { + pub struct Join4 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], + pub fourth: I4 [3], + } +} + +mk_join! { + pub struct Join5 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], + pub fourth: I4 [3], + pub fifth: I5 [4], + } +} + +mk_join! { + pub struct Join6 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], + pub fourth: I4 [3], + pub fifth: I5 [4], + pub sixth: I6 [5], + } +} + +mk_join! { + pub struct Join7 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], + pub fourth: I4 [3], + pub fifth: I5 [4], + pub sixth: I6 [5], + pub seventh: I7 [6], + } +} diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs new file mode 100644 index 0000000..af3da5e --- /dev/null +++ b/src/widgets/layer.rs @@ -0,0 +1,201 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +#[derive(Debug, Clone)] +pub struct Layer { + layers: Vec, +} + +impl Layer { + pub fn new(layers: Vec) -> Self { + Self { layers } + } +} + +impl Widget for Layer +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let mut size = Size::ZERO; + for layer in &self.layers { + let lsize = layer.size(widthdb, max_width, max_height)?; + size.width = size.width.max(lsize.width); + size.height = size.height.max(lsize.height); + } + Ok(size) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + for layer in self.layers { + layer.draw(frame)?; + } + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Layer +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let mut size = Size::ZERO; + for layer in &self.layers { + let lsize = layer.size(widthdb, max_width, max_height).await?; + size.width = size.width.max(lsize.width); + size.height = size.height.max(lsize.height); + } + Ok(size) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + for layer in self.layers { + layer.draw(frame).await?; + } + Ok(()) + } +} + +macro_rules! mk_layer { + ( + pub struct $name:ident { + $( pub $arg:ident: $type:ident, )+ + } + ) => { + #[derive(Debug, Clone, Copy)] + pub struct $name< $($type),+ >{ + $( pub $arg: $type, )+ + } + + impl< $($type),+ > $name< $($type),+ >{ + pub fn new( $($arg: $type),+ ) -> Self { + Self { $( $arg, )+ } + } + } + + impl Widget for $name< $($type),+ > + where + $( $type: Widget, )+ + { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let mut size = Size::ZERO; + + $({ + let lsize = self.$arg.size(widthdb, max_width, max_height)?; + size.width = size.width.max(lsize.width); + size.height = size.height.max(lsize.height); + })+ + + Ok(size) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + $( self.$arg.draw(frame)?; )+ + Ok(()) + } + } + + #[async_trait] + impl AsyncWidget for $name< $($type),+ > + where + E: Send, + $( $type: AsyncWidget + Send + Sync, )+ + { + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let mut size = Size::ZERO; + + $({ + let lsize = self.$arg.size(widthdb, max_width, max_height).await?; + size.width = size.width.max(lsize.width); + size.height = size.height.max(lsize.height); + })+ + + Ok(size) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + $( self.$arg.draw(frame).await?; )+ + Ok(()) + } + } + }; +} + +mk_layer!( + pub struct Layer2 { + pub first: I1, + pub second: I2, + } +); + +mk_layer!( + pub struct Layer3 { + pub first: I1, + pub second: I2, + pub third: I3, + } +); + +mk_layer!( + pub struct Layer4 { + pub first: I1, + pub second: I2, + pub third: I3, + pub fourth: I4, + } +); + +mk_layer!( + pub struct Layer5 { + pub first: I1, + pub second: I2, + pub third: I3, + pub fourth: I4, + pub fifth: I5, + } +); + +mk_layer!( + pub struct Layer6 { + pub first: I1, + pub second: I2, + pub third: I3, + pub fourth: I4, + pub fifth: I5, + pub sixth: I6, + } +); + +mk_layer!( + pub struct Layer7 { + pub first: I1, + pub second: I2, + pub third: I3, + pub fourth: I4, + pub fifth: I5, + pub sixth: I6, + pub seventh: I7, + } +); diff --git a/src/widgets/padding.rs b/src/widgets/padding.rs new file mode 100644 index 0000000..be3aff6 --- /dev/null +++ b/src/widgets/padding.rs @@ -0,0 +1,133 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; + +#[derive(Debug, Clone, Copy)] +pub struct Padding { + pub inner: I, + pub left: u16, + pub right: u16, + pub top: u16, + pub bottom: u16, + pub stretch: bool, +} + +impl Padding { + pub fn new(inner: I) -> Self { + Self { + inner, + left: 0, + right: 0, + top: 0, + bottom: 0, + stretch: false, + } + } + + pub fn with_left(mut self, amount: u16) -> Self { + self.left = amount; + self + } + + pub fn with_right(mut self, amount: u16) -> Self { + self.right = amount; + self + } + + pub fn with_top(mut self, amount: u16) -> Self { + self.top = amount; + self + } + + pub fn with_bottom(mut self, amount: u16) -> Self { + self.bottom = amount; + self + } + + pub fn with_horizontal(self, amount: u16) -> Self { + self.with_left(amount).with_right(amount) + } + + pub fn with_vertical(self, amount: u16) -> Self { + self.with_top(amount).with_bottom(amount) + } + + pub fn with_all(self, amount: u16) -> Self { + self.with_horizontal(amount).with_vertical(amount) + } + + pub fn with_stretch(mut self, stretch: bool) -> Self { + self.stretch = stretch; + self + } + + fn pad_size(&self) -> Size { + Size::new(self.left + self.right, self.top + self.bottom) + } + + fn push_inner(&self, frame: &mut Frame) { + let size = frame.size(); + let pad_size = self.pad_size(); + let inner_size = size.saturating_sub(pad_size); + frame.push(Pos::new(self.left.into(), self.top.into()), inner_size); + } +} + +impl Widget for Padding +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let pad_size = self.pad_size(); + let max_width = max_width.map(|w| w.saturating_sub(pad_size.width)); + let max_height = max_height.map(|h| h.saturating_sub(pad_size.height)); + let size = self.inner.size(widthdb, max_width, max_height)?; + Ok(size + pad_size) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + if self.stretch { + self.inner.draw(frame)?; + } else { + self.push_inner(frame); + self.inner.draw(frame)?; + frame.pop(); + } + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Padding +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let pad_size = self.pad_size(); + let max_width = max_width.map(|w| w.saturating_sub(pad_size.width)); + let max_height = max_height.map(|h| h.saturating_sub(pad_size.height)); + let size = self.inner.size(widthdb, max_width, max_height).await?; + Ok(size + pad_size) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + if self.stretch { + self.inner.draw(frame).await?; + } else { + self.push_inner(frame); + self.inner.draw(frame).await?; + frame.pop(); + } + Ok(()) + } +} diff --git a/src/widgets/predrawn.rs b/src/widgets/predrawn.rs new file mode 100644 index 0000000..8301f1e --- /dev/null +++ b/src/widgets/predrawn.rs @@ -0,0 +1,74 @@ +use std::mem; + +use crate::buffer::Buffer; +use crate::{AsyncWidget, Frame, Pos, Size, Style, Styled, Widget, WidthDb}; + +#[derive(Debug, Clone)] +pub struct Predrawn { + buffer: Buffer, +} + +impl Predrawn { + pub fn new>(inner: W, widthdb: &mut WidthDb) -> Result { + let mut tmp_frame = Frame::default(); + + let size = inner.size(widthdb, None, None)?; + tmp_frame.buffer.resize(size); + + mem::swap(widthdb, &mut tmp_frame.widthdb); + inner.draw(&mut tmp_frame)?; + mem::swap(widthdb, &mut tmp_frame.widthdb); + + let buffer = tmp_frame.buffer; + Ok(Self { buffer }) + } + + pub async fn new_async>( + inner: W, + widthdb: &mut WidthDb, + ) -> Result { + let mut tmp_frame = Frame::default(); + + let size = inner.size(widthdb, None, None).await?; + tmp_frame.buffer.resize(size); + + mem::swap(widthdb, &mut tmp_frame.widthdb); + inner.draw(&mut tmp_frame).await?; + mem::swap(widthdb, &mut tmp_frame.widthdb); + + let buffer = tmp_frame.buffer; + Ok(Self { buffer }) + } + + pub fn size(&self) -> Size { + self.buffer.size() + } +} + +impl Widget for Predrawn { + fn size( + &self, + _widthdb: &mut WidthDb, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.buffer.size()) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + for (x, y, cell) in self.buffer.cells() { + let pos = Pos::new(x.into(), y.into()); + let style = Style { + content_style: cell.style, + opaque: true, + }; + frame.write(pos, Styled::new(&cell.content, style)); + } + + if let Some(cursor) = self.buffer.cursor() { + frame.set_cursor(Some(cursor)); + } + + Ok(()) + } +} diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs new file mode 100644 index 0000000..81e30b5 --- /dev/null +++ b/src/widgets/resize.rs @@ -0,0 +1,120 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +#[derive(Debug, Clone, Copy)] +pub struct Resize { + pub inner: I, + pub min_width: Option, + pub min_height: Option, + pub max_width: Option, + pub max_height: Option, +} + +impl Resize { + pub fn new(inner: I) -> Self { + Self { + inner, + min_width: None, + min_height: None, + max_width: None, + max_height: None, + } + } + + pub fn with_min_width(mut self, width: u16) -> Self { + self.min_width = Some(width); + self + } + + pub fn with_min_height(mut self, height: u16) -> Self { + self.min_height = Some(height); + self + } + + pub fn with_max_width(mut self, width: u16) -> Self { + self.max_width = Some(width); + self + } + + pub fn with_max_height(mut self, height: u16) -> Self { + self.max_height = Some(height); + self + } + + fn presize( + &self, + mut width: Option, + mut height: Option, + ) -> (Option, Option) { + if let Some(mw) = self.max_width { + width = Some(width.unwrap_or(mw).min(mw)); + } + if let Some(mh) = self.max_height { + height = Some(height.unwrap_or(mh).max(mh)); + } + (width, height) + } + + fn resize(&self, size: Size) -> Size { + let mut width = size.width; + let mut height = size.height; + + if let Some(min_width) = self.min_width { + width = width.max(min_width); + } + if let Some(min_height) = self.min_height { + height = height.max(min_height); + } + + if let Some(max_width) = self.max_width { + width = width.min(max_width); + } + if let Some(max_height) = self.max_height { + height = height.min(max_height); + } + + Size::new(width, height) + } +} + +impl Widget for Resize +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let (max_width, max_height) = self.presize(max_width, max_height); + let size = self.inner.size(widthdb, max_width, max_height)?; + Ok(self.resize(size)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for Resize +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + let (max_width, max_height) = self.presize(max_width, max_height); + let size = self.inner.size(widthdb, max_width, max_height).await?; + Ok(self.resize(size)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame).await + } +} diff --git a/src/widgets/text.rs b/src/widgets/text.rs new file mode 100644 index 0000000..007f4fe --- /dev/null +++ b/src/widgets/text.rs @@ -0,0 +1,68 @@ +use crate::{Frame, Pos, Size, Styled, Widget, WidthDb}; + +#[derive(Debug, Clone)] +pub struct Text { + pub styled: Styled, + pub wrap: bool, +} + +impl Text { + pub fn new>(styled: S) -> Self { + Self { + styled: styled.into(), + wrap: true, + } + } + + pub fn with_wrap(mut self, active: bool) -> Self { + self.wrap = active; + self + } + + fn wrapped(&self, widthdb: &mut WidthDb, max_width: Option) -> Vec { + let max_width = max_width + .filter(|_| self.wrap) + .map(|w| w as usize) + .unwrap_or(usize::MAX); + + let indices = widthdb.wrap(self.styled.text(), max_width); + self.styled.clone().split_at_indices(&indices) + } +} + +impl Widget for Text { + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + _max_height: Option, + ) -> Result { + let lines = self.wrapped(widthdb, max_width); + + let min_width = lines + .iter() + .map(|l| widthdb.width(l.text().trim_end())) + .max() + .unwrap_or(0); + let min_height = lines.len(); + + let min_width: u16 = min_width.try_into().unwrap_or(u16::MAX); + let min_height: u16 = min_height.try_into().unwrap_or(u16::MAX); + Ok(Size::new(min_width, min_height)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + + for (i, line) in self + .wrapped(frame.widthdb(), Some(size.width)) + .into_iter() + .enumerate() + { + let i: i32 = i.try_into().unwrap_or(i32::MAX); + frame.write(Pos::new(0, i), line); + } + + Ok(()) + } +} diff --git a/src/widgets/title.rs b/src/widgets/title.rs new file mode 100644 index 0000000..c0dc0d4 --- /dev/null +++ b/src/widgets/title.rs @@ -0,0 +1,59 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +#[derive(Debug, Clone)] +pub struct Title { + pub inner: I, + pub title: String, +} + +impl Title { + pub fn new(inner: I, title: S) -> Self { + Self { + inner, + title: title.to_string(), + } + } +} + +impl Widget for Title +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame)?; + frame.set_title(Some(self.title)); + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Title +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame).await?; + frame.set_title(Some(self.title)); + Ok(()) + } +} diff --git a/src/widthdb.rs b/src/widthdb.rs index 6af28c3..fe5a26e 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -6,51 +6,129 @@ use crossterm::style::Print; use crossterm::terminal::{Clear, ClearType}; use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; + +use crate::wrap; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum WidthEstimationMethod { + /// Estimate the width of a grapheme using legacy methods. + /// + /// Different terminal emulators all use different approaches to determine + /// grapheme widths, so this method will never be able to give a fully + /// correct solution. For that, the only possible approach is measuring the + /// actual grapheme width. + #[default] + Legacy, + + /// Estimate the width of a grapheme using the unicode standard in a + /// best-effort manner. + Unicode, +} /// Measures and stores the with (in terminal coordinates) of graphemes. -#[derive(Debug, Default)] -pub struct WidthDB { +#[derive(Debug)] +pub struct WidthDb { + pub(crate) estimate: WidthEstimationMethod, + pub(crate) measure: bool, + pub(crate) tab_width: u8, known: HashMap, requested: HashSet, } -impl WidthDB { +impl Default for WidthDb { + fn default() -> Self { + Self { + estimate: WidthEstimationMethod::default(), + measure: false, + tab_width: 8, + known: Default::default(), + requested: Default::default(), + } + } +} + +impl WidthDb { + /// Determine the width of a tab character starting at the specified column. + fn tab_width_at_column(&self, col: usize) -> u8 { + self.tab_width - (col % self.tab_width as usize) as u8 + } + /// Determine the width of a grapheme. /// - /// If the width has not been measured yet, it is estimated using the - /// Unicode Standard Annex #11. - pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { + /// If the grapheme is a tab, the column is used to determine its width. + /// + /// If the width has not been measured yet or measurements are turned off, + /// it is estimated using the Unicode Standard Annex #11. + pub fn grapheme_width(&mut self, grapheme: &str, col: usize) -> u8 { assert_eq!(Some(grapheme), grapheme.graphemes(true).next()); - if let Some(width) = self.known.get(grapheme) { - *width - } else { + if grapheme == "\t" { + return self.tab_width_at_column(col); + } + + if self.measure { + if let Some(width) = self.known.get(grapheme) { + return *width; + } self.requested.insert(grapheme.to_string()); - grapheme.width() as u8 + } + + match self.estimate { + // A character-wise width calculation is a simple and obvious + // approach to compute character widths. The idea is that dumb + // terminal emulators tend to do something roughly like this, and + // smart terminal emulators try to emulate dumb ones for + // compatibility. In practice, this approach seems to be fairly + // robust. + WidthEstimationMethod::Legacy => grapheme + .chars() + .filter(|c| !c.is_ascii_control()) + .flat_map(|c| c.width()) + .sum::() + .try_into() + .unwrap_or(u8::MAX), + + // The unicode width crate considers control chars to have a width + // of 1 even though they usually have a width of 0 when displayed. + WidthEstimationMethod::Unicode => grapheme + .split(|c: char| c.is_ascii_control()) + .map(|s| s.width()) + .sum::() + .try_into() + .unwrap_or(u8::MAX), } } /// Determine the width of a string based on its graphemes. /// - /// If the width of a grapheme has not been measured yet, it is estimated - /// using the Unicode Standard Annex #11. + /// If a grapheme is a tab, its column is used to determine its width. + /// + /// If the width of a grapheme has not been measured yet or measurements are + /// turned off, it is estimated using the Unicode Standard Annex #11. pub fn width(&mut self, s: &str) -> usize { let mut total: usize = 0; for grapheme in s.graphemes(true) { - total += if let Some(width) = self.known.get(grapheme) { - (*width).into() - } else { - self.requested.insert(grapheme.to_string()); - grapheme.width() - }; + total += self.grapheme_width(grapheme, total) as usize; } total } + /// Perform primitive word wrapping with the specified maximum width. + /// + /// Returns the byte offsets at which the string should be split into lines. + /// An offset of 1 would mean the first line contains only a single byte. + /// These offsets lie on grapheme boundaries. + /// + /// This function does not support bidirectional script. It assumes the + /// entire text has the same direction. + pub fn wrap(&mut self, text: &str, width: usize) -> Vec { + wrap::wrap(self, text, width) + } + /// Whether any new graphemes have been seen since the last time /// [`Self::measure_widths`] was called. - pub fn measuring_required(&self) -> bool { - !self.requested.is_empty() + pub(crate) fn measuring_required(&self) -> bool { + self.measure && !self.requested.is_empty() } /// Measure the width of all new graphemes that have been seen since the @@ -59,8 +137,20 @@ impl WidthDB { /// This function measures the actual width of graphemes by writing them to /// the terminal. After it finishes, the terminal's contents should be /// assumed to be garbage and a full redraw should be performed. - pub fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { + pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { + if !self.measure { + return Ok(()); + } for grapheme in self.requested.drain() { + if grapheme.chars().any(|c| c.is_ascii_control()) { + // ASCII control characters like the escape character or the + // bell character tend to be interpreted specially by terminals. + // This may break width measurements. To avoid this, we just + // assign each control character a with of 0. + self.known.insert(grapheme, 0); + continue; + } + out.queue(Clear(ClearType::All))? .queue(MoveTo(0, 0))? .queue(Print(&grapheme))?; diff --git a/src/wrap.rs b/src/wrap.rs index 309214a..a1ef1d4 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -3,21 +3,21 @@ use unicode_linebreak::BreakOpportunity; use unicode_segmentation::UnicodeSegmentation; -use crate::widthdb::WidthDB; +use crate::WidthDb; -// TODO Handle tabs separately? -// TODO Convert into an iterator? -pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { +pub fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { let mut breaks = vec![]; let mut break_options = unicode_linebreak::linebreaks(text).peekable(); // The last valid break point encountered and its width let mut valid_break = None; - let mut valid_break_width = 0; - // Width of the line at the current grapheme + // Starting index and width of the line at the current grapheme (with and + // without trailing whitespace) + let mut current_start = 0; let mut current_width = 0; + let mut current_width_trimmed = 0; for (gi, g) in text.grapheme_indices(true) { // Advance break options @@ -36,60 +36,56 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { BreakOpportunity::Mandatory => { breaks.push(bi); valid_break = None; - valid_break_width = 0; + current_start = bi; current_width = 0; + current_width_trimmed = 0; } BreakOpportunity::Allowed => { valid_break = Some(bi); - valid_break_width = current_width; } } } - let grapheme_width: usize = widthdb.grapheme_width(g).into(); - if current_width + grapheme_width > width { - if current_width == 0 { - // The grapheme is wider than the maximum width, so we'll allow - // it, thereby forcing the following grapheme to break no matter - // what (either because of a mandatory or allowed break, or via - // a forced break). - } else if let Some(bi) = valid_break { - // We can't fit the grapheme onto the current line, so we'll - // just break at the last valid break point. + // Calculate widths after current grapheme + let g_is_whitespace = g.chars().all(|c| c.is_whitespace()); + let g_width = widthdb.grapheme_width(g, current_width) as usize; + current_width += g_width; + if !g_is_whitespace { + current_width_trimmed = current_width; + } + + // Wrap at last break point if necessary + if current_width_trimmed > width { + if let Some(bi) = valid_break { + let new_line = &text[bi..gi + g.len()]; + breaks.push(bi); - current_width -= valid_break_width; valid_break = None; - valid_break_width = 0; - } else { - // Forced break in the midde of a normally non-breakable chunk - // because there have been no valid break points yet. - breaks.push(gi); - valid_break = None; - valid_break_width = 0; - current_width = 0; + current_start = bi; + current_width = widthdb.width(new_line); + current_width_trimmed = widthdb.width(new_line.trim_end()); } } - current_width += grapheme_width; + // Perform a forced break if still necessary + if current_width_trimmed > width { + if current_start == gi { + // The grapheme is the only thing on the current line and it is + // wider than the maximum width, so we'll allow it, thereby + // forcing the following grapheme to break no matter what + // (either because of a mandatory or allowed break, or via a + // forced break). + } else { + // Forced break in the middle of a normally non-breakable chunk + // because there are no valid break points. + breaks.push(gi); + valid_break = None; + current_start = gi; + current_width = widthdb.grapheme_width(g, 0).into(); + current_width_trimmed = if g_is_whitespace { 0 } else { current_width }; + } + } } breaks } - -pub fn split_at_indices<'a>(s: &'a str, indices: &[usize]) -> Vec<&'a str> { - let mut slices = vec![]; - - let mut rest = s; - let mut offset = 0; - - for i in indices { - let (left, right) = rest.split_at(i - offset); - slices.push(left); - rest = right; - offset = *i; - } - - slices.push(rest); - - slices -}