Style IME pre-edit text with italics and a dashed underline

Pre-edit text was drawn in reverse video, which is a solid inverted block
moving through the line as you compose. Draw it as italic with a dashed
underline in the highlight color instead.

Underline rather than reverse video is what other terminals do: VTE renders the
pre-edit run with the input method's own attributes and marks only the cursored
cell distinctly, and foot underlines the run explicitly, excluding the part
under the cursor. Dashed avoids the underline styles applications already use:
curly for spell checking and straight for hyperlinks, which is the collision
that made reverse video look like the safer choice.

The styling is applied to the overlay's own cursor and restored afterwards,
alongside the wrap, cursor-visibility and insert-replace modes already saved
there. The previous code toggled reverse with XOR, which restored itself; the
three new attributes are saved and put back explicitly.

Closes #10382
This commit is contained in:
bjohas 2026-08-22 07:22:55 +01:00
parent 17d2814b4d
commit dbabd12ba6
2 changed files with 44 additions and 2 deletions

View file

@ -4785,7 +4785,24 @@ screen_draw_overlay_line(Screen *self) {
self->modes.mIRM = false;
Cursor *orig_cursor = self->cursor;
self->cursor = &(self->overlay_line.original_line.cursor);
self->cursor->sgr.reverse ^= true;
// Mark the pre-edit text as distinct from committed text: italic, with a
// dashed underline in the highlight color. Underline rather than reverse
// video matches what other terminals do (VTE and foot both underline), and
// dashed avoids colliding with the styles applications already use: curly
// for spell checking and straight for hyperlinks. Saved and restored around
// the draw, like the modes above.
const bool orig_italic = self->cursor->sgr.italic;
const uint8_t orig_decoration = self->cursor->sgr.decoration;
const color_type orig_decoration_fg = self->cursor->sgr.decoration_fg;
self->cursor->sgr.italic = true;
self->cursor->sgr.decoration = 5; // dashed
self->cursor->sgr.decoration_fg =
((colorprofile_to_color_with_fallback(self->color_profile, self->color_profile->overridden.highlight_bg,
self->color_profile->configured.highlight_bg, self->color_profile->overridden.default_fg,
self->color_profile->configured.default_fg)
& COL_MASK)
<< 8)
| 2;
self->cursor->x = xstart;
self->cursor->y = self->overlay_line.ynum;
self->overlay_line.xnum = 0;
@ -4837,7 +4854,9 @@ screen_draw_overlay_line(Screen *self) {
self->overlay_line.xnum += len;
}
self->overlay_line.cursor_x = self->cursor->x;
self->cursor->sgr.reverse ^= true;
self->cursor->sgr.italic = orig_italic;
self->cursor->sgr.decoration = orig_decoration;
self->cursor->sgr.decoration_fg = orig_decoration_fg;
self->cursor = orig_cursor;
self->modes.mDECAWM = orig_line_wrap_mode;
self->modes.mDECTCEM = orig_cursor_enable_mode;

View file

@ -2119,6 +2119,29 @@ class TestScreen(BaseTest):
s.reset()
self.assertTrue(s.is_main_linebuf())
def test_ime_preedit_styling(self):
# Pre-edit text is marked with italics and a dashed underline in the
# highlight color, rather than reverse video.
s = self.create_screen(cols=10, lines=3)
s.draw('xy')
before = s.line(0).cursor_from(0)
s.test_draw_overlay_line('ab', 0, 0)
line = s.line(0)
for x in (0, 1):
c = line.cursor_from(x)
self.assertTrue(c.italic, f'pre-edit cell {x} is not italic')
self.assertFalse(c.reverse, f'pre-edit cell {x} is in reverse video')
self.ae(c.decoration, 5, f'pre-edit cell {x} is not dashed-underlined')
# decoration_fg is packed as (rgb << 8) | type, type 2 being RGB
self.ae(c.decoration_fg & 0xff, 2)
self.ae(c.decoration_fg >> 8, 0xfffacd) # default selection_background
# the styling is applied to the overlay's own cursor and must be
# restored, not leaked into the screen's cursor
self.assertFalse(s.cursor.italic)
self.ae(s.cursor.decoration, before.decoration)
self.ae(s.cursor.decoration_fg, before.decoration_fg)
def detect_url(self, scale=1):
s = self.create_screen(cols=30 * scale)