diff --git a/docs/changelog.rst b/docs/changelog.rst index 5e54496a0..149a2e384 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -202,6 +202,8 @@ Detailed list of changes 0.49.0 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Thai/Lao: Render the vowel sign AM (U+0E33/U+0EB3) correctly by widening the cell it combines into, matching wcwidth() based programs + - Support for :doc:`/custom-shaders` for adding various graphical effects (:iss:`10344`) - Various throughput performance improvements for a 15-35% real world improvement depending on workload diff --git a/docs/text-sizing-protocol.rst b/docs/text-sizing-protocol.rst index 360da6c79..83f9ef5a1 100644 --- a/docs/text-sizing-protocol.rst +++ b/docs/text-sizing-protocol.rst @@ -412,7 +412,10 @@ For each decoded code point: #. If there is no boundary, the current code point is added to the previous cell and processing of the code point is finished. See the :ref:`var_select` - section below for handling of Unicode Variation selectors. + section below for handling of Unicode Variation selectors. If the code point has + ``Grapheme_Cluster_Break=SpacingMark`` and a non-zero width and the previous + cell is one cell wide, the previous cell becomes two cells wide. This affects + only :code:`U+0E33 THAI CHARACTER SARA AM` and :code:`U+0EB3 LAO VOWEL SIGN AM`. #. If there is a boundary, but the width of the current code point is zero, it is added to the previous cell and processing is finished. diff --git a/kitty/screen.c b/kitty/screen.c index c8c2c439e..72a4947d8 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1172,6 +1172,32 @@ move_widened_char_past_multiline_chars(Screen *self, text_loop_state *s, CPUCell else init_text_loop_line(self, s); } +static void +widen_cell_to_two(Screen *self, text_loop_state *s, CPUCell *cp, GPUCell *gp, index_type xpos) { + CPUCell *cpu_cell = cp + xpos; + GPUCell *gpu_cell = gp + xpos; + cpu_cell->is_multicell = true; + cpu_cell->width = 2; + cpu_cell->natural_width = true; + if (!cpu_cell->scale) cpu_cell->scale = 1; + if (xpos + 1 < self->columns) { + CPUCell *second = cp + xpos + 1; + if (second->is_multicell) { + if (second->y) { + move_widened_char_past_multiline_chars(self, s, cpu_cell, gpu_cell, xpos, s->prev.y); + return; + } + nuke_multicell_char_at(self, xpos + 1, s->prev.y, false); + } + zero_cells(s, second, gp + xpos + 1); + self->cursor->x++; + *second = *cpu_cell; + second->x = 1; + } else { + move_widened_char_past_multiline_chars(self, s, cpu_cell, gpu_cell, xpos, s->prev.y); + } +} + static bool is_emoji_presentation_base(char_type ch) { return char_props_for(ch).is_emoji_presentation_base == 1; @@ -1187,29 +1213,8 @@ draw_combining_char(Screen *self, text_loop_state *s, char_type ch) { if (!add_combining_char(self, ch, xpos, s->prev.y) || self->lc->count < 2) return; unsigned base_pos = self->lc->count - 2; if (ch == VS16) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji - CPUCell *cpu_cell = cp + xpos; - GPUCell *gpu_cell = gp + xpos; - if (self->lc->chars[base_pos + 1] == VS16 && !cpu_cell->is_multicell && is_emoji_presentation_base(self->lc->chars[base_pos])) { - cpu_cell->is_multicell = true; - cpu_cell->width = 2; - cpu_cell->natural_width = true; - if (!cpu_cell->scale) cpu_cell->scale = 1; - if (xpos + 1 < self->columns) { - CPUCell *second = cp + xpos + 1; - if (second->is_multicell) { - if (second->y) { - move_widened_char_past_multiline_chars(self, s, cpu_cell, gpu_cell, xpos, s->prev.y); - return; - } - nuke_multicell_char_at(self, xpos + 1, s->prev.y, false); - } - zero_cells(s, second, gp + xpos + 1); - self->cursor->x++; - *second = *cpu_cell; - second->x = 1; - } else { - move_widened_char_past_multiline_chars(self, s, cpu_cell, gpu_cell, xpos, s->prev.y); - } + if (self->lc->chars[base_pos + 1] == VS16 && !cp[xpos].is_multicell && is_emoji_presentation_base(self->lc->chars[base_pos])) { + widen_cell_to_two(self, s, cp, gp, xpos); } } else if (ch == VS15) { const CPUCell *cpu_cell = cp + xpos; @@ -1220,6 +1225,10 @@ draw_combining_char(Screen *self, text_loop_state *s, char_type ch) { init_segmentation_state(self, s); } } + } else { + // Thai/Lao SARA AM is a SpacingMark with non-zero width, it widens a narrow base cell + CharProps ch_props = char_props_for(ch); + if (ch_props.grapheme_break == GBP_SpacingMark && wcwidth_std(ch_props) > 0 && !cp[xpos].is_multicell) widen_cell_to_two(self, s, cp, gp, xpos); } } diff --git a/kitty/wcswidth.c b/kitty/wcswidth.c index d758a14bb..4fcaa5f9c 100644 --- a/kitty/wcswidth.c +++ b/kitty/wcswidth.c @@ -49,6 +49,13 @@ wcswidth_step(WCSState *state, const char_type ch) { state->prev_width = 1; } else state->prev_width = 0; break; + default: + // Thai/Lao SARA AM: a SpacingMark with non-zero width widens a narrow base cell + if (cp.grapheme_break == GBP_SpacingMark && wcwidth_std(cp) > 0 && state->prev_width == 1) { + ans = 1; + state->prev_width = 2; + } + break; } break; } diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index bfd92009a..abe82c397 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -578,6 +578,12 @@ class TestDataTypes(BaseTest): self.ae(wcswidth('\U0001f610\ufe0e'), 1) self.ae(wcswidth('\U0001f1e6a'), 3) self.ae(wcswidth('\U0001f1e6a\U0001f1e8a'), 6) + # Thai/Lao SARA AM is a SpacingMark with width 1, it widens the cell it combines into + self.ae(wcswidth('จำ'), 2) + self.ae(wcswidth('ำ'), 1) + self.ae(wcswidth('กิ'), 1) + self.ae(wcswidth('จำำ'), 2) + self.ae(wcswidth('ກຳ'), 2) self.ae(wcswidth('\U0001f1e6\U0001f1e8a'), 3) self.ae(wcswidth('\U0001f1e6\U0001f1e8\U0001f1e6'), 4) self.ae(wcswidth('a\u00adb'), 2) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 8242584a5..fd81a7a86 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1067,6 +1067,38 @@ class TestScreen(BaseTest): s.reset() s.draw('\N{HEAVY EXCLAMATION MARK SYMBOL}' + 4500 * '\N{VARIATION SELECTOR-16}') + def test_spacing_mark_widens_narrow_cell(self): + # Thai SARA AM and Lao AM are SpacingMarks with width 1 that widen the cell they combine into + s = self.create_screen(cols=5) + s.draw('จำ') + self.ae(s.cursor.x, 2) + self.assertTrue(str(s.line(0)).startswith('จำ')) + c = s.cpu_cells(0, 0) + self.assertTrue(c['mcd']) + self.ae(c['mcd']['width'], 2) + self.ae(c['x'], 0) + self.ae(s.cpu_cells(0, 1)['x'], 1) + s.reset() + s.draw('จ') + self.ae(s.cursor.x, 1) + s.draw('ำ') + self.ae(s.cursor.x, 2) + s.reset() + s.draw('จำำ') + self.ae(s.cursor.x, 2) + s.reset() + s.draw('ກຳ') + self.ae(s.cursor.x, 2) + s.reset() + s.draw('กิ') + self.ae(s.cursor.x, 1) + # widened char on the last column wraps onto the next line + s = self.create_screen(cols=2) + s.draw('aจำ') + self.ae((s.cursor.x, s.cursor.y), (2, 1)) + self.ae(str(s.line(0)), 'a') + self.ae(str(s.line(1)), 'จำ') + def test_writing_with_cursor_on_trailer_of_wide_character(self): s = self.create_screen() diff --git a/tools/wcswidth/wcswidth.go b/tools/wcswidth/wcswidth.go index 332ac08f6..bc736ed4a 100644 --- a/tools/wcswidth/wcswidth.go +++ b/tools/wcswidth/wcswidth.go @@ -84,6 +84,12 @@ func (self *WCWidthIterator) handle_rune(ch rune) error { self.current_width -= 1 self.prev_width = 1 } + default: + // Thai/Lao SARA AM: a SpacingMark with non-zero width widens a narrow base cell + if cp.Grapheme_break() == uint8(GBP_SpacingMark) && cp.Width() > 0 && self.prev_width == 1 { + self.current_width += 1 + self.prev_width = 2 + } } } else { width := cp.Width() diff --git a/tools/wcswidth/wcswidth_test.go b/tools/wcswidth/wcswidth_test.go index fd283b89a..3ab81cee5 100644 --- a/tools/wcswidth/wcswidth_test.go +++ b/tools/wcswidth/wcswidth_test.go @@ -36,6 +36,12 @@ func TestWCSWidth(t *testing.T) { wcswidth("\U0001F1E6\U0001F1E8a", 3) wcswidth("\U0001F1E6\U0001F1E8\U0001F1E6", 4) wcswidth("a\u00adb", 2) + // Thai/Lao SARA AM is a SpacingMark with width 1, it widens the cell it combines into + wcswidth("จำ", 2) + wcswidth("ำ", 1) + wcswidth("กิ", 1) + wcswidth("จำำ", 2) + wcswidth("ກຳ", 2) wcswidth("a\x1b[22bcd", 25) // Flags individually and together wcwidth("\U0001f1ee\U0001f1f3", 2, 2)