terminal: Fix rendering of zero-width combining characters (#39526)

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:

<img width="874" height="688" alt="SCR-20251004-udnj"
src="https://github.com/user-attachments/assets/8d9f9c9f-dac4-4382-92c2-8b6c1d817abd"
/>

After:

<img width="873" height="686" alt="SCR-20251004-ulsw"
src="https://github.com/user-attachments/assets/fbd5cdc7-fdd6-44dc-8b05-cc425644f1a0"
/>
This commit is contained in:
Ratazzi
2025-10-06 10:02:27 +02:00
committed by GitHub
parent 5160510ed0
commit 9c7369f54d
+53 -6
View File
@@ -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();