mirror of
https://github.com/wez/wezterm.git
synced 2024-12-29 00:21:57 +03:00
Basic handling of double and zero width graphemes
This commit is contained in:
parent
5c76670acd
commit
4640b735c4
@ -78,6 +78,10 @@ impl<'a> Widget for MainScreen<'a> {
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
fn get_size_constraints(&self) -> layout::Constraints {
|
||||
layout::Constraints::with_fixed_width_height(80, 24)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
|
11
src/cell.rs
11
src/cell.rs
@ -190,6 +190,12 @@ impl Cell {
|
||||
return;
|
||||
}
|
||||
|
||||
if text.as_slice() == &[b'\r', b'\n'] {
|
||||
text.remove(1);
|
||||
text[0] = b' ';
|
||||
return;
|
||||
}
|
||||
|
||||
if text.len() != 1 {
|
||||
return;
|
||||
}
|
||||
@ -208,6 +214,7 @@ impl Cell {
|
||||
storage.resize(len, 0);
|
||||
text.encode_utf8(&mut storage);
|
||||
Self::nerf_control_char(&mut storage);
|
||||
|
||||
Self {
|
||||
text: storage,
|
||||
attrs,
|
||||
@ -223,7 +230,6 @@ impl Cell {
|
||||
/// graphemes.
|
||||
pub fn new_grapheme(text: &str, attrs: CellAttributes) -> Self {
|
||||
let mut storage = SmallVec::from_slice(text.as_bytes());
|
||||
|
||||
Self::nerf_control_char(&mut storage);
|
||||
|
||||
Self {
|
||||
@ -239,6 +245,7 @@ impl Cell {
|
||||
unsafe { std::str::from_utf8_unchecked(&self.text) }
|
||||
}
|
||||
|
||||
/// Returns the number of cells visually occupied by this grapheme
|
||||
pub fn width(&self) -> usize {
|
||||
UnicodeWidthStr::width(self.str())
|
||||
}
|
||||
@ -277,7 +284,7 @@ mod test {
|
||||
assert_eq!(cell.str(), " ");
|
||||
}
|
||||
|
||||
for g in &["", " ", "\n", "\r", "\t"] {
|
||||
for g in &["", " ", "\n", "\r", "\t", "\r\n"] {
|
||||
let cell = Cell::new_grapheme(g, CellAttributes::default());
|
||||
assert_eq!(cell.str(), " ");
|
||||
}
|
||||
|
115
src/surface.rs
115
src/surface.rs
@ -122,6 +122,61 @@ impl Line {
|
||||
self.cells.resize(width, Cell::default());
|
||||
}
|
||||
|
||||
/// If we're about to modify a cell obscured by a double-width
|
||||
/// character ahead of that cell, we need to nerf that sequence
|
||||
/// of cells to avoid partial rendering concerns.
|
||||
/// Similarly, when we assign a cell, we need to blank out those
|
||||
/// occluded successor cells.
|
||||
/// Note that an invalid index will be silently ignored; attempting
|
||||
/// to assign to an out of bounds index will not extend the cell array,
|
||||
/// and it will not flag an error.
|
||||
fn set_cell(&mut self, idx: usize, cell: Cell) {
|
||||
// Assumption: that the width of a grapheme is never > 2.
|
||||
// This constrains the amount of look-back that we need to do here.
|
||||
if idx > 0 {
|
||||
let prior = idx - 1;
|
||||
let width = self.cells[prior].width();
|
||||
if width > 1 {
|
||||
let attrs = self.cells[prior].attrs().clone();
|
||||
for nerf in prior..prior + width {
|
||||
self.cells[nerf] = Cell::new(' ', attrs.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For double-wide or wider chars, ensure that the cells that
|
||||
// are overlapped by this one are blanked out.
|
||||
let width = cell.width();
|
||||
for i in 1..=width.saturating_sub(1) {
|
||||
self.cells
|
||||
.get_mut(idx + i)
|
||||
.map(|target| *target = Cell::new(' ', cell.attrs().clone()));
|
||||
}
|
||||
|
||||
self.cells.get_mut(idx).map(|target| *target = cell);
|
||||
}
|
||||
|
||||
/// Iterates the visible cells, respecting the width of the cell.
|
||||
/// For instance, a double-width cell overlaps the following (blank)
|
||||
/// cell, so that blank cell is omitted from the iterator results.
|
||||
/// The iterator yields (column_index, Cell). Column index is the
|
||||
/// index into Self::cells, and due to the possibility of skipping
|
||||
/// the characters that follow wide characters, the column index may
|
||||
/// skip some positions. It is returned as a convenience to the consumer
|
||||
/// as using .enumerate() on this iterator wouldn't be as useful.
|
||||
fn visible_cells(&self) -> impl Iterator<Item = (usize, &Cell)> {
|
||||
let mut skip_width = 0;
|
||||
self.cells.iter().enumerate().filter(move |(_idx, cell)| {
|
||||
if skip_width > 0 {
|
||||
skip_width -= 1;
|
||||
false
|
||||
} else {
|
||||
skip_width = cell.width().saturating_sub(1);
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Given a starting attribute value, produce a series of Change
|
||||
/// entries to recreate the current line
|
||||
fn changes(&self, start_attr: &CellAttributes) -> Vec<Change> {
|
||||
@ -129,7 +184,7 @@ impl Line {
|
||||
let mut attr = start_attr.clone();
|
||||
let mut text_run = String::new();
|
||||
|
||||
for cell in &self.cells {
|
||||
for (_, cell) in self.visible_cells() {
|
||||
if *cell.attrs() == attr {
|
||||
text_run.push_str(cell.str());
|
||||
} else {
|
||||
@ -346,7 +401,11 @@ impl Surface {
|
||||
.set_background(color.clone())
|
||||
.clone();
|
||||
let cleared = Cell::new(' ', self.attributes.clone());
|
||||
for cell in self.lines[self.ypos].cells.iter_mut().skip(self.xpos) {
|
||||
// Ensure that we have the opportunity to fixup any damage to double-wide
|
||||
// chararcters that overlap the start of the current position.
|
||||
self.lines[self.ypos].set_cell(self.xpos, cleared.clone());
|
||||
// Now we can safely directly poke all remaining cells with a space
|
||||
for cell in self.lines[self.ypos].cells.iter_mut().skip(self.xpos + 1) {
|
||||
*cell = cleared.clone();
|
||||
}
|
||||
for line in &mut self.lines.iter_mut().skip(self.ypos + 1) {
|
||||
@ -361,7 +420,10 @@ impl Surface {
|
||||
.set_background(color.clone())
|
||||
.clone();
|
||||
let cleared = Cell::new(' ', self.attributes.clone());
|
||||
for cell in self.lines[self.ypos].cells.iter_mut().skip(self.xpos) {
|
||||
// Ensure that we have the opportunity to fixup any damage to double-wide
|
||||
// chararcters that overlap the start of the current position.
|
||||
self.lines[self.ypos].set_cell(self.xpos, cleared.clone());
|
||||
for cell in self.lines[self.ypos].cells.iter_mut().skip(self.xpos + 1) {
|
||||
*cell = cleared.clone();
|
||||
}
|
||||
}
|
||||
@ -409,12 +471,20 @@ impl Surface {
|
||||
self.xpos = 0;
|
||||
}
|
||||
|
||||
self.lines[self.ypos].cells[self.xpos] = Cell::new_grapheme(g, self.attributes.clone());
|
||||
let cell = Cell::new_grapheme(g, self.attributes.clone());
|
||||
// the max(1) here is to ensure that we advance to the next cell
|
||||
// position for zero-width graphemes. We want to make sure that
|
||||
// they occupy a cell so that we can re-emit them when we output them.
|
||||
// If we didn't do this, then we'd effectively filter them out from
|
||||
// the model, which seems like a lossy design choice.
|
||||
let mut width = cell.width().max(1);
|
||||
|
||||
self.lines[self.ypos].set_cell(self.xpos, cell);
|
||||
|
||||
// Increment the position now; we'll defer processing
|
||||
// wrapping until the next printed character, otherwise
|
||||
// we'll eagerly scroll when we reach the right margin.
|
||||
self.xpos += 1;
|
||||
self.xpos += width;
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,7 +531,7 @@ impl Surface {
|
||||
let mut s = String::new();
|
||||
|
||||
for line in &self.lines {
|
||||
for cell in &line.cells {
|
||||
for (_, cell) in line.visible_cells() {
|
||||
s.push_str(cell.str());
|
||||
}
|
||||
s.push('\n');
|
||||
@ -717,12 +787,10 @@ impl Surface {
|
||||
.take_while(|(row_num, _)| *row_num < y + height)
|
||||
.zip(other.lines.iter().skip(other_y))
|
||||
{
|
||||
for ((col_num, cell), other_cell) in line.cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
for ((col_num, cell), (_, other_cell)) in line.visible_cells()
|
||||
.skip(x)
|
||||
.take_while(|(col_num, _)| *col_num < x + width)
|
||||
.zip(other_line.cells.iter().skip(other_x))
|
||||
.zip(other_line.visible_cells().skip(other_x))
|
||||
{
|
||||
if cell != other_cell {
|
||||
cursor = match cursor.take() {
|
||||
@ -1475,4 +1543,31 @@ mod test {
|
||||
azaa\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_width() {
|
||||
let mut s = Surface::new(4, 1);
|
||||
s.add_change("🤷12");
|
||||
assert_eq!(s.screen_chars_to_string(), "🤷12\n");
|
||||
s.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(1),
|
||||
y: Position::Absolute(0),
|
||||
});
|
||||
s.add_change("a🤷");
|
||||
assert_eq!(s.screen_chars_to_string(), " a🤷\n");
|
||||
s.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(2),
|
||||
y: Position::Absolute(0),
|
||||
});
|
||||
s.add_change("x");
|
||||
assert_eq!(s.screen_chars_to_string(), " ax \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_width() {
|
||||
let mut s = Surface::new(4, 1);
|
||||
// https://en.wikipedia.org/wiki/Zero-width_space
|
||||
s.add_change("A\u{200b}B");
|
||||
assert_eq!(s.screen_chars_to_string(), "A\u{200b}B \n");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user