From 9c7369f54d83e6746fe8bb00fca0fa04c258e055 Mon Sep 17 00:00:00 2001 From: Ratazzi Date: Mon, 6 Oct 2025 16:02:27 +0800 Subject: [PATCH] terminal: Fix rendering of zero-width combining characters (#39526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for rendering Unicode combining characters (diacritics) in the terminal's batched text runs. - Add append_zero_width_chars() to handle combining marks - Integrate zero-width chars into all batching code paths - Update cell extras tracking logic - Add test for combining character rendering Fixes display of é, ñ, ô and other diacritics. Closes #39525 Release Notes: - Fixed: NFD/NFKD normalized text (e.g., é as e + ◌́) not rendering in integrated terminal Before: SCR-20251004-udnj After: SCR-20251004-ulsw --- crates/terminal_view/src/terminal_element.rs | 59 ++++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 91c2b3443f7b4b5ba84b2199a19dbbcfdc46df7c..e06e8e9c63633746f336d308614ed9963740c4f8 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -114,8 +114,20 @@ impl BatchedTextRun { } fn append_char(&mut self, c: char) { + self.append_char_internal(c, true); + } + + fn append_zero_width_chars(&mut self, chars: &[char]) { + for &c in chars { + self.append_char_internal(c, false); + } + } + + fn append_char_internal(&mut self, c: char, counts_cell: bool) { self.text.push(c); - self.cell_count += 1; + if counts_cell { + self.cell_count += 1; + } self.style.len += c.len_utf8(); } @@ -380,7 +392,8 @@ impl TerminalElement { continue; } // Update tracking for next iteration - previous_cell_had_extras = cell.extra.is_some(); + previous_cell_had_extras = + matches!(cell.zerowidth(), Some(chars) if !chars.is_empty()); //Layout current cell text { @@ -397,6 +410,7 @@ impl TerminalElement { ); let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32); + let zero_width_chars = cell.zerowidth(); // Try to batch with existing run if let Some(ref mut batch) = current_batch { @@ -406,25 +420,36 @@ impl TerminalElement { == cell_point.column { batch.append_char(cell.c); + if let Some(chars) = zero_width_chars { + batch.append_zero_width_chars(chars); + } } else { // Flush current batch and start new one let old_batch = current_batch.take().unwrap(); batched_runs.push(old_batch); - current_batch = Some(BatchedTextRun::new_from_char( + let mut new_batch = BatchedTextRun::new_from_char( cell_point, cell.c, cell_style, text_style.font_size, - )); + ); + if let Some(chars) = zero_width_chars { + new_batch.append_zero_width_chars(chars); + } + current_batch = Some(new_batch); } } else { // Start new batch - current_batch = Some(BatchedTextRun::new_from_char( + let mut new_batch = BatchedTextRun::new_from_char( cell_point, cell.c, cell_style, text_style.font_size, - )); + ); + if let Some(chars) = zero_width_chars { + new_batch.append_zero_width_chars(chars); + } + current_batch = Some(new_batch); } }; } @@ -1913,6 +1938,28 @@ mod tests { assert_eq!(batch.style.len, 6); // 1 + 1 + 4 bytes for emoji } + #[test] + fn test_batched_text_run_append_zero_width_char() { + let style = TextRun { + len: 1, + font: font("Helvetica"), + color: Hsla::red(), + background_color: None, + underline: None, + strikethrough: None, + }; + + let font_size = AbsoluteLength::Pixels(px(12.0)); + let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); + + let combining = '\u{0301}'; + batch.append_zero_width_chars(&[combining]); + + assert_eq!(batch.text, format!("x{}", combining)); + assert_eq!(batch.cell_count, 1); + assert_eq!(batch.style.len, 1 + combining.len_utf8()); + } + #[test] fn test_background_region_can_merge() { let color1 = Hsla::red();