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,