From bbaea3b5bf7b10350739bb99d51593ed439b4e15 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 21 May 2022 19:59:07 +0200 Subject: [PATCH 001/144] Create simple terminal and buffer abstractions --- Cargo.toml | 3 + examples/hello_world.rs | 35 +++++++++ src/buffer.rs | 164 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 10 +-- src/terminal.rs | 86 +++++++++++++++++++++ 5 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 examples/hello_world.rs create mode 100644 src/buffer.rs create mode 100644 src/terminal.rs diff --git a/Cargo.toml b/Cargo.toml index 46d5c0f..b714450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] +crossterm = "0.23.2" +unicode-segmentation = "1.9.0" +unicode-width = "0.1.9" diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 0000000..0b4d6ca --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,35 @@ +use std::io; + +use crossterm::style::{ContentStyle, Stylize}; +use toss::buffer::Pos; +use toss::terminal::Terminal; + +fn main() -> io::Result<()> { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new(Box::new(io::stdout()))?; + + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize()?; + + // Render things to the buffer + let b = term.buffer(); + b.write( + Pos::new(0, 0), + "Hello world!", + ContentStyle::default().green(), + ); + b.write( + Pos::new(0, 1), + "Press any key to exit", + ContentStyle::default().on_dark_blue(), + ); + + // Show the buffer's contents on screen + term.present()?; + + // Wait for input before exiting + let _ = crossterm::event::read(); + + Ok(()) +} diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..256e3ab --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,164 @@ +use crossterm::style::ContentStyle; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Size { + pub width: u16, + pub height: u16, +} + +impl Size { + pub const ZERO: Self = Self { + width: 0, + height: 0, + }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pos { + pub x: i32, + pub y: i32, +} + +impl Pos { + pub const ZERO: Self = Self { x: 0, y: 0 }; + + pub fn new(x: i32, y: i32) -> Self { + Self { x, y } + } +} + +#[derive(Debug, Clone)] +pub struct Cell { + pub content: Box, + pub style: ContentStyle, + pub width: u8, + pub offset: u8, +} + +impl Cell { + pub fn empty() -> Self { + Self { + content: " ".to_string().into_boxed_str(), + style: ContentStyle::default(), + width: 1, + offset: 0, + } + } +} + +pub struct Buffer { + size: Size, + data: Vec, +} + +impl Buffer { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + size: Size::ZERO, + data: vec![], + } + } + + 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 + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn resize(&mut self, size: Size) { + 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::empty); + } + + /// Reset all cells of the buffer to be empty and have no styling. + pub fn reset(&mut self) { + self.resize(self.size); + } + + pub fn write(&mut self, mut pos: Pos, content: &str, style: ContentStyle) { + if pos.y < 0 || pos.y >= self.size.height as i32 { + return; + } + + for grapheme in content.graphemes(true) { + let width = grapheme.width().max(1) as u8; // TODO Use actual width + if pos.x >= 0 && pos.x + width as i32 <= self.size.width as i32 { + // Grapheme fits on buffer in its entirety + let grapheme = grapheme.to_string().into_boxed_str(); + + for offset in 0..width { + let i = self.index(pos.x as u16 + offset as u16, pos.y as u16); + self.data[i] = Cell { + content: grapheme.clone(), + style, + width, + offset, + }; + } + } + + pos.x += width as i32; + } + } + + 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> Cells<'a> { + fn at(&self, x: u16, y: u16) -> &'a Cell { + assert!(x < self.buffer.size.width); + assert!(y < self.buffer.size.height); + &self.buffer.data[self.buffer.index(x, y)] + } +} + +impl<'a> Iterator for Cells<'a> { + type Item = (u16, u16, &'a Cell); + + fn next(&mut self) -> Option { + if self.y >= self.buffer.size.height { + return None; + } + + let (x, y) = (self.x, self.y); + let cell = self.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/lib.rs b/src/lib.rs index 1b4a90c..7c6e2ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,2 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} +pub mod buffer; +pub mod terminal; diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..482644b --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,86 @@ +use std::io::Write; +use std::{io, mem}; + +use crossterm::cursor::MoveTo; +use crossterm::style::{PrintStyledContent, StyledContent}; +use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::{ExecutableCommand, QueueableCommand}; + +use crate::buffer::{Buffer, Size}; + +pub struct Terminal { + out: Box, + /// Currently visible on screen. + prev_buffer: Buffer, + /// Buffer to render to. + curr_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.out.execute(LeaveAlternateScreen); + let _ = crossterm::terminal::disable_raw_mode(); + } +} + +impl Terminal { + pub fn new(out: Box) -> io::Result { + let mut result = Self { + out, + prev_buffer: Buffer::new(), + curr_buffer: Buffer::new(), + full_redraw: true, + }; + crossterm::terminal::enable_raw_mode()?; + result.out.execute(EnterAlternateScreen)?; + Ok(result) + } + + pub fn buffer(&mut self) -> &mut Buffer { + &mut self.curr_buffer + } + + pub fn autoresize(&mut self) -> io::Result<()> { + let (width, height) = crossterm::terminal::size()?; + let size = Size { width, height }; + if size != self.curr_buffer.size() { + self.prev_buffer.resize(size); + self.curr_buffer.resize(size); + self.full_redraw = true; + } + + Ok(()) + } + + /// Display the contents of the buffer on the screen and prepare rendering + /// the next frame. + pub fn present(&mut self) -> io::Result<()> { + if self.full_redraw { + io::stdout().queue(Clear(ClearType::All))?; + self.prev_buffer.reset(); + self.full_redraw = false; + } + + self.draw_differences()?; + self.out.flush()?; + + mem::swap(&mut self.prev_buffer, &mut self.curr_buffer); + + Ok(()) + } + + fn draw_differences(&mut self) -> io::Result<()> { + // TODO Only draw the differences + for (x, y, cell) in self.curr_buffer.cells() { + let content = StyledContent::new(cell.style, &cell.content as &str); + self.out + .queue(MoveTo(x, y))? + .queue(PrintStyledContent(content))?; + } + + Ok(()) + } +} From cc2f102141b75d4c5e93b39a3a1b3de2736c01fc Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 21 May 2022 21:07:24 +0200 Subject: [PATCH 002/144] Position cursor via buffer --- examples/hello_world.rs | 1 + src/buffer.rs | 39 +++++++++++++++++++++++++++++++-------- src/terminal.rs | 20 +++++++++++++++++++- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 0b4d6ca..44c10fa 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -24,6 +24,7 @@ fn main() -> io::Result<()> { "Press any key to exit", ContentStyle::default().on_dark_blue(), ); + b.set_cursor(Some(Pos::new(16, 0))); // Show the buffer's contents on screen term.present()?; diff --git a/src/buffer.rs b/src/buffer.rs index 256e3ab..c666cda 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -51,6 +51,7 @@ impl Cell { pub struct Buffer { size: Size, data: Vec, + cursor: Option, } impl Buffer { @@ -59,6 +60,7 @@ impl Buffer { Self { size: Size::ZERO, data: vec![], + cursor: None, } } @@ -77,19 +79,32 @@ impl Buffer { self.size } + /// Resize the buffer and reset its contents. + /// + /// The buffer's contents are reset even if the buffer is already the + /// correct size. pub fn resize(&mut self, size: Size) { - let width: usize = size.width.into(); - let height: usize = size.height.into(); - let len = width * height; + if size == self.size() { + self.data.fill_with(Cell::empty); + self.cursor = None; + } 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::empty); + self.size = size; + self.data.clear(); + self.data.resize_with(len, Cell::empty); + self.cursor = None; + } } - /// Reset all cells of the buffer to be empty and have no styling. + /// Reset the contents of the buffer. + /// + /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. pub fn reset(&mut self) { - self.resize(self.size); + self.data.fill_with(Cell::empty); + self.cursor = None; } pub fn write(&mut self, mut pos: Pos, content: &str, style: ContentStyle) { @@ -118,6 +133,14 @@ impl Buffer { } } + pub fn cursor(&self) -> Option { + self.cursor + } + + pub fn set_cursor(&mut self, pos: Option) { + self.cursor = pos; + } + pub fn cells(&self) -> Cells<'_> { Cells { buffer: self, diff --git a/src/terminal.rs b/src/terminal.rs index 482644b..7653855 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,7 +1,7 @@ use std::io::Write; use std::{io, mem}; -use crossterm::cursor::MoveTo; +use crossterm::cursor::{Hide, MoveTo, Show}; use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::{ExecutableCommand, QueueableCommand}; @@ -65,9 +65,11 @@ impl Terminal { } self.draw_differences()?; + self.update_cursor()?; self.out.flush()?; mem::swap(&mut self.prev_buffer, &mut self.curr_buffer); + self.curr_buffer.reset(); Ok(()) } @@ -80,7 +82,23 @@ impl Terminal { .queue(MoveTo(x, y))? .queue(PrintStyledContent(content))?; } + Ok(()) + } + fn update_cursor(&mut self) -> io::Result<()> { + if let Some(pos) = self.curr_buffer.cursor() { + let size = self.curr_buffer.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(()) } } From add2f25aba10d8d39ba96567ba8b719eed3187c7 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 21 May 2022 22:33:37 +0200 Subject: [PATCH 003/144] Use Frame for rendering instead of Buffer --- examples/hello_world.rs | 16 ++++++------ src/buffer.rs | 24 ++---------------- src/frame.rs | 41 +++++++++++++++++++++++++++++++ src/lib.rs | 3 ++- src/terminal.rs | 54 ++++++++++++++++++++++++----------------- 5 files changed, 85 insertions(+), 53 deletions(-) create mode 100644 src/frame.rs diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 44c10fa..f8414a2 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,32 +1,32 @@ use std::io; use crossterm::style::{ContentStyle, Stylize}; -use toss::buffer::Pos; +use toss::frame::Pos; use toss::terminal::Terminal; fn main() -> io::Result<()> { // Automatically enters alternate screen and enables raw mode - let mut term = Terminal::new(Box::new(io::stdout()))?; + let mut term = Terminal::new()?; // Must be called before rendering, otherwise the terminal has out-of-date // size information and will present garbage. term.autoresize()?; - // Render things to the buffer - let b = term.buffer(); - b.write( + // Render stuff onto the next frame + let f = term.frame(); + f.write( Pos::new(0, 0), "Hello world!", ContentStyle::default().green(), ); - b.write( + f.write( Pos::new(0, 1), "Press any key to exit", ContentStyle::default().on_dark_blue(), ); - b.set_cursor(Some(Pos::new(16, 0))); + f.show_cursor(Pos::new(16, 0)); - // Show the buffer's contents on screen + // Show the next frame on the screen term.present()?; // Wait for input before exiting diff --git a/src/buffer.rs b/src/buffer.rs index c666cda..266b0ab 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -2,7 +2,7 @@ use crossterm::style::ContentStyle; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Size { pub width: u16, pub height: u16, @@ -48,22 +48,13 @@ impl Cell { } } +#[derive(Debug, Default)] pub struct Buffer { size: Size, data: Vec, - cursor: Option, } impl Buffer { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self { - size: Size::ZERO, - data: vec![], - cursor: None, - } - } - fn index(&self, x: u16, y: u16) -> usize { assert!(x < self.size.width); assert!(y < self.size.height); @@ -86,7 +77,6 @@ impl Buffer { pub fn resize(&mut self, size: Size) { if size == self.size() { self.data.fill_with(Cell::empty); - self.cursor = None; } else { let width: usize = size.width.into(); let height: usize = size.height.into(); @@ -95,7 +85,6 @@ impl Buffer { self.size = size; self.data.clear(); self.data.resize_with(len, Cell::empty); - self.cursor = None; } } @@ -104,7 +93,6 @@ impl Buffer { /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. pub fn reset(&mut self) { self.data.fill_with(Cell::empty); - self.cursor = None; } pub fn write(&mut self, mut pos: Pos, content: &str, style: ContentStyle) { @@ -133,14 +121,6 @@ impl Buffer { } } - pub fn cursor(&self) -> Option { - self.cursor - } - - pub fn set_cursor(&mut self, pos: Option) { - self.cursor = pos; - } - pub fn cells(&self) -> Cells<'_> { Cells { buffer: self, diff --git a/src/frame.rs b/src/frame.rs new file mode 100644 index 0000000..ae54a2e --- /dev/null +++ b/src/frame.rs @@ -0,0 +1,41 @@ +use crossterm::style::ContentStyle; + +use crate::buffer::Buffer; +pub use crate::buffer::{Pos, Size}; + +#[derive(Debug, Default)] +pub struct Frame { + pub(crate) buffer: Buffer, + cursor: Option, +} + +impl Frame { + pub fn size(&self) -> Size { + self.buffer.size() + } + + pub fn reset(&mut self) { + self.buffer.reset(); + self.cursor = None; + } + + pub fn cursor(&self) -> Option { + self.cursor + } + + pub fn set_cursor(&mut self, pos: Option) { + self.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 write(&mut self, pos: Pos, content: &str, style: ContentStyle) { + self.buffer.write(pos, content, style); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7c6e2ac..56465e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ -pub mod buffer; +mod buffer; +pub mod frame; pub mod terminal; diff --git a/src/terminal.rs b/src/terminal.rs index 7653855..cfb7510 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -7,13 +7,15 @@ use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternate use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::{Buffer, Size}; +use crate::frame::Frame; pub struct Terminal { + /// Render target. out: Box, - /// Currently visible on screen. - prev_buffer: Buffer, - /// Buffer to render to. - curr_buffer: Buffer, + /// 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, @@ -27,11 +29,15 @@ impl Drop for Terminal { } impl Terminal { - pub fn new(out: Box) -> io::Result { + pub fn new() -> io::Result { + Self::with_target(Box::new(io::stdout())) + } + + pub fn with_target(out: Box) -> io::Result { let mut result = Self { out, - prev_buffer: Buffer::new(), - curr_buffer: Buffer::new(), + frame: Frame::default(), + prev_frame_buffer: Buffer::default(), full_redraw: true, }; crossterm::terminal::enable_raw_mode()?; @@ -39,28 +45,32 @@ impl Terminal { Ok(result) } - pub fn buffer(&mut self) -> &mut Buffer { - &mut self.curr_buffer - } - + /// Resize the frame and other internal buffers if the terminal size has + /// changed. pub fn autoresize(&mut self) -> io::Result<()> { let (width, height) = crossterm::terminal::size()?; let size = Size { width, height }; - if size != self.curr_buffer.size() { - self.prev_buffer.resize(size); - self.curr_buffer.resize(size); + if size != self.frame.size() { + self.frame.buffer.resize(size); + self.prev_frame_buffer.resize(size); self.full_redraw = true; } Ok(()) } - /// Display the contents of the buffer on the screen and prepare rendering - /// the next frame. + pub fn frame(&mut self) -> &mut Frame { + &mut self.frame + } + + /// Display the current frame on the screen and prepare the next frame. + /// + /// After calling this function, the frame returned by [`Self::frame`] will + /// be empty again and have no cursor position. pub fn present(&mut self) -> io::Result<()> { if self.full_redraw { io::stdout().queue(Clear(ClearType::All))?; - self.prev_buffer.reset(); + self.prev_frame_buffer.reset(); // Because the screen is now empty self.full_redraw = false; } @@ -68,15 +78,15 @@ impl Terminal { self.update_cursor()?; self.out.flush()?; - mem::swap(&mut self.prev_buffer, &mut self.curr_buffer); - self.curr_buffer.reset(); + mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); + self.frame.reset(); Ok(()) } fn draw_differences(&mut self) -> io::Result<()> { // TODO Only draw the differences - for (x, y, cell) in self.curr_buffer.cells() { + for (x, y, cell) in self.frame.buffer.cells() { let content = StyledContent::new(cell.style, &cell.content as &str); self.out .queue(MoveTo(x, y))? @@ -86,8 +96,8 @@ impl Terminal { } fn update_cursor(&mut self) -> io::Result<()> { - if let Some(pos) = self.curr_buffer.cursor() { - let size = self.curr_buffer.size(); + 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 { From 9512ddaa3b45369543002e0a62fb537c74a028e0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 21 May 2022 23:32:15 +0200 Subject: [PATCH 004/144] Measure actual width of displayed characters --- examples/hello_world.rs | 31 +++++++++++++++------------ src/buffer.rs | 13 +++++++++--- src/frame.rs | 8 ++++++- src/lib.rs | 1 + src/terminal.rs | 17 +++++++++++++-- src/widthdb.rs | 46 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 src/widthdb.rs diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f8414a2..13c1475 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,19 +1,10 @@ use std::io; use crossterm::style::{ContentStyle, Stylize}; -use toss::frame::Pos; +use toss::frame::{Frame, Pos}; use toss::terminal::Terminal; -fn main() -> io::Result<()> { - // Automatically enters alternate screen and enables raw mode - let mut term = Terminal::new()?; - - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. - term.autoresize()?; - - // Render stuff onto the next frame - let f = term.frame(); +fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), "Hello world!", @@ -25,9 +16,23 @@ fn main() -> io::Result<()> { ContentStyle::default().on_dark_blue(), ); f.show_cursor(Pos::new(16, 0)); +} - // Show the next frame on the screen - term.present()?; +fn main() -> io::Result<()> { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new()?; + + loop { + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize()?; + + draw(term.frame()); + + if !term.present()? { + break; + } + } // Wait for input before exiting let _ = crossterm::event::read(); diff --git a/src/buffer.rs b/src/buffer.rs index 266b0ab..d8c169c 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,6 +1,7 @@ use crossterm::style::ContentStyle; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; + +use crate::widthdb::WidthDB; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Size { @@ -95,13 +96,19 @@ impl Buffer { self.data.fill_with(Cell::empty); } - pub fn write(&mut self, mut pos: Pos, content: &str, style: ContentStyle) { + pub fn write( + &mut self, + widthdb: &mut WidthDB, + mut pos: Pos, + content: &str, + style: ContentStyle, + ) { if pos.y < 0 || pos.y >= self.size.height as i32 { return; } for grapheme in content.graphemes(true) { - let width = grapheme.width().max(1) as u8; // TODO Use actual width + let width = widthdb.width(grapheme); if pos.x >= 0 && pos.x + width as i32 <= self.size.width as i32 { // Grapheme fits on buffer in its entirety let grapheme = grapheme.to_string().into_boxed_str(); diff --git a/src/frame.rs b/src/frame.rs index ae54a2e..908872a 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -2,9 +2,11 @@ use crossterm::style::ContentStyle; use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; +use crate::widthdb::WidthDB; #[derive(Debug, Default)] pub struct Frame { + pub(crate) widthdb: WidthDB, pub(crate) buffer: Buffer, cursor: Option, } @@ -35,7 +37,11 @@ impl Frame { self.set_cursor(None); } + pub fn width(&mut self, s: &str) -> u8 { + self.widthdb.width(s) + } + pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) { - self.buffer.write(pos, content, style); + self.buffer.write(&mut self.widthdb, pos, content, style); } } diff --git a/src/lib.rs b/src/lib.rs index 56465e4..3720d23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ mod buffer; pub mod frame; pub mod terminal; +mod widthdb; diff --git a/src/terminal.rs b/src/terminal.rs index cfb7510..2101e8b 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -64,10 +64,23 @@ impl Terminal { } /// Display the current frame on the screen and prepare the next frame. + /// Returns `true` if an immediate redraw is required. /// /// 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<()> { + pub fn present(&mut self) -> io::Result { + if self.frame.widthdb.measuring_required() { + self.frame.widthdb.measure_widths(&mut self.out)?; + // Since we messed up the screen by measuring widths, we'll need to + // do a full redraw the next time around. + self.full_redraw = true; + // Throwing away the current frame because its content were rendered + // with unconfirmed width data. Also, this function guarantees that + // after it is called, the frame is empty. + self.frame.reset(); + return Ok(true); + } + if self.full_redraw { io::stdout().queue(Clear(ClearType::All))?; self.prev_frame_buffer.reset(); // Because the screen is now empty @@ -81,7 +94,7 @@ impl Terminal { mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); self.frame.reset(); - Ok(()) + Ok(false) } fn draw_differences(&mut self) -> io::Result<()> { diff --git a/src/widthdb.rs b/src/widthdb.rs new file mode 100644 index 0000000..f9da5f9 --- /dev/null +++ b/src/widthdb.rs @@ -0,0 +1,46 @@ +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::UnicodeWidthStr; + +#[derive(Debug, Default)] +pub struct WidthDB { + known: HashMap, + requested: HashSet, +} + +impl WidthDB { + pub fn width(&mut self, s: &str) -> u8 { + let mut total = 0; + for grapheme in s.graphemes(true) { + total += if let Some(width) = self.known.get(grapheme) { + *width + } else { + self.requested.insert(grapheme.to_string()); + grapheme.width() as u8 + }; + } + total + } + + pub fn measuring_required(&self) -> bool { + !self.requested.is_empty() + } + + pub fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { + for grapheme in self.requested.drain() { + out.queue(Clear(ClearType::All))? + .queue(MoveTo(0, 0))? + .queue(Print(&grapheme))?; + out.flush()?; + let width = crossterm::cursor::position()?.0.max(1) as u8; + self.known.insert(grapheme, width); + } + Ok(()) + } +} From 67f8919630be6be1e1af8a298d6c74e0a984314b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 21 May 2022 23:38:37 +0200 Subject: [PATCH 005/144] Only draw differences from previous frame --- src/buffer.rs | 18 ++++++++---------- src/terminal.rs | 5 ++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index d8c169c..5b9092e 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -30,7 +30,7 @@ impl Pos { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { pub content: Box, pub style: ContentStyle, @@ -67,6 +67,12 @@ impl Buffer { y * width + x } + pub(crate) fn at(&self, x: u16, y: u16) -> &Cell { + assert!(x < self.size.width); + assert!(y < self.size.height); + &self.data[self.index(x, y)] + } + pub fn size(&self) -> Size { self.size } @@ -143,14 +149,6 @@ pub struct Cells<'a> { y: u16, } -impl<'a> Cells<'a> { - fn at(&self, x: u16, y: u16) -> &'a Cell { - assert!(x < self.buffer.size.width); - assert!(y < self.buffer.size.height); - &self.buffer.data[self.buffer.index(x, y)] - } -} - impl<'a> Iterator for Cells<'a> { type Item = (u16, u16, &'a Cell); @@ -160,7 +158,7 @@ impl<'a> Iterator for Cells<'a> { } let (x, y) = (self.x, self.y); - let cell = self.at(self.x, self.y); + let cell = self.buffer.at(self.x, self.y); assert!(cell.offset == 0); self.x += cell.width as u16; diff --git a/src/terminal.rs b/src/terminal.rs index 2101e8b..b0055e6 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -98,8 +98,11 @@ impl Terminal { } fn draw_differences(&mut self) -> io::Result<()> { - // TODO Only draw the differences 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))? From fe424b337684dd67c50a91b491ce5abbffefc230 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 22 May 2022 20:41:55 +0200 Subject: [PATCH 006/144] Fix grapheme rendering Previously, when overwriting wide graphemes, those were not first cleared from the buffer. This could sometimes result in partial graphemes, which is an invalid buffer state. Now, when a wide grapheme is overwritten, it is removed from the buffer entirely and replaced with spaces. Those spaces retain the style of the removed grapheme. A similar thing now happens with graphemes that only partially overlap the buffer, either on the left or the right border. Those parts that are inside the buffer are rendered as spaces with the grapheme's style. --- examples/overlapping_graphemes.rs | 81 +++++++++++++++++++++++++++ src/buffer.rs | 93 ++++++++++++++++++++++++------- 2 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 examples/overlapping_graphemes.rs diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs new file mode 100644 index 0000000..67b6a31 --- /dev/null +++ b/examples/overlapping_graphemes.rs @@ -0,0 +1,81 @@ +use std::io; + +use crossterm::style::{ContentStyle, Stylize}; +use toss::frame::{Frame, Pos}; +use toss::terminal::Terminal; + +fn draw(f: &mut Frame) { + f.write( + Pos::new(0, 0), + "Writing over wide graphemes removes the entire overwritten grapheme.", + ContentStyle::default(), + ); + let under = ContentStyle::default().white().on_dark_blue(); + let over = ContentStyle::default().black().on_dark_yellow(); + 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", + ContentStyle::default(), + ); + f.write( + Pos::new(0, 7), + "actually rendered.", + ContentStyle::default(), + ); + 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", + ContentStyle::default(), + ); + f.write( + Pos::new(0, 13), + "scientist emoji as a woman and a microscope: 👩‍🔬", + ContentStyle::default(), + ); + for i in 0..(f.width(scientist) + 4) { + f.write(Pos::new(2, 15 + i as i32), scientist, under); + f.write(Pos::new(i as i32, 15 + i as i32), "x", over); + } +} + +fn main() -> io::Result<()> { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new()?; + + loop { + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize()?; + + draw(term.frame()); + + if !term.present()? { + break; + } + } + + // Wait for input before exiting + let _ = crossterm::event::read(); + + Ok(()) +} diff --git a/src/buffer.rs b/src/buffer.rs index 5b9092e..e51f20e 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -38,8 +38,8 @@ pub struct Cell { pub offset: u8, } -impl Cell { - pub fn empty() -> Self { +impl Default for Cell { + fn default() -> Self { Self { content: " ".to_string().into_boxed_str(), style: ContentStyle::default(), @@ -70,7 +70,15 @@ impl Buffer { pub(crate) fn at(&self, x: u16, y: u16) -> &Cell { assert!(x < self.size.width); assert!(y < self.size.height); - &self.data[self.index(x, y)] + let i = self.index(x, y); + &self.data[i] + } + + 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] } pub fn size(&self) -> Size { @@ -83,7 +91,7 @@ impl Buffer { /// correct size. pub fn resize(&mut self, size: Size) { if size == self.size() { - self.data.fill_with(Cell::empty); + self.data.fill_with(Cell::default); } else { let width: usize = size.width.into(); let height: usize = size.height.into(); @@ -91,7 +99,7 @@ impl Buffer { self.size = size; self.data.clear(); - self.data.resize_with(len, Cell::empty); + self.data.resize_with(len, Cell::default); } } @@ -99,7 +107,24 @@ impl Buffer { /// /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. pub fn reset(&mut self) { - self.data.fill_with(Cell::empty); + self.data.fill_with(Cell::default); + } + + /// Remove the grapheme at the specified coordinates from the buffer. + /// + /// Removes the entire grapheme, not just the cell at the coordinates. + /// Preserves the style of the affected cells. Works even if the coordinates + /// don't point to the beginning of the grapheme. + fn erase(&mut self, x: u16, y: u16) { + let cell = self.at(x, y); + let width: u16 = cell.width.into(); + let offset: u16 = cell.offset.into(); + for x in (x - offset)..(x - offset + width) { + let cell = self.at_mut(x, y); + let style = cell.style; + *cell = Cell::default(); + cell.style = style; + } } pub fn write( @@ -109,31 +134,57 @@ impl Buffer { content: &str, style: ContentStyle, ) { + // If we're not even visible, there's nothing to do if pos.y < 0 || pos.y >= self.size.height as i32 { return; } + let y = pos.y as u16; for grapheme in content.graphemes(true) { let width = widthdb.width(grapheme); - if pos.x >= 0 && pos.x + width as i32 <= self.size.width as i32 { - // Grapheme fits on buffer in its entirety - let grapheme = grapheme.to_string().into_boxed_str(); - - for offset in 0..width { - let i = self.index(pos.x as u16 + offset as u16, pos.y as u16); - self.data[i] = Cell { - content: grapheme.clone(), - style, - width, - offset, - }; - } - } - + self.write_grapheme(pos.x, y, width, grapheme, style); pos.x += width as i32; } } + /// Assumes that `pos.y` is in range. + fn write_grapheme(&mut self, x: i32, y: u16, width: u8, grapheme: &str, style: ContentStyle) { + let min_x = 0; + let max_x = self.size.width as i32 - 1; // Last possible cell + + 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 + 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, + 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 { + self.erase(x, y); + *self.at_mut(x, y) = Cell { + style, + ..Default::default() + }; + } + } + } + pub fn cells(&self) -> Cells<'_> { Cells { buffer: self, From 79e88138845ea5cf5cbd92728a17c00fc2b4b791 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 22 May 2022 20:47:45 +0200 Subject: [PATCH 007/144] Restore cursor on exit --- src/terminal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal.rs b/src/terminal.rs index b0055e6..5f4e320 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -23,8 +23,9 @@ pub struct Terminal { impl Drop for Terminal { fn drop(&mut self) { - let _ = self.out.execute(LeaveAlternateScreen); let _ = crossterm::terminal::disable_raw_mode(); + let _ = self.out.execute(LeaveAlternateScreen); + let _ = self.out.execute(Show); } } From 833defd1ce52a03352f208f7fec59c26cf11b397 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 28 May 2022 10:14:15 +0200 Subject: [PATCH 008/144] Document WidthDB --- src/frame.rs | 2 ++ src/terminal.rs | 2 ++ src/widthdb.rs | 13 +++++++++++++ 3 files changed, 17 insertions(+) diff --git a/src/frame.rs b/src/frame.rs index 908872a..eef783e 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,3 +1,5 @@ +//! Rendering the next frame. + use crossterm::style::ContentStyle; use crate::buffer::Buffer; diff --git a/src/terminal.rs b/src/terminal.rs index 5f4e320..2f2b35f 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,3 +1,5 @@ +//! Displaying frames on a terminal. + use std::io::Write; use std::{io, mem}; diff --git a/src/widthdb.rs b/src/widthdb.rs index f9da5f9..e3605c3 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -8,6 +8,7 @@ use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; +/// Measures and stores the with (in terminal coordinates) of graphemes. #[derive(Debug, Default)] pub struct WidthDB { known: HashMap, @@ -15,6 +16,10 @@ pub struct WidthDB { } impl WidthDB { + /// Determine the width of a string based on its graphemes. + /// + /// If the width of a grapheme has not been measured yet, it is estimated + /// using the Unicode Standard Annex #11. pub fn width(&mut self, s: &str) -> u8 { let mut total = 0; for grapheme in s.graphemes(true) { @@ -28,10 +33,18 @@ impl WidthDB { total } + /// Whether any new graphemes have been seen since the last time + /// [`Self::measure_widths`] was called. pub fn measuring_required(&self) -> bool { !self.requested.is_empty() } + /// 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 fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { for grapheme in self.requested.drain() { out.queue(Clear(ClearType::All))? From 3b2ea37ba51621f1d020dc2a71b84c58477cc02f Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 28 May 2022 11:15:18 +0200 Subject: [PATCH 009/144] Get individual grapheme's width --- src/buffer.rs | 2 +- src/frame.rs | 14 +++++++++++++- src/widthdb.rs | 22 ++++++++++++++++++---- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index e51f20e..77756f3 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -141,7 +141,7 @@ impl Buffer { let y = pos.y as u16; for grapheme in content.graphemes(true) { - let width = widthdb.width(grapheme); + let width = widthdb.grapheme_width(grapheme); self.write_grapheme(pos.x, y, width, grapheme, style); pos.x += width as i32; } diff --git a/src/frame.rs b/src/frame.rs index eef783e..762246a 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -39,7 +39,19 @@ impl Frame { self.set_cursor(None); } - pub fn width(&mut self, s: &str) -> u8 { + /// Determine the width of a grapheme. + /// + /// If the width has not been measured yet, it is estimated using the + /// Unicode Standard Annex #11. + pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { + self.widthdb.grapheme_width(grapheme) + } + + /// Determine the width of a string based on its graphemes. + /// + /// If the width of a grapheme has not been measured yet, it is estimated + /// using the Unicode Standard Annex #11. + pub fn width(&mut self, s: &str) -> usize { self.widthdb.width(s) } diff --git a/src/widthdb.rs b/src/widthdb.rs index e3605c3..9c91879 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -16,18 +16,32 @@ pub struct WidthDB { } impl WidthDB { + /// Determine the width of a grapheme. + /// + /// If the width has not been measured yet, it is estimated using the + /// Unicode Standard Annex #11. + pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { + assert_eq!(Some(grapheme), grapheme.graphemes(true).next()); + if let Some(width) = self.known.get(grapheme) { + *width + } else { + self.requested.insert(grapheme.to_string()); + grapheme.width() as u8 + } + } + /// Determine the width of a string based on its graphemes. /// /// If the width of a grapheme has not been measured yet, it is estimated /// using the Unicode Standard Annex #11. - pub fn width(&mut self, s: &str) -> u8 { - let mut total = 0; + pub fn width(&mut self, s: &str) -> usize { + let mut total: usize = 0; for grapheme in s.graphemes(true) { total += if let Some(width) = self.known.get(grapheme) { - *width + (*width).into() } else { self.requested.insert(grapheme.to_string()); - grapheme.width() as u8 + grapheme.width() }; } total From 8fae7d2bf1666ef7187d120ec1a6f7d51084437d Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 28 May 2022 19:05:31 +0200 Subject: [PATCH 010/144] Return Redraw enum instead of bool Boolean values were too easy to accidentally interpret the wrong way. --- examples/hello_world.rs | 35 ++++++++++++++++++------------- examples/overlapping_graphemes.rs | 35 ++++++++++++++++++------------- src/terminal.rs | 12 ++++++++--- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 13c1475..57cc976 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,8 +1,7 @@ -use std::io; - +use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; use toss::frame::{Frame, Pos}; -use toss::terminal::Terminal; +use toss::terminal::{Redraw, Terminal}; fn draw(f: &mut Frame) { f.write( @@ -18,24 +17,32 @@ fn draw(f: &mut Frame) { f.show_cursor(Pos::new(16, 0)); } -fn main() -> io::Result<()> { - // Automatically enters alternate screen and enables raw mode - let mut term = Terminal::new()?; - +fn render_frame(term: &mut Terminal) { loop { // Must be called before rendering, otherwise the terminal has out-of-date // size information and will present garbage. - term.autoresize()?; + term.autoresize().unwrap(); draw(term.frame()); - if !term.present()? { + if term.present().unwrap() == Redraw::NotRequired { + break; + } + } +} + +fn main() { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new().unwrap(); + + 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; } } - - // Wait for input before exiting - let _ = crossterm::event::read(); - - Ok(()) } diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index 67b6a31..db98bae 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -1,8 +1,7 @@ -use std::io; - +use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; use toss::frame::{Frame, Pos}; -use toss::terminal::Terminal; +use toss::terminal::{Redraw, Terminal}; fn draw(f: &mut Frame) { f.write( @@ -58,24 +57,32 @@ fn draw(f: &mut Frame) { } } -fn main() -> io::Result<()> { - // Automatically enters alternate screen and enables raw mode - let mut term = Terminal::new()?; - +fn render_frame(term: &mut Terminal) { loop { // Must be called before rendering, otherwise the terminal has out-of-date // size information and will present garbage. - term.autoresize()?; + term.autoresize().unwrap(); draw(term.frame()); - if !term.present()? { + if term.present().unwrap() == Redraw::NotRequired { + break; + } + } +} + +fn main() { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new().unwrap(); + + 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; } } - - // Wait for input before exiting - let _ = crossterm::event::read(); - - Ok(()) } diff --git a/src/terminal.rs b/src/terminal.rs index 2f2b35f..eb5645e 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -11,6 +11,12 @@ use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::{Buffer, Size}; use crate::frame::Frame; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Redraw { + Required, + NotRequired, +} + pub struct Terminal { /// Render target. out: Box, @@ -71,7 +77,7 @@ impl Terminal { /// /// 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 { + pub fn present(&mut self) -> io::Result { if self.frame.widthdb.measuring_required() { self.frame.widthdb.measure_widths(&mut self.out)?; // Since we messed up the screen by measuring widths, we'll need to @@ -81,7 +87,7 @@ impl Terminal { // with unconfirmed width data. Also, this function guarantees that // after it is called, the frame is empty. self.frame.reset(); - return Ok(true); + return Ok(Redraw::Required); } if self.full_redraw { @@ -97,7 +103,7 @@ impl Terminal { mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); self.frame.reset(); - Ok(false) + Ok(Redraw::NotRequired) } fn draw_differences(&mut self) -> io::Result<()> { From 37634139b026605d65db210491ca04d7aad5957f Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 28 May 2022 20:55:22 +0200 Subject: [PATCH 011/144] Wrap text in a unicode-aware way --- Cargo.toml | 1 + examples/text_wrapping.rs | 60 +++++++++++++++++++++++++ src/frame.rs | 5 +++ src/lib.rs | 3 ++ src/wrap.rs | 95 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+) create mode 100644 examples/text_wrapping.rs create mode 100644 src/wrap.rs diff --git a/Cargo.toml b/Cargo.toml index b714450..ecf6494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,6 @@ edition = "2021" [dependencies] crossterm = "0.23.2" +unicode-linebreak = "0.1.2" unicode-segmentation = "1.9.0" unicode-width = "0.1.9" diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs new file mode 100644 index 0000000..add9b5a --- /dev/null +++ b/examples/text_wrapping.rs @@ -0,0 +1,60 @@ +use crossterm::event::Event; +use crossterm::style::ContentStyle; +use toss::frame::{Frame, Pos}; +use toss::terminal::{Redraw, 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 nonbreaking spaces: ", + "This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}nonbreaking\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).", + ); + // TODO Actually use nbsp + + let breaks = f.wrap(text, f.size().width.into()); + let lines = toss::split_at_indices(text, &breaks); + for (i, line) in lines.iter().enumerate() { + f.write( + Pos::new(0, i as i32), + line.trim_end(), + ContentStyle::default(), + ); + } +} + +fn render_frame(term: &mut Terminal) { + loop { + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize().unwrap(); + + draw(term.frame()); + + if term.present().unwrap() == Redraw::NotRequired { + break; + } + } +} + +fn main() { + // Automatically enters alternate screen and enables raw mode + let mut term = Terminal::new().unwrap(); + + 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/frame.rs b/src/frame.rs index 762246a..f8c46af 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -5,6 +5,7 @@ use crossterm::style::ContentStyle; use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; use crate::widthdb::WidthDB; +use crate::wrap; #[derive(Debug, Default)] pub struct Frame { @@ -55,6 +56,10 @@ impl Frame { self.widthdb.width(s) } + pub fn wrap(&mut self, text: &str, width: usize) -> Vec { + wrap::wrap(text, width, &mut self.widthdb) + } + pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) { self.buffer.write(&mut self.widthdb, pos, content, style); } diff --git a/src/lib.rs b/src/lib.rs index 3720d23..9cbdd9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,6 @@ mod buffer; pub mod frame; pub mod terminal; mod widthdb; +mod wrap; + +pub use wrap::split_at_indices; diff --git a/src/wrap.rs b/src/wrap.rs new file mode 100644 index 0000000..309214a --- /dev/null +++ b/src/wrap.rs @@ -0,0 +1,95 @@ +//! Word wrapping for text. + +use unicode_linebreak::BreakOpportunity; +use unicode_segmentation::UnicodeSegmentation; + +use crate::widthdb::WidthDB; + +// TODO Handle tabs separately? +// TODO Convert into an iterator? +pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { + let mut breaks = vec![]; + + let mut break_options = unicode_linebreak::linebreaks(text).peekable(); + + // The last valid break point encountered and its width + let mut valid_break = None; + let mut valid_break_width = 0; + + // Width of the line at the current grapheme + let mut current_width = 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; + valid_break_width = 0; + current_width = 0; + } + BreakOpportunity::Allowed => { + valid_break = Some(bi); + valid_break_width = current_width; + } + } + } + + let grapheme_width: usize = widthdb.grapheme_width(g).into(); + if current_width + grapheme_width > width { + if current_width == 0 { + // The grapheme is wider than the maximum width, so we'll allow + // it, thereby forcing the following grapheme to break no matter + // what (either because of a mandatory or allowed break, or via + // a forced break). + } else if let Some(bi) = valid_break { + // We can't fit the grapheme onto the current line, so we'll + // just break at the last valid break point. + breaks.push(bi); + current_width -= valid_break_width; + valid_break = None; + valid_break_width = 0; + } else { + // Forced break in the midde of a normally non-breakable chunk + // because there have been no valid break points yet. + breaks.push(gi); + valid_break = None; + valid_break_width = 0; + current_width = 0; + } + } + + current_width += grapheme_width; + } + + breaks +} + +pub fn split_at_indices<'a>(s: &'a str, indices: &[usize]) -> Vec<&'a str> { + let mut slices = vec![]; + + let mut rest = s; + let mut offset = 0; + + for i in indices { + let (left, right) = rest.split_at(i - offset); + slices.push(left); + rest = right; + offset = *i; + } + + slices.push(rest); + + slices +} From 6b9e4cbc63f9a101e08632049531ec62941f947f Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 28 May 2022 22:43:10 +0200 Subject: [PATCH 012/144] Ignore graphemes of width 0 when writing to buffer --- examples/text_wrapping.rs | 4 ++-- src/buffer.rs | 4 +++- src/widthdb.rs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index add9b5a..2c48dd6 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -10,8 +10,8 @@ fn draw(f: &mut Frame) { "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 nonbreaking spaces: ", - "This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}nonbreaking\u{00a0}spaces.\n", + "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).", diff --git a/src/buffer.rs b/src/buffer.rs index 77756f3..4707b53 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -142,7 +142,9 @@ impl Buffer { for grapheme in content.graphemes(true) { let width = widthdb.grapheme_width(grapheme); - self.write_grapheme(pos.x, y, width, grapheme, style); + if width > 0 { + self.write_grapheme(pos.x, y, width, grapheme, style); + } pos.x += width as i32; } } diff --git a/src/widthdb.rs b/src/widthdb.rs index 9c91879..6af28c3 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -65,7 +65,7 @@ impl WidthDB { .queue(MoveTo(0, 0))? .queue(Print(&grapheme))?; out.flush()?; - let width = crossterm::cursor::position()?.0.max(1) as u8; + let width = crossterm::cursor::position()?.0 as u8; self.known.insert(grapheme, width); } Ok(()) From 33264b4aec27066e6abb7cc7d15bd680b43fcd5a Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 29 May 2022 12:02:15 +0200 Subject: [PATCH 013/144] Remove TODO --- examples/text_wrapping.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index 2c48dd6..f150142 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -16,7 +16,6 @@ fn draw(f: &mut Frame) { "It can also properly handle wide graphemes (like emoji 🤔), ", "including ones usually displayed incorrectly by terminal emulators, like 👩‍🔬 (a female scientist emoji).", ); - // TODO Actually use nbsp let breaks = f.wrap(text, f.size().width.into()); let lines = toss::split_at_indices(text, &breaks); From 333cf74fba56080043a13b9f55c0b62695e2fa4a Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 8 Jun 2022 17:38:38 +0200 Subject: [PATCH 014/144] Make width measuring optional and disabled by default --- examples/hello_world.rs | 8 +++++-- examples/overlapping_graphemes.rs | 7 ++++-- examples/text_wrapping.rs | 7 ++++-- src/terminal.rs | 40 +++++++++++++++---------------- src/widthdb.rs | 12 +++++++++- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 57cc976..fbad68c 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,7 +1,7 @@ use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use toss::terminal::Terminal; fn draw(f: &mut Frame) { f.write( @@ -24,8 +24,11 @@ fn render_frame(term: &mut Terminal) { term.autoresize().unwrap(); draw(term.frame()); + term.present().unwrap(); - if term.present().unwrap() == Redraw::NotRequired { + if term.measuring_required() { + term.measure_widths().unwrap(); + } else { break; } } @@ -34,6 +37,7 @@ fn render_frame(term: &mut Terminal) { fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); + term.set_measuring(true); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index db98bae..061f830 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -1,7 +1,7 @@ use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use toss::terminal::Terminal; fn draw(f: &mut Frame) { f.write( @@ -65,7 +65,9 @@ fn render_frame(term: &mut Terminal) { draw(term.frame()); - if term.present().unwrap() == Redraw::NotRequired { + if term.measuring_required() { + term.measure_widths().unwrap(); + } else { break; } } @@ -74,6 +76,7 @@ fn render_frame(term: &mut Terminal) { fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); + term.set_measuring(true); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index f150142..19d4172 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -1,7 +1,7 @@ use crossterm::event::Event; use crossterm::style::ContentStyle; use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use toss::terminal::Terminal; fn draw(f: &mut Frame) { let text = concat!( @@ -36,7 +36,9 @@ fn render_frame(term: &mut Terminal) { draw(term.frame()); - if term.present().unwrap() == Redraw::NotRequired { + if term.measuring_required() { + term.measure_widths().unwrap(); + } else { break; } } @@ -45,6 +47,7 @@ fn render_frame(term: &mut Terminal) { fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); + term.set_measuring(true); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/src/terminal.rs b/src/terminal.rs index eb5645e..91706f9 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -11,12 +11,6 @@ use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::{Buffer, Size}; use crate::frame::Frame; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Redraw { - Required, - NotRequired, -} - pub struct Terminal { /// Render target. out: Box, @@ -54,6 +48,24 @@ impl Terminal { Ok(result) } + pub fn set_measuring(&mut self, active: bool) { + self.frame.widthdb.active = active; + } + + pub fn measuring(&self) -> bool { + self.frame.widthdb.active + } + + pub fn measuring_required(&self) -> bool { + self.frame.widthdb.measuring_required() + } + + pub fn measure_widths(&mut self) -> io::Result<()> { + self.frame.widthdb.measure_widths(&mut self.out)?; + self.full_redraw = true; + Ok(()) + } + /// Resize the frame and other internal buffers if the terminal size has /// changed. pub fn autoresize(&mut self) -> io::Result<()> { @@ -77,19 +89,7 @@ impl Terminal { /// /// After calling this function, the frame returned by [`Self::frame`] will /// be empty again and have no cursor position. - pub fn present(&mut self) -> io::Result { - if self.frame.widthdb.measuring_required() { - self.frame.widthdb.measure_widths(&mut self.out)?; - // Since we messed up the screen by measuring widths, we'll need to - // do a full redraw the next time around. - self.full_redraw = true; - // Throwing away the current frame because its content were rendered - // with unconfirmed width data. Also, this function guarantees that - // after it is called, the frame is empty. - self.frame.reset(); - return Ok(Redraw::Required); - } - + pub fn present(&mut self) -> io::Result<()> { if self.full_redraw { io::stdout().queue(Clear(ClearType::All))?; self.prev_frame_buffer.reset(); // Because the screen is now empty @@ -103,7 +103,7 @@ impl Terminal { mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); self.frame.reset(); - Ok(Redraw::NotRequired) + Ok(()) } fn draw_differences(&mut self) -> io::Result<()> { diff --git a/src/widthdb.rs b/src/widthdb.rs index 6af28c3..9072995 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -11,6 +11,7 @@ use unicode_width::UnicodeWidthStr; /// Measures and stores the with (in terminal coordinates) of graphemes. #[derive(Debug, Default)] pub struct WidthDB { + pub active: bool, known: HashMap, requested: HashSet, } @@ -22,6 +23,9 @@ impl WidthDB { /// Unicode Standard Annex #11. pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { assert_eq!(Some(grapheme), grapheme.graphemes(true).next()); + if !self.active { + return grapheme.width() as u8; + } if let Some(width) = self.known.get(grapheme) { *width } else { @@ -35,6 +39,9 @@ impl WidthDB { /// If the width of a grapheme has not been measured yet, it is estimated /// using the Unicode Standard Annex #11. pub fn width(&mut self, s: &str) -> usize { + if !self.active { + return s.width(); + } let mut total: usize = 0; for grapheme in s.graphemes(true) { total += if let Some(width) = self.known.get(grapheme) { @@ -50,7 +57,7 @@ impl WidthDB { /// Whether any new graphemes have been seen since the last time /// [`Self::measure_widths`] was called. pub fn measuring_required(&self) -> bool { - !self.requested.is_empty() + self.active && !self.requested.is_empty() } /// Measure the width of all new graphemes that have been seen since the @@ -60,6 +67,9 @@ impl WidthDB { /// the terminal. After it finishes, the terminal's contents should be /// assumed to be garbage and a full redraw should be performed. pub fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { + if !self.active { + return Ok(()); + } for grapheme in self.requested.drain() { out.queue(Clear(ClearType::All))? .queue(MoveTo(0, 0))? From a0602a941c8a620515f5f3f317e229033b5e2108 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 15 Jun 2022 14:02:30 +0200 Subject: [PATCH 015/144] Fix examples --- examples/overlapping_graphemes.rs | 1 + examples/text_wrapping.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index 061f830..e5277b1 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -64,6 +64,7 @@ fn render_frame(term: &mut Terminal) { term.autoresize().unwrap(); draw(term.frame()); + term.present().unwrap(); if term.measuring_required() { term.measure_widths().unwrap(); diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index 19d4172..ee15dec 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -35,6 +35,7 @@ fn render_frame(term: &mut Terminal) { term.autoresize().unwrap(); draw(term.frame()); + term.present().unwrap(); if term.measuring_required() { term.measure_widths().unwrap(); From 761519c1a7cdc950eab70fd6539c71bf22919a50 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Jun 2022 19:52:02 +0200 Subject: [PATCH 016/144] Suspend and unsuspend terminal --- src/terminal.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/terminal.rs b/src/terminal.rs index 91706f9..a293656 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -25,9 +25,7 @@ pub struct Terminal { impl Drop for Terminal { fn drop(&mut self) { - let _ = crossterm::terminal::disable_raw_mode(); - let _ = self.out.execute(LeaveAlternateScreen); - let _ = self.out.execute(Show); + let _ = self.suspend(); } } @@ -43,11 +41,24 @@ impl Terminal { prev_frame_buffer: Buffer::default(), full_redraw: true, }; - crossterm::terminal::enable_raw_mode()?; - result.out.execute(EnterAlternateScreen)?; + result.unsuspend()?; Ok(result) } + pub fn suspend(&mut self) -> io::Result<()> { + crossterm::terminal::disable_raw_mode()?; + self.out.execute(LeaveAlternateScreen)?; + self.out.execute(Show)?; + Ok(()) + } + + pub fn unsuspend(&mut self) -> io::Result<()> { + crossterm::terminal::enable_raw_mode()?; + self.out.execute(EnterAlternateScreen)?; + self.full_redraw = true; + Ok(()) + } + pub fn set_measuring(&mut self, active: bool) { self.frame.widthdb.active = active; } From 9b0d80873f734df4c2e1ef5ea5fa5f132830d1c0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 4 Jul 2022 10:55:25 +0200 Subject: [PATCH 017/144] Use styled chunks of text instead of plain strings --- examples/hello_world.rs | 9 +- examples/overlapping_graphemes.rs | 36 +++---- examples/text_wrapping.rs | 13 +-- src/buffer.rs | 21 ++-- src/frame.rs | 5 +- src/lib.rs | 3 +- src/styled.rs | 170 ++++++++++++++++++++++++++++++ src/wrap.rs | 18 ---- 8 files changed, 209 insertions(+), 66 deletions(-) create mode 100644 src/styled.rs diff --git a/examples/hello_world.rs b/examples/hello_world.rs index fbad68c..b7b670d 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -6,13 +6,14 @@ use toss::terminal::Terminal; fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), - "Hello world!", - ContentStyle::default().green(), + ("Hello world!", ContentStyle::default().green()), ); f.write( Pos::new(0, 1), - "Press any key to exit", - ContentStyle::default().on_dark_blue(), + ( + "Press any key to exit", + ContentStyle::default().on_dark_blue(), + ), ); f.show_cursor(Pos::new(16, 0)); } diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index e5277b1..adf610c 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -7,53 +7,45 @@ fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), "Writing over wide graphemes removes the entire overwritten grapheme.", - ContentStyle::default(), ); let under = ContentStyle::default().white().on_dark_blue(); let over = ContentStyle::default().black().on_dark_yellow(); for i in 0..6 { let delta = i - 2; - f.write(Pos::new(2 + i * 7, 2), "a😀", under); - f.write(Pos::new(2 + i * 7, 3), "a😀", under); - f.write(Pos::new(2 + i * 7, 4), "a😀", under); - f.write(Pos::new(2 + i * 7 + delta, 3), "b", over); - f.write(Pos::new(2 + i * 7 + delta, 4), "😈", over); + f.write(Pos::new(2 + i * 7, 2), ("a😀", under)); + f.write(Pos::new(2 + i * 7, 3), ("a😀", under)); + f.write(Pos::new(2 + i * 7, 4), ("a😀", under)); + f.write(Pos::new(2 + i * 7 + delta, 3), ("b", over)); + f.write(Pos::new(2 + i * 7 + delta, 4), ("😈", over)); } f.write( Pos::new(0, 6), "Wide graphemes at the edges of the screen apply their style, but are not", - ContentStyle::default(), - ); - f.write( - Pos::new(0, 7), - "actually rendered.", - ContentStyle::default(), ); + f.write(Pos::new(0, 7), "actually rendered."); let x1 = -1; let x2 = f.size().width as i32 / 2 - 3; let x3 = f.size().width as i32 - 5; - f.write(Pos::new(x1, 9), "123456", under); - f.write(Pos::new(x1, 10), "😀😀😀", under); - f.write(Pos::new(x2, 9), "123456", under); - f.write(Pos::new(x2, 10), "😀😀😀", under); - f.write(Pos::new(x3, 9), "123456", under); - f.write(Pos::new(x3, 10), "😀😀😀", under); + f.write(Pos::new(x1, 9), ("123456", under)); + f.write(Pos::new(x1, 10), ("😀😀😀", under)); + f.write(Pos::new(x2, 9), ("123456", under)); + f.write(Pos::new(x2, 10), ("😀😀😀", under)); + f.write(Pos::new(x3, 9), ("123456", under)); + f.write(Pos::new(x3, 10), ("😀😀😀", under)); let scientist = "👩‍🔬"; f.write( Pos::new(0, 12), "Most terminals ignore the zero width joiner and display this female", - ContentStyle::default(), ); f.write( Pos::new(0, 13), "scientist emoji as a woman and a microscope: 👩‍🔬", - ContentStyle::default(), ); for i in 0..(f.width(scientist) + 4) { - f.write(Pos::new(2, 15 + i as i32), scientist, under); - f.write(Pos::new(i as i32, 15 + i as i32), "x", over); + f.write(Pos::new(2, 15 + i as i32), (scientist, under)); + f.write(Pos::new(i as i32, 15 + i as i32), ("x", over)); } } diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index ee15dec..c82022e 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -1,6 +1,6 @@ use crossterm::event::Event; -use crossterm::style::ContentStyle; use toss::frame::{Frame, Pos}; +use toss::styled::Styled; use toss::terminal::Terminal; fn draw(f: &mut Frame) { @@ -18,13 +18,10 @@ fn draw(f: &mut Frame) { ); let breaks = f.wrap(text, f.size().width.into()); - let lines = toss::split_at_indices(text, &breaks); - for (i, line) in lines.iter().enumerate() { - f.write( - Pos::new(0, i as i32), - line.trim_end(), - ContentStyle::default(), - ); + let lines = Styled::default().then(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); } } diff --git a/src/buffer.rs b/src/buffer.rs index 4707b53..f71c86e 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,6 +1,7 @@ use crossterm::style::ContentStyle; use unicode_segmentation::UnicodeSegmentation; +use crate::styled::Styled; use crate::widthdb::WidthDB; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -127,23 +128,23 @@ impl Buffer { } } - pub fn write( - &mut self, - widthdb: &mut WidthDB, - mut pos: Pos, - content: &str, - style: ContentStyle, - ) { + pub fn write(&mut self, widthdb: &mut WidthDB, mut pos: Pos, styled: &Styled) { // If we're not even visible, there's nothing to do if pos.y < 0 || pos.y >= self.size.height as i32 { return; } let y = pos.y as u16; - for grapheme in content.graphemes(true) { - let width = widthdb.grapheme_width(grapheme); + for styled_grapheme in styled.styled_graphemes() { + let width = widthdb.grapheme_width(styled_grapheme.content()); if width > 0 { - self.write_grapheme(pos.x, y, width, grapheme, style); + self.write_grapheme( + pos.x, + y, + width, + styled_grapheme.content(), + *styled_grapheme.style(), + ); } pos.x += width as i32; } diff --git a/src/frame.rs b/src/frame.rs index f8c46af..1566fac 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -4,6 +4,7 @@ use crossterm::style::ContentStyle; use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; +use crate::styled::Styled; use crate::widthdb::WidthDB; use crate::wrap; @@ -60,7 +61,7 @@ impl Frame { wrap::wrap(text, width, &mut self.widthdb) } - pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) { - self.buffer.write(&mut self.widthdb, pos, content, style); + pub fn write>(&mut self, pos: Pos, styled: S) { + self.buffer.write(&mut self.widthdb, pos, &styled.into()); } } diff --git a/src/lib.rs b/src/lib.rs index 9cbdd9f..d73aeed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ mod buffer; pub mod frame; +pub mod styled; pub mod terminal; mod widthdb; mod wrap; - -pub use wrap::split_at_indices; diff --git a/src/styled.rs b/src/styled.rs new file mode 100644 index 0000000..2ee4efb --- /dev/null +++ b/src/styled.rs @@ -0,0 +1,170 @@ +use crossterm::style::{ContentStyle, StyledContent}; +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Debug, Clone)] +pub struct Chunk { + string: String, + style: ContentStyle, +} + +impl Chunk { + pub fn new(string: S, style: ContentStyle) -> Self { + Self { + string: string.to_string(), + style, + } + } + + pub fn plain(string: S) -> Self { + Self::new(string, ContentStyle::default()) + } + + pub fn split_at(&self, mid: usize) -> (Self, Self) { + let (lstr, rstr) = self.string.split_at(mid); + let left = Self { + string: lstr.to_string(), + style: self.style, + }; + let right = Self { + string: rstr.to_string(), + style: self.style, + }; + (left, right) + } +} + +impl From<&str> for Chunk { + fn from(str: &str) -> Self { + Self::plain(str) + } +} + +impl From for Chunk { + fn from(string: String) -> Self { + Self::plain(string) + } +} + +impl From<&String> for Chunk { + fn from(string: &String) -> Self { + Self::plain(string) + } +} + +impl From<(S,)> for Chunk { + fn from(tuple: (S,)) -> Self { + Self::plain(tuple.0) + } +} + +impl From<(S, ContentStyle)> for Chunk { + fn from(tuple: (S, ContentStyle)) -> Self { + Self::new(tuple.0, tuple.1) + } +} + +#[derive(Debug, Default, Clone)] +pub struct Styled(Vec); + +impl Styled { + pub fn new>(chunk: C) -> Self { + Self::default().then(chunk) + } + + pub fn then>(mut self, chunk: C) -> Self { + self.0.push(chunk.into()); + self + } + + pub fn and_then(mut self, other: Styled) -> Self { + self.0.extend(other.0); + self + } + + pub fn text(&self) -> String { + self.0.iter().flat_map(|c| c.string.chars()).collect() + } + + pub fn graphemes(&self) -> impl Iterator { + self.0.iter().flat_map(|c| c.string.graphemes(true)) + } + + pub fn grapheme_indices(&self) -> impl Iterator { + self.0 + .iter() + .scan(0, |s, c| { + let offset = *s; + *s += c.string.len(); + Some((offset, c)) + }) + .flat_map(|(o, c)| { + c.string + .grapheme_indices(true) + .map(move |(gi, g)| (o + gi, g)) + }) + } + + pub fn styled_graphemes(&self) -> impl Iterator> { + self.0.iter().flat_map(|c| { + c.string + .graphemes(true) + .map(|g| StyledContent::new(c.style, g)) + }) + } + + pub fn split_at(self, mid: usize) -> (Self, Self) { + let mut left = vec![]; + let mut right = vec![]; + let mut offset = 0; + for chunk in self.0 { + let len = chunk.string.len(); + if offset >= mid { + right.push(chunk); + } else if offset + len > mid { + let (lchunk, rchunk) = chunk.split_at(mid - offset); + left.push(lchunk); + right.push(rchunk); + } else { + left.push(chunk); + } + offset += len; + } + (Self(left), Self(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) { + while let Some(last) = self.0.last_mut() { + let trimmed = last.string.trim_end(); + if trimmed.is_empty() { + self.0.pop(); + } else { + last.string = trimmed.to_string(); + break; + } + } + } +} + +impl> From for Styled { + fn from(chunk: C) -> Self { + Self::new(chunk) + } +} diff --git a/src/wrap.rs b/src/wrap.rs index 309214a..1159557 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -75,21 +75,3 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { breaks } - -pub fn split_at_indices<'a>(s: &'a str, indices: &[usize]) -> Vec<&'a str> { - let mut slices = vec![]; - - let mut rest = s; - let mut offset = 0; - - for i in indices { - let (left, right) = rest.split_at(i - offset); - slices.push(left); - rest = right; - offset = *i; - } - - slices.push(rest); - - slices -} From 11b2211fade10dc4b81b81b1ddd8ffd040eacac3 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 4 Jul 2022 18:56:52 +0200 Subject: [PATCH 018/144] Improve word wrapping Now supports long trailing whitespace as well as tabs. --- examples/text_wrapping.rs | 16 ++++++++- src/buffer.rs | 30 +++++++++------- src/frame.rs | 21 +++++++++--- src/terminal.rs | 8 +++++ src/wrap.rs | 72 ++++++++++++++++++++++++++------------- 5 files changed, 106 insertions(+), 41 deletions(-) diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index c82022e..ecf49aa 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -14,7 +14,20 @@ fn draw(f: &mut Frame) { "This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}non-\u{2060}breaking\u{00a0}spaces.\n", "\n", "It can also properly handle wide graphemes (like emoji 🤔), ", - "including ones usually displayed incorrectly by terminal emulators, like 👩‍🔬 (a female scientist emoji).", + "including ones usually displayed incorrectly by terminal emulators, like 👩‍🔬 (a female scientist emoji).\n", + "\n", + "Finally, tabs are supported as well. ", + "The following text is rendered with a tab width of 4:\n", + "\tx\n", + "1\tx\n", + "12\tx\n", + "123\tx\n", + "1234\tx\n", + "12345\tx\n", + "123456\tx\n", + "1234567\tx\n", + "12345678\tx\n", + "123456789\tx\n", ); let breaks = f.wrap(text, f.size().width.into()); @@ -46,6 +59,7 @@ fn main() { // Automatically enters alternate screen and enables raw mode let mut term = Terminal::new().unwrap(); term.set_measuring(true); + term.set_tab_width(4); loop { // Render and display a frame. A full frame is displayed on the terminal diff --git a/src/buffer.rs b/src/buffer.rs index f71c86e..e6ffe26 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,8 +1,8 @@ use crossterm::style::ContentStyle; -use unicode_segmentation::UnicodeSegmentation; use crate::styled::Styled; use crate::widthdb::WidthDB; +use crate::wrap; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Size { @@ -128,25 +128,31 @@ impl Buffer { } } - pub fn write(&mut self, widthdb: &mut WidthDB, mut pos: Pos, styled: &Styled) { + pub fn write(&mut self, widthdb: &mut WidthDB, tab_width: u8, pos: Pos, styled: &Styled) { // If we're not even visible, there's nothing to do if pos.y < 0 || pos.y >= self.size.height as i32 { return; } let y = pos.y as u16; + let mut col: usize = 0; for styled_grapheme in styled.styled_graphemes() { - let width = widthdb.grapheme_width(styled_grapheme.content()); - if width > 0 { - self.write_grapheme( - pos.x, - y, - width, - styled_grapheme.content(), - *styled_grapheme.style(), - ); + let x = pos.x + col as i32; + let g = *styled_grapheme.content(); + let style = *styled_grapheme.style(); + if g == "\t" { + let width = wrap::tab_width_at_column(tab_width, col); + col += width as usize; + for dx in 0..width { + self.write_grapheme(x + dx as i32, y, width, " ", style); + } + } else { + let width = widthdb.grapheme_width(g); + col += width as usize; + if width > 0 { + self.write_grapheme(x, y, width, g, style); + } } - pos.x += width as i32; } } diff --git a/src/frame.rs b/src/frame.rs index 1566fac..2ef1b51 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,18 +1,28 @@ //! Rendering the next frame. -use crossterm::style::ContentStyle; - use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; use crate::styled::Styled; use crate::widthdb::WidthDB; use crate::wrap; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Frame { pub(crate) widthdb: WidthDB, pub(crate) buffer: Buffer, cursor: Option, + pub(crate) tab_width: u8, +} + +impl Default for Frame { + fn default() -> Self { + Self { + widthdb: Default::default(), + buffer: Default::default(), + cursor: None, + tab_width: 8, + } + } } impl Frame { @@ -58,10 +68,11 @@ impl Frame { } pub fn wrap(&mut self, text: &str, width: usize) -> Vec { - wrap::wrap(text, width, &mut self.widthdb) + wrap::wrap(&mut self.widthdb, self.tab_width, text, width) } pub fn write>(&mut self, pos: Pos, styled: S) { - self.buffer.write(&mut self.widthdb, pos, &styled.into()); + self.buffer + .write(&mut self.widthdb, self.tab_width, pos, &styled.into()); } } diff --git a/src/terminal.rs b/src/terminal.rs index a293656..f7e1d05 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -59,6 +59,14 @@ impl Terminal { Ok(()) } + pub fn set_tab_width(&mut self, tab_width: u8) { + self.frame.tab_width = tab_width; + } + + pub fn tab_width(&self) -> u8 { + self.frame.tab_width + } + pub fn set_measuring(&mut self, active: bool) { self.frame.widthdb.active = active; } diff --git a/src/wrap.rs b/src/wrap.rs index 1159557..80f318f 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -5,9 +5,11 @@ use unicode_segmentation::UnicodeSegmentation; use crate::widthdb::WidthDB; -// TODO Handle tabs separately? -// TODO Convert into an iterator? -pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { +pub fn tab_width_at_column(tab_width: u8, col: usize) -> u8 { + tab_width - (col % tab_width as usize) as u8 +} + +pub fn wrap(widthdb: &mut WidthDB, tab_width: u8, text: &str, width: usize) -> Vec { let mut breaks = vec![]; let mut break_options = unicode_linebreak::linebreaks(text).peekable(); @@ -16,8 +18,10 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { let mut valid_break = None; let mut valid_break_width = 0; - // Width of the line at the current grapheme + // Width of the line at the current grapheme (with and without trailing + // whitespace) let mut current_width = 0; + let mut current_width_trimmed = 0; for (gi, g) in text.grapheme_indices(true) { // Advance break options @@ -38,6 +42,7 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { valid_break = None; valid_break_width = 0; current_width = 0; + current_width_trimmed = 0; } BreakOpportunity::Allowed => { valid_break = Some(bi); @@ -46,31 +51,52 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { } } - let grapheme_width: usize = widthdb.grapheme_width(g).into(); - if current_width + grapheme_width > width { - if current_width == 0 { - // The grapheme is wider than the maximum width, so we'll allow - // it, thereby forcing the following grapheme to break no matter - // what (either because of a mandatory or allowed break, or via - // a forced break). - } else if let Some(bi) = valid_break { - // We can't fit the grapheme onto the current line, so we'll - // just break at the last valid break point. + // Calculate widths after current grapheme + let g_width = if g == "\t" { + tab_width_at_column(tab_width, current_width) as usize + } else { + widthdb.grapheme_width(g) as usize + }; + let mut new_width = current_width + g_width; + let mut new_width_trimmed = if g.chars().all(|c| c.is_whitespace()) { + current_width_trimmed + } else { + new_width + }; + + // Wrap at last break point if necessary + if new_width_trimmed > width { + if let Some(bi) = valid_break { breaks.push(bi); - current_width -= valid_break_width; + new_width -= valid_break_width; + new_width_trimmed = new_width_trimmed.saturating_sub(valid_break_width); valid_break = None; valid_break_width = 0; - } else { - // Forced break in the midde of a normally non-breakable chunk - // because there have been no valid break points yet. - breaks.push(gi); - valid_break = None; - valid_break_width = 0; - current_width = 0; } } - current_width += grapheme_width; + // Perform a forced break if still necessary + if new_width_trimmed > width { + if new_width == g_width { + // 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 midde of a normally non-breakable chunk + // because there are no valid break points. + breaks.push(gi); + new_width = 0; + new_width_trimmed = 0; + valid_break = None; + valid_break_width = 0; + } + } + + // Update current width + current_width = new_width; + current_width_trimmed = new_width_trimmed; } breaks From ee9d6018c0efb5e6eb02db94dff7483d33b96a43 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 4 Jul 2022 19:31:27 +0200 Subject: [PATCH 019/144] Add constructors and traits for Size and Pos --- src/buffer.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index e6ffe26..7031f74 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,3 +1,5 @@ +use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; + use crossterm::style::ContentStyle; use crate::styled::Styled; @@ -11,10 +13,41 @@ pub struct Size { } impl Size { - pub const ZERO: Self = Self { - width: 0, - height: 0, - }; + pub const ZERO: Self = Self::new(0, 0); + + pub const fn new(width: u16, height: u16) -> Self { + Self { width, 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; + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -24,13 +57,51 @@ pub struct Pos { } impl Pos { - pub const ZERO: Self = Self { x: 0, y: 0 }; + pub const ZERO: Self = Self::new(0, 0); - pub fn new(x: i32, y: i32) -> Self { + pub const fn new(x: i32, y: i32) -> Self { Self { x, y } } } +impl Add for Pos { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +impl AddAssign for Pos { + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl Sub for Pos { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self::new(self.x - rhs.x, self.y - rhs.y) + } +} + +impl SubAssign for Pos { + fn sub_assign(&mut self, rhs: Self) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +impl Neg for Pos { + type Output = Self; + + fn neg(self) -> Self { + Self::new(-self.x, -self.y) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { pub content: Box, From 26bf89023e254778b9dcb826840f677ed7105292 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Jul 2022 11:21:18 +0200 Subject: [PATCH 020/144] Update dependencies --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ecf6494..1d10f1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -crossterm = "0.23.2" +crossterm = "0.24.0" unicode-linebreak = "0.1.2" unicode-segmentation = "1.9.0" unicode-width = "0.1.9" From f0af4ddc40ee1c1cca749a01423bfe4ec60300fe Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 7 Jul 2022 16:17:12 +0200 Subject: [PATCH 021/144] Expose chunks and chunk contents --- src/styled.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/styled.rs b/src/styled.rs index 2ee4efb..1a19540 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -15,6 +15,14 @@ impl Chunk { } } + pub fn string(&self) -> &str { + &self.string + } + + pub fn style(&self) -> ContentStyle { + self.style + } + pub fn plain(string: S) -> Self { Self::new(string, ContentStyle::default()) } @@ -81,6 +89,10 @@ impl Styled { self } + pub fn chunks(&self) -> &[Chunk] { + &self.0 + } + pub fn text(&self) -> String { self.0.iter().flat_map(|c| c.string.chars()).collect() } From d693712dab61d806c3ac36083d27016e67794154 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 7 Jul 2022 16:21:05 +0200 Subject: [PATCH 022/144] Expose tab width calculation --- src/frame.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/frame.rs b/src/frame.rs index 2ef1b51..7df7928 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -67,6 +67,10 @@ impl Frame { self.widthdb.width(s) } + pub fn tab_width_at_column(&self, col: usize) -> u8 { + wrap::tab_width_at_column(self.tab_width, col) + } + pub fn wrap(&mut self, text: &str, width: usize) -> Vec { wrap::wrap(&mut self.widthdb, self.tab_width, text, width) } From e4e1454e8064269350ff7f10b2dcbb388d26c57d Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 12 Jul 2022 10:22:14 +0200 Subject: [PATCH 023/144] Calculate width of Styleds directly --- src/frame.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/frame.rs b/src/frame.rs index 7df7928..bec8cb6 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -67,6 +67,15 @@ impl Frame { self.widthdb.width(s) } + /// Like [`Self::width`] for [`Styled`]. + pub fn width_styled(&mut self, s: &Styled) -> usize { + let mut total: usize = 0; + for grapheme in s.graphemes() { + total += self.widthdb.grapheme_width(grapheme) as usize; + } + total + } + pub fn tab_width_at_column(&self, col: usize) -> u8 { wrap::tab_width_at_column(self.tab_width, col) } From 14aedaf25212cd50924566821ad37645a4cacf28 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 13 Jul 2022 10:38:26 +0200 Subject: [PATCH 024/144] Add stack of drawable areas This lets the user restrict the drawable area to a sub-area of the buffer. This lets the user draw without caring about the absolute position, and guarantees that no glyphs or glyph parts appear outside of the drawable area. --- src/buffer.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++----- src/frame.rs | 8 ++++ 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 7031f74..ef4fe4f 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,4 +1,4 @@ -use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Neg, Range, Sub, SubAssign}; use crossterm::style::ContentStyle; @@ -64,6 +64,12 @@ impl Pos { } } +impl From for Pos { + fn from(s: Size) -> Self { + Self::new(s.width.into(), s.height.into()) + } +} + impl Add for Pos { type Output = Self; @@ -72,6 +78,14 @@ impl Add for Pos { } } +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; @@ -79,6 +93,13 @@ impl AddAssign for Pos { } } +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; @@ -87,6 +108,14 @@ impl Sub for Pos { } } +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; @@ -94,6 +123,13 @@ impl SubAssign for Pos { } } +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; @@ -125,9 +161,17 @@ impl Default for Cell { pub struct Buffer { size: Size, data: Vec, + /// 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<(Pos, Size)>, } impl Buffer { + /// Ignores the stack. fn index(&self, x: u16, y: u16) -> usize { assert!(x < self.size.width); assert!(y < self.size.height); @@ -139,6 +183,7 @@ impl Buffer { y * width + x } + /// Ignores the stack. pub(crate) fn at(&self, x: u16, y: u16) -> &Cell { assert!(x < self.size.width); assert!(y < self.size.height); @@ -146,6 +191,7 @@ impl Buffer { &self.data[i] } + /// Ignores the stack. fn at_mut(&mut self, x: u16, y: u16) -> &mut Cell { assert!(x < self.size.width); assert!(y < self.size.height); @@ -153,16 +199,41 @@ impl Buffer { &mut self.data[i] } + pub fn push(&mut self, pos: Pos, size: Size) { + let cur_pos = self.stack.last().map(|(p, _)| *p).unwrap_or(Pos::ZERO); + self.stack.push((cur_pos + pos, size)); + } + + pub fn pop(&mut self) { + self.stack.pop(); + } + + fn drawable_area(&self) -> (Pos, Size) { + self.stack.last().copied().unwrap_or((Pos::ZERO, self.size)) + } + + /// Size of the currently drawable area. pub fn size(&self) -> Size { - self.size + self.drawable_area().1 + } + + /// Min (inclusive) and max (not inclusive) coordinates of the currently + /// drawable area. + fn legal_ranges(&self) -> (Range, Range) { + let (top_left, size) = self.drawable_area(); + let min_x = top_left.x.max(0); + let min_y = top_left.y.max(0); + let max_x = (top_left.x + size.width as i32).min(self.size.width as i32); + let max_y = (top_left.y + size.height as i32).min(self.size.height as i32); + (min_x..max_x, min_y..max_y) } /// Resize the buffer and reset its contents. /// /// The buffer's contents are reset even if the buffer is already the - /// correct size. + /// correct size. The stack is reset as well. pub fn resize(&mut self, size: Size) { - if size == self.size() { + if size == self.size { self.data.fill_with(Cell::default); } else { let width: usize = size.width.into(); @@ -173,13 +244,16 @@ impl Buffer { self.data.clear(); self.data.resize_with(len, Cell::default); } + + self.stack.clear(); } - /// Reset the contents of the buffer. + /// Reset the contents and stack of the buffer. /// /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. pub fn reset(&mut self) { self.data.fill_with(Cell::default); + self.stack.clear(); } /// Remove the grapheme at the specified coordinates from the buffer. @@ -187,6 +261,8 @@ impl Buffer { /// Removes the entire grapheme, not just the cell at the coordinates. /// Preserves the style of the affected cells. Works even if the coordinates /// don't point to the beginning of the grapheme. + /// + /// Ignores the stack. fn erase(&mut self, x: u16, y: u16) { let cell = self.at(x, y); let width: u16 = cell.width.into(); @@ -200,8 +276,10 @@ impl Buffer { } pub fn write(&mut self, widthdb: &mut WidthDB, tab_width: u8, pos: Pos, styled: &Styled) { + let pos = self.drawable_area().0 + pos; + let (xrange, yrange) = self.legal_ranges(); // If we're not even visible, there's nothing to do - if pos.y < 0 || pos.y >= self.size.height as i32 { + if !yrange.contains(&pos.y) { return; } let y = pos.y as u16; @@ -215,22 +293,30 @@ impl Buffer { let width = wrap::tab_width_at_column(tab_width, col); col += width as usize; for dx in 0..width { - self.write_grapheme(x + dx as i32, y, width, " ", style); + self.write_grapheme(&xrange, x + dx as i32, y, width, " ", style); } } else { let width = widthdb.grapheme_width(g); col += width as usize; if width > 0 { - self.write_grapheme(x, y, width, g, style); + self.write_grapheme(&xrange, x, y, width, g, style); } } } } /// Assumes that `pos.y` is in range. - fn write_grapheme(&mut self, x: i32, y: u16, width: u8, grapheme: &str, style: ContentStyle) { - let min_x = 0; - let max_x = self.size.width as i32 - 1; // Last possible cell + fn write_grapheme( + &mut self, + xrange: &Range, + x: i32, + y: u16, + width: u8, + grapheme: &str, + style: ContentStyle, + ) { + 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 diff --git a/src/frame.rs b/src/frame.rs index bec8cb6..75fc091 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -26,6 +26,14 @@ impl Default for Frame { } 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() } From 53b2728c827c9f93a743d5860c1b31cb1399875b Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 21 Jul 2022 14:31:23 +0200 Subject: [PATCH 025/144] Fix Size subtraction --- src/buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buffer.rs b/src/buffer.rs index ef4fe4f..359fef7 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -39,7 +39,7 @@ impl Sub for Size { type Output = Self; fn sub(self, rhs: Self) -> Self { - Self::new(self.width + rhs.width, self.height + rhs.height) + Self::new(self.width - rhs.width, self.height - rhs.height) } } From c1907bb8ee215d534daf4188824293203b60dd9c Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 23 Jul 2022 22:24:28 +0200 Subject: [PATCH 026/144] Fix frame cursor functions ignoring stack --- src/buffer.rs | 10 +++++++++- src/frame.rs | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 359fef7..da77ebc 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -184,7 +184,7 @@ impl Buffer { } /// Ignores the stack. - pub(crate) fn at(&self, x: u16, y: u16) -> &Cell { + 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); @@ -208,6 +208,14 @@ impl Buffer { self.stack.pop(); } + pub fn local_to_global(&self, pos: Pos) -> Pos { + pos + self.stack.last().map(|(p, _)| *p).unwrap_or(Pos::ZERO) + } + + pub fn global_to_local(&self, pos: Pos) -> Pos { + pos - self.stack.last().map(|(p, _)| *p).unwrap_or(Pos::ZERO) + } + fn drawable_area(&self) -> (Pos, Size) { self.stack.last().copied().unwrap_or((Pos::ZERO, self.size)) } diff --git a/src/frame.rs b/src/frame.rs index 75fc091..5859e4f 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -44,11 +44,11 @@ impl Frame { } pub fn cursor(&self) -> Option { - self.cursor + self.cursor.map(|p| self.buffer.global_to_local(p)) } pub fn set_cursor(&mut self, pos: Option) { - self.cursor = pos; + self.cursor = pos.map(|p| self.buffer.local_to_global(p)); } pub fn show_cursor(&mut self, pos: Pos) { From 464aefa6d744e671806789dfd6fbd22c7e333273 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 1 Aug 2022 00:06:36 +0200 Subject: [PATCH 027/144] Forbid stack frames from expanding the drawable area --- src/buffer.rs | 121 +++++++++++++++++++++++++++++++++++--------------- src/frame.rs | 7 +-- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index da77ebc..c02ca2a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -157,6 +157,73 @@ impl Default for Cell { } } +#[derive(Debug, Clone, Copy)] +pub 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 + } + } + + pub 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)); + + StackFrame { + pos, + size, + drawable_area, + } + } + + pub fn local_to_global(&self, local_pos: Pos) -> Pos { + local_pos + self.pos + } + + pub 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. + pub 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)] pub struct Buffer { size: Size, @@ -167,7 +234,7 @@ pub struct Buffer { /// 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<(Pos, Size)>, + stack: Vec, } impl Buffer { @@ -187,6 +254,7 @@ impl Buffer { 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] } @@ -195,47 +263,27 @@ impl Buffer { 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] } + pub 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) { - let cur_pos = self.stack.last().map(|(p, _)| *p).unwrap_or(Pos::ZERO); - self.stack.push((cur_pos + pos, size)); + self.stack.push(self.current_frame().then(pos, size)); } pub fn pop(&mut self) { self.stack.pop(); } - pub fn local_to_global(&self, pos: Pos) -> Pos { - pos + self.stack.last().map(|(p, _)| *p).unwrap_or(Pos::ZERO) - } - - pub fn global_to_local(&self, pos: Pos) -> Pos { - pos - self.stack.last().map(|(p, _)| *p).unwrap_or(Pos::ZERO) - } - - fn drawable_area(&self) -> (Pos, Size) { - self.stack.last().copied().unwrap_or((Pos::ZERO, self.size)) - } - - /// Size of the currently drawable area. - pub fn size(&self) -> Size { - self.drawable_area().1 - } - - /// Min (inclusive) and max (not inclusive) coordinates of the currently - /// drawable area. - fn legal_ranges(&self) -> (Range, Range) { - let (top_left, size) = self.drawable_area(); - let min_x = top_left.x.max(0); - let min_y = top_left.y.max(0); - let max_x = (top_left.x + size.width as i32).min(self.size.width as i32); - let max_y = (top_left.y + size.height as i32).min(self.size.height as i32); - (min_x..max_x, min_y..max_y) - } - /// Resize the buffer and reset its contents. /// /// The buffer's contents are reset even if the buffer is already the @@ -284,11 +332,14 @@ impl Buffer { } pub fn write(&mut self, widthdb: &mut WidthDB, tab_width: u8, pos: Pos, styled: &Styled) { - let pos = self.drawable_area().0 + pos; - let (xrange, yrange) = self.legal_ranges(); - // If we're not even visible, there's nothing to do + 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; + return; // Outside of drawable area } let y = pos.y as u16; diff --git a/src/frame.rs b/src/frame.rs index 5859e4f..84cb240 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -35,7 +35,7 @@ impl Frame { } pub fn size(&self) -> Size { - self.buffer.size() + self.buffer.current_frame().size } pub fn reset(&mut self) { @@ -44,11 +44,12 @@ impl Frame { } pub fn cursor(&self) -> Option { - self.cursor.map(|p| self.buffer.global_to_local(p)) + self.cursor + .map(|p| self.buffer.current_frame().global_to_local(p)) } pub fn set_cursor(&mut self, pos: Option) { - self.cursor = pos.map(|p| self.buffer.local_to_global(p)); + self.cursor = pos.map(|p| self.buffer.current_frame().local_to_global(p)); } pub fn show_cursor(&mut self, pos: Pos) { From 26a8936cf50ee4b775fd4d1bf96f1b3077421e5c Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 1 Aug 2022 19:02:57 +0200 Subject: [PATCH 028/144] Store Styled with contiguous string The previous implementation of Styled used chunks that consisted of a String and a ContentStyle. The current implementation instead stores a single String and chunks consisting of a ContentStyle and an ending index. This implementation may reduce allocations and makes width-related operations easier, for example getting the width of a Styled with its whitespace trimmed. --- examples/text_wrapping.rs | 2 +- src/buffer.rs | 2 +- src/frame.rs | 9 -- src/styled.rs | 282 ++++++++++++++++++++------------------ 4 files changed, 149 insertions(+), 146 deletions(-) diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index ecf49aa..bc5d569 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -31,7 +31,7 @@ fn draw(f: &mut Frame) { ); let breaks = f.wrap(text, f.size().width.into()); - let lines = Styled::default().then(text).split_at_indices(&breaks); + 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); diff --git a/src/buffer.rs b/src/buffer.rs index c02ca2a..e25035a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -344,7 +344,7 @@ impl Buffer { let y = pos.y as u16; let mut col: usize = 0; - for styled_grapheme in styled.styled_graphemes() { + for (_, styled_grapheme) in styled.styled_grapheme_indices() { let x = pos.x + col as i32; let g = *styled_grapheme.content(); let style = *styled_grapheme.style(); diff --git a/src/frame.rs b/src/frame.rs index 84cb240..2943c8e 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -76,15 +76,6 @@ impl Frame { self.widthdb.width(s) } - /// Like [`Self::width`] for [`Styled`]. - pub fn width_styled(&mut self, s: &Styled) -> usize { - let mut total: usize = 0; - for grapheme in s.graphemes() { - total += self.widthdb.grapheme_width(grapheme) as usize; - } - total - } - pub fn tab_width_at_column(&self, col: usize) -> u8 { wrap::tab_width_at_column(self.tab_width, col) } diff --git a/src/styled.rs b/src/styled.rs index 1a19540..68f32c4 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -1,147 +1,81 @@ +use std::iter::Peekable; +use std::{slice, vec}; + use crossterm::style::{ContentStyle, StyledContent}; -use unicode_segmentation::UnicodeSegmentation; - -#[derive(Debug, Clone)] -pub struct Chunk { - string: String, - style: ContentStyle, -} - -impl Chunk { - pub fn new(string: S, style: ContentStyle) -> Self { - Self { - string: string.to_string(), - style, - } - } - - pub fn string(&self) -> &str { - &self.string - } - - pub fn style(&self) -> ContentStyle { - self.style - } - - pub fn plain(string: S) -> Self { - Self::new(string, ContentStyle::default()) - } - - pub fn split_at(&self, mid: usize) -> (Self, Self) { - let (lstr, rstr) = self.string.split_at(mid); - let left = Self { - string: lstr.to_string(), - style: self.style, - }; - let right = Self { - string: rstr.to_string(), - style: self.style, - }; - (left, right) - } -} - -impl From<&str> for Chunk { - fn from(str: &str) -> Self { - Self::plain(str) - } -} - -impl From for Chunk { - fn from(string: String) -> Self { - Self::plain(string) - } -} - -impl From<&String> for Chunk { - fn from(string: &String) -> Self { - Self::plain(string) - } -} - -impl From<(S,)> for Chunk { - fn from(tuple: (S,)) -> Self { - Self::plain(tuple.0) - } -} - -impl From<(S, ContentStyle)> for Chunk { - fn from(tuple: (S, ContentStyle)) -> Self { - Self::new(tuple.0, tuple.1) - } -} +use unicode_segmentation::{GraphemeIndices, Graphemes, UnicodeSegmentation}; #[derive(Debug, Default, Clone)] -pub struct Styled(Vec); +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<(ContentStyle, usize)>, +} impl Styled { - pub fn new>(chunk: C) -> Self { - Self::default().then(chunk) + pub fn new>(text: S, style: ContentStyle) -> Self { + Self::default().then(text, style) } - pub fn then>(mut self, chunk: C) -> Self { - self.0.push(chunk.into()); + pub fn new_plain>(text: S) -> Self { + Self::default().then_plain(text) + } + + pub fn then>(mut self, text: S, style: ContentStyle) -> 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 and_then(mut self, other: Styled) -> Self { - self.0.extend(other.0); + pub fn then_plain>(self, text: S) -> Self { + self.then(text, ContentStyle::default()) + } + + pub fn and_then(mut self, mut other: Styled) -> 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 chunks(&self) -> &[Chunk] { - &self.0 - } - - pub fn text(&self) -> String { - self.0.iter().flat_map(|c| c.string.chars()).collect() - } - - pub fn graphemes(&self) -> impl Iterator { - self.0.iter().flat_map(|c| c.string.graphemes(true)) - } - - pub fn grapheme_indices(&self) -> impl Iterator { - self.0 - .iter() - .scan(0, |s, c| { - let offset = *s; - *s += c.string.len(); - Some((offset, c)) - }) - .flat_map(|(o, c)| { - c.string - .grapheme_indices(true) - .map(move |(gi, g)| (o + gi, g)) - }) - } - - pub fn styled_graphemes(&self) -> impl Iterator> { - self.0.iter().flat_map(|c| { - c.string - .graphemes(true) - .map(|g| StyledContent::new(c.style, g)) - }) + pub fn text(&self) -> &str { + &self.text } pub fn split_at(self, mid: usize) -> (Self, Self) { - let mut left = vec![]; - let mut right = vec![]; - let mut offset = 0; - for chunk in self.0 { - let len = chunk.string.len(); - if offset >= mid { - right.push(chunk); - } else if offset + len > mid { - let (lchunk, rchunk) = chunk.split_at(mid - offset); - left.push(lchunk); - right.push(rchunk); - } else { - left.push(chunk); + 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.max(mid))); } - offset += len; + if mid < until { + right_styles.push((style, until.saturating_sub(mid))); + } + from = until; } - (Self(left), Self(right)) + + 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 { @@ -163,20 +97,98 @@ impl Styled { } pub fn trim_end(&mut self) { - while let Some(last) = self.0.last_mut() { - let trimmed = last.string.trim_end(); - if trimmed.is_empty() { - self.0.pop(); - } else { - last.string = trimmed.to_string(); + 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(); + } } } -impl> From for Styled { - fn from(chunk: C) -> Self { - Self::new(chunk) +////////////////////////////// +// Iterating over graphemes // +////////////////////////////// + +pub struct StyledGraphemeIndices<'a> { + text: GraphemeIndices<'a>, + styles: Peekable>, +} + +impl<'a> Iterator for StyledGraphemeIndices<'a> { + type Item = (usize, StyledContent<&'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, StyledContent::new(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, ContentStyle)> for Styled { + fn from((text, style): (S, ContentStyle)) -> Self { + Self::new(text, style) + } +} + +impl> From<&[(S, ContentStyle)]> for Styled { + fn from(segments: &[(S, ContentStyle)]) -> Self { + let mut result = Self::default(); + for (text, style) in segments { + result = result.then(text, *style); + } + result } } From dfc10f9d092bf9ac85a8718f1b80df28aeb0ec23 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 3 Aug 2022 13:19:46 +0200 Subject: [PATCH 029/144] Fix splitting Styleds --- src/styled.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styled.rs b/src/styled.rs index 68f32c4..9871aab 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -57,7 +57,7 @@ impl Styled { let mut from = 0; for (style, until) in self.styles { if from < mid { - left_styles.push((style, until.max(mid))); + left_styles.push((style, until.min(mid))); } if mid < until { right_styles.push((style, until.saturating_sub(mid))); From d186291ef7f8d94963638c88446726d6375961b1 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 3 Aug 2022 21:57:15 +0200 Subject: [PATCH 030/144] Fix word wrapping with successive forced breaks If there were multiple forced breaks in succession, all except the first would be a bit too wide since I forgot to include the current grapheme in the new line's width. --- src/wrap.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/wrap.rs b/src/wrap.rs index 80f318f..ac9964c 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -52,13 +52,15 @@ pub fn wrap(widthdb: &mut WidthDB, tab_width: u8, text: &str, width: usize) -> V } // Calculate widths after current grapheme + let g_is_whitespace = g.chars().all(|c| c.is_whitespace()); let g_width = if g == "\t" { tab_width_at_column(tab_width, current_width) as usize } else { widthdb.grapheme_width(g) as usize }; + let g_width_trimmed = if g_is_whitespace { 0 } else { g_width }; let mut new_width = current_width + g_width; - let mut new_width_trimmed = if g.chars().all(|c| c.is_whitespace()) { + let mut new_width_trimmed = if g_is_whitespace { current_width_trimmed } else { new_width @@ -87,8 +89,8 @@ pub fn wrap(widthdb: &mut WidthDB, tab_width: u8, text: &str, width: usize) -> V // Forced break in the midde of a normally non-breakable chunk // because there are no valid break points. breaks.push(gi); - new_width = 0; - new_width_trimmed = 0; + new_width = g_width; + new_width_trimmed = g_width_trimmed; valid_break = None; valid_break_width = 0; } From 31bb2de87b98233321d8aef0189cd29dc366bc69 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 4 Aug 2022 01:36:33 +0200 Subject: [PATCH 031/144] Make WidthDB tab-width-aware --- src/buffer.rs | 15 +++++---------- src/frame.rs | 31 +++++++++---------------------- src/terminal.rs | 4 ++-- src/widthdb.rs | 38 +++++++++++++++++++++++++++----------- src/wrap.rs | 8 ++------ 5 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index e25035a..9345e8c 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -4,7 +4,6 @@ use crossterm::style::ContentStyle; use crate::styled::Styled; use crate::widthdb::WidthDB; -use crate::wrap; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Size { @@ -331,7 +330,7 @@ impl Buffer { } } - pub fn write(&mut self, widthdb: &mut WidthDB, tab_width: u8, pos: Pos, styled: &Styled) { + 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, @@ -348,18 +347,14 @@ impl Buffer { let x = pos.x + col as i32; let g = *styled_grapheme.content(); let style = *styled_grapheme.style(); + let width = widthdb.grapheme_width(g, col); + col += width as usize; if g == "\t" { - let width = wrap::tab_width_at_column(tab_width, col); - col += width as usize; for dx in 0..width { self.write_grapheme(&xrange, x + dx as i32, y, width, " ", style); } - } else { - let width = widthdb.grapheme_width(g); - col += width as usize; - if width > 0 { - self.write_grapheme(&xrange, x, y, width, g, style); - } + } else if width > 0 { + self.write_grapheme(&xrange, x, y, width, g, style); } } } diff --git a/src/frame.rs b/src/frame.rs index 2943c8e..ae0c364 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -6,23 +6,11 @@ use crate::styled::Styled; use crate::widthdb::WidthDB; use crate::wrap; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Frame { pub(crate) widthdb: WidthDB, pub(crate) buffer: Buffer, cursor: Option, - pub(crate) tab_width: u8, -} - -impl Default for Frame { - fn default() -> Self { - Self { - widthdb: Default::default(), - buffer: Default::default(), - cursor: None, - tab_width: 8, - } - } } impl Frame { @@ -62,30 +50,29 @@ impl Frame { /// 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, it is estimated using the /// Unicode Standard Annex #11. - pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { - self.widthdb.grapheme_width(grapheme) + pub fn grapheme_width(&mut self, grapheme: &str, col: usize) -> u8 { + self.widthdb.grapheme_width(grapheme, col) } /// 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, it is estimated /// using the Unicode Standard Annex #11. pub fn width(&mut self, s: &str) -> usize { self.widthdb.width(s) } - pub fn tab_width_at_column(&self, col: usize) -> u8 { - wrap::tab_width_at_column(self.tab_width, col) - } - pub fn wrap(&mut self, text: &str, width: usize) -> Vec { - wrap::wrap(&mut self.widthdb, self.tab_width, text, width) + wrap::wrap(&mut self.widthdb, text, width) } pub fn write>(&mut self, pos: Pos, styled: S) { - self.buffer - .write(&mut self.widthdb, self.tab_width, pos, &styled.into()); + self.buffer.write(&mut self.widthdb, pos, &styled.into()); } } diff --git a/src/terminal.rs b/src/terminal.rs index f7e1d05..8307b47 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -60,11 +60,11 @@ impl Terminal { } pub fn set_tab_width(&mut self, tab_width: u8) { - self.frame.tab_width = tab_width; + self.frame.widthdb.tab_width = tab_width; } pub fn tab_width(&self) -> u8 { - self.frame.tab_width + self.frame.widthdb.tab_width } pub fn set_measuring(&mut self, active: bool) { diff --git a/src/widthdb.rs b/src/widthdb.rs index 9072995..00a5995 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -9,20 +9,42 @@ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; /// Measures and stores the with (in terminal coordinates) of graphemes. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct WidthDB { pub active: bool, known: HashMap, requested: HashSet, + pub(crate) tab_width: u8, +} + +impl Default for WidthDB { + fn default() -> Self { + Self { + active: false, + known: Default::default(), + requested: Default::default(), + tab_width: 8, + } + } } 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, it is estimated using the /// Unicode Standard Annex #11. - pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { + 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.active { return grapheme.width() as u8; } @@ -36,20 +58,14 @@ impl WidthDB { /// 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, it is estimated /// using the Unicode Standard Annex #11. pub fn width(&mut self, s: &str) -> usize { - if !self.active { - return s.width(); - } let mut total: usize = 0; for grapheme in s.graphemes(true) { - total += if let Some(width) = self.known.get(grapheme) { - (*width).into() - } else { - self.requested.insert(grapheme.to_string()); - grapheme.width() - }; + total += self.grapheme_width(grapheme, total) as usize; } total } diff --git a/src/wrap.rs b/src/wrap.rs index ac9964c..21e1814 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -5,11 +5,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::widthdb::WidthDB; -pub fn tab_width_at_column(tab_width: u8, col: usize) -> u8 { - tab_width - (col % tab_width as usize) as u8 -} - -pub fn wrap(widthdb: &mut WidthDB, tab_width: u8, text: &str, width: usize) -> Vec { +pub fn wrap(widthdb: &mut WidthDB, text: &str, width: usize) -> Vec { let mut breaks = vec![]; let mut break_options = unicode_linebreak::linebreaks(text).peekable(); @@ -54,7 +50,7 @@ pub fn wrap(widthdb: &mut WidthDB, tab_width: u8, text: &str, width: usize) -> V // Calculate widths after current grapheme let g_is_whitespace = g.chars().all(|c| c.is_whitespace()); let g_width = if g == "\t" { - tab_width_at_column(tab_width, current_width) as usize + widthdb.tab_width_at_column(current_width) as usize } else { widthdb.grapheme_width(g) as usize }; From 3b2a2105fe73007e98c9f27f60eab6ab1b76f82a Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 4 Aug 2022 02:02:58 +0200 Subject: [PATCH 032/144] Fix incorrect width for tab-replacing spaces --- src/buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buffer.rs b/src/buffer.rs index 9345e8c..ca95782 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -351,7 +351,7 @@ impl Buffer { col += width as usize; if g == "\t" { for dx in 0..width { - self.write_grapheme(&xrange, x + dx as i32, y, width, " ", style); + self.write_grapheme(&xrange, x + dx as i32, y, 1, " ", style); } } else if width > 0 { self.write_grapheme(&xrange, x, y, width, g, style); From 5957e8e5508a3772b2229fc9d8ac30ce4173d356 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 4 Aug 2022 02:04:27 +0200 Subject: [PATCH 033/144] Fix tab width calculations when word wrapping --- src/wrap.rs | 52 +++++++++++++++++++++------------------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/src/wrap.rs b/src/wrap.rs index 21e1814..4ebed46 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -12,10 +12,10 @@ pub fn wrap(widthdb: &mut WidthDB, text: &str, width: usize) -> Vec { // The last valid break point encountered and its width let mut valid_break = None; - let mut valid_break_width = 0; - // Width of the line at the current grapheme (with and without trailing - // whitespace) + // 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; @@ -36,65 +36,55 @@ pub fn wrap(widthdb: &mut WidthDB, text: &str, width: usize) -> Vec { BreakOpportunity::Mandatory => { breaks.push(bi); valid_break = None; - valid_break_width = 0; + current_start = bi; current_width = 0; current_width_trimmed = 0; } BreakOpportunity::Allowed => { valid_break = Some(bi); - valid_break_width = current_width; } } } // Calculate widths after current grapheme let g_is_whitespace = g.chars().all(|c| c.is_whitespace()); - let g_width = if g == "\t" { - widthdb.tab_width_at_column(current_width) as usize - } else { - widthdb.grapheme_width(g) as usize - }; - let g_width_trimmed = if g_is_whitespace { 0 } else { g_width }; - let mut new_width = current_width + g_width; - let mut new_width_trimmed = if g_is_whitespace { - current_width_trimmed - } else { - new_width - }; + 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 new_width_trimmed > width { + if current_width_trimmed > width { if let Some(bi) = valid_break { + let new_line = &text[bi..gi + g.len()]; + breaks.push(bi); - new_width -= valid_break_width; - new_width_trimmed = new_width_trimmed.saturating_sub(valid_break_width); valid_break = None; - valid_break_width = 0; + 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 new_width_trimmed > width { - if new_width == g_width { + 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 midde of a normally non-breakable chunk + // Forced break in the middle of a normally non-breakable chunk // because there are no valid break points. breaks.push(gi); - new_width = g_width; - new_width_trimmed = g_width_trimmed; valid_break = None; - valid_break_width = 0; + current_start = gi; + current_width = widthdb.grapheme_width(g, 0).into(); + current_width_trimmed = if g_is_whitespace { 0 } else { current_width }; } } - - // Update current width - current_width = new_width; - current_width_trimmed = new_width_trimmed; } breaks From fbe9e065fcc76f445c8e4feee04dcbf230586a4c Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 10 Aug 2022 22:51:22 +0200 Subject: [PATCH 034/144] Update dependencies --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1d10f1e..ab7022f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -crossterm = "0.24.0" +crossterm = "0.25.0" unicode-linebreak = "0.1.2" unicode-segmentation = "1.9.0" unicode-width = "0.1.9" From 7e429132458514e8dc99ab6be789b9c8225ed00e Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 10 Aug 2022 23:30:56 +0200 Subject: [PATCH 035/144] Enable bracketed paste mode in Terminal Only on non-windows platforms though, crossterm doesn't support paste events on windows. --- src/terminal.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/terminal.rs b/src/terminal.rs index 8307b47..7f01c90 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -4,6 +4,7 @@ use std::io::Write; use std::{io, mem}; use crossterm::cursor::{Hide, MoveTo, Show}; +use crossterm::event::{DisableBracketedPaste, EnableBracketedPaste}; use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::{ExecutableCommand, QueueableCommand}; @@ -48,6 +49,8 @@ impl Terminal { pub fn suspend(&mut self) -> io::Result<()> { crossterm::terminal::disable_raw_mode()?; self.out.execute(LeaveAlternateScreen)?; + #[cfg(not(windows))] + self.out.execute(DisableBracketedPaste)?; self.out.execute(Show)?; Ok(()) } @@ -55,6 +58,8 @@ impl Terminal { pub fn unsuspend(&mut self) -> io::Result<()> { crossterm::terminal::enable_raw_mode()?; self.out.execute(EnterAlternateScreen)?; + #[cfg(not(windows))] + self.out.execute(EnableBracketedPaste)?; self.full_redraw = true; Ok(()) } From 45ece466c235cce6e998bbd404f915cad3628c8c Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 11 Aug 2022 23:15:09 +0200 Subject: [PATCH 036/144] Support more modifiers on some terminal emulators If a terminal emulator supports the kitty protocol, this change will allow cove to take advantage of this. This means that more modifiers will work on special keys like enter or escape. See also this section of the kitty protocol docs: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate-escape-codes --- src/terminal.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/terminal.rs b/src/terminal.rs index 7f01c90..9feca31 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -4,7 +4,10 @@ use std::io::Write; use std::{io, mem}; use crossterm::cursor::{Hide, MoveTo, Show}; -use crossterm::event::{DisableBracketedPaste, EnableBracketedPaste}; +use crossterm::event::{ + DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, + PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::{ExecutableCommand, QueueableCommand}; @@ -50,7 +53,10 @@ impl Terminal { crossterm::terminal::disable_raw_mode()?; self.out.execute(LeaveAlternateScreen)?; #[cfg(not(windows))] - self.out.execute(DisableBracketedPaste)?; + { + self.out.execute(DisableBracketedPaste)?; + self.out.execute(PopKeyboardEnhancementFlags)?; + } self.out.execute(Show)?; Ok(()) } @@ -59,7 +65,12 @@ impl Terminal { crossterm::terminal::enable_raw_mode()?; self.out.execute(EnterAlternateScreen)?; #[cfg(not(windows))] - self.out.execute(EnableBracketedPaste)?; + { + self.out.execute(EnableBracketedPaste)?; + self.out.execute(PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES, + ))?; + } self.full_redraw = true; Ok(()) } From 24fd0050fbfdd72ac2f03029148370b362291777 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 8 Sep 2022 18:13:13 +0200 Subject: [PATCH 037/144] Fix drawing widgets on cursor not removing cursor --- src/buffer.rs | 61 ++++++++++++++++++++++++++++++++++++++++++--------- src/frame.rs | 9 +++----- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index ca95782..44d50d5 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -157,7 +157,7 @@ impl Default for Cell { } #[derive(Debug, Clone, Copy)] -pub struct StackFrame { +struct StackFrame { pub pos: Pos, pub size: Size, pub drawable_area: Option<(Pos, Size)>, @@ -188,7 +188,7 @@ impl StackFrame { } } - pub fn then(&self, pos: Pos, size: Size) -> Self { + fn then(&self, pos: Pos, size: Size) -> Self { let pos = self.local_to_global(pos); let drawable_area = self @@ -202,17 +202,17 @@ impl StackFrame { } } - pub fn local_to_global(&self, local_pos: Pos) -> Pos { + fn local_to_global(&self, local_pos: Pos) -> Pos { local_pos + self.pos } - pub fn global_to_local(&self, global_pos: Pos) -> 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. - pub fn legal_ranges(&self) -> Option<(Range, Range)> { + 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; @@ -227,6 +227,8 @@ impl StackFrame { 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 @@ -237,6 +239,9 @@ pub struct Buffer { } 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); @@ -249,6 +254,9 @@ impl Buffer { 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); @@ -258,6 +266,9 @@ impl Buffer { &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); @@ -267,7 +278,7 @@ impl Buffer { &mut self.data[i] } - pub fn current_frame(&self) -> StackFrame { + fn current_frame(&self) -> StackFrame { self.stack.last().copied().unwrap_or(StackFrame { pos: Pos::ZERO, size: self.size, @@ -283,6 +294,19 @@ impl Buffer { 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 @@ -300,6 +324,8 @@ impl Buffer { self.data.resize_with(len, Cell::default); } + self.cursor = None; + self.stack.clear(); } @@ -307,29 +333,35 @@ impl Buffer { /// /// `buf.reset()` is equivalent to `buf.resize(buf.size())`. pub fn reset(&mut self) { - self.data.fill_with(Cell::default); - self.stack.clear(); + self.resize(self.size); } /// Remove the grapheme at the specified coordinates from the buffer. /// /// Removes the entire grapheme, not just the cell at the coordinates. - /// Preserves the style of the affected cells. Works even if the coordinates - /// don't point to the beginning of the grapheme. + /// Preserves the style of the affected cells. Preserves the cursor. Works + /// even if the coordinates don't point to the beginning of the grapheme. /// /// Ignores the stack. fn erase(&mut self, x: u16, y: u16) { let cell = self.at(x, y); let width: u16 = cell.width.into(); let offset: u16 = cell.offset.into(); + for x in (x - offset)..(x - offset + width) { let cell = self.at_mut(x, y); let style = cell.style; + *cell = Cell::default(); cell.style = style; } } + /// 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() { @@ -359,6 +391,8 @@ impl Buffer { } } + /// Write a single grapheme to the buffer, respecting its width. + /// /// Assumes that `pos.y` is in range. fn write_grapheme( &mut self, @@ -403,6 +437,13 @@ impl Buffer { }; } } + + 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<'_> { diff --git a/src/frame.rs b/src/frame.rs index ae0c364..38fab3f 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -10,7 +10,6 @@ use crate::wrap; pub struct Frame { pub(crate) widthdb: WidthDB, pub(crate) buffer: Buffer, - cursor: Option, } impl Frame { @@ -23,21 +22,19 @@ impl Frame { } pub fn size(&self) -> Size { - self.buffer.current_frame().size + self.buffer.size() } pub fn reset(&mut self) { self.buffer.reset(); - self.cursor = None; } pub fn cursor(&self) -> Option { - self.cursor - .map(|p| self.buffer.current_frame().global_to_local(p)) + self.buffer.cursor() } pub fn set_cursor(&mut self, pos: Option) { - self.cursor = pos.map(|p| self.buffer.current_frame().local_to_global(p)); + self.buffer.set_cursor(pos); } pub fn show_cursor(&mut self, pos: Pos) { From f258c840948fa67e7ce624331fcca15ec004302f Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 26 Sep 2022 16:55:37 +0200 Subject: [PATCH 038/144] Expose Widthdb directly via Frame --- examples/overlapping_graphemes.rs | 2 +- src/frame.rs | 20 ++------------------ src/lib.rs | 2 +- src/widthdb.rs | 16 +++++++++++----- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index adf610c..e87dff7 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -43,7 +43,7 @@ fn draw(f: &mut Frame) { Pos::new(0, 13), "scientist emoji as a woman and a microscope: 👩‍🔬", ); - for i in 0..(f.width(scientist) + 4) { + 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)); } diff --git a/src/frame.rs b/src/frame.rs index 38fab3f..235c7dd 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -45,24 +45,8 @@ impl Frame { self.set_cursor(None); } - /// 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, it is estimated using the - /// Unicode Standard Annex #11. - pub fn grapheme_width(&mut self, grapheme: &str, col: usize) -> u8 { - self.widthdb.grapheme_width(grapheme, col) - } - - /// 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, it is estimated - /// using the Unicode Standard Annex #11. - pub fn width(&mut self, s: &str) -> usize { - self.widthdb.width(s) + pub fn widthdb(&mut self) -> &mut WidthDB { + &mut self.widthdb } pub fn wrap(&mut self, text: &str, width: usize) -> Vec { diff --git a/src/lib.rs b/src/lib.rs index d73aeed..0afea1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,5 +2,5 @@ mod buffer; pub mod frame; pub mod styled; pub mod terminal; -mod widthdb; +pub mod widthdb; mod wrap; diff --git a/src/widthdb.rs b/src/widthdb.rs index 00a5995..d926c26 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -8,22 +8,24 @@ use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; +use crate::wrap; + /// Measures and stores the with (in terminal coordinates) of graphemes. #[derive(Debug)] pub struct WidthDB { - pub active: bool, + pub(crate) active: bool, + pub(crate) tab_width: u8, known: HashMap, requested: HashSet, - pub(crate) tab_width: u8, } impl Default for WidthDB { fn default() -> Self { Self { active: false, + tab_width: 8, known: Default::default(), requested: Default::default(), - tab_width: 8, } } } @@ -70,9 +72,13 @@ impl WidthDB { total } + pub fn wrap(&mut self, text: &str, width: usize) -> Vec { + wrap::wrap(self, text, width) + } + /// Whether any new graphemes have been seen since the last time /// [`Self::measure_widths`] was called. - pub fn measuring_required(&self) -> bool { + pub(crate) fn measuring_required(&self) -> bool { self.active && !self.requested.is_empty() } @@ -82,7 +88,7 @@ impl WidthDB { /// This function measures the actual width of graphemes by writing them to /// the terminal. After it finishes, the terminal's contents should be /// assumed to be garbage and a full redraw should be performed. - pub fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { + pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { if !self.active { return Ok(()); } From 6ed47ad9161be389cb7e7e3077de1d5fd187ce52 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 26 Sep 2022 17:01:49 +0200 Subject: [PATCH 039/144] Rename WidthDB to WidthDb --- src/buffer.rs | 4 ++-- src/frame.rs | 6 +++--- src/widthdb.rs | 6 +++--- src/wrap.rs | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 44d50d5..f004cf3 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -3,7 +3,7 @@ use std::ops::{Add, AddAssign, Neg, Range, Sub, SubAssign}; use crossterm::style::ContentStyle; use crate::styled::Styled; -use crate::widthdb::WidthDB; +use crate::widthdb::WidthDb; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Size { @@ -362,7 +362,7 @@ impl Buffer { /// /// 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) { + 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, diff --git a/src/frame.rs b/src/frame.rs index 235c7dd..5bb4436 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -3,12 +3,12 @@ use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; use crate::styled::Styled; -use crate::widthdb::WidthDB; +use crate::widthdb::WidthDb; use crate::wrap; #[derive(Debug, Default)] pub struct Frame { - pub(crate) widthdb: WidthDB, + pub(crate) widthdb: WidthDb, pub(crate) buffer: Buffer, } @@ -45,7 +45,7 @@ impl Frame { self.set_cursor(None); } - pub fn widthdb(&mut self) -> &mut WidthDB { + pub fn widthdb(&mut self) -> &mut WidthDb { &mut self.widthdb } diff --git a/src/widthdb.rs b/src/widthdb.rs index d926c26..585bc4f 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -12,14 +12,14 @@ use crate::wrap; /// Measures and stores the with (in terminal coordinates) of graphemes. #[derive(Debug)] -pub struct WidthDB { +pub struct WidthDb { pub(crate) active: bool, pub(crate) tab_width: u8, known: HashMap, requested: HashSet, } -impl Default for WidthDB { +impl Default for WidthDb { fn default() -> Self { Self { active: false, @@ -30,7 +30,7 @@ impl Default for WidthDB { } } -impl WidthDB { +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 diff --git a/src/wrap.rs b/src/wrap.rs index 4ebed46..a0f4e0d 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -3,9 +3,9 @@ use unicode_linebreak::BreakOpportunity; use unicode_segmentation::UnicodeSegmentation; -use crate::widthdb::WidthDB; +use crate::widthdb::WidthDb; -pub fn wrap(widthdb: &mut WidthDB, text: &str, width: usize) -> Vec { +pub fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { let mut breaks = vec![]; let mut break_options = unicode_linebreak::linebreaks(text).peekable(); From f48901f5434541703acac99554cca3b91e913e20 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 26 Sep 2022 17:21:48 +0200 Subject: [PATCH 040/144] Expose WidthDb from Terminal --- src/terminal.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/terminal.rs b/src/terminal.rs index 9feca31..878a49a 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -14,6 +14,7 @@ use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::{Buffer, Size}; use crate::frame::Frame; +use crate::widthdb::WidthDb; pub struct Terminal { /// Render target. @@ -119,6 +120,10 @@ impl Terminal { &mut self.frame } + pub fn widthdb(&mut self) -> &mut WidthDb { + &mut self.frame.widthdb + } + /// Display the current frame on the screen and prepare the next frame. /// Returns `true` if an immediate redraw is required. /// From 06aefd562bd66f5564f7c1ea73a4959f51be74e7 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 26 Sep 2022 17:34:38 +0200 Subject: [PATCH 041/144] Remove wrap method from Frame --- examples/text_wrapping.rs | 3 ++- src/frame.rs | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index bc5d569..1f711fa 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -30,7 +30,8 @@ fn draw(f: &mut Frame) { "123456789\tx\n", ); - let breaks = f.wrap(text, f.size().width.into()); + 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(); diff --git a/src/frame.rs b/src/frame.rs index 5bb4436..3eef23b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -4,7 +4,6 @@ use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; use crate::styled::Styled; use crate::widthdb::WidthDb; -use crate::wrap; #[derive(Debug, Default)] pub struct Frame { @@ -49,10 +48,6 @@ impl Frame { &mut self.widthdb } - pub fn wrap(&mut self, text: &str, width: usize) -> Vec { - wrap::wrap(&mut self.widthdb, text, width) - } - pub fn write>(&mut self, pos: Pos, styled: S) { self.buffer.write(&mut self.widthdb, pos, &styled.into()); } From 8942b381f5d4086416cdaf19ce2f23baa3933ba4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 11 Dec 2022 20:44:06 +0100 Subject: [PATCH 042/144] Add and fix some lints --- src/buffer.rs | 2 +- src/lib.rs | 11 +++++++++++ src/styled.rs | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index f004cf3..721ffa7 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -195,7 +195,7 @@ impl StackFrame { .drawable_area .and_then(|(da_pos, da_size)| Self::intersect_areas(da_pos, da_size, pos, size)); - StackFrame { + Self { pos, size, drawable_area, diff --git a/src/lib.rs b/src/lib.rs index 0afea1c..9a03207 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,14 @@ +#![forbid(unsafe_code)] +// Rustc lint groups +#![warn(future_incompatible)] +#![warn(rust_2018_idioms)] +#![warn(unused)] +// Rustc lints +#![warn(noop_method_call)] +#![warn(single_use_lifetimes)] +// Clippy lints +#![warn(clippy::use_self)] + mod buffer; pub mod frame; pub mod styled; diff --git a/src/styled.rs b/src/styled.rs index 9871aab..99b346d 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -34,7 +34,7 @@ impl Styled { self.then(text, ContentStyle::default()) } - pub fn and_then(mut self, mut other: Styled) -> Self { + pub fn and_then(mut self, mut other: Self) -> Self { let delta = self.text.len(); for (_, until) in &mut other.styles { *until += delta; From 0a3b193f796bcb5e1a211d0e3c1589565d9848c2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 5 Jan 2023 17:34:30 +0100 Subject: [PATCH 043/144] Fix clippy lint --- src/styled.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styled.rs b/src/styled.rs index 99b346d..872e4c5 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -167,7 +167,7 @@ impl From<&str> for Styled { impl From for Styled { fn from(text: String) -> Self { - Self::new_plain(&text) + Self::new_plain(text) } } From 0d59116012a51516a821991e2969b1cf4779770f Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 11 Feb 2023 21:21:14 +0100 Subject: [PATCH 044/144] Update dependencies --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab7022f..c2572b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -crossterm = "0.25.0" -unicode-linebreak = "0.1.2" -unicode-segmentation = "1.9.0" -unicode-width = "0.1.9" +crossterm = "0.26.0" +unicode-linebreak = "0.1.4" +unicode-segmentation = "1.10.1" +unicode-width = "0.1.10" From 4ffaae067e18e0b4373aa5e62ef30f44b1159767 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 09:18:08 +0100 Subject: [PATCH 045/144] Export all types at top level --- examples/hello_world.rs | 3 +-- examples/overlapping_graphemes.rs | 3 +-- examples/text_wrapping.rs | 4 +--- src/buffer.rs | 3 +-- src/frame.rs | 4 +--- src/lib.rs | 14 ++++++++++---- src/styled.rs | 2 +- src/terminal.rs | 9 ++++----- src/wrap.rs | 2 +- 9 files changed, 21 insertions(+), 23 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index b7b670d..874740e 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,7 +1,6 @@ use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; -use toss::frame::{Frame, Pos}; -use toss::terminal::Terminal; +use toss::{Frame, Pos, Terminal}; fn draw(f: &mut Frame) { f.write( diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index e87dff7..b3b07e1 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -1,7 +1,6 @@ use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; -use toss::frame::{Frame, Pos}; -use toss::terminal::Terminal; +use toss::{Frame, Pos, Terminal}; fn draw(f: &mut Frame) { f.write( diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index 1f711fa..1943fae 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -1,7 +1,5 @@ use crossterm::event::Event; -use toss::frame::{Frame, Pos}; -use toss::styled::Styled; -use toss::terminal::Terminal; +use toss::{Frame, Pos, Styled, Terminal}; fn draw(f: &mut Frame) { let text = concat!( diff --git a/src/buffer.rs b/src/buffer.rs index 721ffa7..b8b9882 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -2,8 +2,7 @@ use std::ops::{Add, AddAssign, Neg, Range, Sub, SubAssign}; use crossterm::style::ContentStyle; -use crate::styled::Styled; -use crate::widthdb::WidthDb; +use crate::{Styled, WidthDb}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Size { diff --git a/src/frame.rs b/src/frame.rs index 3eef23b..2e9bad1 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,9 +1,7 @@ //! Rendering the next frame. use crate::buffer::Buffer; -pub use crate::buffer::{Pos, Size}; -use crate::styled::Styled; -use crate::widthdb::WidthDb; +use crate::{Pos, Size, Styled, WidthDb}; #[derive(Debug, Default)] pub struct Frame { diff --git a/src/lib.rs b/src/lib.rs index 9a03207..4daa5ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,14 @@ #![warn(clippy::use_self)] mod buffer; -pub mod frame; -pub mod styled; -pub mod terminal; -pub mod widthdb; +mod frame; +mod styled; +mod terminal; +mod widthdb; mod wrap; + +pub use buffer::{Pos, Size}; +pub use frame::*; +pub use styled::*; +pub use terminal::*; +pub use widthdb::*; diff --git a/src/styled.rs b/src/styled.rs index 872e4c5..425a717 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -1,5 +1,5 @@ use std::iter::Peekable; -use std::{slice, vec}; +use std::slice; use crossterm::style::{ContentStyle, StyledContent}; use unicode_segmentation::{GraphemeIndices, Graphemes, UnicodeSegmentation}; diff --git a/src/terminal.rs b/src/terminal.rs index 878a49a..2f1bf5b 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,7 +1,7 @@ //! Displaying frames on a terminal. -use std::io::Write; -use std::{io, mem}; +use std::io::{self, Write}; +use std::mem; use crossterm::cursor::{Hide, MoveTo, Show}; use crossterm::event::{ @@ -12,9 +12,8 @@ use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::{ExecutableCommand, QueueableCommand}; -use crate::buffer::{Buffer, Size}; -use crate::frame::Frame; -use crate::widthdb::WidthDb; +use crate::buffer::Buffer; +use crate::{Frame, Size, WidthDb}; pub struct Terminal { /// Render target. diff --git a/src/wrap.rs b/src/wrap.rs index a0f4e0d..a1ef1d4 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -3,7 +3,7 @@ use unicode_linebreak::BreakOpportunity; use unicode_segmentation::UnicodeSegmentation; -use crate::widthdb::WidthDb; +use crate::WidthDb; pub fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { let mut breaks = vec![]; From 904f5c16fa1cb3d94cc76e8c44dcac5006c4c3e2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 09:27:18 +0100 Subject: [PATCH 046/144] Add Widget and AsyncWidget traits --- Cargo.toml | 1 + src/lib.rs | 2 ++ src/widget.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/widget.rs diff --git a/Cargo.toml b/Cargo.toml index c2572b2..b1d625c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +async-trait = "0.1.64" crossterm = "0.26.0" unicode-linebreak = "0.1.4" unicode-segmentation = "1.10.1" diff --git a/src/lib.rs b/src/lib.rs index 4daa5ec..8b4f086 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod buffer; mod frame; mod styled; mod terminal; +mod widget; mod widthdb; mod wrap; @@ -20,4 +21,5 @@ pub use buffer::{Pos, Size}; pub use frame::*; pub use styled::*; pub use terminal::*; +pub use widget::*; pub use widthdb::*; diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 0000000..36fb1e7 --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,28 @@ +use async_trait::async_trait; + +use crate::{Frame, Size}; + +// TODO Feature-gate these traits + +pub trait Widget { + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result; + + fn draw(self, frame: &mut Frame) -> Result<(), E>; +} + +#[async_trait] +pub trait AsyncWidget { + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result; + + async fn draw(self, frame: &mut Frame) -> Result<(), E>; +} From 70d33d4d5df997ba353ad59799d0f0457af16e99 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 09:36:53 +0100 Subject: [PATCH 047/144] Add vscode settings --- .vscode/settings.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a89179 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.insertFinalNewline": true, + "rust-analyzer.cargo.features": "all", + "rust-analyzer.imports.granularity.enforce": true, + "rust-analyzer.imports.granularity.group": "module", + "rust-analyzer.imports.group.enable": true, + "evenBetterToml.formatter.columnWidth": 100, +} From f793ec79ac55aa8d95356fb894e4120341699be5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 09:58:18 +0100 Subject: [PATCH 048/144] Add Text widget --- src/lib.rs | 1 + src/widgets.rs | 3 ++ src/widgets/text.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/widgets.rs create mode 100644 src/widgets/text.rs diff --git a/src/lib.rs b/src/lib.rs index 8b4f086..d1312f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ mod frame; mod styled; mod terminal; mod widget; +pub mod widgets; mod widthdb; mod wrap; diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..3021f9a --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,3 @@ +mod text; + +pub use text::*; diff --git a/src/widgets/text.rs b/src/widgets/text.rs new file mode 100644 index 0000000..94a74c3 --- /dev/null +++ b/src/widgets/text.rs @@ -0,0 +1,92 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Styled, Widget, WidthDb}; + +pub struct Text { + styled: Styled, + wrap: bool, +} + +impl Text { + pub fn new>(styled: S) -> Self { + Self { + styled: styled.into(), + wrap: true, + } + } + + pub fn wrap(mut self, wrap: bool) -> Self { + self.wrap = wrap; + 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) + } + + fn size(&self, widthdb: &mut WidthDb, max_width: Option) -> Size { + 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); + Size::new(min_width, min_height) + } + + fn draw(self, frame: &mut Frame) { + 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); + } + } +} + +impl Widget for Text { + fn size( + &self, + frame: &mut Frame, + max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.size(frame.widthdb(), max_width)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.draw(frame); + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Text { + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.size(frame.widthdb(), max_width)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.draw(frame); + Ok(()) + } +} From 6a0c0474ec4857ec9ff62928e4445d1133f6ff11 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 10:08:45 +0100 Subject: [PATCH 049/144] Add widget hello world example --- examples/hello_world_widgets.rs | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 examples/hello_world_widgets.rs diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs new file mode 100644 index 0000000..d57bf7b --- /dev/null +++ b/examples/hello_world_widgets.rs @@ -0,0 +1,51 @@ +use std::convert::Infallible; + +use crossterm::event::Event; +use crossterm::style::{ContentStyle, Stylize}; +use toss::widgets::Text; +use toss::{Styled, Terminal, Widget}; + +fn widget() -> impl Widget { + Text::new( + Styled::new("Hello world!", ContentStyle::default().green()) + .then_plain("\n") + .then( + "Press any key to exit", + ContentStyle::default().on_dark_blue(), + ), + ) +} + +fn render_frame(term: &mut Terminal) { + loop { + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize().unwrap(); + + widget().draw(term.frame()).unwrap(); + term.present().unwrap(); + + if term.measuring_required() { + term.measure_widths().unwrap(); + } else { + break; + } + } +} + +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; + } + } +} From 964f3bf011141cd7c331ab1a91517cc3b4907315 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 10:57:41 +0100 Subject: [PATCH 050/144] Add Border widget --- examples/hello_world_widgets.rs | 8 +- src/widgets.rs | 2 + src/widgets/border.rs | 201 ++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 src/widgets/border.rs diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index d57bf7b..507feea 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -2,18 +2,20 @@ use std::convert::Infallible; use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; -use toss::widgets::Text; +use toss::widgets::{Border, BorderLook, Text}; use toss::{Styled, Terminal, Widget}; fn widget() -> impl Widget { - Text::new( + Border::new(Text::new( Styled::new("Hello world!", ContentStyle::default().green()) .then_plain("\n") .then( "Press any key to exit", ContentStyle::default().on_dark_blue(), ), - ) + )) + .look(BorderLook::LINE_DOUBLE) + .style(ContentStyle::default().dark_red()) } fn render_frame(term: &mut Terminal) { diff --git a/src/widgets.rs b/src/widgets.rs index 3021f9a..27280a5 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,3 +1,5 @@ +mod border; mod text; +pub use border::*; pub use text::*; diff --git a/src/widgets/border.rs b/src/widgets/border.rs new file mode 100644 index 0000000..ac087b1 --- /dev/null +++ b/src/widgets/border.rs @@ -0,0 +1,201 @@ +use async_trait::async_trait; +use crossterm::style::ContentStyle; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget}; + +#[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 { + /// ``` + /// +-------+ + /// | Hello | + /// +-------+ + /// ``` + pub const ASCII: Self = Self { + top_left: "+", + top_right: "+", + bottom_left: "+", + bottom_right: "+", + top: "-", + bottom: "-", + left: "|", + right: "|", + }; + + /// ``` + /// ┌───────┐ + /// │ Hello │ + /// └───────┘ + /// ``` + pub const LINE: Self = Self { + top_left: "┌", + top_right: "┐", + bottom_left: "└", + bottom_right: "┘", + top: "─", + bottom: "─", + left: "│", + right: "│", + }; + + /// ``` + /// ┏━━━━━━━┓ + /// ┃ Hello ┃ + /// ┗━━━━━━━┛ + /// ``` + pub const LINE_HEAVY: Self = Self { + top_left: "┏", + top_right: "┓", + bottom_left: "┗", + bottom_right: "┛", + top: "━", + bottom: "━", + left: "┃", + right: "┃", + }; + + /// ``` + /// ╔═══════╗ + /// ║ 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 + } +} + +pub struct Border { + inner: I, + look: BorderLook, + style: ContentStyle, +} + +impl Border { + pub fn new(inner: I) -> Self { + Self { + inner, + look: BorderLook::default(), + style: ContentStyle::default(), + } + } + + pub fn look(mut self, look: BorderLook) -> Self { + self.look = look; + self + } + + pub fn style(mut self, style: ContentStyle) -> 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, + frame: &mut Frame, + 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(frame, 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, + frame: &mut Frame, + 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(frame, 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(()) + } +} From dbafc4070011454e8b78ba3547a303491b293c80 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 14:30:12 +0100 Subject: [PATCH 051/144] Add Padding widget --- src/buffer.rs | 14 +++++ src/widgets.rs | 2 + src/widgets/padding.rs | 117 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/widgets/padding.rs diff --git a/src/buffer.rs b/src/buffer.rs index b8b9882..4804fcb 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -16,6 +16,20 @@ impl Size { pub const fn new(width: u16, height: u16) -> Self { Self { width, height } } + + pub const fn saturating_add(self, rhs: Self) -> Self { + Self::new( + self.width.saturating_add(rhs.width), + self.height.saturating_add(rhs.height), + ) + } + + 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 { diff --git a/src/widgets.rs b/src/widgets.rs index 27280a5..7a938af 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,5 +1,7 @@ mod border; +mod padding; mod text; pub use border::*; +pub use padding::*; pub use text::*; diff --git a/src/widgets/padding.rs b/src/widgets/padding.rs new file mode 100644 index 0000000..9cdcb68 --- /dev/null +++ b/src/widgets/padding.rs @@ -0,0 +1,117 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget}; + +pub struct Padding { + inner: I, + left: u16, + right: u16, + top: u16, + bottom: u16, +} + +impl Padding { + pub fn new(inner: I) -> Self { + Self { + inner, + left: 0, + right: 0, + top: 0, + bottom: 0, + } + } + + pub fn left(mut self, amount: u16) -> Self { + self.left = amount; + self + } + + pub fn right(mut self, amount: u16) -> Self { + self.right = amount; + self + } + + pub fn top(mut self, amount: u16) -> Self { + self.top = amount; + self + } + + pub fn bottom(mut self, amount: u16) -> Self { + self.bottom = amount; + self + } + + pub fn horizontal(self, amount: u16) -> Self { + self.left(amount).right(amount) + } + + pub fn vertical(self, amount: u16) -> Self { + self.top(amount).bottom(amount) + } + + pub fn all(self, amount: u16) -> Self { + self.horizontal(amount).vertical(amount) + } + + 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, + frame: &mut Frame, + 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(frame, max_width, max_height)?; + Ok(size + pad_size) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + 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, + frame: &mut Frame, + 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(frame, max_width, max_height).await?; + Ok(size + pad_size) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.push_inner(frame); + self.inner.draw(frame).await?; + frame.pop(); + Ok(()) + } +} From bcc07dc9bab08a260cbd841b612fdcef1706fa45 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 14:43:48 +0100 Subject: [PATCH 052/144] Add WidgetExt and AsyncWidgetExt traits --- examples/hello_world_widgets.rs | 9 +++++---- src/widget.rs | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 507feea..99393ec 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -2,18 +2,19 @@ use std::convert::Infallible; use crossterm::event::Event; use crossterm::style::{ContentStyle, Stylize}; -use toss::widgets::{Border, BorderLook, Text}; -use toss::{Styled, Terminal, Widget}; +use toss::widgets::{BorderLook, Text}; +use toss::{Styled, Terminal, Widget, WidgetExt}; fn widget() -> impl Widget { - Border::new(Text::new( + Text::new( Styled::new("Hello world!", ContentStyle::default().green()) .then_plain("\n") .then( "Press any key to exit", ContentStyle::default().on_dark_blue(), ), - )) + ) + .border() .look(BorderLook::LINE_DOUBLE) .style(ContentStyle::default().dark_red()) } diff --git a/src/widget.rs b/src/widget.rs index 36fb1e7..4ac3710 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; +use crate::widgets::{Border, Padding}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -26,3 +27,27 @@ pub trait AsyncWidget { async fn draw(self, frame: &mut Frame) -> Result<(), E>; } + +pub trait WidgetExt: Sized { + fn border(self) -> Border { + Border::new(self) + } + + fn padding(self) -> Padding { + Padding::new(self) + } +} + +impl WidgetExt for W {} + +pub trait AsyncWidgetExt: Sized { + fn border(self) -> Border { + Border::new(self) + } + + fn padding(self) -> Padding { + Padding::new(self) + } +} + +impl AsyncWidgetExt for W {} From 575faf9bbf61f2c95206081ed614794049eb11b5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 15:12:27 +0100 Subject: [PATCH 053/144] Add Float widget --- examples/hello_world_widgets.rs | 25 +++---- src/widget.rs | 10 ++- src/widgets.rs | 2 + src/widgets/float.rs | 112 ++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 src/widgets/float.rs diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 99393ec..3bf1d24 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -6,17 +6,20 @@ use toss::widgets::{BorderLook, Text}; use toss::{Styled, Terminal, Widget, WidgetExt}; fn widget() -> impl Widget { - Text::new( - Styled::new("Hello world!", ContentStyle::default().green()) - .then_plain("\n") - .then( - "Press any key to exit", - ContentStyle::default().on_dark_blue(), - ), - ) - .border() - .look(BorderLook::LINE_DOUBLE) - .style(ContentStyle::default().dark_red()) + let styled = Styled::new("Hello world!", ContentStyle::default().green()) + .then_plain("\n") + .then( + "Press any key to exit", + ContentStyle::default().on_dark_blue(), + ); + Text::new(styled) + .padding() + .horizontal(1) + .border() + .look(BorderLook::LINE_DOUBLE) + .style(ContentStyle::default().dark_red()) + .float() + .all(0.5) } fn render_frame(term: &mut Terminal) { diff --git a/src/widget.rs b/src/widget.rs index 4ac3710..b4df21b 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::widgets::{Border, Padding}; +use crate::widgets::{Border, Float, Padding}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -33,6 +33,10 @@ pub trait WidgetExt: Sized { Border::new(self) } + fn float(self) -> Float { + Float::new(self) + } + fn padding(self) -> Padding { Padding::new(self) } @@ -45,6 +49,10 @@ pub trait AsyncWidgetExt: Sized { Border::new(self) } + fn float(self) -> Float { + Float::new(self) + } + fn padding(self) -> Padding { Padding::new(self) } diff --git a/src/widgets.rs b/src/widgets.rs index 7a938af..c1021ec 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,7 +1,9 @@ mod border; +mod float; mod padding; mod text; pub use border::*; +pub use float::*; pub use padding::*; pub use text::*; diff --git a/src/widgets/float.rs b/src/widgets/float.rs new file mode 100644 index 0000000..c19aa27 --- /dev/null +++ b/src/widgets/float.rs @@ -0,0 +1,112 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget}; + +pub struct Float { + inner: I, + horizontal: Option, + vertical: Option, +} + +impl Float { + pub fn new(inner: I) -> Self { + Self { + inner, + horizontal: None, + vertical: None, + } + } + + pub fn horizontal(mut self, position: f32) -> Self { + self.horizontal = Some(position); + self + } + + pub fn vertical(mut self, position: f32) -> Self { + self.vertical = Some(position); + self + } + + pub fn all(self, position: f32) -> Self { + self.horizontal(position).vertical(position) + } + + fn push_inner(&self, frame: &mut Frame, size: Size, mut inner_size: Size) { + inner_size.width = inner_size.width.min(size.width); + inner_size.height = inner_size.height.min(size.height); + + let mut inner_pos = Pos::ZERO; + + if let Some(horizontal) = self.horizontal { + let available = (size.width - 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; + } + + if let Some(vertical) = self.vertical { + let available = (size.height - 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; + } + + frame.push(inner_pos, inner_size); + } +} + +impl Widget for Float +where + I: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let inner_size = self + .inner + .size(frame, 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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(frame, 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, Some(size.width), Some(size.height)) + .await?; + + self.push_inner(frame, size, inner_size); + self.inner.draw(frame).await?; + frame.pop(); + + Ok(()) + } +} From 47df35d9dbee7c093ba03ad37dce944340306b3e Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 16:07:05 +0100 Subject: [PATCH 054/144] Add Empty widget --- src/widgets.rs | 2 ++ src/widgets/empty.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/widgets/empty.rs diff --git a/src/widgets.rs b/src/widgets.rs index c1021ec..7503859 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,9 +1,11 @@ mod border; +mod empty; mod float; mod padding; mod text; pub use border::*; +pub use empty::*; pub use float::*; pub use padding::*; pub use text::*; diff --git a/src/widgets/empty.rs b/src/widgets/empty.rs new file mode 100644 index 0000000..86881e2 --- /dev/null +++ b/src/widgets/empty.rs @@ -0,0 +1,60 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget}; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Empty { + size: Size, +} + +impl Empty { + pub fn new() -> Self { + Self { size: Size::ZERO } + } + + pub fn width(mut self, width: u16) -> Self { + self.size.width = width; + self + } + + pub fn height(mut self, height: u16) -> Self { + self.size.height = height; + self + } + + pub fn size(mut self, size: Size) -> Self { + self.size = size; + self + } +} + +impl Widget for Empty { + fn size( + &self, + _frame: &mut Frame, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.size) + } + + fn draw(self, _frame: &mut Frame) -> Result<(), E> { + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Empty { + async fn size( + &self, + _frame: &mut Frame, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.size) + } + + async fn draw(self, _frame: &mut Frame) -> Result<(), E> { + Ok(()) + } +} From b327dee3c38c350985650becf9fd3ef81be774da Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 16:08:42 +0100 Subject: [PATCH 055/144] Remove unnecessary AsyncWidgetExt trait --- src/widget.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index b4df21b..ed9053f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -42,20 +42,14 @@ pub trait WidgetExt: Sized { } } +// 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 {} - -pub trait AsyncWidgetExt: Sized { - fn border(self) -> Border { - Border::new(self) - } - - fn float(self) -> Float { - Float::new(self) - } - - fn padding(self) -> Padding { - Padding::new(self) - } -} - -impl AsyncWidgetExt for W {} From 3f7e985b3f7672b58318216a24b156f95ab0b6b2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 16:26:47 +0100 Subject: [PATCH 056/144] Add Background widget --- examples/hello_world_widgets.rs | 2 + src/widget.rs | 6 ++- src/widgets.rs | 2 + src/widgets/background.rs | 71 +++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/widgets/background.rs diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 3bf1d24..9dd5e24 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -18,6 +18,8 @@ fn widget() -> impl Widget { .border() .look(BorderLook::LINE_DOUBLE) .style(ContentStyle::default().dark_red()) + .background() + .style(ContentStyle::default().on_dark_yellow()) .float() .all(0.5) } diff --git a/src/widget.rs b/src/widget.rs index ed9053f..ea1d131 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::widgets::{Border, Float, Padding}; +use crate::widgets::{Background, Border, Float, Padding}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -29,6 +29,10 @@ pub trait AsyncWidget { } pub trait WidgetExt: Sized { + fn background(self) -> Background { + Background::new(self) + } + fn border(self) -> Border { Border::new(self) } diff --git a/src/widgets.rs b/src/widgets.rs index 7503859..37f4701 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,9 +1,11 @@ +mod background; mod border; mod empty; mod float; mod padding; mod text; +pub use background::*; pub use border::*; pub use empty::*; pub use float::*; diff --git a/src/widgets/background.rs b/src/widgets/background.rs new file mode 100644 index 0000000..5fd91e8 --- /dev/null +++ b/src/widgets/background.rs @@ -0,0 +1,71 @@ +use async_trait::async_trait; +use crossterm::style::ContentStyle; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget}; + +pub struct Background { + inner: I, + style: ContentStyle, +} + +impl Background { + pub fn new(inner: I) -> Self { + Self { + inner, + style: ContentStyle::default(), + } + } + + pub fn style(mut self, style: ContentStyle) -> 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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(frame, 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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.fill(frame); + self.inner.draw(frame).await + } +} From eb36bfa2ea5f2766de27a9eac6b568bc2350780d Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 20:40:20 +0100 Subject: [PATCH 057/144] Fix code blocks in docstrings --- src/widgets/border.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widgets/border.rs b/src/widgets/border.rs index ac087b1..6b7d02e 100644 --- a/src/widgets/border.rs +++ b/src/widgets/border.rs @@ -16,7 +16,7 @@ pub struct BorderLook { } impl BorderLook { - /// ``` + /// ```text /// +-------+ /// | Hello | /// +-------+ @@ -32,7 +32,7 @@ impl BorderLook { right: "|", }; - /// ``` + /// ```text /// ┌───────┐ /// │ Hello │ /// └───────┘ @@ -48,7 +48,7 @@ impl BorderLook { right: "│", }; - /// ``` + /// ```text /// ┏━━━━━━━┓ /// ┃ Hello ┃ /// ┗━━━━━━━┛ @@ -64,7 +64,7 @@ impl BorderLook { right: "┃", }; - /// ``` + /// ```text /// ╔═══════╗ /// ║ Hello ║ /// ╚═══════╝ From 67f703cf68e7a4f9262c8416fe5953777b8a3bff Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 21:03:10 +0100 Subject: [PATCH 058/144] Extract Pos and Size to separate file --- src/buffer.rs | 150 +------------------------------------------------- src/coords.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +- 3 files changed, 151 insertions(+), 149 deletions(-) create mode 100644 src/coords.rs diff --git a/src/buffer.rs b/src/buffer.rs index 4804fcb..d433aed 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,154 +1,8 @@ -use std::ops::{Add, AddAssign, Neg, Range, Sub, SubAssign}; +use std::ops::Range; use crossterm::style::ContentStyle; -use crate::{Styled, WidthDb}; - -#[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 } - } - - pub const fn saturating_add(self, rhs: Self) -> Self { - Self::new( - self.width.saturating_add(rhs.width), - self.height.saturating_add(rhs.height), - ) - } - - 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; - } -} - -#[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) - } -} +use crate::{Pos, Size, Styled, WidthDb}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { diff --git a/src/coords.rs b/src/coords.rs new file mode 100644 index 0000000..01450a6 --- /dev/null +++ b/src/coords.rs @@ -0,0 +1,147 @@ +use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; + +#[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 } + } + + pub const fn saturating_add(self, rhs: Self) -> Self { + Self::new( + self.width.saturating_add(rhs.width), + self.height.saturating_add(rhs.height), + ) + } + + 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; + } +} + +#[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/lib.rs b/src/lib.rs index d1312f1..34cb81b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ #![warn(clippy::use_self)] mod buffer; +mod coords; mod frame; mod styled; mod terminal; @@ -18,7 +19,7 @@ pub mod widgets; mod widthdb; mod wrap; -pub use buffer::{Pos, Size}; +pub use coords::*; pub use frame::*; pub use styled::*; pub use terminal::*; From 4c304ffe795b11f798b7bb40f8abb3fc77686f7f Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 20:53:57 +0100 Subject: [PATCH 059/144] Add own Style type --- src/lib.rs | 2 ++ src/style.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/style.rs diff --git a/src/lib.rs b/src/lib.rs index 34cb81b..a204e8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod buffer; mod coords; mod frame; +mod style; mod styled; mod terminal; mod widget; @@ -21,6 +22,7 @@ mod wrap; pub use coords::*; pub use frame::*; +pub use style::*; pub use styled::*; pub use terminal::*; pub use widget::*; diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..2528572 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,63 @@ +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: Self) -> Self { + if self.opaque { + return self; + } + + Self { + content_style: merge_cs(base.content_style, self.content_style), + opaque: base.opaque, + } + } +} + +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 + } +} From 9ff8007cae05015d932a8f01b58026007f8d96c6 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 21:24:52 +0100 Subject: [PATCH 060/144] Switch usages of ContentStyle to Style --- examples/hello_world.rs | 14 ++++---------- examples/hello_world_widgets.rs | 15 ++++++--------- examples/overlapping_graphemes.rs | 8 ++++---- src/buffer.rs | 20 ++++++++++---------- src/style.rs | 9 +++------ src/styled.rs | 25 +++++++++++++------------ src/widgets/background.rs | 9 ++++----- src/widgets/border.rs | 9 ++++----- 8 files changed, 48 insertions(+), 61 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 874740e..e2c6e94 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,18 +1,12 @@ use crossterm::event::Event; -use crossterm::style::{ContentStyle, Stylize}; -use toss::{Frame, Pos, Terminal}; +use crossterm::style::Stylize; +use toss::{Frame, Pos, Style, Terminal}; fn draw(f: &mut Frame) { - f.write( - Pos::new(0, 0), - ("Hello world!", ContentStyle::default().green()), - ); + f.write(Pos::new(0, 0), ("Hello world!", Style::new().green())); f.write( Pos::new(0, 1), - ( - "Press any key to exit", - ContentStyle::default().on_dark_blue(), - ), + ("Press any key to exit", Style::new().on_dark_blue()), ); f.show_cursor(Pos::new(16, 0)); } diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 9dd5e24..c495f81 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -1,25 +1,22 @@ use std::convert::Infallible; use crossterm::event::Event; -use crossterm::style::{ContentStyle, Stylize}; +use crossterm::style::Stylize; use toss::widgets::{BorderLook, Text}; -use toss::{Styled, Terminal, Widget, WidgetExt}; +use toss::{Style, Styled, Terminal, Widget, WidgetExt}; fn widget() -> impl Widget { - let styled = Styled::new("Hello world!", ContentStyle::default().green()) + let styled = Styled::new("Hello world!", Style::new().green()) .then_plain("\n") - .then( - "Press any key to exit", - ContentStyle::default().on_dark_blue(), - ); + .then("Press any key to exit", Style::new().on_dark_blue()); Text::new(styled) .padding() .horizontal(1) .border() .look(BorderLook::LINE_DOUBLE) - .style(ContentStyle::default().dark_red()) + .style(Style::new().dark_red()) .background() - .style(ContentStyle::default().on_dark_yellow()) + .style(Style::new().on_dark_yellow().opaque()) .float() .all(0.5) } diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index b3b07e1..562553f 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -1,14 +1,14 @@ use crossterm::event::Event; -use crossterm::style::{ContentStyle, Stylize}; -use toss::{Frame, Pos, Terminal}; +use crossterm::style::Stylize; +use toss::{Frame, Pos, Style, Terminal}; fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), "Writing over wide graphemes removes the entire overwritten grapheme.", ); - let under = ContentStyle::default().white().on_dark_blue(); - let over = ContentStyle::default().black().on_dark_yellow(); + let under = Style::new().white().on_dark_blue(); + let over = Style::new().black().on_dark_yellow(); for i in 0..6 { let delta = i - 2; f.write(Pos::new(2 + i * 7, 2), ("a😀", under)); diff --git a/src/buffer.rs b/src/buffer.rs index d433aed..6d4b7b9 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -2,7 +2,7 @@ use std::ops::Range; use crossterm::style::ContentStyle; -use crate::{Pos, Size, Styled, WidthDb}; +use crate::{Pos, Size, Style, Styled, WidthDb}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { @@ -242,18 +242,16 @@ impl Buffer { let y = pos.y as u16; let mut col: usize = 0; - for (_, styled_grapheme) in styled.styled_grapheme_indices() { + for (_, style, grapheme) in styled.styled_grapheme_indices() { let x = pos.x + col as i32; - let g = *styled_grapheme.content(); - let style = *styled_grapheme.style(); - let width = widthdb.grapheme_width(g, col); + let width = widthdb.grapheme_width(grapheme, col); col += width as usize; - if g == "\t" { + 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, g, style); + self.write_grapheme(&xrange, x, y, width, grapheme, style); } } } @@ -268,7 +266,7 @@ impl Buffer { y: u16, width: u8, grapheme: &str, - style: ContentStyle, + style: Style, ) { let min_x = xrange.start; let max_x = xrange.end - 1; // Last possible cell @@ -280,6 +278,8 @@ impl Buffer { return; // Not visible } + // TODO Merge styles + if start_x >= min_x && end_x <= max_x { // Fully visible, write actual grapheme for offset in 0..width { @@ -287,7 +287,7 @@ impl Buffer { self.erase(x, y); *self.at_mut(x, y) = Cell { content: grapheme.to_string().into_boxed_str(), - style, + style: style.content_style, width, offset, }; @@ -299,7 +299,7 @@ impl Buffer { for x in start_x..=end_x { self.erase(x, y); *self.at_mut(x, y) = Cell { - style, + style: style.content_style, ..Default::default() }; } diff --git a/src/style.rs b/src/style.rs index 2528572..56c66ce 100644 --- a/src/style.rs +++ b/src/style.rs @@ -30,15 +30,12 @@ impl Style { self } - pub fn cover(self, base: Self) -> Self { + pub fn cover(self, base: ContentStyle) -> ContentStyle { if self.opaque { - return self; + return self.content_style; } - Self { - content_style: merge_cs(base.content_style, self.content_style), - opaque: base.opaque, - } + merge_cs(base, self.content_style) } } diff --git a/src/styled.rs b/src/styled.rs index 425a717..33a8285 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -1,19 +1,20 @@ use std::iter::Peekable; use std::slice; -use crossterm::style::{ContentStyle, StyledContent}; 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<(ContentStyle, usize)>, + styles: Vec<(Style, usize)>, } impl Styled { - pub fn new>(text: S, style: ContentStyle) -> Self { + pub fn new>(text: S, style: Style) -> Self { Self::default().then(text, style) } @@ -21,7 +22,7 @@ impl Styled { Self::default().then_plain(text) } - pub fn then>(mut self, text: S, style: ContentStyle) -> Self { + pub fn then>(mut self, text: S, style: Style) -> Self { let text = text.as_ref(); if !text.is_empty() { self.text.push_str(text); @@ -31,7 +32,7 @@ impl Styled { } pub fn then_plain>(self, text: S) -> Self { - self.then(text, ContentStyle::default()) + self.then(text, Style::new()) } pub fn and_then(mut self, mut other: Self) -> Self { @@ -121,11 +122,11 @@ impl Styled { pub struct StyledGraphemeIndices<'a> { text: GraphemeIndices<'a>, - styles: Peekable>, + styles: Peekable>, } impl<'a> Iterator for StyledGraphemeIndices<'a> { - type Item = (usize, StyledContent<&'a str>); + type Item = (usize, Style, &'a str); fn next(&mut self) -> Option { let (gi, grapheme) = self.text.next()?; @@ -134,7 +135,7 @@ impl<'a> Iterator for StyledGraphemeIndices<'a> { self.styles.next(); (style, until) = **self.styles.peek().expect("styles cover entire text"); } - Some((gi, StyledContent::new(style, grapheme))) + Some((gi, style, grapheme)) } } @@ -177,14 +178,14 @@ impl> From<(S,)> for Styled { } } -impl> From<(S, ContentStyle)> for Styled { - fn from((text, style): (S, ContentStyle)) -> Self { +impl> From<(S, Style)> for Styled { + fn from((text, style): (S, Style)) -> Self { Self::new(text, style) } } -impl> From<&[(S, ContentStyle)]> for Styled { - fn from(segments: &[(S, ContentStyle)]) -> Self { +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); diff --git a/src/widgets/background.rs b/src/widgets/background.rs index 5fd91e8..78f362b 100644 --- a/src/widgets/background.rs +++ b/src/widgets/background.rs @@ -1,22 +1,21 @@ use async_trait::async_trait; -use crossterm::style::ContentStyle; -use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget}; pub struct Background { inner: I, - style: ContentStyle, + style: Style, } impl Background { pub fn new(inner: I) -> Self { Self { inner, - style: ContentStyle::default(), + style: Style::default(), } } - pub fn style(mut self, style: ContentStyle) -> Self { + pub fn style(mut self, style: Style) -> Self { self.style = style; self } diff --git a/src/widgets/border.rs b/src/widgets/border.rs index 6b7d02e..fe6afbc 100644 --- a/src/widgets/border.rs +++ b/src/widgets/border.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; -use crossterm::style::ContentStyle; -use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget}; #[derive(Debug, Clone, Copy)] pub struct BorderLook { @@ -90,7 +89,7 @@ impl Default for BorderLook { pub struct Border { inner: I, look: BorderLook, - style: ContentStyle, + style: Style, } impl Border { @@ -98,7 +97,7 @@ impl Border { Self { inner, look: BorderLook::default(), - style: ContentStyle::default(), + style: Style::default(), } } @@ -107,7 +106,7 @@ impl Border { self } - pub fn style(mut self, style: ContentStyle) -> Self { + pub fn style(mut self, style: Style) -> Self { self.style = style; self } From c689d97974ab37acc8def222749f1b009d0b8635 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 21:27:09 +0100 Subject: [PATCH 061/144] Write transparent Style to Buffer correctly --- src/buffer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 6d4b7b9..1a73c80 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -278,16 +278,15 @@ impl Buffer { return; // Not visible } - // TODO Merge styles - 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.content_style, + style: style.cover(base_style), width, offset, }; @@ -297,9 +296,10 @@ impl Buffer { let start_x = start_x.max(0) as u16; let end_x = end_x.min(max_x) as u16; for x in start_x..=end_x { + let base_style = self.at(x, y).style; self.erase(x, y); *self.at_mut(x, y) = Cell { - style: style.content_style, + style: style.cover(base_style), ..Default::default() }; } From 845d88c93f14b48098c26fbababdd4a24ef00bab Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 16 Feb 2023 21:32:47 +0100 Subject: [PATCH 062/144] Make widget example look slightly less horrible --- examples/hello_world_widgets.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index c495f81..a0dc4d1 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -6,7 +6,7 @@ use toss::widgets::{BorderLook, Text}; use toss::{Style, Styled, Terminal, Widget, WidgetExt}; fn widget() -> impl Widget { - let styled = Styled::new("Hello world!", Style::new().green()) + 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) @@ -16,7 +16,7 @@ fn widget() -> impl Widget { .look(BorderLook::LINE_DOUBLE) .style(Style::new().dark_red()) .background() - .style(Style::new().on_dark_yellow().opaque()) + .style(Style::new().on_yellow().opaque()) .float() .all(0.5) } From 5a15838989833f9dcd509632ec8182a763b2aa3c Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 00:15:46 +0100 Subject: [PATCH 063/144] Fix Float sizing for unset directions --- src/widgets/float.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/widgets/float.rs b/src/widgets/float.rs index c19aa27..6c35504 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -32,9 +32,6 @@ impl Float { } fn push_inner(&self, frame: &mut Frame, size: Size, mut inner_size: Size) { - inner_size.width = inner_size.width.min(size.width); - inner_size.height = inner_size.height.min(size.height); - let mut inner_pos = Pos::ZERO; if let Some(horizontal) = self.horizontal { @@ -42,6 +39,9 @@ impl Float { // 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 { @@ -49,6 +49,9 @@ impl Float { // 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); From 2dee39c03ce4f4d52187c99e4f93d062155f65d0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 12:13:56 +0100 Subject: [PATCH 064/144] Add Cursor widget --- src/widgets.rs | 2 ++ src/widgets/cursor.rs | 67 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/widgets/cursor.rs diff --git a/src/widgets.rs b/src/widgets.rs index 37f4701..81d4dab 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,5 +1,6 @@ mod background; mod border; +mod cursor; mod empty; mod float; mod padding; @@ -7,6 +8,7 @@ mod text; pub use background::*; pub use border::*; +pub use cursor::*; pub use empty::*; pub use float::*; pub use padding::*; diff --git a/src/widgets/cursor.rs b/src/widgets/cursor.rs new file mode 100644 index 0000000..feeb045 --- /dev/null +++ b/src/widgets/cursor.rs @@ -0,0 +1,67 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget}; + +pub struct Cursor { + inner: I, + at: Pos, +} + +impl Cursor { + pub fn new(inner: I) -> Self { + Self { + inner, + at: Pos::ZERO, + } + } + + pub fn at(mut self, pos: Pos) -> Self { + self.at = pos; + self + } + + pub fn at_xy(self, x: i32, y: i32) -> Self { + self.at(Pos::new(x, y)) + } +} + +impl Widget for Cursor +where + I: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame)?; + frame.show_cursor(self.at); + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Cursor +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame).await?; + frame.show_cursor(self.at); + Ok(()) + } +} From e3365fdc023fdfd97e9c8f57598928a212e99e55 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 13:35:18 +0100 Subject: [PATCH 065/144] Improve WidthDb documentation --- src/widthdb.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/widthdb.rs b/src/widthdb.rs index 585bc4f..7d18570 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -40,8 +40,8 @@ impl WidthDb { /// /// If the grapheme is a tab, the column is used to determine its width. /// - /// If the width has not been measured yet, it is estimated using the - /// Unicode Standard Annex #11. + /// 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" { @@ -62,8 +62,8 @@ impl WidthDb { /// /// 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, it is estimated - /// using the Unicode Standard Annex #11. + /// 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) { @@ -72,6 +72,14 @@ impl WidthDb { 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) } From ed14ea9023ed27201c3cf335e28450775fba00ca Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 13:59:04 +0100 Subject: [PATCH 066/144] Measure automatically in Terminal::present --- examples/hello_world.rs | 16 +++++----------- examples/hello_world_widgets.rs | 20 +++++++------------ examples/overlapping_graphemes.rs | 16 +++++----------- examples/text_wrapping.rs | 16 +++++----------- src/terminal.rs | 32 ++++++++++++++++++------------- 5 files changed, 41 insertions(+), 59 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index e2c6e94..391075e 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -12,19 +12,13 @@ fn draw(f: &mut Frame) { } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize().unwrap(); + draw(term.frame()); + while term.present().unwrap() { term.autoresize().unwrap(); - draw(term.frame()); - term.present().unwrap(); - - if term.measuring_required() { - term.measure_widths().unwrap(); - } else { - break; - } } } diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index a0dc4d1..23dc43b 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -1,11 +1,11 @@ -use std::convert::Infallible; +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 { +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()); @@ -22,19 +22,13 @@ fn widget() -> impl Widget { } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize().unwrap(); + widget().draw(term.frame()).unwrap(); + while term.present().unwrap() { term.autoresize().unwrap(); - widget().draw(term.frame()).unwrap(); - term.present().unwrap(); - - if term.measuring_required() { - term.measure_widths().unwrap(); - } else { - break; - } } } diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index 562553f..0a8f90b 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -49,19 +49,13 @@ fn draw(f: &mut Frame) { } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize().unwrap(); + draw(term.frame()); + while term.present().unwrap() { term.autoresize().unwrap(); - draw(term.frame()); - term.present().unwrap(); - - if term.measuring_required() { - term.measure_widths().unwrap(); - } else { - break; - } } } diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index 1943fae..21f0ffe 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -38,19 +38,13 @@ fn draw(f: &mut Frame) { } fn render_frame(term: &mut Terminal) { - loop { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. + // Must be called before rendering, otherwise the terminal has out-of-date + // size information and will present garbage. + term.autoresize().unwrap(); + draw(term.frame()); + while term.present().unwrap() { term.autoresize().unwrap(); - draw(term.frame()); - term.present().unwrap(); - - if term.measuring_required() { - term.measure_widths().unwrap(); - } else { - break; - } } } diff --git a/src/terminal.rs b/src/terminal.rs index 2f1bf5b..2828600 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -91,16 +91,6 @@ impl Terminal { self.frame.widthdb.active } - pub fn measuring_required(&self) -> bool { - self.frame.widthdb.measuring_required() - } - - pub fn measure_widths(&mut self) -> io::Result<()> { - self.frame.widthdb.measure_widths(&mut self.out)?; - self.full_redraw = true; - Ok(()) - } - /// Resize the frame and other internal buffers if the terminal size has /// changed. pub fn autoresize(&mut self) -> io::Result<()> { @@ -124,11 +114,27 @@ impl Terminal { } /// Display the current frame on the screen and prepare the next frame. - /// Returns `true` if an immediate redraw is required. + /// + /// Before drawing and presenting a frame, [`Self::autoresize`] should be + /// called. [`Self::present`] does **not** call it automatically. + /// + /// If width measurements are turned on, any new graphemes encountered since + /// the last [`Self::present`] call will be measured. This can lead to the + /// screen flickering or being mostly blank until measurements complete. + /// + /// Returns `true` if any new graphemes were measured. Since their widths + /// may have changed because of the measurements, the application using this + /// [`Terminal`] should re-draw and re-present the current frame. /// /// 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<()> { + pub fn present(&mut self) -> io::Result { + let measure = self.frame.widthdb.measuring_required(); + if measure { + self.frame.widthdb.measure_widths(&mut self.out)?; + self.full_redraw = true; + } + if self.full_redraw { io::stdout().queue(Clear(ClearType::All))?; self.prev_frame_buffer.reset(); // Because the screen is now empty @@ -142,7 +148,7 @@ impl Terminal { mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); self.frame.reset(); - Ok(()) + Ok(measure) } fn draw_differences(&mut self) -> io::Result<()> { From ac2546ba9710f0b1bf8b2cdb6316345401913ddb Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 14:00:28 +0100 Subject: [PATCH 067/144] Add Terminal::{present_widget, present_async_widget} --- examples/hello_world_widgets.rs | 9 +------- src/terminal.rs | 37 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 23dc43b..27c81c1 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -22,14 +22,7 @@ fn widget() -> impl Widget { } fn render_frame(term: &mut Terminal) { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. - term.autoresize().unwrap(); - widget().draw(term.frame()).unwrap(); - while term.present().unwrap() { - term.autoresize().unwrap(); - widget().draw(term.frame()).unwrap(); - } + while term.present_widget(widget()).unwrap() {} } fn main() { diff --git a/src/terminal.rs b/src/terminal.rs index 2828600..1d9175f 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -13,8 +13,13 @@ use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternate use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::Buffer; -use crate::{Frame, Size, WidthDb}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; +/// Wrapper that manages terminal output. +/// +/// This struct wraps around stdout (usually) 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, @@ -151,6 +156,36 @@ impl Terminal { Ok(measure) } + /// Display a [`Widget`] on the screen. + /// + /// Internally calls [`Self::autoresize`] and [`Self::present`], and passes + /// on the value returned by [`Self::present`]. + pub fn present_widget(&mut self, widget: W) -> Result + where + E: From, + W: Widget, + { + self.autoresize()?; + widget.draw(self.frame())?; + let dirty = self.present()?; + Ok(dirty) + } + + /// Display an [`AsyncWidget`] on the screen. + /// + /// Internally calls [`Self::autoresize`] and [`Self::present`], and passes + /// on the value returned by [`Self::present`]. + pub async fn present_async_widget(&mut self, widget: W) -> Result + where + E: From, + W: AsyncWidget, + { + self.autoresize()?; + widget.draw(self.frame()).await?; + let dirty = self.present()?; + Ok(dirty) + } + 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 { From b2d87543d719cf404866dc5f7c0b9556df02e42c Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 14:15:03 +0100 Subject: [PATCH 068/144] Improve Terminal documentation --- src/terminal.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/terminal.rs b/src/terminal.rs index 1d9175f..86f96f0 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -17,7 +17,7 @@ use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; /// Wrapper that manages terminal output. /// -/// This struct wraps around stdout (usually) and handles showing things on the +/// 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 { @@ -39,10 +39,12 @@ impl Drop for Terminal { } 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, @@ -54,6 +56,13 @@ impl Terminal { 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()?; self.out.execute(LeaveAlternateScreen)?; @@ -66,6 +75,10 @@ impl Terminal { 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)?; @@ -80,24 +93,53 @@ impl Terminal { 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 } + /// Enable or disable grapheme width measurements. + /// + /// For more details, see [`Self::measuring`]. pub fn set_measuring(&mut self, active: bool) { self.frame.widthdb.active = 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 a + /// new frame is presented. 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.active } /// 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 }; @@ -110,10 +152,12 @@ impl Terminal { Ok(()) } + /// The current frame. pub fn frame(&mut self) -> &mut Frame { &mut self.frame } + /// A database of grapheme widths. pub fn widthdb(&mut self) -> &mut WidthDb { &mut self.frame.widthdb } From fae12a4b9f9ebad6bb0126094c66c4b945d0655a Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 15:04:56 +0100 Subject: [PATCH 069/144] Improve coords documentation --- src/coords.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/coords.rs b/src/coords.rs index 01450a6..1735746 100644 --- a/src/coords.rs +++ b/src/coords.rs @@ -1,5 +1,6 @@ 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, @@ -13,6 +14,7 @@ impl Size { 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), @@ -20,6 +22,7 @@ impl Size { ) } + /// 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), @@ -58,6 +61,9 @@ impl SubAssign for Size { } } +/// 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, From ba6ee451102f28e650213d8e79d763521ce8a52b Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 18:59:25 +0100 Subject: [PATCH 070/144] Don't measure widths while presenting --- examples/hello_world.rs | 9 ++-- examples/hello_world_widgets.rs | 6 ++- examples/overlapping_graphemes.rs | 9 ++-- examples/text_wrapping.rs | 9 ++-- src/terminal.rs | 72 +++++++++++++++++-------------- 5 files changed, 56 insertions(+), 49 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 391075e..59603dc 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -12,13 +12,12 @@ fn draw(f: &mut Frame) { } fn render_frame(term: &mut Terminal) { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. - term.autoresize().unwrap(); - draw(term.frame()); - while term.present().unwrap() { + let mut dirty = true; + while dirty { + dirty = term.measure_widths().unwrap(); term.autoresize().unwrap(); draw(term.frame()); + term.present().unwrap(); } } diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 27c81c1..9949dc6 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -22,7 +22,11 @@ fn widget() -> impl Widget { } fn render_frame(term: &mut Terminal) { - while term.present_widget(widget()).unwrap() {} + let mut dirty = true; + while dirty { + dirty = term.measure_widths().unwrap(); + term.present_widget(widget()).unwrap(); + } } fn main() { diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index 0a8f90b..0108e3f 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -49,13 +49,12 @@ fn draw(f: &mut Frame) { } fn render_frame(term: &mut Terminal) { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. - term.autoresize().unwrap(); - draw(term.frame()); - while term.present().unwrap() { + let mut dirty = true; + while dirty { + dirty = term.measure_widths().unwrap(); term.autoresize().unwrap(); draw(term.frame()); + term.present().unwrap(); } } diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index 21f0ffe..c5791d9 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -38,13 +38,12 @@ fn draw(f: &mut Frame) { } fn render_frame(term: &mut Terminal) { - // Must be called before rendering, otherwise the terminal has out-of-date - // size information and will present garbage. - term.autoresize().unwrap(); - draw(term.frame()); - while term.present().unwrap() { + let mut dirty = true; + while dirty { + dirty = term.measure_widths().unwrap(); term.autoresize().unwrap(); draw(term.frame()); + term.present().unwrap(); } } diff --git a/src/terminal.rs b/src/terminal.rs index 86f96f0..0adf87d 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -121,11 +121,12 @@ impl Terminal { /// 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 a - /// new frame is presented. 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 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 @@ -134,6 +135,25 @@ impl Terminal { self.frame.widthdb.active } + /// Measure widths of newly encountered graphemes. + /// + /// If width measurements are disabled, this function does nothing. For more + /// info, see [`Self::measuring`]. + /// + /// Returns `true` if graphemes were measured and the screen must be + /// redrawn. Keep in mind that after redrawing the screen, new 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. /// @@ -164,26 +184,12 @@ impl Terminal { /// Display the current frame on the screen and prepare the next frame. /// - /// Before drawing and presenting a frame, [`Self::autoresize`] should be - /// called. [`Self::present`] does **not** call it automatically. - /// - /// If width measurements are turned on, any new graphemes encountered since - /// the last [`Self::present`] call will be measured. This can lead to the - /// screen flickering or being mostly blank until measurements complete. - /// - /// Returns `true` if any new graphemes were measured. Since their widths - /// may have changed because of the measurements, the application using this - /// [`Terminal`] should re-draw and re-present the current 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 { - let measure = self.frame.widthdb.measuring_required(); - if measure { - self.frame.widthdb.measure_widths(&mut self.out)?; - self.full_redraw = true; - } - + pub fn present(&mut self) -> io::Result<()> { if self.full_redraw { io::stdout().queue(Clear(ClearType::All))?; self.prev_frame_buffer.reset(); // Because the screen is now empty @@ -197,37 +203,37 @@ impl Terminal { mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); self.frame.reset(); - Ok(measure) + Ok(()) } /// Display a [`Widget`] on the screen. /// - /// Internally calls [`Self::autoresize`] and [`Self::present`], and passes - /// on the value returned by [`Self::present`]. - pub fn present_widget(&mut self, widget: W) -> Result + /// Before creating and presenting a widget, [`Self::masure_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())?; - let dirty = self.present()?; - Ok(dirty) + self.present()?; + Ok(()) } /// Display an [`AsyncWidget`] on the screen. /// - /// Internally calls [`Self::autoresize`] and [`Self::present`], and passes - /// on the value returned by [`Self::present`]. - pub async fn present_async_widget(&mut self, widget: W) -> Result + /// Before creating and presenting a widget, [`Self::masure_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?; - let dirty = self.present()?; - Ok(dirty) + self.present()?; + Ok(()) } fn draw_differences(&mut self) -> io::Result<()> { From 72b44fb3fcda8178e0ec9c0f6ce0b023a127fd85 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 20:25:23 +0100 Subject: [PATCH 071/144] Add back optional Terminal::measuring_required --- src/terminal.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/terminal.rs b/src/terminal.rs index 0adf87d..52906af 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -135,13 +135,21 @@ impl Terminal { self.frame.widthdb.active } - /// Measure widths of newly encountered graphemes. + /// 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 graphemes were measured and the screen must be - /// redrawn. Keep in mind that after redrawing the screen, new graphemes may + /// 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 { From 7c3277a8222d028d608e797ce9118cb515723ea5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 21:06:12 +0100 Subject: [PATCH 072/144] Add Layer widget --- src/widget.rs | 10 ++++++- src/widgets.rs | 2 ++ src/widgets/layer.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/widgets/layer.rs diff --git a/src/widget.rs b/src/widget.rs index ea1d131..85a5eb9 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::widgets::{Background, Border, Float, Padding}; +use crate::widgets::{Background, Border, Float, Padding, Layer}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -41,6 +41,14 @@ pub trait WidgetExt: Sized { Float::new(self) } + fn below(self, top: W) -> Layer { + Layer::new(self, top) + } + + fn above(self, bottom: W) -> Layer { + Layer::new(bottom, self) + } + fn padding(self) -> Padding { Padding::new(self) } diff --git a/src/widgets.rs b/src/widgets.rs index 81d4dab..fe1346d 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -3,6 +3,7 @@ mod border; mod cursor; mod empty; mod float; +mod layer; mod padding; mod text; @@ -11,5 +12,6 @@ pub use border::*; pub use cursor::*; pub use empty::*; pub use float::*; +pub use layer::*; pub use padding::*; pub use text::*; diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs new file mode 100644 index 0000000..39cd9d7 --- /dev/null +++ b/src/widgets/layer.rs @@ -0,0 +1,65 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget}; + +pub struct Layer { + bottom: I1, + top: I2, +} + +impl Layer { + pub fn new(bottom: I1, top: I2) -> Self { + Self { bottom, top } + } + + fn size(bottom: Size, top: Size) -> Size { + Size::new(bottom.width.max(top.width), bottom.height.max(top.height)) + } +} + +impl Widget for Layer +where + I1: Widget, + I2: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + let bottom = self.bottom.size(frame, max_width, max_height)?; + let top = self.top.size(frame, max_width, max_height)?; + Ok(Self::size(bottom, top)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.bottom.draw(frame)?; + self.top.draw(frame)?; + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Layer +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + let bottom = self.bottom.size(frame, max_width, max_height).await?; + let top = self.top.size(frame, max_width, max_height).await?; + Ok(Self::size(bottom, top)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.bottom.draw(frame).await?; + self.top.draw(frame).await?; + Ok(()) + } +} From 8834bb6d9db4ad1b72d9575905ca1b739320e540 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 21:14:33 +0100 Subject: [PATCH 073/144] Add more Float functions --- src/widgets/float.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/widgets/float.rs b/src/widgets/float.rs index 6c35504..602c793 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -31,6 +31,34 @@ impl Float { self.horizontal(position).vertical(position) } + pub fn left(self) -> Self { + self.horizontal(0.0) + } + + pub fn right(self) -> Self { + self.horizontal(1.0) + } + + pub fn top(self) -> Self { + self.vertical(0.0) + } + + pub fn bottom(self) -> Self { + self.vertical(1.0) + } + + pub fn center_h(self) -> Self { + self.horizontal(0.5) + } + + pub fn center_v(self) -> Self { + self.vertical(0.5) + } + + pub fn center(self) -> Self { + self.all(0.5) + } + fn push_inner(&self, frame: &mut Frame, size: Size, mut inner_size: Size) { let mut inner_pos = Pos::ZERO; From 95a01d5fc8821dc88dc69bbf566ebef19d9d003f Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 17 Feb 2023 21:27:46 +0100 Subject: [PATCH 074/144] Add Either widget --- src/widget.rs | 10 +++++++- src/widgets.rs | 2 ++ src/widgets/either.rs | 59 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/widgets/either.rs diff --git a/src/widget.rs b/src/widget.rs index 85a5eb9..d54cebd 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::widgets::{Background, Border, Float, Padding, Layer}; +use crate::widgets::{Background, Border, Either, Float, Layer, Padding}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -37,6 +37,14 @@ pub trait WidgetExt: Sized { Border::new(self) } + fn first(self) -> Either { + Either::First(self) + } + + fn second(self) -> Either { + Either::Second(self) + } + fn float(self) -> Float { Float::new(self) } diff --git a/src/widgets.rs b/src/widgets.rs index fe1346d..abb6b8b 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,6 +1,7 @@ mod background; mod border; mod cursor; +mod either; mod empty; mod float; mod layer; @@ -10,6 +11,7 @@ mod text; pub use background::*; pub use border::*; pub use cursor::*; +pub use either::*; pub use empty::*; pub use float::*; pub use layer::*; diff --git a/src/widgets/either.rs b/src/widgets/either.rs new file mode 100644 index 0000000..facb2d9 --- /dev/null +++ b/src/widgets/either.rs @@ -0,0 +1,59 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget}; + +pub enum Either { + First(I1), + Second(I2), +} + +impl Widget for Either +where + I1: Widget, + I2: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + Self::First(l) => l.size(frame, max_width, max_height), + Self::Second(r) => r.size(frame, max_width, max_height), + } + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + Self::First(l) => l.draw(frame), + Self::Second(r) => r.draw(frame), + } + } +} + +#[async_trait] +impl AsyncWidget for Either +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + Self::First(l) => l.size(frame, max_width, max_height).await, + Self::Second(r) => r.size(frame, max_width, max_height).await, + } + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + Self::First(l) => l.draw(frame).await, + Self::Second(r) => r.draw(frame).await, + } + } +} From f25ce49e77f4813b3c9e80fa61ba10cf7f9d24ef Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 02:49:52 +0100 Subject: [PATCH 075/144] Rename Layer parts --- src/widget.rs | 8 ++++---- src/widgets/layer.rs | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index d54cebd..97e5ee2 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -49,12 +49,12 @@ pub trait WidgetExt: Sized { Float::new(self) } - fn below(self, top: W) -> Layer { - Layer::new(self, top) + fn below(self, above: W) -> Layer { + Layer::new(self, above) } - fn above(self, bottom: W) -> Layer { - Layer::new(bottom, self) + fn above(self, below: W) -> Layer { + Layer::new(below, self) } fn padding(self) -> Padding { diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs index 39cd9d7..cb1e4a9 100644 --- a/src/widgets/layer.rs +++ b/src/widgets/layer.rs @@ -3,17 +3,17 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget}; pub struct Layer { - bottom: I1, - top: I2, + below: I1, + above: I2, } impl Layer { - pub fn new(bottom: I1, top: I2) -> Self { - Self { bottom, top } + pub fn new(below: I1, above: I2) -> Self { + Self { below, above } } - fn size(bottom: Size, top: Size) -> Size { - Size::new(bottom.width.max(top.width), bottom.height.max(top.height)) + fn size(below: Size, above: Size) -> Size { + Size::new(below.width.max(above.width), below.height.max(above.height)) } } @@ -28,14 +28,14 @@ where max_width: Option, max_height: Option, ) -> Result { - let bottom = self.bottom.size(frame, max_width, max_height)?; - let top = self.top.size(frame, max_width, max_height)?; + let bottom = self.below.size(frame, max_width, max_height)?; + let top = self.above.size(frame, max_width, max_height)?; Ok(Self::size(bottom, top)) } fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.bottom.draw(frame)?; - self.top.draw(frame)?; + self.below.draw(frame)?; + self.above.draw(frame)?; Ok(()) } } @@ -52,14 +52,14 @@ where max_width: Option, max_height: Option, ) -> Result { - let bottom = self.bottom.size(frame, max_width, max_height).await?; - let top = self.top.size(frame, max_width, max_height).await?; + let bottom = self.below.size(frame, max_width, max_height).await?; + let top = self.above.size(frame, max_width, max_height).await?; Ok(Self::size(bottom, top)) } async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.bottom.draw(frame).await?; - self.top.draw(frame).await?; + self.below.draw(frame).await?; + self.above.draw(frame).await?; Ok(()) } } From caca3b6ef1f238c20ddd58a9424fee4af9b029da Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 14:07:03 +0100 Subject: [PATCH 076/144] Derive Debug, Clone, Copy for widgets --- src/widgets/background.rs | 1 + src/widgets/border.rs | 1 + src/widgets/cursor.rs | 1 + src/widgets/either.rs | 1 + src/widgets/float.rs | 1 + src/widgets/layer.rs | 1 + src/widgets/padding.rs | 1 + src/widgets/text.rs | 1 + 8 files changed, 8 insertions(+) diff --git a/src/widgets/background.rs b/src/widgets/background.rs index 78f362b..85d8df2 100644 --- a/src/widgets/background.rs +++ b/src/widgets/background.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget}; +#[derive(Debug, Clone, Copy)] pub struct Background { inner: I, style: Style, diff --git a/src/widgets/border.rs b/src/widgets/border.rs index fe6afbc..588f738 100644 --- a/src/widgets/border.rs +++ b/src/widgets/border.rs @@ -86,6 +86,7 @@ impl Default for BorderLook { } } +#[derive(Debug, Clone, Copy)] pub struct Border { inner: I, look: BorderLook, diff --git a/src/widgets/cursor.rs b/src/widgets/cursor.rs index feeb045..5f79ed8 100644 --- a/src/widgets/cursor.rs +++ b/src/widgets/cursor.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +#[derive(Debug, Clone, Copy)] pub struct Cursor { inner: I, at: Pos, diff --git a/src/widgets/either.rs b/src/widgets/either.rs index facb2d9..9092d4f 100644 --- a/src/widgets/either.rs +++ b/src/widgets/either.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget}; +#[derive(Debug, Clone, Copy)] pub enum Either { First(I1), Second(I2), diff --git a/src/widgets/float.rs b/src/widgets/float.rs index 602c793..e9c7ed8 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +#[derive(Debug, Clone, Copy)] pub struct Float { inner: I, horizontal: Option, diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs index cb1e4a9..289f854 100644 --- a/src/widgets/layer.rs +++ b/src/widgets/layer.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget}; +#[derive(Debug, Clone, Copy)] pub struct Layer { below: I1, above: I2, diff --git a/src/widgets/padding.rs b/src/widgets/padding.rs index 9cdcb68..a8f30b0 100644 --- a/src/widgets/padding.rs +++ b/src/widgets/padding.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +#[derive(Debug, Clone, Copy)] pub struct Padding { inner: I, left: u16, diff --git a/src/widgets/text.rs b/src/widgets/text.rs index 94a74c3..3c2ceee 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Styled, Widget, WidthDb}; +#[derive(Debug, Clone)] pub struct Text { styled: Styled, wrap: bool, From e666d5c092b3c62e15891961db17056f8175a807 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 14:07:25 +0100 Subject: [PATCH 077/144] Ensure Float position is in range 0.0..=1.0 --- src/widgets/float.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/widgets/float.rs b/src/widgets/float.rs index e9c7ed8..d3eff4b 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -19,16 +19,19 @@ impl Float { } pub fn horizontal(mut self, position: f32) -> Self { + assert!((0.0..=1.0).contains(&position)); self.horizontal = Some(position); self } pub fn vertical(mut self, position: f32) -> Self { + assert!((0.0..=1.0).contains(&position)); self.vertical = Some(position); self } pub fn all(self, position: f32) -> Self { + assert!((0.0..=1.0).contains(&position)); self.horizontal(position).vertical(position) } From 15e30dfdb286baaf502dbcd1e45f964039b7205e Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 18:20:55 +0100 Subject: [PATCH 078/144] Implement join widget spacing algorithm --- src/widgets.rs | 2 + src/widgets/join.rs | 191 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/widgets/join.rs diff --git a/src/widgets.rs b/src/widgets.rs index abb6b8b..f2fe1d7 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -4,6 +4,7 @@ mod cursor; mod either; mod empty; mod float; +mod join; mod layer; mod padding; mod text; @@ -14,6 +15,7 @@ pub use cursor::*; pub use either::*; pub use empty::*; pub use float::*; +pub use join::*; pub use layer::*; pub use padding::*; pub use text::*; diff --git a/src/widgets/join.rs b/src/widgets/join.rs new file mode 100644 index 0000000..4cc301b --- /dev/null +++ b/src/widgets/join.rs @@ -0,0 +1,191 @@ +use std::cmp::Ordering; + +// 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. + +struct Segment { + size: u16, + weight: f32, +} + +fn balance(segments: &mut [Segment], available: u16) { + if segments.is_empty() { + return; + } + + let total_size = segments.iter().map(|s| s.size).sum::(); + match total_size.cmp(&available) { + Ordering::Less => grow(segments, available), + Ordering::Greater => shrink(segments, available), + Ordering::Equal => {} + } + + assert!(available >= segments.iter().map(|s| s.size).sum::()); +} + +fn grow(segments: &mut [Segment], mut available: u16) { + assert!(available > segments.iter().map(|s| s.size).sum::()); + let mut segments = segments.iter_mut().collect::>(); + + // 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 = segments.iter().map(|s| s.weight).sum::(); + + // 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 changed = false; + segments.retain(|s| { + let allotment = s.weight / total_weight * available as f32; + if (s.size as f32) < allotment { + return true; // May need to grow + } + available -= s.size; + changed = true; + false + }); + + // If all segments were at least as large as their allotments, we would + // be trying to shrink, not grow them. Hence, there must be at least one + // segment that is smaller than its allotment. + assert!(!segments.is_empty()); + + if !changed { + break; // All remaining segments are smaller than their allotments + } + } + + // Size each remaining segment according to its allotment. + let total_weight = segments.iter().map(|s| s.weight).sum::(); + let mut used = 0; + for segment in &mut segments { + let allotment = segment.weight / total_weight * available as f32; + segment.size = allotment.floor() as u16; + used += segment.size; + } + + // 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.size += 1; + } +} + +fn shrink(segments: &mut [Segment], mut available: u16) { + assert!(available < segments.iter().map(|s| s.size).sum::()); + let mut segments = segments.iter_mut().collect::>(); + + // 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 = segments.iter().map(|s| s.weight).sum::(); + + // 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 changed = false; + segments.retain(|s| { + let allotment = s.weight / total_weight * available as f32; + if (s.size as f32) > allotment { + return true; // May need to shrink + } + + // The size subtracted from `available` is always smaller than or + // equal to its allotment. It must be smaller in at least one case, + // or we wouldn't be shrinking. Since `available` is the sum of all + // allotments, it never reaches 0. + assert!(available > s.size); + + available -= s.size; + changed = true; + false + }); + + // If all segments were smaller or the same size as their allotments, we + // would be trying to grow, not shrink them. Hence, there must be at + // least one segment bigger than its allotment. + assert!(!segments.is_empty()); + + if !changed { + break; // All segments want more than their weight allows. + } + } + + // Size each remaining segment according to its allotment. + let total_weight = segments.iter().map(|s| s.weight).sum::(); + let mut used = 0; + for segment in &mut segments { + let allotment = segment.weight / total_weight * available as f32; + segment.size = allotment.floor() as u16; + used += segment.size; + } + + // 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.size += 1; + } +} From 828bba464ab2a7d18f95a94c8472288c354a9b45 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 18:30:53 +0100 Subject: [PATCH 079/144] Add Either3 widget --- src/widget.rs | 14 +++++++- src/widgets/either.rs | 80 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index 97e5ee2..2fc3eb9 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::widgets::{Background, Border, Either, Float, Layer, Padding}; +use crate::widgets::{Background, Border, Either, Either3, Float, Layer, Padding}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -45,6 +45,18 @@ pub trait WidgetExt: Sized { Either::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) } diff --git a/src/widgets/either.rs b/src/widgets/either.rs index 9092d4f..8c4f79a 100644 --- a/src/widgets/either.rs +++ b/src/widgets/either.rs @@ -20,15 +20,15 @@ where max_height: Option, ) -> Result { match self { - Self::First(l) => l.size(frame, max_width, max_height), - Self::Second(r) => r.size(frame, max_width, max_height), + Self::First(w) => w.size(frame, max_width, max_height), + Self::Second(w) => w.size(frame, max_width, max_height), } } fn draw(self, frame: &mut Frame) -> Result<(), E> { match self { - Self::First(l) => l.draw(frame), - Self::Second(r) => r.draw(frame), + Self::First(w) => w.draw(frame), + Self::Second(w) => w.draw(frame), } } } @@ -46,15 +46,79 @@ where max_height: Option, ) -> Result { match self { - Self::First(l) => l.size(frame, max_width, max_height).await, - Self::Second(r) => r.size(frame, max_width, max_height).await, + Self::First(w) => w.size(frame, max_width, max_height).await, + Self::Second(w) => w.size(frame, max_width, max_height).await, } } async fn draw(self, frame: &mut Frame) -> Result<(), E> { match self { - Self::First(l) => l.draw(frame).await, - Self::Second(r) => r.draw(frame).await, + Self::First(w) => w.draw(frame).await, + Self::Second(w) => w.draw(frame).await, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Either3 { + First(I1), + Second(I2), + Third(I3), +} + +impl Widget for Either3 +where + I1: Widget, + I2: Widget, + I3: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + Self::First(w) => w.size(frame, max_width, max_height), + Self::Second(w) => w.size(frame, max_width, max_height), + Self::Third(w) => w.size(frame, max_width, max_height), + } + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + Self::First(w) => w.draw(frame), + Self::Second(w) => w.draw(frame), + Self::Third(w) => w.draw(frame), + } + } +} + +#[async_trait] +impl AsyncWidget for Either3 +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, + I3: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + Self::First(w) => w.size(frame, max_width, max_height).await, + Self::Second(w) => w.size(frame, max_width, max_height).await, + Self::Third(w) => w.size(frame, max_width, max_height).await, + } + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + Self::First(w) => w.draw(frame).await, + Self::Second(w) => w.draw(frame).await, + Self::Third(w) => w.draw(frame).await, } } } From f581fa6c470b9788b02b75ea46e793c52dec1eb6 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 18:54:51 +0100 Subject: [PATCH 080/144] Add JoinH and JoinV --- src/widget.rs | 6 +- src/widgets/join.rs | 319 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 1 deletion(-) diff --git a/src/widget.rs b/src/widget.rs index 2fc3eb9..ef575cd 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::widgets::{Background, Border, Either, Either3, Float, Layer, Padding}; +use crate::widgets::{Background, Border, Either, Either3, Float, JoinSegment, Layer, Padding}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -61,6 +61,10 @@ pub trait WidgetExt: Sized { Float::new(self) } + fn segment(self) -> JoinSegment { + JoinSegment::new(self) + } + fn below(self, above: W) -> Layer { Layer::new(self, above) } diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 4cc301b..90156a2 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -1,5 +1,9 @@ use std::cmp::Ordering; +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Pos, Size, Widget}; + // The following algorithm has three goals, listed in order of importance: // // 1. Use the available space @@ -50,6 +54,22 @@ struct Segment { weight: f32, } +impl Segment { + fn horizontal(size: Size, segment: &JoinSegment) -> Self { + Self { + size: size.width, + weight: segment.weight, + } + } + + fn vertical(size: Size, segment: &JoinSegment) -> Self { + Self { + size: size.height, + weight: segment.weight, + } + } +} + fn balance(segments: &mut [Segment], available: u16) { if segments.is_empty() { return; @@ -189,3 +209,302 @@ fn shrink(segments: &mut [Segment], mut available: u16) { segment.size += 1; } } + +pub struct JoinSegment { + inner: I, + weight: f32, +} + +impl JoinSegment { + pub fn new(inner: I) -> Self { + Self { inner, weight: 1.0 } + } + + pub fn weight(mut self, weight: f32) -> Self { + assert!(weight >= 0.0); + self.weight = weight; + self + } +} + +pub struct JoinH { + segments: Vec>, +} + +impl JoinH { + pub fn new(segments: Vec>) -> Self { + Self { segments } + } +} + +impl Widget for JoinH +where + I: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + if let Some(max_width) = max_width { + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment.inner.size(frame, Some(max_width), max_height)?; + balanced_segments.push(Segment::horizontal(size, segment)); + } + balance(&mut balanced_segments, max_width); + + let mut width = 0; + let mut height = 0; + for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { + let size = segment.inner.size(frame, Some(balanced.size), max_height)?; + width += size.width; + height = height.max(size.height); + } + Ok(Size::new(width, height)) + } else { + let mut width = 0; + let mut height = 0; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height)?; + width += size.width; + height = height.max(size.height); + } + Ok(Size::new(width, height)) + } + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let max_width = Some(size.width); + let max_height = Some(size.height); + + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height)?; + balanced_segments.push(Segment::horizontal(size, segment)); + } + balance(&mut balanced_segments, size.width); + + let mut x = 0; + for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { + frame.push(Pos::new(x, 0), Size::new(balanced.size, size.height)); + segment.inner.draw(frame)?; + frame.pop(); + x += balanced.size as i32; + } + + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for JoinH +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + if let Some(max_width) = max_width { + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment + .inner + .size(frame, Some(max_width), max_height) + .await?; + balanced_segments.push(Segment::horizontal(size, segment)); + } + balance(&mut balanced_segments, max_width); + + let mut width = 0; + let mut height = 0; + for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { + let size = segment + .inner + .size(frame, Some(balanced.size), max_height) + .await?; + width += size.width; + height = height.max(size.height); + } + Ok(Size::new(width, height)) + } else { + let mut width = 0; + let mut height = 0; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height).await?; + width += size.width; + height = height.max(size.height); + } + Ok(Size::new(width, height)) + } + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let max_width = Some(size.width); + let max_height = Some(size.height); + + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height).await?; + balanced_segments.push(Segment::horizontal(size, segment)); + } + balance(&mut balanced_segments, size.width); + + let mut x = 0; + for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { + frame.push(Pos::new(x, 0), Size::new(balanced.size, size.height)); + segment.inner.draw(frame).await?; + frame.pop(); + x += balanced.size as i32; + } + + Ok(()) + } +} + +pub struct JoinV { + segments: Vec>, +} + +impl JoinV { + pub fn new(segments: Vec>) -> Self { + Self { segments } + } +} + +impl Widget for JoinV +where + I: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + if let Some(max_height) = max_height { + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, Some(max_height))?; + balanced_segments.push(Segment::vertical(size, segment)); + } + balance(&mut balanced_segments, max_height); + + let mut width = 0; + let mut height = 0; + for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { + let size = segment.inner.size(frame, max_width, Some(balanced.size))?; + width = width.max(size.width); + height += size.height; + } + Ok(Size::new(width, height)) + } else { + let mut width = 0; + let mut height = 0; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height)?; + width = width.max(size.width); + height += size.height; + } + Ok(Size::new(width, height)) + } + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let max_width = Some(size.width); + let max_height = Some(size.height); + + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height)?; + balanced_segments.push(Segment::vertical(size, segment)); + } + balance(&mut balanced_segments, size.height); + + let mut y = 0; + for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { + frame.push(Pos::new(0, y), Size::new(size.width, balanced.size)); + segment.inner.draw(frame)?; + frame.pop(); + y += balanced.size as i32; + } + + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for JoinV +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + if let Some(max_height) = max_height { + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment + .inner + .size(frame, max_width, Some(max_height)) + .await?; + balanced_segments.push(Segment::vertical(size, segment)); + } + balance(&mut balanced_segments, max_height); + + let mut width = 0; + let mut height = 0; + for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { + let size = segment + .inner + .size(frame, max_width, Some(balanced.size)) + .await?; + width = width.max(size.width); + height += size.height; + } + Ok(Size::new(width, height)) + } else { + let mut width = 0; + let mut height = 0; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height).await?; + width = width.max(size.width); + height += size.height; + } + Ok(Size::new(width, height)) + } + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let max_width = Some(size.width); + let max_height = Some(size.height); + + let mut balanced_segments = vec![]; + for segment in &self.segments { + let size = segment.inner.size(frame, max_width, max_height).await?; + balanced_segments.push(Segment::vertical(size, segment)); + } + balance(&mut balanced_segments, size.height); + + let mut y = 0; + for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { + frame.push(Pos::new(0, y), Size::new(size.width, balanced.size)); + segment.inner.draw(frame).await?; + frame.pop(); + y += balanced.size as i32; + } + + Ok(()) + } +} From d449c61f2717e11480737e275dee882e7180fda9 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 19:05:27 +0100 Subject: [PATCH 081/144] Add JoinH2, JoinH3, JoinV2, JoinV3 --- src/widgets/join.rs | 238 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 90156a2..ebd2733 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -4,6 +4,8 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use super::{Either, Either3}; + // The following algorithm has three goals, listed in order of importance: // // 1. Use the available space @@ -508,3 +510,239 @@ where Ok(()) } } + +pub struct JoinH2(JoinH>); + +impl JoinH2 { + pub fn new(left: JoinSegment, right: JoinSegment) -> Self { + Self(JoinH::new(vec![ + JoinSegment { + inner: Either::First(left.inner), + weight: left.weight, + }, + JoinSegment { + inner: Either::Second(right.inner), + weight: right.weight, + }, + ])) + } +} + +impl Widget for JoinH2 +where + I1: Widget, + I2: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for JoinH2 +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame).await + } +} + +pub struct JoinH3(JoinH>); + +impl JoinH3 { + pub fn new(left: JoinSegment, middle: JoinSegment, right: JoinSegment) -> Self { + Self(JoinH::new(vec![ + JoinSegment { + inner: Either3::First(left.inner), + weight: left.weight, + }, + JoinSegment { + inner: Either3::Second(middle.inner), + weight: middle.weight, + }, + JoinSegment { + inner: Either3::Third(right.inner), + weight: right.weight, + }, + ])) + } +} + +impl Widget for JoinH3 +where + I1: Widget, + I2: Widget, + I3: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for JoinH3 +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, + I3: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame).await + } +} + +pub struct JoinV2(JoinV>); + +impl JoinV2 { + pub fn new(top: JoinSegment, bottom: JoinSegment) -> Self { + Self(JoinV::new(vec![ + JoinSegment { + inner: Either::First(top.inner), + weight: top.weight, + }, + JoinSegment { + inner: Either::Second(bottom.inner), + weight: bottom.weight, + }, + ])) + } +} + +impl Widget for JoinV2 +where + I1: Widget, + I2: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for JoinV2 +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame).await + } +} + +pub struct JoinV3(JoinV>); + +impl JoinV3 { + pub fn new(top: JoinSegment, middle: JoinSegment, bottom: JoinSegment) -> Self { + Self(JoinV::new(vec![ + JoinSegment { + inner: Either3::First(top.inner), + weight: top.weight, + }, + JoinSegment { + inner: Either3::Second(middle.inner), + weight: middle.weight, + }, + JoinSegment { + inner: Either3::Third(bottom.inner), + weight: bottom.weight, + }, + ])) + } +} + +impl Widget for JoinV3 +where + I1: Widget, + I2: Widget, + I3: Widget, +{ + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for JoinV3 +where + I1: AsyncWidget + Send + Sync, + I2: AsyncWidget + Send + Sync, + I3: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame).await + } +} From 42d22e2a49c6c558de7e081e50f69a8a92ac274c Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 18:56:16 +0100 Subject: [PATCH 082/144] Fix typo --- src/terminal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal.rs b/src/terminal.rs index 52906af..cb4a186 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -216,7 +216,7 @@ impl Terminal { /// Display a [`Widget`] on the screen. /// - /// Before creating and presenting a widget, [`Self::masure_widths`] should + /// 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 @@ -231,7 +231,7 @@ impl Terminal { /// Display an [`AsyncWidget`] on the screen. /// - /// Before creating and presenting a widget, [`Self::masure_widths`] should + /// 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 From 3fb3a7b92b05a739792ff306aafccb38585b3225 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 19:40:24 +0100 Subject: [PATCH 083/144] Fix sizing bug The available space was updated while removing elements, but the check for the next element expected the old value. --- src/widgets/join.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index ebd2733..9b3d397 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -105,23 +105,23 @@ fn grow(segments: &mut [Segment], mut available: u16) { total_weight = segments.len() as f32; } - let mut changed = false; + let mut removed = 0; segments.retain(|s| { let allotment = s.weight / total_weight * available as f32; if (s.size as f32) < allotment { return true; // May need to grow } - available -= s.size; - changed = true; + removed += s.size; false }); + available -= removed; // If all segments were at least as large as their allotments, we would // be trying to shrink, not grow them. Hence, there must be at least one // segment that is smaller than its allotment. assert!(!segments.is_empty()); - if !changed { + if removed == 0 { break; // All remaining segments are smaller than their allotments } } From ba716dd0891b0134125b39f183e21a24cab4234f Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 20:13:47 +0100 Subject: [PATCH 084/144] Fix examples not measuring widths immediately --- examples/hello_world.rs | 2 +- examples/hello_world_widgets.rs | 2 +- examples/overlapping_graphemes.rs | 2 +- examples/text_wrapping.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 59603dc..62c0c75 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -14,10 +14,10 @@ fn draw(f: &mut Frame) { fn render_frame(term: &mut Terminal) { let mut dirty = true; while dirty { - dirty = term.measure_widths().unwrap(); term.autoresize().unwrap(); draw(term.frame()); term.present().unwrap(); + dirty = term.measure_widths().unwrap(); } } diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index 9949dc6..c80f10d 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -24,8 +24,8 @@ fn widget() -> impl Widget { fn render_frame(term: &mut Terminal) { let mut dirty = true; while dirty { - dirty = term.measure_widths().unwrap(); term.present_widget(widget()).unwrap(); + dirty = term.measure_widths().unwrap(); } } diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index 0108e3f..c90c4ae 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -51,10 +51,10 @@ fn draw(f: &mut Frame) { fn render_frame(term: &mut Terminal) { let mut dirty = true; while dirty { - dirty = term.measure_widths().unwrap(); term.autoresize().unwrap(); draw(term.frame()); term.present().unwrap(); + dirty = term.measure_widths().unwrap(); } } diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index c5791d9..5292378 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -40,10 +40,10 @@ fn draw(f: &mut Frame) { fn render_frame(term: &mut Terminal) { let mut dirty = true; while dirty { - dirty = term.measure_widths().unwrap(); term.autoresize().unwrap(); draw(term.frame()); term.present().unwrap(); + dirty = term.measure_widths().unwrap(); } } From b27cb816422abe093faf06887b30c44cddbf0c2d Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 20:24:56 +0100 Subject: [PATCH 085/144] Add Resize widget --- src/widget.rs | 8 +++- src/widgets.rs | 2 + src/widgets/resize.rs | 103 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/widgets/resize.rs diff --git a/src/widget.rs b/src/widget.rs index ef575cd..d2ae61e 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,6 +1,8 @@ use async_trait::async_trait; -use crate::widgets::{Background, Border, Either, Either3, Float, JoinSegment, Layer, Padding}; +use crate::widgets::{ + Background, Border, Either, Either3, Float, JoinSegment, Layer, Padding, Resize, +}; use crate::{Frame, Size}; // TODO Feature-gate these traits @@ -76,6 +78,10 @@ pub trait WidgetExt: Sized { fn padding(self) -> Padding { Padding::new(self) } + + fn resize(self) -> Resize { + Resize::new(self) + } } // It would be nice if this could be restricted to types implementing Widget. diff --git a/src/widgets.rs b/src/widgets.rs index f2fe1d7..608f50e 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -7,6 +7,7 @@ mod float; mod join; mod layer; mod padding; +mod resize; mod text; pub use background::*; @@ -18,4 +19,5 @@ pub use float::*; pub use join::*; pub use layer::*; pub use padding::*; +pub use resize::*; pub use text::*; diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs new file mode 100644 index 0000000..bf80801 --- /dev/null +++ b/src/widgets/resize.rs @@ -0,0 +1,103 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget}; + +pub struct Resize { + inner: I, + min_width: Option, + min_height: Option, + max_width: Option, + 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 min_width(mut self, width: u16) -> Self { + self.min_width = Some(width); + self + } + + pub fn min_height(mut self, height: u16) -> Self { + self.min_height = Some(height); + self + } + + pub fn max_width(mut self, width: u16) -> Self { + self.max_width = Some(width); + self + } + + pub fn max_height(mut self, height: u16) -> Self { + self.max_height = Some(height); + self + } + + 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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + let size = self.inner.size(frame, 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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + let size = self.inner.size(frame, max_width, max_height).await?; + Ok(self.resize(size)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame).await + } +} From 204540f375d3856df2c289665fe68062ed9bd85b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 20:37:41 +0100 Subject: [PATCH 086/144] Create Either{2,7} via macros --- src/widget.rs | 10 +- src/widgets/either.rs | 208 ++++++++++++++++++++---------------------- src/widgets/join.rs | 14 +-- 3 files changed, 113 insertions(+), 119 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index d2ae61e..5d3faa4 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use crate::widgets::{ - Background, Border, Either, Either3, Float, JoinSegment, Layer, Padding, Resize, + Background, Border, Either2, Either3, Float, JoinSegment, Layer, Padding, Resize, }; use crate::{Frame, Size}; @@ -39,12 +39,12 @@ pub trait WidgetExt: Sized { Border::new(self) } - fn first(self) -> Either { - Either::First(self) + fn first2(self) -> Either2 { + Either2::First(self) } - fn second(self) -> Either { - Either::Second(self) + fn second2(self) -> Either2 { + Either2::Second(self) } fn first3(self) -> Either3 { diff --git a/src/widgets/either.rs b/src/widgets/either.rs index 8c4f79a..ea74da4 100644 --- a/src/widgets/either.rs +++ b/src/widgets/either.rs @@ -2,123 +2,117 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget}; -#[derive(Debug, Clone, Copy)] -pub enum Either { - First(I1), - Second(I2), +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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + $( Self::$constr(w) => w.size(frame, 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, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + match self { + $( Self::$constr(w) => w.size(frame, max_width, max_height).await, )+ + } + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + match self { + $( Self::$constr(w) => w.draw(frame).await, )+ + } + } + } + }; } -impl Widget for Either -where - I1: Widget, - I2: Widget, -{ - fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - match self { - Self::First(w) => w.size(frame, max_width, max_height), - Self::Second(w) => w.size(frame, max_width, max_height), - } - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - match self { - Self::First(w) => w.draw(frame), - Self::Second(w) => w.draw(frame), - } +mk_either! { + pub enum Either2 { + First(I1), + Second(I2), } } -#[async_trait] -impl AsyncWidget for Either -where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - match self { - Self::First(w) => w.size(frame, max_width, max_height).await, - Self::Second(w) => w.size(frame, max_width, max_height).await, - } - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - match self { - Self::First(w) => w.draw(frame).await, - Self::Second(w) => w.draw(frame).await, - } +mk_either! { + pub enum Either3 { + First(I1), + Second(I2), + Third(I3), } } -#[derive(Debug, Clone, Copy)] -pub enum Either3 { - First(I1), - Second(I2), - Third(I3), -} - -impl Widget for Either3 -where - I1: Widget, - I2: Widget, - I3: Widget, -{ - fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - match self { - Self::First(w) => w.size(frame, max_width, max_height), - Self::Second(w) => w.size(frame, max_width, max_height), - Self::Third(w) => w.size(frame, max_width, max_height), - } - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - match self { - Self::First(w) => w.draw(frame), - Self::Second(w) => w.draw(frame), - Self::Third(w) => w.draw(frame), - } +mk_either! { + pub enum Either4 { + First(I1), + Second(I2), + Third(I3), + Fourth(I4), } } -#[async_trait] -impl AsyncWidget for Either3 -where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, - I3: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - match self { - Self::First(w) => w.size(frame, max_width, max_height).await, - Self::Second(w) => w.size(frame, max_width, max_height).await, - Self::Third(w) => w.size(frame, max_width, max_height).await, - } - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - match self { - Self::First(w) => w.draw(frame).await, - Self::Second(w) => w.draw(frame).await, - Self::Third(w) => w.draw(frame).await, - } +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/join.rs b/src/widgets/join.rs index 9b3d397..68ffd73 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget}; -use super::{Either, Either3}; +use super::{Either2, Either3}; // The following algorithm has three goals, listed in order of importance: // @@ -511,17 +511,17 @@ where } } -pub struct JoinH2(JoinH>); +pub struct JoinH2(JoinH>); impl JoinH2 { pub fn new(left: JoinSegment, right: JoinSegment) -> Self { Self(JoinH::new(vec![ JoinSegment { - inner: Either::First(left.inner), + inner: Either2::First(left.inner), weight: left.weight, }, JoinSegment { - inner: Either::Second(right.inner), + inner: Either2::Second(right.inner), weight: right.weight, }, ])) @@ -629,17 +629,17 @@ where } } -pub struct JoinV2(JoinV>); +pub struct JoinV2(JoinV>); impl JoinV2 { pub fn new(top: JoinSegment, bottom: JoinSegment) -> Self { Self(JoinV::new(vec![ JoinSegment { - inner: Either::First(top.inner), + inner: Either2::First(top.inner), weight: top.weight, }, JoinSegment { - inner: Either::Second(bottom.inner), + inner: Either2::Second(bottom.inner), weight: bottom.weight, }, ])) From bdc1549268854b8d031fbd4636b1f6103f6564b4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 20:56:36 +0100 Subject: [PATCH 087/144] Create Join[HV]{2,7} via macros --- src/widgets/join.rs | 336 ++++++++++++++++++-------------------------- 1 file changed, 136 insertions(+), 200 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 68ffd73..1acbf84 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget}; -use super::{Either2, Either3}; +use super::{Either2, Either3, Either4, Either5, Either6, Either7}; // The following algorithm has three goals, listed in order of importance: // @@ -511,238 +511,174 @@ where } } -pub struct JoinH2(JoinH>); +macro_rules! mk_join { + ( + $name:ident: $base:ident + $either:ident { + $( $arg:ident: $constr:ident ($ty:ident), )+ + } + ) => { + pub struct $name< $( $ty ),+ >($base<$either< $( $ty ),+ >>); -impl JoinH2 { - pub fn new(left: JoinSegment, right: JoinSegment) -> Self { - Self(JoinH::new(vec![ - JoinSegment { - inner: Either2::First(left.inner), - weight: left.weight, - }, - JoinSegment { - inner: Either2::Second(right.inner), - weight: right.weight, - }, - ])) + impl< $( $ty ),+ > $name< $( $ty ),+ > { + pub fn new( $( $arg: JoinSegment<$ty> ),+ ) -> Self { + Self($base::new(vec![ $( + JoinSegment { + inner: $either::$constr($arg.inner), + weight: $arg.weight, + }, + )+ ])) + } + } + + impl Widget for $name< $( $ty ),+ > + where + $( $ty: Widget, )+ + { + fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame) + } + } + + #[async_trait] + impl AsyncWidget for $name< $( $ty ),+ > + where + $( $ty: AsyncWidget + Send + Sync, )+ + { + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(frame, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.0.draw(frame).await + } + } + }; +} + +mk_join! { + JoinH2: JoinH + Either2 { + first: First(I1), + second: Second(I2), } } -impl Widget for JoinH2 -where - I1: Widget, - I2: Widget, -{ - fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height) - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame) +mk_join! { + JoinH3: JoinH + Either3 { + first: First(I1), + second: Second(I2), + third: Third(I3), } } -#[async_trait] -impl AsyncWidget for JoinH2 -where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height).await - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame).await +mk_join! { + JoinH4: JoinH + Either4 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), } } -pub struct JoinH3(JoinH>); - -impl JoinH3 { - pub fn new(left: JoinSegment, middle: JoinSegment, right: JoinSegment) -> Self { - Self(JoinH::new(vec![ - JoinSegment { - inner: Either3::First(left.inner), - weight: left.weight, - }, - JoinSegment { - inner: Either3::Second(middle.inner), - weight: middle.weight, - }, - JoinSegment { - inner: Either3::Third(right.inner), - weight: right.weight, - }, - ])) +mk_join! { + JoinH5: JoinH + Either5 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), + fifth: Fifth(I5), } } -impl Widget for JoinH3 -where - I1: Widget, - I2: Widget, - I3: Widget, -{ - fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height) - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame) +mk_join! { + JoinH6: JoinH + Either6 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), + fifth: Fifth(I5), + sixth: Sixth(I6), } } -#[async_trait] -impl AsyncWidget for JoinH3 -where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, - I3: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height).await - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame).await +mk_join! { + JoinH7: JoinH + Either7 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), + fifth: Fifth(I5), + sixth: Sixth(I6), + seventh: Seventh(I7), } } -pub struct JoinV2(JoinV>); - -impl JoinV2 { - pub fn new(top: JoinSegment, bottom: JoinSegment) -> Self { - Self(JoinV::new(vec![ - JoinSegment { - inner: Either2::First(top.inner), - weight: top.weight, - }, - JoinSegment { - inner: Either2::Second(bottom.inner), - weight: bottom.weight, - }, - ])) +mk_join! { + JoinV2: JoinV + Either2 { + first: First(I1), + second: Second(I2), } } -impl Widget for JoinV2 -where - I1: Widget, - I2: Widget, -{ - fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height) - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame) +mk_join! { + JoinV3: JoinV + Either3 { + first: First(I1), + second: Second(I2), + third: Third(I3), } } -#[async_trait] -impl AsyncWidget for JoinV2 -where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height).await - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame).await +mk_join! { + JoinV4: JoinV + Either4 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), } } -pub struct JoinV3(JoinV>); - -impl JoinV3 { - pub fn new(top: JoinSegment, middle: JoinSegment, bottom: JoinSegment) -> Self { - Self(JoinV::new(vec![ - JoinSegment { - inner: Either3::First(top.inner), - weight: top.weight, - }, - JoinSegment { - inner: Either3::Second(middle.inner), - weight: middle.weight, - }, - JoinSegment { - inner: Either3::Third(bottom.inner), - weight: bottom.weight, - }, - ])) +mk_join! { + JoinV5: JoinV + Either5 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), + fifth: Fifth(I5), } } -impl Widget for JoinV3 -where - I1: Widget, - I2: Widget, - I3: Widget, -{ - fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height) - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame) +mk_join! { + JoinV6: JoinV + Either6 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), + fifth: Fifth(I5), + sixth: Sixth(I6), } } -#[async_trait] -impl AsyncWidget for JoinV3 -where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, - I3: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - frame: &mut Frame, - max_width: Option, - max_height: Option, - ) -> Result { - self.0.size(frame, max_width, max_height).await - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.0.draw(frame).await +mk_join! { + JoinV7: JoinV + Either7 { + first: First(I1), + second: Second(I2), + third: Third(I3), + fourth: Fourth(I4), + fifth: Fifth(I5), + sixth: Sixth(I6), + seventh: Seventh(I7), } } From a8876e94f3dacd32cd8079b62a0e7a87ef4d1b06 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 18 Feb 2023 21:24:51 +0100 Subject: [PATCH 088/144] Make default background style opaque --- src/widgets/background.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/background.rs b/src/widgets/background.rs index 85d8df2..edd1fdf 100644 --- a/src/widgets/background.rs +++ b/src/widgets/background.rs @@ -12,7 +12,7 @@ impl Background { pub fn new(inner: I) -> Self { Self { inner, - style: Style::default(), + style: Style::new().opaque(), } } From b1c276ec38006d62c5829b9ce731986f66fd054c Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 19 Feb 2023 02:02:27 +0100 Subject: [PATCH 089/144] Fix panic with zero-weighted segments --- src/widgets/join.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 1acbf84..ef6e989 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -51,6 +51,8 @@ use super::{Either2, Either3, Either4, Either5, Either6, Either7}; // removes all segments that are at least as small as their allotment. It then // resizes the remaining segments to their allotments. +// TODO Handle overflows and other sizing issues correctly + struct Segment { size: u16, weight: f32, @@ -164,30 +166,29 @@ fn shrink(segments: &mut [Segment], mut available: u16) { total_weight = segments.len() as f32; } - let mut changed = false; + let mut removed = 0; segments.retain(|s| { let allotment = s.weight / total_weight * available as f32; if (s.size as f32) > allotment { return true; // May need to shrink } - // The size subtracted from `available` is always smaller than or - // equal to its allotment. It must be smaller in at least one case, - // or we wouldn't be shrinking. Since `available` is the sum of all - // allotments, it never reaches 0. - assert!(available > s.size); + // 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.size <= available); - available -= s.size; - changed = true; + removed += s.size; false }); + available -= removed; // If all segments were smaller or the same size as their allotments, we // would be trying to grow, not shrink them. Hence, there must be at // least one segment bigger than its allotment. assert!(!segments.is_empty()); - if !changed { + if removed == 0 { break; // All segments want more than their weight allows. } } From 8f155dc6a21a3f5bd8582ad0f8985541680b5369 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 19 Feb 2023 02:09:41 +0100 Subject: [PATCH 090/144] Fix join widgets not handling large sizes --- src/widgets/join.rs | 74 +++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index ef6e989..2a0a7b5 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -51,8 +51,7 @@ use super::{Either2, Either3, Either4, Either5, Either6, Either7}; // removes all segments that are at least as small as their allotment. It then // resizes the remaining segments to their allotments. -// TODO Handle overflows and other sizing issues correctly - +#[derive(Debug)] struct Segment { size: u16, weight: f32, @@ -74,13 +73,24 @@ impl Segment { } } +fn total_size(segments: &[Segment]) -> u16 { + let mut total = 0_u16; + for segment in segments { + total = total.saturating_add(segment.size); + } + total +} + +fn total_weight(segments: &[&mut Segment]) -> f32 { + segments.iter().map(|s| s.weight).sum() +} + fn balance(segments: &mut [Segment], available: u16) { if segments.is_empty() { return; } - let total_size = segments.iter().map(|s| s.size).sum::(); - match total_size.cmp(&available) { + match total_size(segments).cmp(&available) { Ordering::Less => grow(segments, available), Ordering::Greater => shrink(segments, available), Ordering::Equal => {} @@ -90,13 +100,13 @@ fn balance(segments: &mut [Segment], available: u16) { } fn grow(segments: &mut [Segment], mut available: u16) { - assert!(available > segments.iter().map(|s| s.size).sum::()); + assert!(available > total_size(segments)); let mut segments = segments.iter_mut().collect::>(); // 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 = segments.iter().map(|s| s.weight).sum::(); + let mut total_weight = total_weight(&segments); // If there are no segments with a weight > 0, space is distributed // evenly among all remaining segments. @@ -149,13 +159,13 @@ fn grow(segments: &mut [Segment], mut available: u16) { } fn shrink(segments: &mut [Segment], mut available: u16) { - assert!(available < segments.iter().map(|s| s.size).sum::()); + assert!(available < total_size(segments)); let mut segments = segments.iter_mut().collect::>(); // 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 = segments.iter().map(|s| s.weight).sum::(); + let mut total_weight = total_weight(&segments); // If there are no segments with a weight > 0, space is distributed // evenly among all remaining segments. @@ -258,20 +268,20 @@ where } balance(&mut balanced_segments, max_width); - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { let size = segment.inner.size(frame, Some(balanced.size), max_height)?; - width += size.width; + width = width.saturating_add(size.width); height = height.max(size.height); } Ok(Size::new(width, height)) } else { - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for segment in &self.segments { let size = segment.inner.size(frame, max_width, max_height)?; - width += size.width; + width = width.saturating_add(size.width); height = height.max(size.height); } Ok(Size::new(width, height)) @@ -324,23 +334,23 @@ where } balance(&mut balanced_segments, max_width); - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { let size = segment .inner .size(frame, Some(balanced.size), max_height) .await?; - width += size.width; + width = width.saturating_add(size.width); height = height.max(size.height); } Ok(Size::new(width, height)) } else { - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for segment in &self.segments { let size = segment.inner.size(frame, max_width, max_height).await?; - width += size.width; + width = width.saturating_add(size.width); height = height.max(size.height); } Ok(Size::new(width, height)) @@ -399,21 +409,21 @@ where } balance(&mut balanced_segments, max_height); - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { let size = segment.inner.size(frame, max_width, Some(balanced.size))?; width = width.max(size.width); - height += size.height; + height = height.saturating_add(size.height); } Ok(Size::new(width, height)) } else { - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for segment in &self.segments { let size = segment.inner.size(frame, max_width, max_height)?; width = width.max(size.width); - height += size.height; + height = height.saturating_add(size.height); } Ok(Size::new(width, height)) } @@ -465,24 +475,24 @@ where } balance(&mut balanced_segments, max_height); - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { let size = segment .inner .size(frame, max_width, Some(balanced.size)) .await?; width = width.max(size.width); - height += size.height; + height = height.saturating_add(size.height); } Ok(Size::new(width, height)) } else { - let mut width = 0; - let mut height = 0; + let mut width = 0_u16; + let mut height = 0_u16; for segment in &self.segments { let size = segment.inner.size(frame, max_width, max_height).await?; width = width.max(size.width); - height += size.height; + height = height.saturating_add(size.height); } Ok(Size::new(width, height)) } From 783e57a9ab64b9c521d3b565f03c0050da6c575b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 19 Feb 2023 02:30:07 +0100 Subject: [PATCH 091/144] Allow join segments to be fixed I'm not sure if this is a good abstraction, or if I should instead re-think the algorithm. --- src/widgets/join.rs | 51 ++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 2a0a7b5..6e56d35 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -55,6 +55,7 @@ use super::{Either2, Either3, Either4, Either5, Either6, Either7}; struct Segment { size: u16, weight: f32, + fixed: bool, } impl Segment { @@ -62,6 +63,7 @@ impl Segment { Self { size: size.width, weight: segment.weight, + fixed: segment.fixed, } } @@ -69,11 +71,12 @@ impl Segment { Self { size: size.height, weight: segment.weight, + fixed: segment.fixed, } } } -fn total_size(segments: &[Segment]) -> u16 { +fn total_size(segments: &[&mut Segment]) -> u16 { let mut total = 0_u16; for segment in segments { total = total.saturating_add(segment.size); @@ -85,23 +88,31 @@ fn total_weight(segments: &[&mut Segment]) -> f32 { segments.iter().map(|s| s.weight).sum() } -fn balance(segments: &mut [Segment], available: u16) { - if segments.is_empty() { +fn balance(segments: &mut [Segment], mut available: u16) { + let mut borrowed_segments = segments.iter_mut().collect::>(); + + // Remove fixed segments + borrowed_segments.retain(|s| { + if !s.fixed { + return true; + } + available = available.saturating_sub(s.size); + false + }); + + if borrowed_segments.is_empty() || available == 0 { return; } - match total_size(segments).cmp(&available) { - Ordering::Less => grow(segments, available), - Ordering::Greater => shrink(segments, available), + match total_size(&borrowed_segments).cmp(&available) { + Ordering::Less => grow(borrowed_segments, available), + Ordering::Greater => shrink(borrowed_segments, available), Ordering::Equal => {} } - - assert!(available >= segments.iter().map(|s| s.size).sum::()); } -fn grow(segments: &mut [Segment], mut available: u16) { - assert!(available > total_size(segments)); - let mut segments = segments.iter_mut().collect::>(); +fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { + assert!(available > total_size(&segments)); // Repeatedly remove all segments that do not need to grow, i. e. that are // at least as large as their allotment. @@ -158,9 +169,8 @@ fn grow(segments: &mut [Segment], mut available: u16) { } } -fn shrink(segments: &mut [Segment], mut available: u16) { - assert!(available < total_size(segments)); - let mut segments = segments.iter_mut().collect::>(); +fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { + assert!(available < total_size(&segments)); // Repeatedly remove all segments that do not need to shrink, i. e. that are // at least as small as their allotment. @@ -226,11 +236,16 @@ fn shrink(segments: &mut [Segment], mut available: u16) { pub struct JoinSegment { inner: I, weight: f32, + fixed: bool, } impl JoinSegment { pub fn new(inner: I) -> Self { - Self { inner, weight: 1.0 } + Self { + inner, + weight: 1.0, + fixed: false, + } } pub fn weight(mut self, weight: f32) -> Self { @@ -238,6 +253,11 @@ impl JoinSegment { self.weight = weight; self } + + pub fn fixed(mut self, fixed: bool) -> Self { + self.fixed = fixed; + self + } } pub struct JoinH { @@ -536,6 +556,7 @@ macro_rules! mk_join { JoinSegment { inner: $either::$constr($arg.inner), weight: $arg.weight, + fixed: $arg.fixed, }, )+ ])) } From 397d3a6eac25b1ce106c0e029ec70463dd6e433b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 19 Feb 2023 14:18:45 +0100 Subject: [PATCH 092/144] Add Editor widget --- src/widgets.rs | 2 + src/widgets/editor.rs | 512 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 514 insertions(+) create mode 100644 src/widgets/editor.rs diff --git a/src/widgets.rs b/src/widgets.rs index 608f50e..a36f231 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,6 +1,7 @@ mod background; mod border; mod cursor; +mod editor; mod either; mod empty; mod float; @@ -13,6 +14,7 @@ mod text; pub use background::*; pub use border::*; pub use cursor::*; +pub use editor::*; pub use either::*; pub use empty::*; pub use float::*; diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs new file mode 100644 index 0000000..e433e88 --- /dev/null +++ b/src/widgets/editor.rs @@ -0,0 +1,512 @@ +use std::iter; + +use async_trait::async_trait; +use crossterm::style::Stylize; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{AsyncWidget, 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 // +/////////// + +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, + + /// Width of the text when the editor was last rendered. + /// + /// Does not include additional column for cursor. + last_width: u16, +} + +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_width: u16::MAX, + 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 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 // +//////////// + +pub struct Editor<'a> { + state: &'a mut EditorState, + highlighted: Styled, + hidden: Option, + focus: bool, +} + +impl Editor<'_> { + pub fn state(&mut self) -> &mut EditorState { + self.state + } + + pub fn highlight(mut self, highlight: F) -> Self + where + F: FnOnce(&str) -> Styled, + { + self.highlighted = highlight(&self.state.text); + assert_eq!(self.state.text, self.highlighted.text()); + self + } + + pub fn focus(mut self, active: bool) -> Self { + self.focus = active; + self + } + + pub fn hidden(self) -> Self { + self.hidden_with_placeholder(("", Style::new().grey().italic())) + } + + pub fn hidden_with_placeholder>(mut self, placeholder: S) -> Self { + self.hidden = Some(placeholder.into()); + 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) + } + + pub fn cursor_row(&self, widthdb: &mut WidthDb) -> usize { + let width = self.state.last_width; + let text_width = (width - 1) as usize; + let indices = wrap(widthdb, &self.state.text, text_width); + let (row, _) = Self::wrapped_cursor(self.state.cursor_idx, &indices); + row + } + + 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); + wrap(widthdb, self.state.text(), max_width) + } + + fn rows(&self, indices: &[usize]) -> Vec { + let text = self.hidden.as_ref().unwrap_or(&self.highlighted); + text.clone().split_at_indices(indices) + } + + fn size(widthdb: &mut WidthDb, rows: &[Styled]) -> Size { + 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); + Size::new(width, height) + } + + fn cursor( + &self, + widthdb: &mut WidthDb, + width: u16, + indices: &[usize], + rows: &[Styled], + ) -> Option { + if !self.focus { + return None; + } + + if self.hidden.is_some() { + return Some(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); + Some(Pos::new(cursor_row, cursor_col)) + } + + fn draw(frame: &mut Frame, rows: Vec, cursor: Option) { + for (i, row) in rows.into_iter().enumerate() { + frame.write(Pos::new(0, i as i32), row); + } + + if let Some(cursor) = cursor { + frame.set_cursor(Some(cursor)); + } + } +} + +impl Widget for Editor<'_> { + fn size( + &self, + frame: &mut Frame, + max_width: Option, + _max_height: Option, + ) -> Result { + let widthdb = frame.widthdb(); + let indices = self.indices(widthdb, max_width); + let rows = self.rows(&indices); + Ok(Self::size(widthdb, &rows)) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let widthdb = frame.widthdb(); + let indices = self.indices(widthdb, Some(size.width)); + let rows = self.rows(&indices); + let cursor = self.cursor(widthdb, size.width, &indices, &rows); + Self::draw(frame, rows, cursor); + Ok(()) + } +} + +#[allow(single_use_lifetimes)] +#[async_trait] +impl AsyncWidget for Editor<'_> { + async fn size( + &self, + frame: &mut Frame, + max_width: Option, + _max_height: Option, + ) -> Result { + let widthdb = frame.widthdb(); + let indices = self.indices(widthdb, max_width); + let rows = self.rows(&indices); + Ok(Self::size(widthdb, &rows)) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + let size = frame.size(); + let widthdb = frame.widthdb(); + let indices = self.indices(widthdb, Some(size.width)); + let rows = self.rows(&indices); + let cursor = self.cursor(widthdb, size.width, &indices, &rows); + Self::draw(frame, rows, cursor); + Ok(()) + } +} From cb483431cc849ea7dd86ec3e94a586fada6243b8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 19 Feb 2023 14:24:28 +0100 Subject: [PATCH 093/144] Make widget submodules public This will make the generated documentation more readable once I add module docstrings. I'm still reexporting the module contents to keep imports nice and tidy. --- src/widgets.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/widgets.rs b/src/widgets.rs index a36f231..dc80eb7 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,15 +1,15 @@ -mod background; -mod border; -mod cursor; -mod editor; -mod either; -mod empty; -mod float; -mod join; -mod layer; -mod padding; -mod resize; -mod text; +pub mod background; +pub mod border; +pub mod cursor; +pub mod editor; +pub mod either; +pub mod empty; +pub mod float; +pub mod join; +pub mod layer; +pub mod padding; +pub mod resize; +pub mod text; pub use background::*; pub use border::*; From 607c11fea4113dd02528ca5ef4e5ff628883a492 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 20 Feb 2023 15:53:49 +0100 Subject: [PATCH 094/144] Use new with_* naming scheme All builder-like functions are now named with_*. There is also now a way to set each property imperatively with only a mutable reference. The only widgets I haven't yet converted to this style are the Join* widgets; they're a bit harder to figure out an appropriate API for. --- examples/hello_world_widgets.rs | 10 ++--- src/widgets/background.rs | 6 +-- src/widgets/border.rs | 10 ++--- src/widgets/cursor.rs | 18 ++++----- src/widgets/editor.rs | 36 +++++++++++++----- src/widgets/empty.rs | 8 ++-- src/widgets/float.rs | 67 +++++++++++++++++++++------------ src/widgets/layer.rs | 4 +- src/widgets/padding.rs | 30 +++++++-------- src/widgets/resize.rs | 18 ++++----- src/widgets/text.rs | 8 ++-- 11 files changed, 125 insertions(+), 90 deletions(-) diff --git a/examples/hello_world_widgets.rs b/examples/hello_world_widgets.rs index c80f10d..000cf91 100644 --- a/examples/hello_world_widgets.rs +++ b/examples/hello_world_widgets.rs @@ -11,14 +11,14 @@ fn widget() -> impl Widget { .then("Press any key to exit", Style::new().on_dark_blue()); Text::new(styled) .padding() - .horizontal(1) + .with_horizontal(1) .border() - .look(BorderLook::LINE_DOUBLE) - .style(Style::new().dark_red()) + .with_look(BorderLook::LINE_DOUBLE) + .with_style(Style::new().dark_red()) .background() - .style(Style::new().on_yellow().opaque()) + .with_style(Style::new().on_yellow().opaque()) .float() - .all(0.5) + .with_all(0.5) } fn render_frame(term: &mut Terminal) { diff --git a/src/widgets/background.rs b/src/widgets/background.rs index edd1fdf..d72add6 100644 --- a/src/widgets/background.rs +++ b/src/widgets/background.rs @@ -4,8 +4,8 @@ use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget}; #[derive(Debug, Clone, Copy)] pub struct Background { - inner: I, - style: Style, + pub inner: I, + pub style: Style, } impl Background { @@ -16,7 +16,7 @@ impl Background { } } - pub fn style(mut self, style: Style) -> Self { + pub fn with_style(mut self, style: Style) -> Self { self.style = style; self } diff --git a/src/widgets/border.rs b/src/widgets/border.rs index 588f738..3ee2b4a 100644 --- a/src/widgets/border.rs +++ b/src/widgets/border.rs @@ -88,9 +88,9 @@ impl Default for BorderLook { #[derive(Debug, Clone, Copy)] pub struct Border { - inner: I, - look: BorderLook, - style: Style, + pub inner: I, + pub look: BorderLook, + pub style: Style, } impl Border { @@ -102,12 +102,12 @@ impl Border { } } - pub fn look(mut self, look: BorderLook) -> Self { + pub fn with_look(mut self, look: BorderLook) -> Self { self.look = look; self } - pub fn style(mut self, style: Style) -> Self { + pub fn with_style(mut self, style: Style) -> Self { self.style = style; self } diff --git a/src/widgets/cursor.rs b/src/widgets/cursor.rs index 5f79ed8..47dca56 100644 --- a/src/widgets/cursor.rs +++ b/src/widgets/cursor.rs @@ -4,25 +4,25 @@ use crate::{AsyncWidget, Frame, Pos, Size, Widget}; #[derive(Debug, Clone, Copy)] pub struct Cursor { - inner: I, - at: Pos, + pub inner: I, + pub position: Pos, } impl Cursor { pub fn new(inner: I) -> Self { Self { inner, - at: Pos::ZERO, + position: Pos::ZERO, } } - pub fn at(mut self, pos: Pos) -> Self { - self.at = pos; + pub fn with_position(mut self, position: Pos) -> Self { + self.position = position; self } - pub fn at_xy(self, x: i32, y: i32) -> Self { - self.at(Pos::new(x, y)) + pub fn with_position_xy(self, x: i32, y: i32) -> Self { + self.with_position(Pos::new(x, y)) } } @@ -41,7 +41,7 @@ where fn draw(self, frame: &mut Frame) -> Result<(), E> { self.inner.draw(frame)?; - frame.show_cursor(self.at); + frame.show_cursor(self.position); Ok(()) } } @@ -62,7 +62,7 @@ where async fn draw(self, frame: &mut Frame) -> Result<(), E> { self.inner.draw(frame).await?; - frame.show_cursor(self.at); + frame.show_cursor(self.position); Ok(()) } } diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index e433e88..e587a0a 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -340,8 +340,8 @@ impl Default for EditorState { pub struct Editor<'a> { state: &'a mut EditorState, highlighted: Styled, - hidden: Option, - focus: bool, + pub hidden: Option, + pub focus: bool, } impl Editor<'_> { @@ -349,29 +349,45 @@ impl Editor<'_> { self.state } - pub fn highlight(mut self, highlight: F) -> Self + 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 focus(mut self, active: bool) -> Self { - self.focus = active; + pub fn with_visible(mut self) -> Self { + self.hidden = None; self } - pub fn hidden(self) -> Self { - self.hidden_with_placeholder(("", Style::new().grey().italic())) - } - - pub fn hidden_with_placeholder>(mut self, placeholder: S) -> 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; diff --git a/src/widgets/empty.rs b/src/widgets/empty.rs index 86881e2..fe6cfad 100644 --- a/src/widgets/empty.rs +++ b/src/widgets/empty.rs @@ -4,7 +4,7 @@ use crate::{AsyncWidget, Frame, Size, Widget}; #[derive(Debug, Default, Clone, Copy)] pub struct Empty { - size: Size, + pub size: Size, } impl Empty { @@ -12,17 +12,17 @@ impl Empty { Self { size: Size::ZERO } } - pub fn width(mut self, width: u16) -> Self { + pub fn with_width(mut self, width: u16) -> Self { self.size.width = width; self } - pub fn height(mut self, height: u16) -> Self { + pub fn with_height(mut self, height: u16) -> Self { self.size.height = height; self } - pub fn size(mut self, size: Size) -> Self { + pub fn with_size(mut self, size: Size) -> Self { self.size = size; self } diff --git a/src/widgets/float.rs b/src/widgets/float.rs index d3eff4b..b22c108 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -4,7 +4,7 @@ use crate::{AsyncWidget, Frame, Pos, Size, Widget}; #[derive(Debug, Clone, Copy)] pub struct Float { - inner: I, + pub inner: I, horizontal: Option, vertical: Option, } @@ -18,49 +18,68 @@ impl Float { } } - pub fn horizontal(mut self, position: f32) -> Self { - assert!((0.0..=1.0).contains(&position)); - self.horizontal = Some(position); + 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 vertical(mut self, position: f32) -> Self { - assert!((0.0..=1.0).contains(&position)); - self.vertical = Some(position); + pub fn with_vertical(mut self, position: f32) -> Self { + self.set_vertical(Some(position)); self } - pub fn all(self, position: f32) -> Self { - assert!((0.0..=1.0).contains(&position)); - self.horizontal(position).vertical(position) + pub fn with_all(self, position: f32) -> Self { + self.with_horizontal(position).with_vertical(position) } - pub fn left(self) -> Self { - self.horizontal(0.0) + pub fn with_left(self) -> Self { + self.with_horizontal(0.0) } - pub fn right(self) -> Self { - self.horizontal(1.0) + pub fn with_right(self) -> Self { + self.with_horizontal(1.0) } - pub fn top(self) -> Self { - self.vertical(0.0) + pub fn with_top(self) -> Self { + self.with_vertical(0.0) } - pub fn bottom(self) -> Self { - self.vertical(1.0) + pub fn with_bottom(self) -> Self { + self.with_vertical(1.0) } - pub fn center_h(self) -> Self { - self.horizontal(0.5) + pub fn with_center_h(self) -> Self { + self.with_horizontal(0.5) } - pub fn center_v(self) -> Self { - self.vertical(0.5) + pub fn with_center_v(self) -> Self { + self.with_vertical(0.5) } - pub fn center(self) -> Self { - self.all(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) { diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs index 289f854..f8da993 100644 --- a/src/widgets/layer.rs +++ b/src/widgets/layer.rs @@ -4,8 +4,8 @@ use crate::{AsyncWidget, Frame, Size, Widget}; #[derive(Debug, Clone, Copy)] pub struct Layer { - below: I1, - above: I2, + pub below: I1, + pub above: I2, } impl Layer { diff --git a/src/widgets/padding.rs b/src/widgets/padding.rs index a8f30b0..656aec8 100644 --- a/src/widgets/padding.rs +++ b/src/widgets/padding.rs @@ -4,11 +4,11 @@ use crate::{AsyncWidget, Frame, Pos, Size, Widget}; #[derive(Debug, Clone, Copy)] pub struct Padding { - inner: I, - left: u16, - right: u16, - top: u16, - bottom: u16, + pub inner: I, + pub left: u16, + pub right: u16, + pub top: u16, + pub bottom: u16, } impl Padding { @@ -22,36 +22,36 @@ impl Padding { } } - pub fn left(mut self, amount: u16) -> Self { + pub fn with_left(mut self, amount: u16) -> Self { self.left = amount; self } - pub fn right(mut self, amount: u16) -> Self { + pub fn with_right(mut self, amount: u16) -> Self { self.right = amount; self } - pub fn top(mut self, amount: u16) -> Self { + pub fn with_top(mut self, amount: u16) -> Self { self.top = amount; self } - pub fn bottom(mut self, amount: u16) -> Self { + pub fn with_bottom(mut self, amount: u16) -> Self { self.bottom = amount; self } - pub fn horizontal(self, amount: u16) -> Self { - self.left(amount).right(amount) + pub fn with_horizontal(self, amount: u16) -> Self { + self.with_left(amount).with_right(amount) } - pub fn vertical(self, amount: u16) -> Self { - self.top(amount).bottom(amount) + pub fn with_vertical(self, amount: u16) -> Self { + self.with_top(amount).with_bottom(amount) } - pub fn all(self, amount: u16) -> Self { - self.horizontal(amount).vertical(amount) + pub fn with_all(self, amount: u16) -> Self { + self.with_horizontal(amount).with_vertical(amount) } fn pad_size(&self) -> Size { diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index bf80801..5adad8a 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -3,11 +3,11 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget}; pub struct Resize { - inner: I, - min_width: Option, - min_height: Option, - max_width: Option, - max_height: Option, + pub inner: I, + pub min_width: Option, + pub min_height: Option, + pub max_width: Option, + pub max_height: Option, } impl Resize { @@ -21,22 +21,22 @@ impl Resize { } } - pub fn min_width(mut self, width: u16) -> Self { + pub fn with_min_width(mut self, width: u16) -> Self { self.min_width = Some(width); self } - pub fn min_height(mut self, height: u16) -> Self { + pub fn with_min_height(mut self, height: u16) -> Self { self.min_height = Some(height); self } - pub fn max_width(mut self, width: u16) -> Self { + pub fn with_max_width(mut self, width: u16) -> Self { self.max_width = Some(width); self } - pub fn max_height(mut self, height: u16) -> Self { + pub fn with_max_height(mut self, height: u16) -> Self { self.max_height = Some(height); self } diff --git a/src/widgets/text.rs b/src/widgets/text.rs index 3c2ceee..5acdb8e 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -4,8 +4,8 @@ use crate::{AsyncWidget, Frame, Pos, Size, Styled, Widget, WidthDb}; #[derive(Debug, Clone)] pub struct Text { - styled: Styled, - wrap: bool, + pub styled: Styled, + pub wrap: bool, } impl Text { @@ -16,8 +16,8 @@ impl Text { } } - pub fn wrap(mut self, wrap: bool) -> Self { - self.wrap = wrap; + pub fn with_wrap(mut self, active: bool) -> Self { + self.wrap = active; self } From 417f33cc24ec903692f0ffbadac27a6c1c02a90d Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 20 Feb 2023 16:14:24 +0100 Subject: [PATCH 095/144] Differentiate between growing and shrinking segments Also uses the new with_* naming scheme for segments --- src/widgets/join.rs | 111 +++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 43 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 6e56d35..fad2549 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -55,7 +55,8 @@ use super::{Either2, Either3, Either4, Either5, Either6, Either7}; struct Segment { size: u16, weight: f32, - fixed: bool, + growing: bool, + shrinking: bool, } impl Segment { @@ -63,7 +64,8 @@ impl Segment { Self { size: size.width, weight: segment.weight, - fixed: segment.fixed, + growing: segment.growing, + shrinking: segment.shrinking, } } @@ -71,7 +73,8 @@ impl Segment { Self { size: size.height, weight: segment.weight, - fixed: segment.fixed, + growing: segment.growing, + shrinking: segment.shrinking, } } } @@ -88,31 +91,26 @@ fn total_weight(segments: &[&mut Segment]) -> f32 { segments.iter().map(|s| s.weight).sum() } -fn balance(segments: &mut [Segment], mut available: u16) { - let mut borrowed_segments = segments.iter_mut().collect::>(); - - // Remove fixed segments - borrowed_segments.retain(|s| { - if !s.fixed { - return true; - } - available = available.saturating_sub(s.size); - false - }); - - if borrowed_segments.is_empty() || available == 0 { - return; - } - - match total_size(&borrowed_segments).cmp(&available) { - Ordering::Less => grow(borrowed_segments, available), - Ordering::Greater => shrink(borrowed_segments, available), +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)); + 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.size); + false + }); // Repeatedly remove all segments that do not need to grow, i. e. that are // at least as large as their allotment. @@ -139,18 +137,17 @@ fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { }); available -= removed; - // If all segments were at least as large as their allotments, we would - // be trying to shrink, not grow them. Hence, there must be at least one - // segment that is smaller than its allotment. - assert!(!segments.is_empty()); - if removed == 0 { break; // All remaining segments are smaller than their allotments } } - // Size each remaining segment according to its allotment. 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; @@ -170,7 +167,16 @@ fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { } fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { - assert!(available < total_size(&segments)); + 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.size); + false + }); // Repeatedly remove all segments that do not need to shrink, i. e. that are // at least as small as their allotment. @@ -203,18 +209,17 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { }); available -= removed; - // If all segments were smaller or the same size as their allotments, we - // would be trying to grow, not shrink them. Hence, there must be at - // least one segment bigger than its allotment. - assert!(!segments.is_empty()); - if removed == 0 { break; // All segments want more than their weight allows. } } - // Size each remaining segment according to its allotment. 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; @@ -234,9 +239,10 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { } pub struct JoinSegment { - inner: I, + pub inner: I, weight: f32, - fixed: bool, + pub growing: bool, + pub shrinking: bool, } impl JoinSegment { @@ -244,20 +250,38 @@ impl JoinSegment { Self { inner, weight: 1.0, - fixed: false, + growing: true, + shrinking: true, } } - pub fn weight(mut self, weight: f32) -> Self { + 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 fixed(mut self, fixed: bool) -> Self { - self.fixed = fixed; + 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) + } } pub struct JoinH { @@ -556,7 +580,8 @@ macro_rules! mk_join { JoinSegment { inner: $either::$constr($arg.inner), weight: $arg.weight, - fixed: $arg.fixed, + growing: $arg.growing, + shrinking: $arg.shrinking, }, )+ ])) } From 0573fcec779e090a2a18cdceb93ecc4372cd00dd Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 20 Feb 2023 16:59:17 +0100 Subject: [PATCH 096/144] Only provide WidthDb in [Async]Widget::size --- src/widget.rs | 6 ++-- src/widgets/background.rs | 10 +++---- src/widgets/border.rs | 10 +++---- src/widgets/cursor.rs | 10 +++---- src/widgets/editor.rs | 6 ++-- src/widgets/either.rs | 10 +++---- src/widgets/empty.rs | 6 ++-- src/widgets/float.rs | 14 ++++----- src/widgets/join.rs | 60 +++++++++++++++++++++++---------------- src/widgets/layer.rs | 14 ++++----- src/widgets/padding.rs | 10 +++---- src/widgets/resize.rs | 10 +++---- src/widgets/text.rs | 8 +++--- 13 files changed, 91 insertions(+), 83 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index 5d3faa4..dd4859b 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -3,14 +3,14 @@ use async_trait::async_trait; use crate::widgets::{ Background, Border, Either2, Either3, Float, JoinSegment, Layer, Padding, Resize, }; -use crate::{Frame, Size}; +use crate::{Frame, Size, WidthDb}; // TODO Feature-gate these traits pub trait Widget { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result; @@ -22,7 +22,7 @@ pub trait Widget { pub trait AsyncWidget { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result; diff --git a/src/widgets/background.rs b/src/widgets/background.rs index d72add6..d0ba530 100644 --- a/src/widgets/background.rs +++ b/src/widgets/background.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget, WidthDb}; #[derive(Debug, Clone, Copy)] pub struct Background { @@ -37,11 +37,11 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.inner.size(frame, max_width, max_height) + self.inner.size(widthdb, max_width, max_height) } fn draw(self, frame: &mut Frame) -> Result<(), E> { @@ -57,11 +57,11 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.inner.size(frame, max_width, max_height).await + self.inner.size(widthdb, max_width, max_height).await } async fn draw(self, frame: &mut Frame) -> Result<(), E> { diff --git a/src/widgets/border.rs b/src/widgets/border.rs index 3ee2b4a..062cd8f 100644 --- a/src/widgets/border.rs +++ b/src/widgets/border.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget, WidthDb}; #[derive(Debug, Clone, Copy)] pub struct BorderLook { @@ -151,13 +151,13 @@ where { fn size( &self, - frame: &mut Frame, + 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(frame, max_width, max_height)?; + let size = self.inner.size(widthdb, max_width, max_height)?; Ok(size + Size::new(2, 2)) } @@ -179,13 +179,13 @@ where { async fn size( &self, - frame: &mut Frame, + 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(frame, max_width, max_height).await?; + let size = self.inner.size(widthdb, max_width, max_height).await?; Ok(size + Size::new(2, 2)) } diff --git a/src/widgets/cursor.rs b/src/widgets/cursor.rs index 47dca56..2bb8199 100644 --- a/src/widgets/cursor.rs +++ b/src/widgets/cursor.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; #[derive(Debug, Clone, Copy)] pub struct Cursor { @@ -32,11 +32,11 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.inner.size(frame, max_width, max_height) + self.inner.size(widthdb, max_width, max_height) } fn draw(self, frame: &mut Frame) -> Result<(), E> { @@ -53,11 +53,11 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.inner.size(frame, max_width, max_height).await + self.inner.size(widthdb, max_width, max_height).await } async fn draw(self, frame: &mut Frame) -> Result<(), E> { diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index e587a0a..ff95329 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -480,11 +480,10 @@ impl Editor<'_> { impl Widget for Editor<'_> { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, _max_height: Option, ) -> Result { - let widthdb = frame.widthdb(); let indices = self.indices(widthdb, max_width); let rows = self.rows(&indices); Ok(Self::size(widthdb, &rows)) @@ -506,11 +505,10 @@ impl Widget for Editor<'_> { impl AsyncWidget for Editor<'_> { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, _max_height: Option, ) -> Result { - let widthdb = frame.widthdb(); let indices = self.indices(widthdb, max_width); let rows = self.rows(&indices); Ok(Self::size(widthdb, &rows)) diff --git a/src/widgets/either.rs b/src/widgets/either.rs index ea74da4..cb9a55d 100644 --- a/src/widgets/either.rs +++ b/src/widgets/either.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Size, Widget}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; macro_rules! mk_either { ( @@ -19,12 +19,12 @@ macro_rules! mk_either { { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { match self { - $( Self::$constr(w) => w.size(frame, max_width, max_height), )+ + $( Self::$constr(w) => w.size(widthdb, max_width, max_height), )+ } } @@ -42,12 +42,12 @@ macro_rules! mk_either { { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { match self { - $( Self::$constr(w) => w.size(frame, max_width, max_height).await, )+ + $( Self::$constr(w) => w.size(widthdb, max_width, max_height).await, )+ } } diff --git a/src/widgets/empty.rs b/src/widgets/empty.rs index fe6cfad..033ab70 100644 --- a/src/widgets/empty.rs +++ b/src/widgets/empty.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Size, Widget}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; #[derive(Debug, Default, Clone, Copy)] pub struct Empty { @@ -31,7 +31,7 @@ impl Empty { impl Widget for Empty { fn size( &self, - _frame: &mut Frame, + _widthdb: &mut WidthDb, _max_width: Option, _max_height: Option, ) -> Result { @@ -47,7 +47,7 @@ impl Widget for Empty { impl AsyncWidget for Empty { async fn size( &self, - _frame: &mut Frame, + _widthdb: &mut WidthDb, _max_width: Option, _max_height: Option, ) -> Result { diff --git a/src/widgets/float.rs b/src/widgets/float.rs index b22c108..8380538 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; #[derive(Debug, Clone, Copy)] pub struct Float { @@ -115,18 +115,18 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.inner.size(frame, max_width, max_height) + 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, Some(size.width), Some(size.height))?; + .size(frame.widthdb(), Some(size.width), Some(size.height))?; self.push_inner(frame, size, inner_size); self.inner.draw(frame)?; @@ -143,18 +143,18 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.inner.size(frame, max_width, max_height).await + 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, Some(size.width), Some(size.height)) + .size(frame.widthdb(), Some(size.width), Some(size.height)) .await?; self.push_inner(frame, size, inner_size); diff --git a/src/widgets/join.rs b/src/widgets/join.rs index fad2549..0778700 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; use super::{Either2, Either3, Either4, Either5, Either6, Either7}; @@ -300,14 +300,14 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { if let Some(max_width) = max_width { let mut balanced_segments = vec![]; for segment in &self.segments { - let size = segment.inner.size(frame, Some(max_width), max_height)?; + let size = segment.inner.size(widthdb, Some(max_width), max_height)?; balanced_segments.push(Segment::horizontal(size, segment)); } balance(&mut balanced_segments, max_width); @@ -315,7 +315,9 @@ where let mut width = 0_u16; let mut height = 0_u16; for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { - let size = segment.inner.size(frame, Some(balanced.size), max_height)?; + let size = segment + .inner + .size(widthdb, Some(balanced.size), max_height)?; width = width.saturating_add(size.width); height = height.max(size.height); } @@ -324,7 +326,7 @@ where let mut width = 0_u16; let mut height = 0_u16; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height)?; + let size = segment.inner.size(widthdb, max_width, max_height)?; width = width.saturating_add(size.width); height = height.max(size.height); } @@ -339,7 +341,7 @@ where let mut balanced_segments = vec![]; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height)?; + let size = segment.inner.size(frame.widthdb(), max_width, max_height)?; balanced_segments.push(Segment::horizontal(size, segment)); } balance(&mut balanced_segments, size.width); @@ -363,7 +365,7 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { @@ -372,7 +374,7 @@ where for segment in &self.segments { let size = segment .inner - .size(frame, Some(max_width), max_height) + .size(widthdb, Some(max_width), max_height) .await?; balanced_segments.push(Segment::horizontal(size, segment)); } @@ -383,7 +385,7 @@ where for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { let size = segment .inner - .size(frame, Some(balanced.size), max_height) + .size(widthdb, Some(balanced.size), max_height) .await?; width = width.saturating_add(size.width); height = height.max(size.height); @@ -393,7 +395,7 @@ where let mut width = 0_u16; let mut height = 0_u16; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height).await?; + let size = segment.inner.size(widthdb, max_width, max_height).await?; width = width.saturating_add(size.width); height = height.max(size.height); } @@ -408,7 +410,10 @@ where let mut balanced_segments = vec![]; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height).await?; + let size = segment + .inner + .size(frame.widthdb(), max_width, max_height) + .await?; balanced_segments.push(Segment::horizontal(size, segment)); } balance(&mut balanced_segments, size.width); @@ -441,14 +446,14 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { if let Some(max_height) = max_height { let mut balanced_segments = vec![]; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, Some(max_height))?; + let size = segment.inner.size(widthdb, max_width, Some(max_height))?; balanced_segments.push(Segment::vertical(size, segment)); } balance(&mut balanced_segments, max_height); @@ -456,7 +461,9 @@ where let mut width = 0_u16; let mut height = 0_u16; for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { - let size = segment.inner.size(frame, max_width, Some(balanced.size))?; + let size = segment + .inner + .size(widthdb, max_width, Some(balanced.size))?; width = width.max(size.width); height = height.saturating_add(size.height); } @@ -465,7 +472,7 @@ where let mut width = 0_u16; let mut height = 0_u16; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height)?; + let size = segment.inner.size(widthdb, max_width, max_height)?; width = width.max(size.width); height = height.saturating_add(size.height); } @@ -480,7 +487,7 @@ where let mut balanced_segments = vec![]; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height)?; + let size = segment.inner.size(frame.widthdb(), max_width, max_height)?; balanced_segments.push(Segment::vertical(size, segment)); } balance(&mut balanced_segments, size.height); @@ -504,7 +511,7 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { @@ -513,7 +520,7 @@ where for segment in &self.segments { let size = segment .inner - .size(frame, max_width, Some(max_height)) + .size(widthdb, max_width, Some(max_height)) .await?; balanced_segments.push(Segment::vertical(size, segment)); } @@ -524,7 +531,7 @@ where for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { let size = segment .inner - .size(frame, max_width, Some(balanced.size)) + .size(widthdb, max_width, Some(balanced.size)) .await?; width = width.max(size.width); height = height.saturating_add(size.height); @@ -534,7 +541,7 @@ where let mut width = 0_u16; let mut height = 0_u16; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height).await?; + let size = segment.inner.size(widthdb, max_width, max_height).await?; width = width.max(size.width); height = height.saturating_add(size.height); } @@ -549,7 +556,10 @@ where let mut balanced_segments = vec![]; for segment in &self.segments { - let size = segment.inner.size(frame, max_width, max_height).await?; + let size = segment + .inner + .size(frame.widthdb(), max_width, max_height) + .await?; balanced_segments.push(Segment::vertical(size, segment)); } balance(&mut balanced_segments, size.height); @@ -593,11 +603,11 @@ macro_rules! mk_join { { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.0.size(frame, max_width, max_height) + self.0.size(widthdb, max_width, max_height) } fn draw(self, frame: &mut Frame) -> Result<(), E> { @@ -612,11 +622,11 @@ macro_rules! mk_join { { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - self.0.size(frame, max_width, max_height).await + self.0.size(widthdb, max_width, max_height).await } async fn draw(self, frame: &mut Frame) -> Result<(), E> { diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs index f8da993..366f2b5 100644 --- a/src/widgets/layer.rs +++ b/src/widgets/layer.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Size, Widget}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; #[derive(Debug, Clone, Copy)] pub struct Layer { @@ -25,12 +25,12 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - let bottom = self.below.size(frame, max_width, max_height)?; - let top = self.above.size(frame, max_width, max_height)?; + let bottom = self.below.size(widthdb, max_width, max_height)?; + let top = self.above.size(widthdb, max_width, max_height)?; Ok(Self::size(bottom, top)) } @@ -49,12 +49,12 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - let bottom = self.below.size(frame, max_width, max_height).await?; - let top = self.above.size(frame, max_width, max_height).await?; + let bottom = self.below.size(widthdb, max_width, max_height).await?; + let top = self.above.size(widthdb, max_width, max_height).await?; Ok(Self::size(bottom, top)) } diff --git a/src/widgets/padding.rs b/src/widgets/padding.rs index 656aec8..473ad02 100644 --- a/src/widgets/padding.rs +++ b/src/widgets/padding.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Pos, Size, Widget}; +use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; #[derive(Debug, Clone, Copy)] pub struct Padding { @@ -72,14 +72,14 @@ where { fn size( &self, - frame: &mut Frame, + 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(frame, max_width, max_height)?; + let size = self.inner.size(widthdb, max_width, max_height)?; Ok(size + pad_size) } @@ -98,14 +98,14 @@ where { async fn size( &self, - frame: &mut Frame, + 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(frame, max_width, max_height).await?; + let size = self.inner.size(widthdb, max_width, max_height).await?; Ok(size + pad_size) } diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 5adad8a..91bb84a 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{AsyncWidget, Frame, Size, Widget}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; pub struct Resize { pub inner: I, @@ -69,11 +69,11 @@ where { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - let size = self.inner.size(frame, max_width, max_height)?; + let size = self.inner.size(widthdb, max_width, max_height)?; Ok(self.resize(size)) } @@ -89,11 +89,11 @@ where { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, max_height: Option, ) -> Result { - let size = self.inner.size(frame, max_width, max_height).await?; + let size = self.inner.size(widthdb, max_width, max_height).await?; Ok(self.resize(size)) } diff --git a/src/widgets/text.rs b/src/widgets/text.rs index 5acdb8e..3755c8c 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -62,11 +62,11 @@ impl Text { impl Widget for Text { fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, _max_height: Option, ) -> Result { - Ok(self.size(frame.widthdb(), max_width)) + Ok(self.size(widthdb, max_width)) } fn draw(self, frame: &mut Frame) -> Result<(), E> { @@ -79,11 +79,11 @@ impl Widget for Text { impl AsyncWidget for Text { async fn size( &self, - frame: &mut Frame, + widthdb: &mut WidthDb, max_width: Option, _max_height: Option, ) -> Result { - Ok(self.size(frame.widthdb(), max_width)) + Ok(self.size(widthdb, max_width)) } async fn draw(self, frame: &mut Frame) -> Result<(), E> { From 542ea7bc66f51e70ab6669ef95c2f9bd87203fb0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 26 Feb 2023 14:41:51 +0100 Subject: [PATCH 097/144] Simplify Join* implementation --- src/widgets/join.rs | 717 +++++++++++++++++++++----------------------- 1 file changed, 342 insertions(+), 375 deletions(-) diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 0778700..900cd19 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -4,8 +4,6 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb}; -use super::{Either2, Either3, Either4, Either5, Either6, Either7}; - // The following algorithm has three goals, listed in order of importance: // // 1. Use the available space @@ -53,25 +51,18 @@ use super::{Either2, Either3, Either4, Either5, Either6, Either7}; #[derive(Debug)] struct Segment { - size: u16, + major: u16, + minor: u16, weight: f32, growing: bool, shrinking: bool, } impl Segment { - fn horizontal(size: Size, segment: &JoinSegment) -> Self { + fn new(major_minor: (u16, u16), segment: &JoinSegment) -> Self { Self { - size: size.width, - weight: segment.weight, - growing: segment.growing, - shrinking: segment.shrinking, - } - } - - fn vertical(size: Size, segment: &JoinSegment) -> Self { - Self { - size: size.height, + major: major_minor.0, + minor: major_minor.1, weight: segment.weight, growing: segment.growing, shrinking: segment.shrinking, @@ -82,7 +73,7 @@ impl Segment { fn total_size(segments: &[&mut Segment]) -> u16 { let mut total = 0_u16; for segment in segments { - total = total.saturating_add(segment.size); + total = total.saturating_add(segment.major); } total } @@ -108,7 +99,7 @@ fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { if s.growing { return true; } - available = available.saturating_sub(s.size); + available = available.saturating_sub(s.major); false }); @@ -129,10 +120,10 @@ fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { let mut removed = 0; segments.retain(|s| { let allotment = s.weight / total_weight * available as f32; - if (s.size as f32) < allotment { + if (s.major as f32) < allotment { return true; // May need to grow } - removed += s.size; + removed += s.major; false }); available -= removed; @@ -151,8 +142,8 @@ fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { let mut used = 0; for segment in &mut segments { let allotment = segment.weight / total_weight * available as f32; - segment.size = allotment.floor() as u16; - used += segment.size; + segment.major = allotment.floor() as u16; + used += segment.major; } // Distribute remaining unused space from left to right. @@ -162,7 +153,7 @@ fn grow(mut segments: Vec<&mut Segment>, mut available: u16) { let remaining = available - used; assert!(remaining as usize <= segments.len()); for segment in segments.into_iter().take(remaining.into()) { - segment.size += 1; + segment.major += 1; } } @@ -174,7 +165,7 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { if s.shrinking { return true; } - available = available.saturating_sub(s.size); + available = available.saturating_sub(s.major); false }); @@ -195,16 +186,16 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { let mut removed = 0; segments.retain(|s| { let allotment = s.weight / total_weight * available as f32; - if (s.size as f32) > allotment { + 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.size <= available); + assert!(s.major <= available); - removed += s.size; + removed += s.major; false }); available -= removed; @@ -223,8 +214,8 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { let mut used = 0; for segment in &mut segments { let allotment = segment.weight / total_weight * available as f32; - segment.size = allotment.floor() as u16; - used += segment.size; + segment.major = allotment.floor() as u16; + used += segment.major; } // Distribute remaining unused space from left to right. @@ -234,7 +225,7 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { let remaining = available - used; assert!(remaining as usize <= segments.len()); for segment in segments.into_iter().take(remaining.into()) { - segment.size += 1; + segment.major += 1; } } @@ -284,17 +275,106 @@ impl JoinSegment { } } -pub struct JoinH { - segments: Vec>, -} - -impl JoinH { - pub fn new(segments: Vec>) -> Self { - Self { segments } +fn to_mm(horizontal: bool, w: T, h: T) -> (T, T) { + if horizontal { + (w, h) + } else { + (h, w) } } -impl Widget for JoinH +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) +} + +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, { @@ -304,54 +384,51 @@ where max_width: Option, max_height: Option, ) -> Result { - if let Some(max_width) = max_width { - let mut balanced_segments = vec![]; - for segment in &self.segments { - let size = segment.inner.size(widthdb, Some(max_width), max_height)?; - balanced_segments.push(Segment::horizontal(size, segment)); - } - balance(&mut balanced_segments, max_width); + let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height); - let mut width = 0_u16; - let mut height = 0_u16; - for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { - let size = segment - .inner - .size(widthdb, Some(balanced.size), max_height)?; - width = width.saturating_add(size.width); - height = height.max(size.height); - } - Ok(Size::new(width, height)) - } else { - let mut width = 0_u16; - let mut height = 0_u16; - for segment in &self.segments { - let size = segment.inner.size(widthdb, max_width, max_height)?; - width = width.saturating_add(size.width); - height = height.max(size.height); - } - Ok(Size::new(width, 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 size = frame.size(); - let max_width = Some(size.width); - let max_height = Some(size.height); + let frame_size = frame.size(); + let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height); - let mut balanced_segments = vec![]; + let widthdb = frame.widthdb(); + let mut segments = Vec::with_capacity(self.segments.len()); for segment in &self.segments { - let size = segment.inner.size(frame.widthdb(), max_width, max_height)?; - balanced_segments.push(Segment::horizontal(size, segment)); + let major_minor = size(self.horizontal, widthdb, segment, None, Some(max_minor))?; + segments.push(Segment::new(major_minor, segment)); } - balance(&mut balanced_segments, size.width); + balance(&mut segments, max_major); - let mut x = 0; - for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { - frame.push(Pos::new(x, 0), Size::new(balanced.size, size.height)); + 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(); - x += balanced.size as i32; + major += balanced.major as i32; } Ok(()) @@ -359,7 +436,7 @@ where } #[async_trait] -impl AsyncWidget for JoinH +impl AsyncWidget for Join where I: AsyncWidget + Send + Sync, { @@ -369,207 +446,59 @@ where max_width: Option, max_height: Option, ) -> Result { - if let Some(max_width) = max_width { - let mut balanced_segments = vec![]; - for segment in &self.segments { - let size = segment - .inner - .size(widthdb, Some(max_width), max_height) - .await?; - balanced_segments.push(Segment::horizontal(size, segment)); - } - balance(&mut balanced_segments, max_width); + let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height); - let mut width = 0_u16; - let mut height = 0_u16; - for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { - let size = segment - .inner - .size(widthdb, Some(balanced.size), max_height) - .await?; - width = width.saturating_add(size.width); - height = height.max(size.height); - } - Ok(Size::new(width, height)) - } else { - let mut width = 0_u16; - let mut height = 0_u16; - for segment in &self.segments { - let size = segment.inner.size(widthdb, max_width, max_height).await?; - width = width.saturating_add(size.width); - height = height.max(size.height); - } - Ok(Size::new(width, 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 size = frame.size(); - let max_width = Some(size.width); - let max_height = Some(size.height); + let frame_size = frame.size(); + let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height); - let mut balanced_segments = vec![]; + let widthdb = frame.widthdb(); + let mut segments = Vec::with_capacity(self.segments.len()); for segment in &self.segments { - let size = segment - .inner - .size(frame.widthdb(), max_width, max_height) - .await?; - balanced_segments.push(Segment::horizontal(size, segment)); + let major_minor = + size_async(self.horizontal, widthdb, segment, None, Some(max_minor)).await?; + segments.push(Segment::new(major_minor, segment)); } - balance(&mut balanced_segments, size.width); + balance(&mut segments, max_major); - let mut x = 0; - for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { - frame.push(Pos::new(x, 0), Size::new(balanced.size, size.height)); + 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(); - x += balanced.size as i32; - } - - Ok(()) - } -} - -pub struct JoinV { - segments: Vec>, -} - -impl JoinV { - pub fn new(segments: Vec>) -> Self { - Self { segments } - } -} - -impl Widget for JoinV -where - I: Widget, -{ - fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Result { - if let Some(max_height) = max_height { - let mut balanced_segments = vec![]; - for segment in &self.segments { - let size = segment.inner.size(widthdb, max_width, Some(max_height))?; - balanced_segments.push(Segment::vertical(size, segment)); - } - balance(&mut balanced_segments, max_height); - - let mut width = 0_u16; - let mut height = 0_u16; - for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { - let size = segment - .inner - .size(widthdb, max_width, Some(balanced.size))?; - width = width.max(size.width); - height = height.saturating_add(size.height); - } - Ok(Size::new(width, height)) - } else { - let mut width = 0_u16; - let mut height = 0_u16; - for segment in &self.segments { - let size = segment.inner.size(widthdb, max_width, max_height)?; - width = width.max(size.width); - height = height.saturating_add(size.height); - } - Ok(Size::new(width, height)) - } - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - let size = frame.size(); - let max_width = Some(size.width); - let max_height = Some(size.height); - - let mut balanced_segments = vec![]; - for segment in &self.segments { - let size = segment.inner.size(frame.widthdb(), max_width, max_height)?; - balanced_segments.push(Segment::vertical(size, segment)); - } - balance(&mut balanced_segments, size.height); - - let mut y = 0; - for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { - frame.push(Pos::new(0, y), Size::new(size.width, balanced.size)); - segment.inner.draw(frame)?; - frame.pop(); - y += balanced.size as i32; - } - - Ok(()) - } -} - -#[async_trait] -impl AsyncWidget for JoinV -where - I: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Result { - if let Some(max_height) = max_height { - let mut balanced_segments = vec![]; - for segment in &self.segments { - let size = segment - .inner - .size(widthdb, max_width, Some(max_height)) - .await?; - balanced_segments.push(Segment::vertical(size, segment)); - } - balance(&mut balanced_segments, max_height); - - let mut width = 0_u16; - let mut height = 0_u16; - for (segment, balanced) in self.segments.iter().zip(balanced_segments.into_iter()) { - let size = segment - .inner - .size(widthdb, max_width, Some(balanced.size)) - .await?; - width = width.max(size.width); - height = height.saturating_add(size.height); - } - Ok(Size::new(width, height)) - } else { - let mut width = 0_u16; - let mut height = 0_u16; - for segment in &self.segments { - let size = segment.inner.size(widthdb, max_width, max_height).await?; - width = width.max(size.width); - height = height.saturating_add(size.height); - } - Ok(Size::new(width, height)) - } - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - let size = frame.size(); - let max_width = Some(size.width); - let max_height = Some(size.height); - - let mut balanced_segments = vec![]; - for segment in &self.segments { - let size = segment - .inner - .size(frame.widthdb(), max_width, max_height) - .await?; - balanced_segments.push(Segment::vertical(size, segment)); - } - balance(&mut balanced_segments, size.height); - - let mut y = 0; - for (segment, balanced) in self.segments.into_iter().zip(balanced_segments.into_iter()) { - frame.push(Pos::new(0, y), Size::new(size.width, balanced.size)); - segment.inner.draw(frame).await?; - frame.pop(); - y += balanced.size as i32; + major += balanced.major as i32; } Ok(()) @@ -578,28 +507,28 @@ where macro_rules! mk_join { ( - $name:ident: $base:ident + $either:ident { - $( $arg:ident: $constr:ident ($ty:ident), )+ + pub struct $name:ident { + $( pub $arg:ident: $type:ident [$n:expr], )+ } ) => { - pub struct $name< $( $ty ),+ >($base<$either< $( $ty ),+ >>); + pub struct $name< $($type),+ >{ + horizontal: bool, + $( pub $arg: JoinSegment<$type>, )+ + } - impl< $( $ty ),+ > $name< $( $ty ),+ > { - pub fn new( $( $arg: JoinSegment<$ty> ),+ ) -> Self { - Self($base::new(vec![ $( - JoinSegment { - inner: $either::$constr($arg.inner), - weight: $arg.weight, - growing: $arg.growing, - shrinking: $arg.shrinking, - }, - )+ ])) + 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< $( $ty ),+ > + impl Widget for $name< $($type),+ > where - $( $ty: Widget, )+ + $( $type: Widget, )+ { fn size( &self, @@ -607,18 +536,66 @@ macro_rules! mk_join { max_width: Option, max_height: Option, ) -> Result { - self.0.size(widthdb, max_width, max_height) + 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> { - self.0.draw(frame) + 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< $( $ty ),+ > + impl AsyncWidget for $name< $($type),+ > where - $( $ty: AsyncWidget + Send + Sync, )+ + E: Send, + $( $type: AsyncWidget + Send + Sync, )+ { async fn size( &self, @@ -626,126 +603,116 @@ macro_rules! mk_join { max_width: Option, max_height: Option, ) -> Result { - self.0.size(widthdb, max_width, max_height).await + 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> { - self.0.draw(frame).await + 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! { - JoinH2: JoinH + Either2 { - first: First(I1), - second: Second(I2), + pub struct Join2 { + pub first: I1 [0], + pub second: I2 [1], } } mk_join! { - JoinH3: JoinH + Either3 { - first: First(I1), - second: Second(I2), - third: Third(I3), + pub struct Join3 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], } } mk_join! { - JoinH4: JoinH + Either4 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), + pub struct Join4 { + pub first: I1 [0], + pub second: I2 [1], + pub third: I3 [2], + pub fourth: I4 [3], } } mk_join! { - JoinH5: JoinH + Either5 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - fifth: Fifth(I5), + 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! { - JoinH6: JoinH + Either6 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - fifth: Fifth(I5), - sixth: Sixth(I6), + 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! { - JoinH7: JoinH + Either7 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - fifth: Fifth(I5), - sixth: Sixth(I6), - seventh: Seventh(I7), - } -} - -mk_join! { - JoinV2: JoinV + Either2 { - first: First(I1), - second: Second(I2), - } -} - -mk_join! { - JoinV3: JoinV + Either3 { - first: First(I1), - second: Second(I2), - third: Third(I3), - } -} - -mk_join! { - JoinV4: JoinV + Either4 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - } -} - -mk_join! { - JoinV5: JoinV + Either5 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - fifth: Fifth(I5), - } -} - -mk_join! { - JoinV6: JoinV + Either6 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - fifth: Fifth(I5), - sixth: Sixth(I6), - } -} - -mk_join! { - JoinV7: JoinV + Either7 { - first: First(I1), - second: Second(I2), - third: Third(I3), - fourth: Fourth(I4), - fifth: Fifth(I5), - sixth: Sixth(I6), - seventh: Seventh(I7), + 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], } } From ea6be7bf326f50af0f3afd81678d0f1828f9b53d Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 6 Apr 2023 00:18:45 +0200 Subject: [PATCH 098/144] Simplify editor cursor handling and rendering --- src/widgets/editor.rs | 77 ++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index ff95329..cc30330 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -32,10 +32,8 @@ pub struct EditorState { /// horizontally. cursor_col: usize, - /// Width of the text when the editor was last rendered. - /// - /// Does not include additional column for cursor. - last_width: u16, + /// Position of the cursor when the editor was last rendered. + last_cursor_pos: Pos, } impl EditorState { @@ -47,7 +45,7 @@ impl EditorState { Self { cursor_idx: text.len(), cursor_col: 0, - last_width: u16::MAX, + last_cursor_pos: Pos::ZERO, text, } } @@ -317,6 +315,10 @@ impl EditorState { } } + pub fn last_cursor_pos(&self) -> Pos { + self.last_cursor_pos + } + pub fn widget(&mut self) -> Editor<'_> { Editor { highlighted: Styled::new_plain(&self.text), @@ -404,14 +406,6 @@ impl Editor<'_> { (row, line_idx) } - pub fn cursor_row(&self, widthdb: &mut WidthDb) -> usize { - let width = self.state.last_width; - let text_width = (width - 1) as usize; - let indices = wrap(widthdb, &self.state.text, text_width); - let (row, _) = Self::wrapped_cursor(self.state.cursor_idx, &indices); - row - } - fn indices(&self, widthdb: &mut WidthDb, max_width: Option) -> Vec { let max_width = max_width // One extra column for cursor @@ -425,7 +419,10 @@ impl Editor<'_> { text.clone().split_at_indices(indices) } - fn size(widthdb: &mut WidthDb, rows: &[Styled]) -> Size { + fn size_impl(&self, widthdb: &mut WidthDb, max_width: Option) -> Size { + let indices = self.indices(widthdb, max_width); + let rows = self.rows(&indices); + let width = rows .iter() .map(|row| widthdb.width(row.text())) @@ -440,19 +437,9 @@ impl Editor<'_> { Size::new(width, height) } - fn cursor( - &self, - widthdb: &mut WidthDb, - width: u16, - indices: &[usize], - rows: &[Styled], - ) -> Option { - if !self.focus { - return None; - } - + fn cursor(&self, widthdb: &mut WidthDb, width: u16, indices: &[usize], rows: &[Styled]) -> Pos { if self.hidden.is_some() { - return Some(Pos::new(0, 0)); + return Pos::new(0, 0); } let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.state.cursor_idx, indices); @@ -463,17 +450,23 @@ impl Editor<'_> { let cursor_row: i32 = cursor_row.try_into().unwrap_or(i32::MAX); let cursor_col: i32 = cursor_col.try_into().unwrap_or(i32::MAX); - Some(Pos::new(cursor_row, cursor_col)) + Pos::new(cursor_row, cursor_col) } - fn draw(frame: &mut Frame, rows: Vec, cursor: Option) { + fn draw_impl(&mut self, frame: &mut Frame) { + 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 let Some(cursor) = cursor { + if self.focus { frame.set_cursor(Some(cursor)); } + self.state.last_cursor_pos = cursor; } } @@ -484,18 +477,11 @@ impl Widget for Editor<'_> { max_width: Option, _max_height: Option, ) -> Result { - let indices = self.indices(widthdb, max_width); - let rows = self.rows(&indices); - Ok(Self::size(widthdb, &rows)) + Ok(self.size_impl(widthdb, max_width)) } - fn draw(self, frame: &mut Frame) -> Result<(), E> { - let size = frame.size(); - let widthdb = frame.widthdb(); - let indices = self.indices(widthdb, Some(size.width)); - let rows = self.rows(&indices); - let cursor = self.cursor(widthdb, size.width, &indices, &rows); - Self::draw(frame, rows, cursor); + fn draw(mut self, frame: &mut Frame) -> Result<(), E> { + self.draw_impl(frame); Ok(()) } } @@ -509,18 +495,11 @@ impl AsyncWidget for Editor<'_> { max_width: Option, _max_height: Option, ) -> Result { - let indices = self.indices(widthdb, max_width); - let rows = self.rows(&indices); - Ok(Self::size(widthdb, &rows)) + Ok(self.size_impl(widthdb, max_width)) } - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - let size = frame.size(); - let widthdb = frame.widthdb(); - let indices = self.indices(widthdb, Some(size.width)); - let rows = self.rows(&indices); - let cursor = self.cursor(widthdb, size.width, &indices, &rows); - Self::draw(frame, rows, cursor); + async fn draw(mut self, frame: &mut Frame) -> Result<(), E> { + self.draw_impl(frame); Ok(()) } } From 88e66e17ec3da5c37445e5155a483bdc948c2752 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 6 Apr 2023 00:48:53 +0200 Subject: [PATCH 099/144] Add Predrawn widget --- src/widgets.rs | 2 + src/widgets/predrawn.rs | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/widgets/predrawn.rs diff --git a/src/widgets.rs b/src/widgets.rs index dc80eb7..49c65a5 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -8,6 +8,7 @@ pub mod float; pub mod join; pub mod layer; pub mod padding; +pub mod predrawn; pub mod resize; pub mod text; @@ -21,5 +22,6 @@ pub use float::*; pub use join::*; pub use layer::*; pub use padding::*; +pub use predrawn::*; pub use resize::*; pub use text::*; diff --git a/src/widgets/predrawn.rs b/src/widgets/predrawn.rs new file mode 100644 index 0000000..e11d9f8 --- /dev/null +++ b/src/widgets/predrawn.rs @@ -0,0 +1,86 @@ +use std::mem; + +use async_trait::async_trait; + +use crate::buffer::Buffer; +use crate::{AsyncWidget, Frame, Pos, Size, Style, Styled, Widget, WidthDb}; + +pub struct Predrawn { + buffer: Buffer, +} + +impl Predrawn { + pub fn new>(inner: W, frame: &mut Frame) -> Result { + let mut tmp_frame = Frame::default(); + tmp_frame.buffer.resize(frame.size()); + mem::swap(&mut frame.widthdb, &mut tmp_frame.widthdb); + + inner.draw(&mut tmp_frame)?; + + mem::swap(&mut frame.widthdb, &mut tmp_frame.widthdb); + + let buffer = tmp_frame.buffer; + Ok(Self { buffer }) + } + + pub async fn new_async>(inner: W, frame: &mut Frame) -> Result { + let mut tmp_frame = Frame::default(); + tmp_frame.buffer.resize(frame.size()); + mem::swap(&mut frame.widthdb, &mut tmp_frame.widthdb); + + inner.draw(&mut tmp_frame).await?; + + mem::swap(&mut frame.widthdb, &mut tmp_frame.widthdb); + + let buffer = tmp_frame.buffer; + Ok(Self { buffer }) + } + + pub fn size(&self) -> Size { + self.buffer.size() + } + + fn draw_impl(&self, frame: &mut Frame) { + 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)); + } + } +} + +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> { + self.draw_impl(frame); + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Predrawn { + async fn size( + &self, + _widthdb: &mut WidthDb, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(self.buffer.size()) + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.draw_impl(frame); + Ok(()) + } +} From 77e72de9adb94151e771586ef2e2e75a5ebc4175 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Apr 2023 15:21:51 +0200 Subject: [PATCH 100/144] Make Layer* more like Join* --- src/widget.rs | 10 +-- src/widgets/layer.rs | 187 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 165 insertions(+), 32 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index dd4859b..1ac044a 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use crate::widgets::{ - Background, Border, Either2, Either3, Float, JoinSegment, Layer, Padding, Resize, + Background, Border, Either2, Either3, Float, JoinSegment, Layer2, Padding, Resize, }; use crate::{Frame, Size, WidthDb}; @@ -67,12 +67,12 @@ pub trait WidgetExt: Sized { JoinSegment::new(self) } - fn below(self, above: W) -> Layer { - Layer::new(self, above) + fn below(self, above: W) -> Layer2 { + Layer2::new(self, above) } - fn above(self, below: W) -> Layer { - Layer::new(below, self) + fn above(self, below: W) -> Layer2 { + Layer2::new(below, self) } fn padding(self) -> Padding { diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs index 366f2b5..b6f4916 100644 --- a/src/widgets/layer.rs +++ b/src/widgets/layer.rs @@ -2,26 +2,19 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; -#[derive(Debug, Clone, Copy)] -pub struct Layer { - pub below: I1, - pub above: I2, +pub struct Layer { + layers: Vec, } -impl Layer { - pub fn new(below: I1, above: I2) -> Self { - Self { below, above } - } - - fn size(below: Size, above: Size) -> Size { - Size::new(below.width.max(above.width), below.height.max(above.height)) +impl Layer { + pub fn new(layers: Vec) -> Self { + Self { layers } } } -impl Widget for Layer +impl Widget for Layer where - I1: Widget, - I2: Widget, + I: Widget, { fn size( &self, @@ -29,23 +22,27 @@ where max_width: Option, max_height: Option, ) -> Result { - let bottom = self.below.size(widthdb, max_width, max_height)?; - let top = self.above.size(widthdb, max_width, max_height)?; - Ok(Self::size(bottom, top)) + 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> { - self.below.draw(frame)?; - self.above.draw(frame)?; + for layer in self.layers { + layer.draw(frame)?; + } Ok(()) } } #[async_trait] -impl AsyncWidget for Layer +impl AsyncWidget for Layer where - I1: AsyncWidget + Send + Sync, - I2: AsyncWidget + Send + Sync, + I: AsyncWidget + Send + Sync, { async fn size( &self, @@ -53,14 +50,150 @@ where max_width: Option, max_height: Option, ) -> Result { - let bottom = self.below.size(widthdb, max_width, max_height).await?; - let top = self.above.size(widthdb, max_width, max_height).await?; - Ok(Self::size(bottom, top)) + 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> { - self.below.draw(frame).await?; - self.above.draw(frame).await?; + for layer in self.layers { + layer.draw(frame).await?; + } Ok(()) } } + +macro_rules! mk_layer { + ( + pub struct $name:ident { + $( pub $arg:ident: $type:ident, )+ + } + ) => { + 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, + } +); From 007493f136a72679af2a07f94c9a5278bb699c53 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Apr 2023 15:40:14 +0200 Subject: [PATCH 101/144] Derive more traits for widgets --- src/buffer.rs | 2 +- src/widgets/editor.rs | 2 ++ src/widgets/join.rs | 3 +++ src/widgets/layer.rs | 2 ++ src/widgets/predrawn.rs | 1 + src/widgets/resize.rs | 1 + 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/buffer.rs b/src/buffer.rs index 1a73c80..094a143 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -90,7 +90,7 @@ impl StackFrame { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Buffer { size: Size, data: Vec, diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index cc30330..893d4da 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -20,6 +20,7 @@ fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { // State // /////////// +#[derive(Debug, Clone)] pub struct EditorState { text: String, @@ -339,6 +340,7 @@ impl Default for EditorState { // Widget // //////////// +#[derive(Debug)] pub struct Editor<'a> { state: &'a mut EditorState, highlighted: Styled, diff --git a/src/widgets/join.rs b/src/widgets/join.rs index 900cd19..20cd413 100644 --- a/src/widgets/join.rs +++ b/src/widgets/join.rs @@ -229,6 +229,7 @@ fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) { } } +#[derive(Debug, Clone, Copy)] pub struct JoinSegment { pub inner: I, weight: f32, @@ -353,6 +354,7 @@ fn sum_major_max_minor(segments: &[Segment]) -> (u16, u16) { (major, minor) } +#[derive(Debug, Clone)] pub struct Join { horizontal: bool, segments: Vec>, @@ -511,6 +513,7 @@ macro_rules! mk_join { $( pub $arg:ident: $type:ident [$n:expr], )+ } ) => { + #[derive(Debug, Clone, Copy)] pub struct $name< $($type),+ >{ horizontal: bool, $( pub $arg: JoinSegment<$type>, )+ diff --git a/src/widgets/layer.rs b/src/widgets/layer.rs index b6f4916..af3da5e 100644 --- a/src/widgets/layer.rs +++ b/src/widgets/layer.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; +#[derive(Debug, Clone)] pub struct Layer { layers: Vec, } @@ -73,6 +74,7 @@ macro_rules! mk_layer { $( pub $arg:ident: $type:ident, )+ } ) => { + #[derive(Debug, Clone, Copy)] pub struct $name< $($type),+ >{ $( pub $arg: $type, )+ } diff --git a/src/widgets/predrawn.rs b/src/widgets/predrawn.rs index e11d9f8..e4f594a 100644 --- a/src/widgets/predrawn.rs +++ b/src/widgets/predrawn.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use crate::buffer::Buffer; use crate::{AsyncWidget, Frame, Pos, Size, Style, Styled, Widget, WidthDb}; +#[derive(Debug, Clone)] pub struct Predrawn { buffer: Buffer, } diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 91bb84a..5a92e05 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -2,6 +2,7 @@ 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, From 7c6e651f8885a1d48e8945761a31ebecbde55cfa Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Apr 2023 16:00:04 +0200 Subject: [PATCH 102/144] Add Boxed and BoxedAsync widgets --- src/widget.rs | 17 ++++++- src/widgets.rs | 2 + src/widgets/boxed.rs | 116 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/widgets/boxed.rs diff --git a/src/widget.rs b/src/widget.rs index 1ac044a..65f95f8 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; use crate::widgets::{ - Background, Border, Either2, Either3, Float, JoinSegment, Layer2, Padding, Resize, + Background, Border, Boxed, BoxedAsync, Either2, Either3, Float, JoinSegment, Layer2, Padding, + Resize, }; use crate::{Frame, Size, WidthDb}; @@ -39,6 +40,20 @@ pub trait WidgetExt: Sized { Border::new(self) } + fn boxed<'a, E>(self) -> Boxed<'a, E> + where + Self: Widget + 'a, + { + Boxed::new(self) + } + + fn boxed_async<'a, E>(self) -> BoxedAsync<'a, E> + where + Self: AsyncWidget + Send + Sync + 'a, + { + BoxedAsync::new(self) + } + fn first2(self) -> Either2 { Either2::First(self) } diff --git a/src/widgets.rs b/src/widgets.rs index 49c65a5..4991618 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,5 +1,6 @@ pub mod background; pub mod border; +pub mod boxed; pub mod cursor; pub mod editor; pub mod either; @@ -14,6 +15,7 @@ pub mod text; pub use background::*; pub use border::*; +pub use boxed::*; pub use cursor::*; pub use editor::*; pub use either::*; diff --git a/src/widgets/boxed.rs b/src/widgets/boxed.rs new file mode 100644 index 0000000..050514c --- /dev/null +++ b/src/widgets/boxed.rs @@ -0,0 +1,116 @@ +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)) + } +} + +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) + } +} + +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 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] +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 + } +} + +#[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 + } +} From 35aa70de4b5060209e3be21c6e512fd74ef77eaa Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 12 Apr 2023 19:16:29 +0200 Subject: [PATCH 103/144] Fix editor cursor --- src/widgets/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index 893d4da..8b4d91d 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -452,7 +452,7 @@ impl Editor<'_> { 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_row, cursor_col) + Pos::new(cursor_col, cursor_row) } fn draw_impl(&mut self, frame: &mut Frame) { From 810524325e5d0608c1586c8a118c44c3583a2bf8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 13 Apr 2023 02:18:32 +0200 Subject: [PATCH 104/144] Fix hidden placeholder appearing in empty editor --- src/widgets/editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index 8b4d91d..5a7da70 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -417,7 +417,10 @@ impl Editor<'_> { } fn rows(&self, indices: &[usize]) -> Vec { - let text = self.hidden.as_ref().unwrap_or(&self.highlighted); + let text = match self.hidden.as_ref() { + Some(hidden) if !self.highlighted.text().is_empty() => hidden, + _ => &self.highlighted, + }; text.clone().split_at_indices(indices) } From d0b3b9edd43d2c7a3a5bc199c8986df5a5882590 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 14 Apr 2023 01:51:35 +0200 Subject: [PATCH 105/144] Fix Predrawn size calculations Previously, Predrawn would use its parent frame's size. Now, it uses the size requested by the widget. Because of this, it no longer requires a full &mut Frame, but only a &mut WidthDb. To set a maximum size, the widget can be wrapped inside a Resize. --- src/widgets/predrawn.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/widgets/predrawn.rs b/src/widgets/predrawn.rs index e4f594a..f38166d 100644 --- a/src/widgets/predrawn.rs +++ b/src/widgets/predrawn.rs @@ -11,27 +11,32 @@ pub struct Predrawn { } impl Predrawn { - pub fn new>(inner: W, frame: &mut Frame) -> Result { + pub fn new>(inner: W, widthdb: &mut WidthDb) -> Result { let mut tmp_frame = Frame::default(); - tmp_frame.buffer.resize(frame.size()); - mem::swap(&mut frame.widthdb, &mut tmp_frame.widthdb); + 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(&mut frame.widthdb, &mut tmp_frame.widthdb); + mem::swap(widthdb, &mut tmp_frame.widthdb); let buffer = tmp_frame.buffer; Ok(Self { buffer }) } - pub async fn new_async>(inner: W, frame: &mut Frame) -> Result { + pub async fn new_async>( + inner: W, + widthdb: &mut WidthDb, + ) -> Result { let mut tmp_frame = Frame::default(); - tmp_frame.buffer.resize(frame.size()); - mem::swap(&mut frame.widthdb, &mut tmp_frame.widthdb); + 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(&mut frame.widthdb, &mut tmp_frame.widthdb); + mem::swap(widthdb, &mut tmp_frame.widthdb); let buffer = tmp_frame.buffer; Ok(Self { buffer }) From 242a1aed29e6ecce84cd7ffcc8fe3d78bd497438 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 14 Apr 2023 13:57:09 +0200 Subject: [PATCH 106/144] Add option to stretch Padding --- src/widgets/padding.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/widgets/padding.rs b/src/widgets/padding.rs index 473ad02..be3aff6 100644 --- a/src/widgets/padding.rs +++ b/src/widgets/padding.rs @@ -9,6 +9,7 @@ pub struct Padding { pub right: u16, pub top: u16, pub bottom: u16, + pub stretch: bool, } impl Padding { @@ -19,6 +20,7 @@ impl Padding { right: 0, top: 0, bottom: 0, + stretch: false, } } @@ -54,6 +56,11 @@ impl Padding { 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) } @@ -84,9 +91,13 @@ where } fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.push_inner(frame); - self.inner.draw(frame)?; - frame.pop(); + if self.stretch { + self.inner.draw(frame)?; + } else { + self.push_inner(frame); + self.inner.draw(frame)?; + frame.pop(); + } Ok(()) } } @@ -110,9 +121,13 @@ where } async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.push_inner(frame); - self.inner.draw(frame).await?; - frame.pop(); + if self.stretch { + self.inner.draw(frame).await?; + } else { + self.push_inner(frame); + self.inner.draw(frame).await?; + frame.pop(); + } Ok(()) } } From 59710c816269c434b97ece3ab1701d3fef2269cb Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 17 Apr 2023 09:38:00 +0200 Subject: [PATCH 107/144] Fix Predrawn not drawing cursor --- src/widgets/predrawn.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/widgets/predrawn.rs b/src/widgets/predrawn.rs index f38166d..a1f7672 100644 --- a/src/widgets/predrawn.rs +++ b/src/widgets/predrawn.rs @@ -55,6 +55,10 @@ impl Predrawn { }; frame.write(pos, Styled::new(&cell.content, style)); } + + if let Some(cursor) = self.buffer.cursor() { + frame.set_cursor(Some(cursor)); + } } } From 57788a9dd9688bdf3e59bf7366ba6276fc660715 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 17 Apr 2023 16:48:12 +0200 Subject: [PATCH 108/144] Apply Resize's max size to available size too I'm not sure if the input max size and the output max size should be separate, and I'm not sure whether the min size should also have an effect on the input. For now, this works well enough, but I may need to adjust it in the future as I stumble across new edge cases. This change was made because I was using Resize as a way to set the size of widgets containing text that were rendered inside Predraw widgets. After this change, setting a max_width but no max_height has the desired effect of making the inner widgets perform word wrapping. The resulting Predrawn is then as high as it needs to be to contain the wrapped text. --- src/widgets/resize.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 5a92e05..81e30b5 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -42,6 +42,20 @@ impl Resize { 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; @@ -74,6 +88,7 @@ where 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)) } @@ -94,6 +109,7 @@ where 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)) } From 4179e7f56ca4a4df4d7133fb655e7bdff449f35f Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 17 Apr 2023 19:36:44 +0200 Subject: [PATCH 109/144] Add Desync widget to turn Widgets into AsyncWidgets --- src/widget.rs | 7 +++++-- src/widgets.rs | 2 ++ src/widgets/desync.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/widgets/desync.rs diff --git a/src/widget.rs b/src/widget.rs index 65f95f8..9062411 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,8 +1,8 @@ use async_trait::async_trait; use crate::widgets::{ - Background, Border, Boxed, BoxedAsync, Either2, Either3, Float, JoinSegment, Layer2, Padding, - Resize, + Background, Border, Boxed, BoxedAsync, Desync, Either2, Either3, Float, JoinSegment, Layer2, + Padding, Resize, }; use crate::{Frame, Size, WidthDb}; @@ -53,6 +53,9 @@ pub trait WidgetExt: Sized { { BoxedAsync::new(self) } + fn desync(self) -> Desync { + Desync(self) + } fn first2(self) -> Either2 { Either2::First(self) diff --git a/src/widgets.rs b/src/widgets.rs index 4991618..7c3fc55 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -2,6 +2,7 @@ pub mod background; pub mod border; pub mod boxed; pub mod cursor; +pub mod desync; pub mod editor; pub mod either; pub mod empty; @@ -17,6 +18,7 @@ pub use background::*; pub use border::*; pub use boxed::*; pub use cursor::*; +pub use desync::*; pub use editor::*; pub use either::*; pub use empty::*; diff --git a/src/widgets/desync.rs b/src/widgets/desync.rs new file mode 100644 index 0000000..67e7488 --- /dev/null +++ b/src/widgets/desync.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Widget}; + +pub struct Desync(pub I); + +impl Widget for Desync +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut crate::WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut crate::Frame) -> Result<(), E> { + self.0.draw(frame) + } +} + +#[async_trait] +impl AsyncWidget for Desync +where + I: Widget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut crate::WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.0.size(widthdb, max_width, max_height) + } + + async fn draw(self, frame: &mut crate::Frame) -> Result<(), E> { + self.0.draw(frame) + } +} From 968dbe501f581ed040327067abf93f4d1d8c8dcf Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 17 Apr 2023 19:39:14 +0200 Subject: [PATCH 110/144] Remove AsyncWidget impls replaceable by Desync --- src/widgets/editor.rs | 80 ++++++++++++++--------------------------- src/widgets/empty.rs | 20 +---------- src/widgets/predrawn.rs | 48 +++++++------------------ src/widgets/text.rs | 49 +++++++------------------ 4 files changed, 53 insertions(+), 144 deletions(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index 5a7da70..693a69f 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -1,10 +1,9 @@ use std::iter; -use async_trait::async_trait; use crossterm::style::Stylize; use unicode_segmentation::UnicodeSegmentation; -use crate::{AsyncWidget, Frame, Pos, Size, Style, Styled, Widget, WidthDb}; +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. @@ -424,24 +423,6 @@ impl Editor<'_> { text.clone().split_at_indices(indices) } - fn size_impl(&self, widthdb: &mut WidthDb, max_width: Option) -> Size { - 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); - Size::new(width, height) - } - fn cursor(&self, widthdb: &mut WidthDb, width: u16, indices: &[usize], rows: &[Styled]) -> Pos { if self.hidden.is_some() { return Pos::new(0, 0); @@ -457,8 +438,33 @@ impl Editor<'_> { let cursor_col: i32 = cursor_col.try_into().unwrap_or(i32::MAX); Pos::new(cursor_col, cursor_row) } +} - fn draw_impl(&mut self, frame: &mut Frame) { +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(mut self, frame: &mut Frame) -> Result<(), E> { let size = frame.size(); let indices = self.indices(frame.widthdb(), Some(size.width)); let rows = self.rows(&indices); @@ -472,39 +478,7 @@ impl Editor<'_> { frame.set_cursor(Some(cursor)); } self.state.last_cursor_pos = cursor; - } -} -impl Widget for Editor<'_> { - fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - _max_height: Option, - ) -> Result { - Ok(self.size_impl(widthdb, max_width)) - } - - fn draw(mut self, frame: &mut Frame) -> Result<(), E> { - self.draw_impl(frame); - Ok(()) - } -} - -#[allow(single_use_lifetimes)] -#[async_trait] -impl AsyncWidget for Editor<'_> { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - _max_height: Option, - ) -> Result { - Ok(self.size_impl(widthdb, max_width)) - } - - async fn draw(mut self, frame: &mut Frame) -> Result<(), E> { - self.draw_impl(frame); Ok(()) } } diff --git a/src/widgets/empty.rs b/src/widgets/empty.rs index 033ab70..5de4fdf 100644 --- a/src/widgets/empty.rs +++ b/src/widgets/empty.rs @@ -1,6 +1,4 @@ -use async_trait::async_trait; - -use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; +use crate::{Frame, Size, Widget, WidthDb}; #[derive(Debug, Default, Clone, Copy)] pub struct Empty { @@ -42,19 +40,3 @@ impl Widget for Empty { Ok(()) } } - -#[async_trait] -impl AsyncWidget for Empty { - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Result { - Ok(self.size) - } - - async fn draw(self, _frame: &mut Frame) -> Result<(), E> { - Ok(()) - } -} diff --git a/src/widgets/predrawn.rs b/src/widgets/predrawn.rs index a1f7672..8301f1e 100644 --- a/src/widgets/predrawn.rs +++ b/src/widgets/predrawn.rs @@ -1,7 +1,5 @@ use std::mem; -use async_trait::async_trait; - use crate::buffer::Buffer; use crate::{AsyncWidget, Frame, Pos, Size, Style, Styled, Widget, WidthDb}; @@ -45,21 +43,6 @@ impl Predrawn { pub fn size(&self) -> Size { self.buffer.size() } - - fn draw_impl(&self, frame: &mut Frame) { - 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)); - } - } } impl Widget for Predrawn { @@ -73,24 +56,19 @@ impl Widget for Predrawn { } fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.draw_impl(frame); - Ok(()) - } -} - -#[async_trait] -impl AsyncWidget for Predrawn { - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Result { - Ok(self.buffer.size()) - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.draw_impl(frame); + 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/text.rs b/src/widgets/text.rs index 3755c8c..007f4fe 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -1,6 +1,4 @@ -use async_trait::async_trait; - -use crate::{AsyncWidget, Frame, Pos, Size, Styled, Widget, WidthDb}; +use crate::{Frame, Pos, Size, Styled, Widget, WidthDb}; #[derive(Debug, Clone)] pub struct Text { @@ -30,8 +28,15 @@ impl Text { let indices = widthdb.wrap(self.styled.text(), max_width); self.styled.clone().split_at_indices(&indices) } +} - fn size(&self, widthdb: &mut WidthDb, max_width: Option) -> Size { +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 @@ -43,11 +48,12 @@ impl Text { let min_width: u16 = min_width.try_into().unwrap_or(u16::MAX); let min_height: u16 = min_height.try_into().unwrap_or(u16::MAX); - Size::new(min_width, min_height) + Ok(Size::new(min_width, min_height)) } - fn draw(self, frame: &mut Frame) { + 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() @@ -56,38 +62,7 @@ impl Text { let i: i32 = i.try_into().unwrap_or(i32::MAX); frame.write(Pos::new(0, i), line); } - } -} -impl Widget for Text { - fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - _max_height: Option, - ) -> Result { - Ok(self.size(widthdb, max_width)) - } - - fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.draw(frame); - Ok(()) - } -} - -#[async_trait] -impl AsyncWidget for Text { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - _max_height: Option, - ) -> Result { - Ok(self.size(widthdb, max_width)) - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.draw(frame); Ok(()) } } From f414db40d526295c74cbcae6c3d194088da8f1d9 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 17 Apr 2023 20:26:26 +0200 Subject: [PATCH 111/144] Add BoxedSencSync which is Send + Sync --- src/widget.rs | 11 ++++- src/widgets/boxed.rs | 110 ++++++++++++++++++++++++++----------------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index 9062411..58ce562 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,8 +1,8 @@ use async_trait::async_trait; use crate::widgets::{ - Background, Border, Boxed, BoxedAsync, Desync, Either2, Either3, Float, JoinSegment, Layer2, - Padding, Resize, + Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float, + JoinSegment, Layer2, Padding, Resize, }; use crate::{Frame, Size, WidthDb}; @@ -47,6 +47,13 @@ pub trait WidgetExt: Sized { 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, diff --git a/src/widgets/boxed.rs b/src/widgets/boxed.rs index 050514c..3d9713f 100644 --- a/src/widgets/boxed.rs +++ b/src/widgets/boxed.rs @@ -13,6 +13,74 @@ impl<'a, E> Boxed<'a, E> { } } +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, @@ -42,32 +110,6 @@ where } } -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 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] trait AsyncWidgetWrapper { async fn wrap_size( @@ -98,19 +140,3 @@ where (*self).draw(frame).await } } - -#[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 - } -} From 8bfb4b2dc345c3e0ffdb89bdb34f2996487a35cb Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Apr 2023 14:40:37 +0200 Subject: [PATCH 112/144] Fix Join panicking in some situations --- src/widgets/float.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/float.rs b/src/widgets/float.rs index 8380538..5cfb349 100644 --- a/src/widgets/float.rs +++ b/src/widgets/float.rs @@ -86,7 +86,7 @@ impl Float { let mut inner_pos = Pos::ZERO; if let Some(horizontal) = self.horizontal { - let available = (size.width - inner_size.width) as f32; + 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; @@ -96,7 +96,7 @@ impl Float { } if let Some(vertical) = self.vertical { - let available = (size.height - inner_size.height) as f32; + 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; From f005ec10fe1b6034c50f3a4ef24dd44d3e6d5593 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 29 Apr 2023 01:23:50 +0200 Subject: [PATCH 113/144] Fix editor panicking sometimes when hidden --- src/widgets/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index 693a69f..3417ebe 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -412,7 +412,8 @@ impl Editor<'_> { // One extra column for cursor .map(|w| w.saturating_sub(1) as usize) .unwrap_or(usize::MAX); - wrap(widthdb, self.state.text(), max_width) + let text = self.hidden.as_ref().unwrap_or(&self.highlighted); + wrap(widthdb, text.text(), max_width) } fn rows(&self, indices: &[usize]) -> Vec { From 3b9ffe87151f3ca487dc2471eacb5eb258a8fc86 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 10 May 2023 19:25:46 +0200 Subject: [PATCH 114/144] Fix full redraws always using stdout --- src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.rs b/src/terminal.rs index cb4a186..e4352f1 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -199,7 +199,7 @@ impl Terminal { /// be empty again and have no cursor position. pub fn present(&mut self) -> io::Result<()> { if self.full_redraw { - io::stdout().queue(Clear(ClearType::All))?; + self.out.queue(Clear(ClearType::All))?; self.prev_frame_buffer.reset(); // Because the screen is now empty self.full_redraw = false; } From 6eb853e3136dd320272bd67e2957774869c21e4b Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 10 May 2023 19:35:56 +0200 Subject: [PATCH 115/144] Reduce tearing when redrawing screen --- Cargo.toml | 2 +- src/terminal.rs | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1d625c..9641efb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] async-trait = "0.1.64" -crossterm = "0.26.0" +crossterm = "0.26.1" unicode-linebreak = "0.1.4" unicode-segmentation = "1.10.1" unicode-width = "0.1.10" diff --git a/src/terminal.rs b/src/terminal.rs index e4352f1..db7dcdd 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -9,7 +9,10 @@ use crossterm::event::{ PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; use crossterm::style::{PrintStyledContent, StyledContent}; -use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::terminal::{ + BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, + LeaveAlternateScreen, +}; use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::Buffer; @@ -198,14 +201,11 @@ impl Terminal { /// After calling this function, the frame returned by [`Self::frame`] will /// be empty again and have no cursor position. pub fn present(&mut self) -> io::Result<()> { - if self.full_redraw { - self.out.queue(Clear(ClearType::All))?; - self.prev_frame_buffer.reset(); // Because the screen is now empty - self.full_redraw = false; - } + self.out.queue(BeginSynchronizedUpdate)?; + let result = self.draw_to_screen(); + self.out.queue(EndSynchronizedUpdate)?; + result?; - self.draw_differences()?; - self.update_cursor()?; self.out.flush()?; mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); @@ -244,6 +244,19 @@ impl Terminal { 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()?; + + 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 { From a4ec64aa57b39f5b1fc7fe9e5d4164f8fff31a1b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 May 2023 15:57:19 +0200 Subject: [PATCH 116/144] Update dependencies --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9641efb..3de8599 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -async-trait = "0.1.64" +async-trait = "0.1.68" crossterm = "0.26.1" unicode-linebreak = "0.1.4" unicode-segmentation = "1.10.1" From 87723840df5ff75666d7270e62b943621197ce62 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 May 2023 15:57:25 +0200 Subject: [PATCH 117/144] Bump version to 0.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..03bef79 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# 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.1.0 - 2023-05.14 + +Initial release From f6cbba5231f240f87ed56210f681a69bce42564e Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 31 Aug 2023 13:20:54 +0200 Subject: [PATCH 118/144] Update dependencies --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3de8599..fcb3d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -async-trait = "0.1.68" -crossterm = "0.26.1" -unicode-linebreak = "0.1.4" +async-trait = "0.1.73" +crossterm = "0.27.0" +unicode-linebreak = "0.1.5" unicode-segmentation = "1.10.1" unicode-width = "0.1.10" From 2c7888fa413c9b12bec7d55a73051aa96d59386f Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 31 Aug 2023 13:23:00 +0200 Subject: [PATCH 119/144] Bump version to 0.2.0 --- CHANGELOG.md | 7 ++++++- Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bef79..9083adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ Procedure when bumping the version number: ## Unreleased -## v0.1.0 - 2023-05.14 +## 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 fcb3d86..6832db1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] From 77b4f825c98b84f56351bd2423ae0166d9341992 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:23:18 +0100 Subject: [PATCH 120/144] Fix clippy warning --- src/widgets/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index 3417ebe..aa36e8e 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -465,7 +465,7 @@ impl Widget for Editor<'_> { Ok(Size::new(width, height)) } - fn draw(mut self, frame: &mut Frame) -> Result<(), E> { + 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); From 2714deeafbc744edcd96462af9655f25d6fe81ed Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:33:08 +0100 Subject: [PATCH 121/144] Add support for setting window title --- CHANGELOG.md | 3 +++ src/frame.rs | 6 ++++++ src/terminal.rs | 10 +++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9083adf..d029ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- `Frame::set_title` + ## v0.2.0 - 2023-08-31 ### Changed diff --git a/src/frame.rs b/src/frame.rs index 2e9bad1..03fbb04 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -7,6 +7,7 @@ use crate::{Pos, Size, Styled, WidthDb}; pub struct Frame { pub(crate) widthdb: WidthDb, pub(crate) buffer: Buffer, + pub(crate) title: Option, } impl Frame { @@ -24,6 +25,7 @@ impl Frame { pub fn reset(&mut self) { self.buffer.reset(); + self.title = None; } pub fn cursor(&self) -> Option { @@ -42,6 +44,10 @@ impl Frame { self.set_cursor(None); } + pub fn set_title(&mut self, title: Option) { + self.title = title; + } + pub fn widthdb(&mut self) -> &mut WidthDb { &mut self.widthdb } diff --git a/src/terminal.rs b/src/terminal.rs index db7dcdd..545a701 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -11,7 +11,7 @@ use crossterm::event::{ use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::terminal::{ BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, - LeaveAlternateScreen, + LeaveAlternateScreen, SetTitle, }; use crossterm::{ExecutableCommand, QueueableCommand}; @@ -253,6 +253,7 @@ impl Terminal { self.draw_differences()?; self.update_cursor()?; + self.update_title()?; Ok(()) } @@ -287,4 +288,11 @@ impl Terminal { self.out.queue(Hide)?; Ok(()) } + + fn update_title(&mut self) -> io::Result<()> { + if let Some(title) = &self.frame.title { + self.out.queue(SetTitle(title.clone()))?; + } + Ok(()) + } } From b757f1be03e9aa66be1c13e2794f104ddf49139a Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:33:51 +0100 Subject: [PATCH 122/144] Add Title widget --- CHANGELOG.md | 2 ++ src/widget.rs | 6 ++++- src/widgets.rs | 2 ++ src/widgets/title.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/widgets/title.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d029ed5..0b8b087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Procedure when bumping the version number: ### Added - `Frame::set_title` +- `WidgetExt::title` +- `widgets::title` ## v0.2.0 - 2023-08-31 diff --git a/src/widget.rs b/src/widget.rs index 58ce562..8aec3d2 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use crate::widgets::{ Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float, - JoinSegment, Layer2, Padding, Resize, + JoinSegment, Layer2, Padding, Resize, Title, }; use crate::{Frame, Size, WidthDb}; @@ -107,6 +107,10 @@ pub trait WidgetExt: Sized { 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. diff --git a/src/widgets.rs b/src/widgets.rs index 7c3fc55..28b44d8 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -13,6 +13,7 @@ pub mod padding; pub mod predrawn; pub mod resize; pub mod text; +pub mod title; pub use background::*; pub use border::*; @@ -29,3 +30,4 @@ pub use padding::*; pub use predrawn::*; pub use resize::*; pub use text::*; +pub use title::*; diff --git a/src/widgets/title.rs b/src/widgets/title.rs new file mode 100644 index 0000000..c0dc0d4 --- /dev/null +++ b/src/widgets/title.rs @@ -0,0 +1,59 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +#[derive(Debug, Clone)] +pub struct Title { + pub inner: I, + pub title: String, +} + +impl Title { + pub fn new(inner: I, title: S) -> Self { + Self { + inner, + title: title.to_string(), + } + } +} + +impl Widget for Title +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame)?; + frame.set_title(Some(self.title)); + Ok(()) + } +} + +#[async_trait] +impl AsyncWidget for Title +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + self.inner.size(widthdb, max_width, max_height).await + } + + async fn draw(self, frame: &mut Frame) -> Result<(), E> { + self.inner.draw(frame).await?; + frame.set_title(Some(self.title)); + Ok(()) + } +} From 44512f1088246052c1b305d9ba644a8f82f50084 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:53:13 +0100 Subject: [PATCH 123/144] Update dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6832db1..e1c4124 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.2.0" edition = "2021" [dependencies] -async-trait = "0.1.73" +async-trait = "0.1.77" crossterm = "0.27.0" unicode-linebreak = "0.1.5" unicode-segmentation = "1.10.1" -unicode-width = "0.1.10" +unicode-width = "0.1.11" From b01ee297d5bdbb3b28cafe2b5b130c2767667974 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:54:37 +0100 Subject: [PATCH 124/144] Bump version to 0.2.1 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8b087..e6acbb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.2.1 - 2024-01-05 + ### Added - `Frame::set_title` - `WidgetExt::title` diff --git a/Cargo.toml b/Cargo.toml index e1c4124..1c63cad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.0" +version = "0.2.1" edition = "2021" [dependencies] From 2d604d606cc07a411a61d5a040d3414ba0437aa4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Jan 2024 12:32:10 +0100 Subject: [PATCH 125/144] Fix crash when drawing Predrawn with width 0 --- CHANGELOG.md | 3 +++ src/buffer.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6acbb9..d8276ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Crash when drawing `widgets::Predrawn` with width 0 + ## v0.2.1 - 2024-01-05 ### Added diff --git a/src/buffer.rs b/src/buffer.rs index 094a143..022145b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -332,6 +332,9 @@ impl<'a> Iterator for Cells<'a> { type Item = (u16, u16, &'a Cell); fn next(&mut self) -> Option { + if self.x >= self.buffer.size.width { + return None; + } if self.y >= self.buffer.size.height { return None; } From 761e8baeba09b923e2a409ea7df7bb363fc77fd5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Jan 2024 12:38:33 +0100 Subject: [PATCH 126/144] Bump version to 0.2.2 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8276ab..2e012aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.2.2 - 2024-01-14 + ### Fixed - Crash when drawing `widgets::Predrawn` with width 0 diff --git a/Cargo.toml b/Cargo.toml index 1c63cad..ec48349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.1" +version = "0.2.2" edition = "2021" [dependencies] From 94052c5a65b07ab068aa064cd5fe65b874b43b56 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 4 Mar 2024 00:08:27 +0100 Subject: [PATCH 127/144] Fix formatting --- src/widget.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget.rs b/src/widget.rs index 8aec3d2..356a047 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -60,6 +60,7 @@ pub trait WidgetExt: Sized { { BoxedAsync::new(self) } + fn desync(self) -> Desync { Desync(self) } From 8556fd8176d234df6910037c51561203c19ff149 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 19:45:35 +0200 Subject: [PATCH 128/144] Fix control character width measurement --- CHANGELOG.md | 3 +++ src/widthdb.rs | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e012aa..a01f614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Width measurements of ASCII control characters + ## v0.2.2 - 2024-01-14 ### Fixed diff --git a/src/widthdb.rs b/src/widthdb.rs index 7d18570..200765f 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -101,6 +101,15 @@ impl WidthDb { 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))?; From ef6d75c23a229a1a7fec5d695cdbc502eea81236 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 19:56:09 +0200 Subject: [PATCH 129/144] Fix suspend sequence In my kitty-based setup, I observed the following bug: 1. Run cove[1], a toss-based application, in a kitty tab 2. Exit cove 3. Start lazygit[2] 4. Stage some files and enter a commit message 5. Try to press enter and observe garbage appearing in the text box The bug occurred reliably after running cove, but never occurred if cove was not run in that tab. This commit fixes the bug by making the suspend sequence undo the unsuspend sequence's steps in reverse order. --- CHANGELOG.md | 1 + src/terminal.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a01f614..a466408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Procedure when bumping the version number: ### Fixed - Width measurements of ASCII control characters +- Toss messing up the terminal state ## v0.2.2 - 2024-01-14 diff --git a/src/terminal.rs b/src/terminal.rs index 545a701..33c37fa 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -68,12 +68,12 @@ impl Terminal { /// presenting the next frame. pub fn suspend(&mut self) -> io::Result<()> { crossterm::terminal::disable_raw_mode()?; - self.out.execute(LeaveAlternateScreen)?; #[cfg(not(windows))] { - self.out.execute(DisableBracketedPaste)?; self.out.execute(PopKeyboardEnhancementFlags)?; + self.out.execute(DisableBracketedPaste)?; } + self.out.execute(LeaveAlternateScreen)?; self.out.execute(Show)?; Ok(()) } From 0f7505ebb4dbca778b6f24f496dd6a18fce067db Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 20:13:42 +0200 Subject: [PATCH 130/144] Update dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ec48349..a54a858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.2.2" edition = "2021" [dependencies] -async-trait = "0.1.77" +async-trait = "0.1.80" crossterm = "0.27.0" unicode-linebreak = "0.1.5" -unicode-segmentation = "1.10.1" +unicode-segmentation = "1.11.0" unicode-width = "0.1.11" From b1d7221bae9e1bb57d8e5b49c315dc3ca56e947a Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 20:14:26 +0200 Subject: [PATCH 131/144] Bump version to 0.2.3 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a466408..7728464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.2.3 - 2024-04-25 + ### Fixed - Width measurements of ASCII control characters - Toss messing up the terminal state diff --git a/Cargo.toml b/Cargo.toml index a54a858..618da80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.2" +version = "0.2.3" edition = "2021" [dependencies] From 3a5ce3832beb159d657f9dd74b827c53be13d8d2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Nov 2024 22:16:47 +0100 Subject: [PATCH 132/144] Add Terminal::mark_dirty --- CHANGELOG.md | 3 +++ src/terminal.rs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7728464..352a2df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- `Terminal::mark_dirty` + ## v0.2.3 - 2024-04-25 ### Fixed diff --git a/src/terminal.rs b/src/terminal.rs index 33c37fa..439ce4e 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -193,6 +193,12 @@ impl Terminal { &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 From 65f31a2697396f80c796fe485c6d7473e318826a Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Nov 2024 22:13:11 +0100 Subject: [PATCH 133/144] Update dependencies --- CHANGELOG.md | 3 +++ Cargo.toml | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 352a2df..7c2eb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Procedure when bumping the version number: ### Added - `Terminal::mark_dirty` +### Changed +- **(breaking)** Updated dependencies + ## v0.2.3 - 2024-04-25 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 618da80..894e169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.2.3" edition = "2021" [dependencies] -async-trait = "0.1.80" -crossterm = "0.27.0" +async-trait = "0.1.83" +crossterm = "0.28.1" unicode-linebreak = "0.1.5" -unicode-segmentation = "1.11.0" -unicode-width = "0.1.11" +unicode-segmentation = "1.12.0" +unicode-width = "0.2.0" From 73a0268dfd1f90496a92ef2486363f2f5e83770a Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Nov 2024 22:21:20 +0100 Subject: [PATCH 134/144] Bump version to 0.3.0 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2eb3f..13edb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.0 - 2024-11-06 + ### Added - `Terminal::mark_dirty` diff --git a/Cargo.toml b/Cargo.toml index 894e169..4083c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.3" +version = "0.3.0" edition = "2021" [dependencies] From 1618264cb701917e90353b70bc6edd534bfdecd8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 20 Feb 2025 21:24:07 +0100 Subject: [PATCH 135/144] Fix newlines causing bad rendering artifacts The unicode-width crate has started to consider newlines to have a width of 1 instead of 0. --- src/widthdb.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/widthdb.rs b/src/widthdb.rs index 200765f..9815190 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -47,9 +47,13 @@ impl WidthDb { if grapheme == "\t" { return self.tab_width_at_column(col); } + if grapheme.chars().any(|c| c.is_ascii_control()) { + return 0; // See measure_widths function + } if !self.active { return grapheme.width() as u8; } + if let Some(width) = self.known.get(grapheme) { *width } else { @@ -101,7 +105,7 @@ impl WidthDb { return Ok(()); } for grapheme in self.requested.drain() { - if grapheme.chars().any(|c|c.is_ascii_control()){ + 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 From 77a02116a64d300264277bfade5553a1a8d9f01d Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 20 Feb 2025 22:08:10 +0100 Subject: [PATCH 136/144] Fix grapheme width estimation I'm pretty sure it still breaks in lots of terminal emulators, but it works far better than what recent versions of the unicode_width crate were doing. --- CHANGELOG.md | 3 +++ src/widthdb.rs | 30 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13edb2a..e18e88a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Rendering glitches, mainly related to emoji + ## v0.3.0 - 2024-11-06 ### Added diff --git a/src/widthdb.rs b/src/widthdb.rs index 9815190..53f20ec 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -6,7 +6,7 @@ use crossterm::style::Print; use crossterm::terminal::{Clear, ClearType}; use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use crate::wrap; @@ -36,6 +36,26 @@ impl WidthDb { self.tab_width - (col % self.tab_width as usize) as u8 } + /// Estimate what our terminal emulator thinks the width of a grapheme is. + /// + /// Different terminal emulators are all broken in different ways, so this + /// method will never be able to give a correct solution. For that, the only + /// possible method is actually measuring. + /// + /// Instead, it implements a character-wise width calculation. The hope is + /// that dumb terminal emulators do something roughly like this, and smart + /// terminal emulators try to emulate dumb ones for compatibility. In + /// practice, this counting approach seems to be fairly robust. + fn grapheme_width_estimate(grapheme: &str) -> u8 { + grapheme + .chars() + .filter(|c| !c.is_ascii_control()) + .flat_map(|c| c.width()) + .sum::() + .try_into() + .unwrap_or(u8::MAX) + } + /// Determine the width of a grapheme. /// /// If the grapheme is a tab, the column is used to determine its width. @@ -47,18 +67,14 @@ impl WidthDb { if grapheme == "\t" { return self.tab_width_at_column(col); } - if grapheme.chars().any(|c| c.is_ascii_control()) { - return 0; // See measure_widths function - } if !self.active { - return grapheme.width() as u8; + return Self::grapheme_width_estimate(grapheme); } - if let Some(width) = self.known.get(grapheme) { *width } else { self.requested.insert(grapheme.to_string()); - grapheme.width() as u8 + Self::grapheme_width_estimate(grapheme) } } From be7eff0979e0e95d070e7c9cea42c328ffd04cc4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 21 Feb 2025 00:36:39 +0100 Subject: [PATCH 137/144] Bump version to 0.3.1 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e18e88a..d84e1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.1 - 2025-02-21 + ### Fixed - Rendering glitches, mainly related to emoji diff --git a/Cargo.toml b/Cargo.toml index 4083c28..5adf243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] From 423dd100c1360decffc5107ea4757d751ac0f4db Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 23 Feb 2025 17:19:59 +0100 Subject: [PATCH 138/144] Add unicode-based grapheme width estimation method --- CHANGELOG.md | 3 ++ src/terminal.rs | 20 ++++++++++-- src/widthdb.rs | 85 ++++++++++++++++++++++++++++++------------------- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d84e1fe..6292746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- Unicode-based grapheme width estimation method + ## v0.3.1 - 2025-02-21 ### Fixed diff --git a/src/terminal.rs b/src/terminal.rs index 439ce4e..c26b0fc 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -16,7 +16,7 @@ use crossterm::terminal::{ use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::Buffer; -use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod}; /// Wrapper that manages terminal output. /// @@ -112,11 +112,25 @@ impl Terminal { 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.active = active; + self.frame.widthdb.measure = active; } /// Whether grapheme widths should be measured or estimated. @@ -135,7 +149,7 @@ impl Terminal { /// 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.active + self.frame.widthdb.measure } /// Whether any unmeasured graphemes were seen since the last call to diff --git a/src/widthdb.rs b/src/widthdb.rs index 53f20ec..bb21ef6 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -6,14 +6,31 @@ use crossterm::style::Print; use crossterm::terminal::{Clear, ClearType}; use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthChar; +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) active: bool, + pub(crate) estimate: WidthEstimationMethod, + pub(crate) measure: bool, pub(crate) tab_width: u8, known: HashMap, requested: HashSet, @@ -22,7 +39,8 @@ pub struct WidthDb { impl Default for WidthDb { fn default() -> Self { Self { - active: false, + estimate: WidthEstimationMethod::default(), + measure: false, tab_width: 8, known: Default::default(), requested: Default::default(), @@ -36,26 +54,6 @@ impl WidthDb { self.tab_width - (col % self.tab_width as usize) as u8 } - /// Estimate what our terminal emulator thinks the width of a grapheme is. - /// - /// Different terminal emulators are all broken in different ways, so this - /// method will never be able to give a correct solution. For that, the only - /// possible method is actually measuring. - /// - /// Instead, it implements a character-wise width calculation. The hope is - /// that dumb terminal emulators do something roughly like this, and smart - /// terminal emulators try to emulate dumb ones for compatibility. In - /// practice, this counting approach seems to be fairly robust. - fn grapheme_width_estimate(grapheme: &str) -> u8 { - grapheme - .chars() - .filter(|c| !c.is_ascii_control()) - .flat_map(|c| c.width()) - .sum::() - .try_into() - .unwrap_or(u8::MAX) - } - /// Determine the width of a grapheme. /// /// If the grapheme is a tab, the column is used to determine its width. @@ -67,14 +65,37 @@ impl WidthDb { if grapheme == "\t" { return self.tab_width_at_column(col); } - if !self.active { - return Self::grapheme_width_estimate(grapheme); - } - if let Some(width) = self.known.get(grapheme) { - *width - } else { + + if self.measure { + if let Some(width) = self.known.get(grapheme) { + return *width; + } self.requested.insert(grapheme.to_string()); - Self::grapheme_width_estimate(grapheme) + } + + 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 newlines to have a width of 1 + // while the rendering code expects it to have a width of 0. + WidthEstimationMethod::Unicode => grapheme + .split('\n') + .map(|s| s.width()) + .sum::() + .try_into() + .unwrap_or(u8::MAX), } } @@ -107,7 +128,7 @@ impl WidthDb { /// Whether any new graphemes have been seen since the last time /// [`Self::measure_widths`] was called. pub(crate) fn measuring_required(&self) -> bool { - self.active && !self.requested.is_empty() + self.measure && !self.requested.is_empty() } /// Measure the width of all new graphemes that have been seen since the @@ -117,7 +138,7 @@ impl WidthDb { /// 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.active { + if !self.measure { return Ok(()); } for grapheme in self.requested.drain() { From d28ce90ec7590778e6035a7b00b1d85064f03dbf Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 23 Feb 2025 23:31:25 +0100 Subject: [PATCH 139/144] Bump version to 0.3.2 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6292746..0305201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.2 - 2025-02-23 + ### Added - Unicode-based grapheme width estimation method diff --git a/Cargo.toml b/Cargo.toml index 5adf243..af2b654 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.1" +version = "0.3.2" edition = "2021" [dependencies] From 712c1537adbef0db607d39c2320a4d8d0b2a5b36 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Feb 2025 14:29:53 +0100 Subject: [PATCH 140/144] Fix incorrect width estimation of ascii control characters --- CHANGELOG.md | 3 +++ src/widthdb.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0305201..b70bde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Rendering glitches in unicode-based with estimation + ## v0.3.2 - 2025-02-23 ### Added diff --git a/src/widthdb.rs b/src/widthdb.rs index bb21ef6..fe5a26e 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -88,10 +88,10 @@ impl WidthDb { .try_into() .unwrap_or(u8::MAX), - // The unicode width crate considers newlines to have a width of 1 - // while the rendering code expects it to have a width of 0. + // 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('\n') + .split(|c: char| c.is_ascii_control()) .map(|s| s.width()) .sum::() .try_into() From 96b2e13c4a4b0174601d90246d92d148c4230eeb Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Feb 2025 14:30:45 +0100 Subject: [PATCH 141/144] Bump version to 0.3.3 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70bde6..48e4615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.3 - 2025-02-28 + ### Fixed - Rendering glitches in unicode-based with estimation diff --git a/Cargo.toml b/Cargo.toml index af2b654..6868881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.2" +version = "0.3.3" edition = "2021" [dependencies] From 89b4595ed9228df2ce976e4b0630f5dba1208474 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Mar 2025 19:05:38 +0100 Subject: [PATCH 142/144] Print bell character --- CHANGELOG.md | 3 +++ src/frame.rs | 5 +++++ src/terminal.rs | 11 ++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48e4615..c43f3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- `Frame::set_bell` to print a bell character when the frame is displayed + ## v0.3.3 - 2025-02-28 ### Fixed diff --git a/src/frame.rs b/src/frame.rs index 03fbb04..e42ba6b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -8,6 +8,7 @@ pub struct Frame { pub(crate) widthdb: WidthDb, pub(crate) buffer: Buffer, pub(crate) title: Option, + pub(crate) bell: bool, } impl Frame { @@ -48,6 +49,10 @@ impl Frame { self.title = title; } + pub fn set_bell(&mut self, bell: bool) { + self.bell = bell; + } + pub fn widthdb(&mut self) -> &mut WidthDb { &mut self.widthdb } diff --git a/src/terminal.rs b/src/terminal.rs index c26b0fc..07fe686 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -8,7 +8,7 @@ use crossterm::event::{ DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; -use crossterm::style::{PrintStyledContent, StyledContent}; +use crossterm::style::{Print, PrintStyledContent, StyledContent}; use crossterm::terminal::{ BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, @@ -274,6 +274,7 @@ impl Terminal { self.draw_differences()?; self.update_cursor()?; self.update_title()?; + self.ring_bell()?; Ok(()) } @@ -315,4 +316,12 @@ impl Terminal { } Ok(()) } + + fn ring_bell(&mut self) -> io::Result<()> { + if self.frame.bell { + self.out.queue(Print('\x07'))?; + } + self.frame.bell = false; + Ok(()) + } } From e3af509358dd57e24e3073de8a32640d6a3832d5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Mar 2025 19:10:29 +0100 Subject: [PATCH 143/144] Add bell widget --- CHANGELOG.md | 1 + src/widgets.rs | 2 ++ src/widgets/bell.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/widgets/bell.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c43f3dc..c30abdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Procedure when bumping the version number: ### Added - `Frame::set_bell` to print a bell character when the frame is displayed +- `widgets::bell` ## v0.3.3 - 2025-02-28 diff --git a/src/widgets.rs b/src/widgets.rs index 28b44d8..cbbff7c 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,4 +1,5 @@ pub mod background; +pub mod bell; pub mod border; pub mod boxed; pub mod cursor; @@ -16,6 +17,7 @@ pub mod text; pub mod title; pub use background::*; +pub use bell::*; pub use border::*; pub use boxed::*; pub use cursor::*; diff --git a/src/widgets/bell.rs b/src/widgets/bell.rs new file mode 100644 index 0000000..b37fb67 --- /dev/null +++ b/src/widgets/bell.rs @@ -0,0 +1,55 @@ +use crate::{Frame, Size, Widget, WidthDb}; + +/////////// +// State // +/////////// + +#[derive(Debug, Default, Clone)] +pub struct BellState { + // Whether the bell should be rung the next time the widget is displayed. + pub ring: bool, +} + +impl BellState { + pub fn new() -> Self { + Self::default() + } + + pub fn widget(&mut self) -> Bell<'_> { + Bell { state: self } + } +} + +//////////// +// Widget // +//////////// + +#[derive(Debug)] +pub struct Bell<'a> { + state: &'a mut BellState, +} + +impl Bell<'_> { + pub fn state(&mut self) -> &mut BellState { + self.state + } +} + +impl Widget for Bell<'_> { + fn size( + &self, + _widthdb: &mut WidthDb, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(Size::ZERO) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + if self.state.ring { + frame.set_bell(true); + self.state.ring = false + } + Ok(()) + } +} From 57aa8c59308f6f0aa82bde415a42b56c3d6f7c4d Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Mar 2025 19:30:44 +0100 Subject: [PATCH 144/144] Bump version to 0.3.4 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30abdf..bef60bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.4 - 2025-03-8 + ### Added - `Frame::set_bell` to print a bell character when the frame is displayed - `widgets::bell` diff --git a/Cargo.toml b/Cargo.toml index 6868881..22967fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.3" +version = "0.3.4" edition = "2021" [dependencies]