diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a89179..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 deleted file mode 100644 index bef60bc..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,70 +0,0 @@ -# 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 22967fc..566c4c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "toss" -version = "0.3.4" +version = "0.1.0" edition = "2021" [dependencies] -async-trait = "0.1.83" -crossterm = "0.28.1" -unicode-linebreak = "0.1.5" -unicode-segmentation = "1.12.0" -unicode-width = "0.2.0" +anyhow = "1.0.57" +crossterm = "0.23.2" +unicode-blocks = "0.1.4" +unicode-segmentation = "1.9.0" +unicode-width = "0.1.9" diff --git a/examples/hello_world.rs b/examples/hello_world.rs deleted file mode 100644 index 62c0c75..0000000 --- a/examples/hello_world.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crossterm::event::Event; -use crossterm::style::Stylize; -use toss::{Frame, Pos, Style, Terminal}; - -fn draw(f: &mut Frame) { - f.write(Pos::new(0, 0), ("Hello world!", Style::new().green())); - f.write( - Pos::new(0, 1), - ("Press any key to exit", Style::new().on_dark_blue()), - ); - f.show_cursor(Pos::new(16, 0)); -} - -fn render_frame(term: &mut Terminal) { - let mut dirty = true; - while dirty { - term.autoresize().unwrap(); - draw(term.frame()); - 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 - // 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/hello_world_widgets.rs b/examples/hello_world_widgets.rs deleted file mode 100644 index 000cf91..0000000 --- a/examples/hello_world_widgets.rs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index c90c4ae..0000000 --- a/examples/overlapping_graphemes.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crossterm::event::Event; -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.", - ); - 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(0, 6), - "Wide graphemes at the edges of the screen apply their style, but are not", - ); - 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)); - - let scientist = "👩‍🔬"; - f.write( - Pos::new(0, 12), - "Most terminals ignore the zero width joiner and display this female", - ); - f.write( - Pos::new(0, 13), - "scientist emoji as a woman and a microscope: 👩‍🔬", - ); - 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) { - let mut dirty = true; - while dirty { - term.autoresize().unwrap(); - draw(term.frame()); - 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 - // 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/text_wrapping.rs b/examples/text_wrapping.rs deleted file mode 100644 index 5292378..0000000 --- a/examples/text_wrapping.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crossterm::event::Event; -use toss::{Frame, Pos, Styled, Terminal}; - -fn draw(f: &mut Frame) { - let text = concat!( - "This is a short paragraph in order to demonstrate unicode-aware word wrapping. ", - "Resize your terminal to different widths to try it out. ", - "After this sentence come two newlines, so it should always break here.\n", - "\n", - "Since the wrapping algorithm is aware of the Unicode Standard Annex #14, ", - "it understands things like non-breaking spaces and word joiners: ", - "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).\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 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) { - let mut dirty = true; - while dirty { - term.autoresize().unwrap(); - draw(term.frame()); - 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 - // 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/src/buffer.rs b/src/buffer.rs deleted file mode 100644 index 022145b..0000000 --- a/src/buffer.rs +++ /dev/null @@ -1,354 +0,0 @@ -use std::ops::Range; - -use crossterm::style::ContentStyle; - -use crate::{Pos, Size, Style, Styled, WidthDb}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Cell { - pub content: Box, - pub style: ContentStyle, - pub width: u8, - pub offset: u8, -} - -impl Default for Cell { - fn default() -> Self { - Self { - content: " ".to_string().into_boxed_str(), - style: ContentStyle::default(), - width: 1, - offset: 0, - } - } -} - -#[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); - - let x: usize = x.into(); - let y: usize = y.into(); - let width: usize = self.size.width.into(); - - y * width + x - } - - /// 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.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. The stack is reset as well. - pub fn resize(&mut self, size: Size) { - if size == self.size { - self.data.fill_with(Cell::default); - } else { - let width: usize = size.width.into(); - let height: usize = size.height.into(); - let len = width * height; - - self.size = size; - self.data.clear(); - self.data.resize_with(len, Cell::default); - } - - self.cursor = None; - - self.stack.clear(); - } - - /// Reset the contents and stack of the buffer. - /// - /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. - pub fn reset(&mut self) { - 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. 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; - } - } - - /// 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; - - 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); - } - } - } - - /// Write a single grapheme to the buffer, respecting its width. - /// - /// Assumes that `pos.y` is in range. - 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 - - if start_x > max_x || end_x < min_x { - return; // Not visible - } - - 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.cover(base_style), - width, - offset, - }; - } - } else { - // Partially visible, write empty cells with correct style - 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.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<'_> { - Cells { - buffer: self, - x: 0, - y: 0, - } - } -} - -pub struct Cells<'a> { - buffer: &'a Buffer, - x: u16, - y: u16, -} - -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; - } - - let (x, y) = (self.x, self.y); - let cell = self.buffer.at(self.x, self.y); - assert!(cell.offset == 0); - - self.x += cell.width as u16; - if self.x >= self.buffer.size.width { - self.x = 0; - self.y += 1; - } - - Some((x, y, cell)) - } -} diff --git a/src/coords.rs b/src/coords.rs deleted file mode 100644 index 1735746..0000000 --- a/src/coords.rs +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index e42ba6b..0000000 --- a/src/frame.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Rendering the next frame. - -use crate::buffer::Buffer; -use crate::{Pos, Size, Styled, WidthDb}; - -#[derive(Debug, Default)] -pub struct Frame { - pub(crate) widthdb: WidthDb, - pub(crate) buffer: Buffer, - 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.title = None; - } - - pub fn cursor(&self) -> Option { - self.buffer.cursor() - } - - pub fn set_cursor(&mut self, pos: Option) { - self.buffer.set_cursor(pos); - } - - pub fn show_cursor(&mut self, pos: Pos) { - self.set_cursor(Some(pos)); - } - - pub fn hide_cursor(&mut self) { - self.set_cursor(None); - } - - pub fn set_title(&mut self, title: Option) { - self.title = title; - } - - pub fn set_bell(&mut self, bell: bool) { - self.bell = bell; - } - - pub fn widthdb(&mut self) -> &mut WidthDb { - &mut self.widthdb - } - - 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 a204e8c..1b4a90c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,8 @@ -#![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; -mod coords; -mod frame; -mod style; -mod styled; -mod terminal; -mod widget; -pub mod widgets; -mod widthdb; -mod wrap; - -pub use coords::*; -pub use frame::*; -pub use style::*; -pub use styled::*; -pub use terminal::*; -pub use widget::*; -pub use widthdb::*; +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6862b4b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; +use std::{io, slice}; + +use crossterm::cursor::{self, MoveTo}; +use crossterm::execute; +use crossterm::style::Print; +use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; +use unicode_blocks::UnicodeBlock; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +struct WidthDB(HashMap); + +impl WidthDB { + fn new() -> Self { + Self(HashMap::new()) + } + + fn measure(&mut self, s: &str) -> anyhow::Result<()> { + let mut stdout = io::stdout(); + for grapheme in s.graphemes(true) { + execute!(stdout, EnterAlternateScreen, MoveTo(0, 0), Print(grapheme))?; + let width = cursor::position()?.0 as u8; + if width != grapheme.width() as u8 { + self.0.insert(grapheme.to_string(), width); + } + execute!(stdout, LeaveAlternateScreen)?; + } + Ok(()) + } + + fn measure_block(&mut self, block: UnicodeBlock) -> anyhow::Result<()> { + for c in block.start()..=block.end() { + let c = char::from_u32(c).unwrap(); + let s = c.to_string(); + self.measure(&s)?; + } + Ok(()) + } +} + +fn main() { + let mut widthdb = WidthDB::new(); + + // widthdb.measure_block(unicode_blocks::BASIC_LATIN); + // widthdb.measure_block(unicode_blocks::LATIN_1_SUPPLEMENT); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_A); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_ADDITIONAL); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_B); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_C); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_D); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_E); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_F); + // widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_G); + // widthdb.measure_block(unicode_blocks::EMOTICONS); + widthdb.measure_block(unicode_blocks::BOX_DRAWING); + + println!(); + for (grapheme, width) in widthdb.0 { + let expected = grapheme.width(); + println!("{grapheme} = {width} (expected: {expected})"); + } +} diff --git a/src/style.rs b/src/style.rs deleted file mode 100644 index 56c66ce..0000000 --- a/src/style.rs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 33a8285..0000000 --- a/src/styled.rs +++ /dev/null @@ -1,195 +0,0 @@ -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 deleted file mode 100644 index 07fe686..0000000 --- a/src/terminal.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! Displaying frames on a terminal. - -use std::io::{self, Write}; -use std::mem; - -use crossterm::cursor::{Hide, MoveTo, Show}; -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; -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, - /// The frame being currently rendered. - frame: Frame, - /// Buffer from the previous frame. - prev_frame_buffer: Buffer, - /// When the screen is updated next, it must be cleared and redrawn fully - /// instead of performing an incremental update. - full_redraw: bool, -} - -impl Drop for Terminal { - fn drop(&mut self) { - 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, - frame: Frame::default(), - prev_frame_buffer: Buffer::default(), - full_redraw: true, - }; - 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 }; - if size != self.frame.size() { - self.frame.buffer.resize(size); - self.prev_frame_buffer.resize(size); - self.full_redraw = true; - } - - 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. - /// - /// 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<()> { - 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 { - 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.update_title()?; - self.ring_bell()?; - - Ok(()) - } - - fn draw_differences(&mut self) -> io::Result<()> { - for (x, y, cell) in self.frame.buffer.cells() { - if self.prev_frame_buffer.at(x, y) == cell { - continue; - } - - let content = StyledContent::new(cell.style, &cell.content as &str); - self.out - .queue(MoveTo(x, y))? - .queue(PrintStyledContent(content))?; - } - Ok(()) - } - - fn update_cursor(&mut self) -> io::Result<()> { - if let Some(pos) = self.frame.cursor() { - let size = self.frame.size(); - let x_in_bounds = 0 <= pos.x && pos.x < size.width as i32; - let y_in_bounds = 0 <= pos.y && pos.y < size.height as i32; - if x_in_bounds && y_in_bounds { - self.out - .queue(Show)? - .queue(MoveTo(pos.x as u16, pos.y as u16))?; - return Ok(()); - } - } - - 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 deleted file mode 100644 index 356a047..0000000 --- a/src/widget.rs +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index cbbff7c..0000000 --- a/src/widgets.rs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index d0ba530..0000000 --- a/src/widgets/background.rs +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index b37fb67..0000000 --- a/src/widgets/bell.rs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 062cd8f..0000000 --- a/src/widgets/border.rs +++ /dev/null @@ -1,201 +0,0 @@ -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 deleted file mode 100644 index 3d9713f..0000000 --- a/src/widgets/boxed.rs +++ /dev/null @@ -1,142 +0,0 @@ -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 deleted file mode 100644 index 2bb8199..0000000 --- a/src/widgets/cursor.rs +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 67e7488..0000000 --- a/src/widgets/desync.rs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index aa36e8e..0000000 --- a/src/widgets/editor.rs +++ /dev/null @@ -1,485 +0,0 @@ -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 deleted file mode 100644 index cb9a55d..0000000 --- a/src/widgets/either.rs +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 5de4fdf..0000000 --- a/src/widgets/empty.rs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 5cfb349..0000000 --- a/src/widgets/float.rs +++ /dev/null @@ -1,166 +0,0 @@ -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 deleted file mode 100644 index 20cd413..0000000 --- a/src/widgets/join.rs +++ /dev/null @@ -1,721 +0,0 @@ -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 deleted file mode 100644 index af3da5e..0000000 --- a/src/widgets/layer.rs +++ /dev/null @@ -1,201 +0,0 @@ -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 deleted file mode 100644 index be3aff6..0000000 --- a/src/widgets/padding.rs +++ /dev/null @@ -1,133 +0,0 @@ -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 deleted file mode 100644 index 8301f1e..0000000 --- a/src/widgets/predrawn.rs +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 81e30b5..0000000 --- a/src/widgets/resize.rs +++ /dev/null @@ -1,120 +0,0 @@ -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 deleted file mode 100644 index 007f4fe..0000000 --- a/src/widgets/text.rs +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index c0dc0d4..0000000 --- a/src/widgets/title.rs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index fe5a26e..0000000 --- a/src/widthdb.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::io::{self, Write}; - -use crossterm::cursor::MoveTo; -use crossterm::style::Print; -use crossterm::terminal::{Clear, ClearType}; -use crossterm::QueueableCommand; -use unicode_segmentation::UnicodeSegmentation; -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)] -pub struct WidthDb { - pub(crate) estimate: WidthEstimationMethod, - pub(crate) measure: bool, - pub(crate) tab_width: u8, - known: HashMap, - requested: HashSet, -} - -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 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 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()); - } - - 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 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 += 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(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 - /// last time this function was called. - /// - /// 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(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))?; - out.flush()?; - let width = crossterm::cursor::position()?.0 as u8; - self.known.insert(grapheme, width); - } - Ok(()) - } -} diff --git a/src/wrap.rs b/src/wrap.rs deleted file mode 100644 index a1ef1d4..0000000 --- a/src/wrap.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Word wrapping for text. - -use unicode_linebreak::BreakOpportunity; -use unicode_segmentation::UnicodeSegmentation; - -use crate::WidthDb; - -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; - - // 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 - let (bi, b) = loop { - let (bi, b) = break_options.peek().expect("not at end of string yet"); - if *bi < gi { - break_options.next(); - } else { - break (*bi, b); - } - }; - - // Evaluate break options at the current position - if bi == gi { - match b { - BreakOpportunity::Mandatory => { - breaks.push(bi); - valid_break = None; - current_start = bi; - current_width = 0; - current_width_trimmed = 0; - } - BreakOpportunity::Allowed => { - valid_break = Some(bi); - } - } - } - - // 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); - valid_break = None; - current_start = bi; - current_width = widthdb.width(new_line); - current_width_trimmed = widthdb.width(new_line.trim_end()); - } - } - - // 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 -}