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