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.
This commit is contained in:
Joscha 2022-05-22 20:41:55 +02:00
parent 67f8919630
commit fe424b3376
2 changed files with 153 additions and 21 deletions

View file

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

View file

@ -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,