Skip to content

fix: count display units as UTF-16 code units on macOS#583

Merged
trypsynth merged 1 commit into
trypsynth:masterfrom
mikolysz:display-units
Jul 8, 2026
Merged

fix: count display units as UTF-16 code units on macOS#583
trypsynth merged 1 commit into
trypsynth:masterfrom
mikolysz:display-units

Conversation

@mikolysz

@mikolysz mikolysz commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

display_len/ch_width define the "display unit" for all document offsets the core hands to the UI (tables, separators, navigation targets), so they must match the expectations of the platform text control those offsets are fed into via SetInsertionPoint/SetSelection.

Certain text controls (GTK) use Unicode codepoints as their notion of a character / "display unit". Those are equivalent to Rust chars. Other platforms (Windows and MacOS) use UTF-16 code units.

Higher codepoints (those from Unicode planes 1-16, also called Astral or BMP, notably including Emoji), must be expressed with 2 UTF-16 code units.

On macOS, the text control is backed by NSTextView, and wxWidgets passes NSRange values through unconverted (wxNSTextViewControl::GetSelection/ SetSelection in src/osx/cocoa/textctrl.mm; GetLastPosition is [NSString length]), so positions are UTF-16 code units exactly as on Windows. Verified three ways: by reading the vendored wxWidgets source through every layer including the wxdragon passthrough, empirically with a throwaway wxdragon probe on macOS — get_last_position of "A\u{1D11E}B" returns 4 (UTF-16), not 3 (chars), and with a human test, which indeed showed that navigation commands move to the wrong document position if Emojis are present and this patch is not applied.

Counting chars() on macOS therefore made every offset after an astral character (emoji, musical symbols, some CJK) undershoot by one unit per such character. Windows and macOS now share the UTF-16 arm; GTK, which indexes GtkTextIter offsets by Unicode characters, keeps chars().

This also makes the two {xml,html}_table_display_length tests pass unmodified on macOS: their hardcoded expectations asserted the correct UTF-16 semantics all along, and the macOS implementation was the bug.

Side finding, not addressed here: wxTextEntry::GetStringSelection on macOS mixes UTF-16 positions into char-indexed wxString::Mid, so it returns wrong text after astral characters (upstream wx bug). The Android app similarly indexes Kotlin strings (UTF-16) with core offsets, which remain char-based on Android.

display_len/ch_width define the "display unit" for all document
offsets the core hands to the UI (tables, separators, navigation
targets), so they must match the expectations of the platform text
control those offsets are fed into via SetInsertionPoint/SetSelection.

Certain text controls (GTK) use Unicode codepoints as their notion
of a character / "display unit". Those are equivalent to Rust
chars. Other platforms (Windows and MacOS) use UTF-16 code units.

Higher codepoints (those from Unicode planes 1-16, also called Astral
or BMP, notably including Emoji), must be expressed with 2 UTF-16
code units.

On macOS, the text control is backed by NSTextView, and wxWidgets passes
NSRange values through unconverted (wxNSTextViewControl::GetSelection/
SetSelection in src/osx/cocoa/textctrl.mm; GetLastPosition is
[NSString length]), so positions are UTF-16 code units exactly as on
Windows. Verified three ways: by reading the vendored wxWidgets source
through every layer including the wxdragon passthrough,
empirically with a throwaway wxdragon probe on macOS —
get_last_position of "A\u{1D11E}B" returns 4 (UTF-16), not 3 (chars),
and with a human test, which indeed showed that navigation commands
move to the wrong document position if Emojis are present and
this patch is not applied.

Counting chars() on macOS therefore made every offset after an astral
character (emoji, musical symbols, some CJK) undershoot by one unit
per such character. Windows and macOS now share the UTF-16 arm; GTK,
which indexes GtkTextIter offsets by Unicode characters, keeps
chars().

This also makes the two {xml,html}_table_display_length tests pass
unmodified on macOS: their hardcoded expectations asserted the
correct UTF-16 semantics all along, and the macOS implementation was
the bug.

Side finding, not addressed here: wxTextEntry::GetStringSelection on
macOS mixes UTF-16 positions into char-indexed wxString::Mid, so it
returns wrong text after astral characters (upstream wx bug). The
Android app similarly indexes Kotlin strings (UTF-16) with core
offsets, which remain char-based on Android.
@trypsynth trypsynth merged commit e0e1af4 into trypsynth:master Jul 8, 2026
5 checks passed
@mikolysz mikolysz deleted the display-units branch July 9, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants