makepad/code_editor/src/char.rs
Alex cd0667136d fix(code_editor): honour East Asian Wide width in column_count (#1048)
`CharExt::column_count` was hard-coded to return 1 for every char. The
code editor's layouter advances x-position by this value per grapheme
(see `code_editor.rs:1217`), so CJK glyphs — which the text shaper draws
at ~2× the Latin monospace advance — overlap one another. Cursor
placement, selection rectangles, and wrap points suffer the same
off-by-half because they all read from `column_count`.

Match the Unicode East Asian Width property so Wide and Fullwidth
characters (plus common emoji that render at double-width) report 2
columns. Keeps a small literal match table instead of pulling in the
\`unicode-width\` crate, since only broad blocks are needed and perf on
the layout hot path matters.

Ranges covered:
  U+3000..U+30FF   CJK punctuation / Hiragana / Katakana
  U+3400..U+4DBF   CJK Unified Ideographs Extension A
  U+4E00..U+9FFF   CJK Unified Ideographs
  U+AC00..U+D7AF   Hangul Syllables
  U+F900..U+FAFF   CJK Compatibility Ideographs
  U+FF00..U+FF60   Fullwidth forms
  U+FFE0..U+FFE6   Fullwidth sign forms
  U+20000..U+2FFFF CJK Unified Ideographs Extensions B..F
  U+1F300..U+1F9FF Emoticons / symbols / transport / supplemental

Effect: Chinese/Japanese/Korean/emoji in code blocks render with correct
spacing in CodeView (and in any widget that layouts via CodeSession).
Latin-only workflows are unaffected.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 22:00:54 +02:00

61 lines
1.9 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pub trait CharExt {
fn is_opening_delimiter(self) -> bool;
fn is_closing_delimiter(self) -> bool;
fn column_count(self) -> usize;
fn opposite_delimiter(&self) -> Option<char>;
}
impl CharExt for char {
fn is_opening_delimiter(self) -> bool {
match self {
'(' | '[' | '{' => true,
_ => false,
}
}
fn is_closing_delimiter(self) -> bool {
match self {
')' | ']' | '}' => true,
_ => false,
}
}
fn column_count(self) -> usize {
// Unicode East Asian Width: characters in East Asian Wide or Fullwidth
// categories occupy two display columns in a monospace grid. Without
// this, CJK text in the code editor overlaps because each glyph draws
// at 2× advance but the layouter only reserves 1 column.
match self as u32 {
// CJK Symbols and Punctuation, Hiragana, Katakana
0x3000..=0x30FF
// CJK Unified Ideographs Extension A
| 0x3400..=0x4DBF
// CJK Unified Ideographs (main block)
| 0x4E00..=0x9FFF
// Hangul Syllables
| 0xAC00..=0xD7AF
// CJK Compatibility Ideographs
| 0xF900..=0xFAFF
// Fullwidth forms + Halfwidth/Fullwidth punctuation
| 0xFF00..=0xFF60
| 0xFFE0..=0xFFE6
// CJK Unified Ideographs Extensions B..F
| 0x20000..=0x2FFFF
// Emoticons, misc symbols & pictographs, transport, supplemental symbols
| 0x1F300..=0x1F9FF => 2,
_ => 1,
}
}
fn opposite_delimiter(&self) -> Option<char> {
Some(match self {
'(' => ')',
')' => '(',
'[' => ']',
']' => '[',
'{' => '}',
'}' => '{',
_ => return None,
})
}
}