Skip to content

Commit

Permalink
Fix positioning of double-width characters (CJK, emojis)
Browse files Browse the repository at this point in the history
  • Loading branch information
ku1ik committed Oct 15, 2024
1 parent d6ac23f commit b090efa
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 81 deletions.
5 changes: 2 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 5 additions & 9 deletions src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ use rgb::{RGB8, RGBA8};
use crate::theme::Theme;

pub trait Renderer {
fn render(
&mut self,
lines: Vec<Vec<(char, avt::Pen)>>,
cursor: Option<(usize, usize)>,
) -> ImgVec<RGBA8>;
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8>;
fn pixel_size(&self) -> (usize, usize);
}

Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 15 additions & 12 deletions src/renderer/fontdue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,24 +167,24 @@ fn mix_colors(fg: RGBA8, bg: RGBA8, ratio: u8) -> RGBA8 {
}

impl Renderer for FontdueRenderer {
fn render(
&mut self,
lines: Vec<Vec<(char, avt::Pen)>>,
cursor: Option<(usize, usize)>,
) -> ImgVec<RGBA8> {
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8> {
let mut buf: Vec<RGBA8> =
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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -256,6 +257,8 @@ impl Renderer for FontdueRenderer {
buf[idx] = mix_colors(fg, bg, v);
}
}

col += cell.width();
}
}

Expand Down
46 changes: 21 additions & 25 deletions src/renderer/resvg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,15 @@ impl ResvgRenderer {
"</svg></svg>"
}

fn push_lines(
&self,
svg: &mut String,
lines: Vec<Vec<(char, avt::Pen)>>,
cursor: Option<(usize, usize)>,
) {
fn push_lines(&self, svg: &mut String, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) {
self.push_background(svg, &lines, cursor);
self.push_text(svg, &lines, cursor);
}

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;
Expand All @@ -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#"<rect x="{:.3}%" y="{:.3}%" width="{:.3}%" height="{:.3}" style="{}" />"#,
x, y, self.char_width, self.row_height, style
x, y, width, self.row_height, style
);

col += cell.width();
}
}

svg.push_str("</g>");
}

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#"<text class="default-text-fill">"#);
Expand All @@ -188,13 +183,17 @@ impl ResvgRenderer {
let mut did_dy = false;

let _ = write!(svg, r#"<tspan y="{y:.3}%">"#);
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("<tspan ");

Expand Down Expand Up @@ -231,11 +230,12 @@ impl ResvgRenderer {
}

_ => {
svg.push(*ch);
svg.push(ch);
}
}

svg.push_str("</tspan>");
col += cell.width();
}

svg.push_str("</tspan>");
Expand All @@ -246,11 +246,7 @@ impl ResvgRenderer {
}

impl Renderer for ResvgRenderer {
fn render(
&mut self,
lines: Vec<Vec<(char, avt::Pen)>>,
cursor: Option<(usize, usize)>,
) -> ImgVec<RGBA8> {
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8> {
let mut svg = self.header.clone();
self.push_lines(&mut svg, lines, cursor);
svg.push_str(Self::footer());
Expand Down
42 changes: 11 additions & 31 deletions src/vt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use log::debug;
pub fn frames(
stdout: impl Iterator<Item = (f64, String)>,
terminal_size: (usize, usize),
) -> impl Iterator<Item = (f64, Vec<Vec<(char, avt::Pen)>>, Option<(usize, usize)>)> {
) -> impl Iterator<Item = (f64, Vec<avt::Line>, Option<(usize, usize)>)> {
let mut vt = avt::Vt::builder()
.size(terminal_size.0, terminal_size.1)
.scrollback_limit(0)
Expand All @@ -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 {
Expand Down Expand Up @@ -50,42 +45,27 @@ mod tests {
assert_eq!(fs.len(), 3);

let (time, lines, cursor) = &fs[0];
let lines: Vec<String> = 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<String> = 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<String> = 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! ");
}
}

0 comments on commit b090efa

Please sign in to comment.