From b090efa51c2429932646346d8c01aee4c21b5225 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Tue, 15 Oct 2024 10:16:43 +0200 Subject: [PATCH] Fix positioning of double-width characters (CJK, emojis) --- Cargo.lock | 5 ++--- Cargo.toml | 2 +- src/renderer.rs | 14 +++++-------- src/renderer/fontdue.rs | 27 +++++++++++++----------- src/renderer/resvg.rs | 46 +++++++++++++++++++---------------------- src/vt.rs | 42 ++++++++++--------------------------- 6 files changed, 55 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f23e012..40ab0b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,12 +152,11 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "avt" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c67dc0145986662262f8b0f0b791ec6de6b9fcd078f55fa2b31ac691516505" +checksum = "b485f400d02970694eed10e7080f994ad82eaf56a867d6671af5d5e184ed8ee6" dependencies = [ "rgb", - "serde", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index 818ca08..f7bb9ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] anyhow = "1" -avt = "0.13.0" +avt = "0.14.0" clap = { version = "3.2.15", features = ["derive"] } env_logger = "0.10" fontdb = "0.10" diff --git a/src/renderer.rs b/src/renderer.rs index 66f5d7d..82754c4 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -7,11 +7,7 @@ use rgb::{RGB8, RGBA8}; use crate::theme::Theme; pub trait Renderer { - fn render( - &mut self, - lines: Vec>, - cursor: Option<(usize, usize)>, - ) -> ImgVec; + fn render(&mut self, lines: Vec, cursor: Option<(usize, usize)>) -> ImgVec; fn pixel_size(&self) -> (usize, usize); } @@ -41,15 +37,15 @@ struct TextAttrs { } fn text_attrs( - pen: &mut avt::Pen, + pen: &avt::Pen, cursor: &Option<(usize, usize)>, - x: usize, - y: usize, + col: usize, + row: usize, theme: &Theme, ) -> TextAttrs { let mut foreground = pen.foreground(); let mut background = pen.background(); - let inverse = cursor.map_or(false, |(cx, cy)| cx == x && cy == y); + let inverse = cursor.map_or(false, |cursor| cursor.0 == col && cursor.1 == row); if pen.is_bold() { if let Some(avt::Color::Indexed(n)) = foreground { diff --git a/src/renderer/fontdue.rs b/src/renderer/fontdue.rs index 2530f26..65a5a70 100644 --- a/src/renderer/fontdue.rs +++ b/src/renderer/fontdue.rs @@ -167,24 +167,24 @@ fn mix_colors(fg: RGBA8, bg: RGBA8, ratio: u8) -> RGBA8 { } impl Renderer for FontdueRenderer { - fn render( - &mut self, - lines: Vec>, - cursor: Option<(usize, usize)>, - ) -> ImgVec { + fn render(&mut self, lines: Vec, cursor: Option<(usize, usize)>) -> ImgVec { let mut buf: Vec = vec![self.theme.background.alpha(255); self.pixel_width * self.pixel_height]; + let margin_l = self.col_width; let margin_t = (self.row_height / 2.0).round() as usize; - for (row, chars) in lines.iter().enumerate() { + for (row, line) in lines.iter().enumerate() { let y_t = margin_t + (row as f64 * self.row_height).round() as usize; let y_b = margin_t + ((row + 1) as f64 * self.row_height).round() as usize; + let mut col = 0; - for (col, (ch, mut pen)) in chars.iter().enumerate() { + for cell in line.cells() { + let ch = cell.char(); let x_l = (margin_l + col as f64 * self.col_width).round() as usize; - let x_r = (margin_l + (col + 1) as f64 * self.col_width).round() as usize; - let attrs = text_attrs(&mut pen, &cursor, col, row, &self.theme); + let x_r = + (margin_l + (col + cell.width()) as f64 * self.col_width).round() as usize; + let attrs = text_attrs(cell.pen(), &cursor, col, row, &self.theme); if let Some(c) = attrs.background { let c = color_to_rgb(&c, &self.theme); @@ -214,12 +214,13 @@ impl Renderer for FontdueRenderer { } } - if ch == &' ' { + if ch == ' ' { + col += cell.width(); continue; } - self.ensure_glyph(*ch, attrs.bold, attrs.italic); - let glyph = self.get_glyph(*ch, attrs.bold, attrs.italic); + self.ensure_glyph(ch, attrs.bold, attrs.italic); + let glyph = self.get_glyph(ch, attrs.bold, attrs.italic); if glyph.is_none() { continue; @@ -256,6 +257,8 @@ impl Renderer for FontdueRenderer { buf[idx] = mix_colors(fg, bg, v); } } + + col += cell.width(); } } diff --git a/src/renderer/resvg.rs b/src/renderer/resvg.rs index 5a901b5..e8b4284 100644 --- a/src/renderer/resvg.rs +++ b/src/renderer/resvg.rs @@ -129,12 +129,7 @@ impl ResvgRenderer { "" } - fn push_lines( - &self, - svg: &mut String, - lines: Vec>, - cursor: Option<(usize, usize)>, - ) { + fn push_lines(&self, svg: &mut String, lines: Vec, cursor: Option<(usize, usize)>) { self.push_background(svg, &lines, cursor); self.push_text(svg, &lines, cursor); } @@ -142,7 +137,7 @@ impl ResvgRenderer { fn push_background( &self, svg: &mut String, - lines: &[Vec<(char, avt::Pen)>], + lines: &[avt::Line], cursor: Option<(usize, usize)>, ) { let (cols, rows) = self.terminal_size; @@ -151,34 +146,34 @@ impl ResvgRenderer { for (row, line) in lines.iter().enumerate() { let y = 100.0 * (row as f64) / (rows as f64 + 1.0); + let mut col = 0; - for (col, (_ch, mut pen)) in line.iter().enumerate() { - let attrs = text_attrs(&mut pen, &cursor, col, row, &self.theme); + for cell in line.cells() { + let attrs = text_attrs(cell.pen(), &cursor, col, row, &self.theme); if attrs.background.is_none() { + col += cell.width(); continue; } let x = 100.0 * (col as f64) / (cols as f64 + 2.0); let style = rect_style(&attrs, &self.theme); + let width = self.char_width * cell.width() as f64; let _ = write!( svg, r#""#, - x, y, self.char_width, self.row_height, style + x, y, width, self.row_height, style ); + + col += cell.width(); } } svg.push_str(""); } - fn push_text( - &self, - svg: &mut String, - lines: &[Vec<(char, avt::Pen)>], - cursor: Option<(usize, usize)>, - ) { + fn push_text(&self, svg: &mut String, lines: &[avt::Line], cursor: Option<(usize, usize)>) { let (cols, rows) = self.terminal_size; svg.push_str(r#""#); @@ -188,13 +183,17 @@ impl ResvgRenderer { let mut did_dy = false; let _ = write!(svg, r#""#); + let mut col = 0; - for (col, (ch, mut pen)) in line.iter().enumerate() { - if ch == &' ' { + for cell in line.cells() { + let ch = cell.char(); + + if ch == ' ' { + col += cell.width(); continue; } - let attrs = text_attrs(&mut pen, &cursor, col, row, &self.theme); + let attrs = text_attrs(cell.pen(), &cursor, col, row, &self.theme); svg.push_str(" { - svg.push(*ch); + svg.push(ch); } } svg.push_str(""); + col += cell.width(); } svg.push_str(""); @@ -246,11 +246,7 @@ impl ResvgRenderer { } impl Renderer for ResvgRenderer { - fn render( - &mut self, - lines: Vec>, - cursor: Option<(usize, usize)>, - ) -> ImgVec { + fn render(&mut self, lines: Vec, cursor: Option<(usize, usize)>) -> ImgVec { let mut svg = self.header.clone(); self.push_lines(&mut svg, lines, cursor); svg.push_str(Self::footer()); diff --git a/src/vt.rs b/src/vt.rs index b9b3edf..f069283 100644 --- a/src/vt.rs +++ b/src/vt.rs @@ -3,7 +3,7 @@ use log::debug; pub fn frames( stdout: impl Iterator, terminal_size: (usize, usize), -) -> impl Iterator>, Option<(usize, usize)>)> { +) -> impl Iterator, Option<(usize, usize)>)> { let mut vt = avt::Vt::builder() .size(terminal_size.0, terminal_size.1) .scrollback_limit(0) @@ -17,12 +17,7 @@ pub fn frames( if !changed_lines.is_empty() || cursor != prev_cursor { prev_cursor = cursor; - - let lines = vt - .view() - .iter() - .map(|line| line.cells().collect()) - .collect(); + let lines = vt.view().to_vec(); Some((time, lines, cursor)) } else { @@ -50,42 +45,27 @@ mod tests { assert_eq!(fs.len(), 3); let (time, lines, cursor) = &fs[0]; + let lines: Vec = lines.iter().map(|l| l.text()).collect(); assert_eq!(*time, 0.0); assert_eq!(*cursor, Some((3, 0))); - assert_eq!(lines[0][0].0, 'f'); - assert_eq!(lines[0][1].0, 'o'); - assert_eq!(lines[0][2].0, 'o'); - assert_eq!(lines[0][3].0, ' '); - assert_eq!(lines[1][0].0, ' '); - assert_eq!(lines[1][1].0, ' '); - assert_eq!(lines[1][2].0, ' '); - assert_eq!(lines[1][3].0, ' '); + assert_eq!(lines[0], "foo "); + assert_eq!(lines[1], " "); let (time, lines, cursor) = &fs[1]; + let lines: Vec = lines.iter().map(|l| l.text()).collect(); assert_eq!(*time, 2.0); assert_eq!(*cursor, Some((2, 1))); - assert_eq!(lines[0][0].0, 'f'); - assert_eq!(lines[0][1].0, 'o'); - assert_eq!(lines[0][2].0, 'o'); - assert_eq!(lines[0][3].0, 'b'); - assert_eq!(lines[1][0].0, 'a'); - assert_eq!(lines[1][1].0, 'r'); - assert_eq!(lines[1][2].0, ' '); - assert_eq!(lines[1][3].0, ' '); + assert_eq!(lines[0], "foob"); + assert_eq!(lines[1], "ar "); let (time, lines, cursor) = &fs[2]; + let lines: Vec = lines.iter().map(|l| l.text()).collect(); assert_eq!(*time, 3.0); assert_eq!(*cursor, Some((3, 1))); - assert_eq!(lines[0][0].0, 'f'); - assert_eq!(lines[0][1].0, 'o'); - assert_eq!(lines[0][2].0, 'o'); - assert_eq!(lines[0][3].0, 'b'); - assert_eq!(lines[1][0].0, 'a'); - assert_eq!(lines[1][1].0, 'r'); - assert_eq!(lines[1][2].0, '!'); - assert_eq!(lines[1][3].0, ' '); + assert_eq!(lines[0], "foob"); + assert_eq!(lines[1], "ar! "); } }