From 82121f1d787bb5252bffeb8fca08b6f942ded9b6 Mon Sep 17 00:00:00 2001 From: Jiayi Zhuang Date: Tue, 19 Aug 2025 07:05:57 +0800 Subject: [PATCH] feat(dom): multi-width unicode character support --- Cargo.lock | 1 + Cargo.toml | 1 + examples/unicode/Cargo.toml | 9 ++++++++ examples/unicode/index.html | 36 ++++++++++++++++++++++++++++++++ examples/unicode/src/main.rs | 40 ++++++++++++++++++++++++++++++++++++ src/backend/dom.rs | 24 ++++++++++++++++------ src/backend/utils.rs | 20 ++++++++++++------ 7 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 examples/unicode/Cargo.toml create mode 100644 examples/unicode/index.html create mode 100644 examples/unicode/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9d1ce321..a78dfa28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,6 +380,7 @@ dependencies = [ "console_error_panic_hook", "ratatui", "thiserror", + "unicode-width 0.2.0", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index eba627e4..fb5afbfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,4 @@ console_error_panic_hook = "0.1.7" thiserror = "2.0.12" bitvec = { version = "1.0.1", default-features = false, features = ["alloc", "std"] } beamterm-renderer = "0.1.1" +unicode-width = "0.2.0" diff --git a/examples/unicode/Cargo.toml b/examples/unicode/Cargo.toml new file mode 100644 index 00000000..2b516941 --- /dev/null +++ b/examples/unicode/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "minimal" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +ratzilla = { path = "../../" } +examples-shared = { path = "../shared" } diff --git a/examples/unicode/index.html b/examples/unicode/index.html new file mode 100644 index 00000000..f8fd79ad --- /dev/null +++ b/examples/unicode/index.html @@ -0,0 +1,36 @@ + + + + + + + + + Ratzilla + + + + + + \ No newline at end of file diff --git a/examples/unicode/src/main.rs b/examples/unicode/src/main.rs new file mode 100644 index 00000000..1750b5c6 --- /dev/null +++ b/examples/unicode/src/main.rs @@ -0,0 +1,40 @@ +use std::io; + +use ratzilla::ratatui::{ + layout::Alignment, + style::Color, + widgets::{Block, Paragraph}, +}; + +use ratzilla::WebRenderer; + +use examples_shared::backend::{BackendType, MultiBackendBuilder}; + +fn main() -> io::Result<()> { + let terminal = MultiBackendBuilder::with_fallback(BackendType::Dom).build_terminal()?; + + terminal.draw_web(move |f| { + f.render_widget( + Paragraph::new( + [ + "Hello, world!", + "你好,世界!", + "世界、こんにちは。", + // "헬로우 월드!", + // "👨💻👋🌐", + ] + .join("\n"), + ) + .alignment(Alignment::Center) + .block( + Block::bordered() + .title("Ratzilla") + .title_alignment(Alignment::Center) + .border_style(Color::Yellow), + ), + f.area(), + ); + }); + + Ok(()) +} diff --git a/src/backend/dom.rs b/src/backend/dom.rs index 2ce52ebd..13f7fed4 100644 --- a/src/backend/dom.rs +++ b/src/backend/dom.rs @@ -6,6 +6,7 @@ use ratatui::{ layout::{Position, Size}, prelude::Backend, }; +use unicode_width::UnicodeWidthStr; use web_sys::{ wasm_bindgen::{prelude::Closure, JsCast}, window, Document, Element, Window, @@ -148,10 +149,13 @@ impl DomBackend { fn prerender(&mut self) -> Result<(), Error> { for line in self.buffer.iter() { let mut line_cells: Vec = Vec::new(); - let mut hyperlink: Vec = Vec::new(); + let mut hyperlink: Vec<(Cell, bool)> = Vec::new(); + let mut skip = 0; for (i, cell) in line.iter().enumerate() { + let overwritten = skip > 0; + skip = std::cmp::max(skip, cell.symbol().width()).saturating_sub(1); if cell.modifier.contains(HYPERLINK_MODIFIER) { - hyperlink.push(cell.clone()); + hyperlink.push((cell.clone(), overwritten)); // If the next cell is not part of the hyperlink, close it if !line .get(i + 1) @@ -159,8 +163,8 @@ impl DomBackend { .unwrap_or(false) { let anchor = create_anchor(&self.document, &hyperlink)?; - for link_cell in &hyperlink { - let span = create_span(&self.document, link_cell)?; + for (link_cell, overwritten) in &hyperlink { + let span = create_span(&self.document, link_cell, *overwritten)?; self.cells.push(span.clone()); anchor.append_child(&span)?; } @@ -168,7 +172,7 @@ impl DomBackend { hyperlink.clear(); } } else { - let span = create_span(&self.document, cell)?; + let span = create_span(&self.document, cell, overwritten)?; self.cells.push(span.clone()); line_cells.push(span); } @@ -192,14 +196,22 @@ impl DomBackend { /// accordingly. fn update_grid(&mut self) -> Result<(), Error> { for (y, line) in self.buffer.iter().enumerate() { + let mut skip = 0; for (x, cell) in line.iter().enumerate() { + let overwritten = skip > 0; + skip = std::cmp::max(skip, cell.symbol().width()).saturating_sub(1); if cell.modifier.contains(HYPERLINK_MODIFIER) { continue; } if cell != &self.prev_buffer[y][x] { let elem = self.cells[y * self.buffer[0].len() + x].clone(); elem.set_inner_html(cell.symbol()); - elem.set_attribute("style", &get_cell_style_as_css(cell))?; + if overwritten { + // If the cell is overwritten, hide it + elem.set_attribute("style", "display: none;")?; + } else { + elem.set_attribute("style", &get_cell_style_as_css(cell))?; + } } } } diff --git a/src/backend/utils.rs b/src/backend/utils.rs index ed9d2565..726f9b57 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -14,23 +14,31 @@ use web_sys::{ }; /// Creates a new `` element with the given cell. -pub(crate) fn create_span(document: &Document, cell: &Cell) -> Result { +pub(crate) fn create_span( + document: &Document, + cell: &Cell, + overwritten: bool, +) -> Result { let span = document.create_element("span")?; span.set_inner_html(cell.symbol()); - let style = get_cell_style_as_css(cell); - span.set_attribute("style", &style)?; + if overwritten { + span.set_attribute("style", "display: none;")?; + } else { + let style = get_cell_style_as_css(cell); + span.set_attribute("style", &style)?; + } Ok(span) } /// Creates a new `` element with the given cells. -pub(crate) fn create_anchor(document: &Document, cells: &[Cell]) -> Result { +pub(crate) fn create_anchor(document: &Document, cells: &[(Cell, bool)]) -> Result { let anchor = document.create_element("a")?; anchor.set_attribute( "href", - &cells.iter().map(|c| c.symbol()).collect::(), + &cells.iter().map(|c| c.0.symbol()).collect::(), )?; - anchor.set_attribute("style", &get_cell_style_as_css(&cells[0]))?; + anchor.set_attribute("style", &get_cell_style_as_css(&cells[0].0))?; Ok(anchor) }