Skip to content

fix(ime): render preedit inline and stop Windows key-repeat from stalling redraws - #1557

Open
shiena wants to merge 3 commits into
raphamorim:mainfrom
shiena:ime-improvements
Open

shiena wants to merge 3 commits into
raphamorim:mainfrom
shiena:ime-improvements

Conversation

@shiena

@shiena shiena commented Apr 25, 2026 •

Copy link
Copy Markdown
Contributor
  • Follow-up to Fix IME preedit overlay and add tests #1434: the v4 grid-renderer refactor moved emission into grid_emit.rs, so Fix IME preedit overlay and add tests #1434's per-cell
    create_style patch no longer applies. Re-implemented against the new path: PreeditOverlay is threaded through
    build_row_bg / build_row_fg, composition cells render as a wezterm-style block (cursor color bg, glyph inverted),
    Spacers skip emission so wide CJK chars stay contiguous, and the IME caret is a new DecorationStyle::ImeCaret sprite.
    Co-Authored-By Tryanks.
  • Render during IME key-repeat (Windows): sustained IME repeat floods the message queue with WM_KEYDOWN + WM_IME_*
  • TerminalDamaged, starving about_to_wait so the scheduler never fires a paint. Added a custom REDRAW_REQUESTED_MSG
    posted by Window::request_redraw — posted messages return from PeekMessageW ahead of WM_PAINT, so paints interleave
    with the flood. The handler clears the DwmFlush worker's dirty flag to avoid double-painting.
  • Speed up IME key-repeat (Windows): WM_IME_STARTCOMPOSITION / ENDCOMPOSITION were forwarded to DefWindowProc,
    which runs a ~100 ms synchronous TIP handshake — capping corvus-skk direct-input at ~10 cps. Returning 0 (wezterm already
    does this for END) drops the per-keystroke cycle to ~31 ms, matching the OS repeat rate.

@shiena
shiena force-pushed the ime-improvements branch 3 times, most recently from 96559f5 to 8b619ac Compare April 26, 2026 14:27
@shiena shiena changed the title Render IME preedit and unblock IME key-repeat on Windows fix(windows): inline IME preedit and stop key-repeat from stalling redraws Apr 26, 2026
@shiena shiena changed the title fix(windows): inline IME preedit and stop key-repeat from stalling redraws fix(ime): render preedit inline and stop Windows key-repeat from stalling redraws Apr 26, 2026
@shiena
shiena force-pushed the ime-improvements branch 13 times, most recently from 6448121 to 455eacb Compare May 4, 2026 03:02
@shiena

shiena commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #1849. Two commits are gone as a result:

…s live

Under sustained IME key-repeat (corvus-skk hiragana holding a vowel)
each tick floods the message queue with WM_KEYDOWN + the three
WM_IME_* messages + a TerminalDamaged user event. The drain loop
never sees an empty queue, so `about_to_wait` — which is the only
site that calls `scheduler.update()` — never fires. The render
scheduler timer therefore never expires, and even if it did, its
RedrawRequested would still depend on WM_PAINT, the lowest-priority
slot in a Win32 message queue and also starved by the same flood.
Net effect: every `あ` is pasted to the PTY and echoed by the shell
while held, but the screen only repaints when the key is released
and the queue finally drains.

Register a custom `REDRAW_REQUESTED_MSG` via `RegisterWindowMessageA`
and post it from `Window::request_redraw`. Regular posted messages
are returned by `PeekMessageW` ahead of the synthesized WM_PAINT,
so the paint trigger interleaves with the IME flood. On the
application side, `RioEvent::TerminalDamaged` skips the scheduler
timer and calls `request_redraw` directly — vblank-rate pacing
still belongs to the DwmFlush VSync worker via the dirty flag, so
we aren't burning frames.
The previous commit's `Window::request_redraw` both posts
`REDRAW_REQUESTED_MSG` *and* raises the DwmFlush worker's dirty
flag, so each paint request caused two `RedrawRequested` dispatches
per vblank: one via the custom message, one via the worker's
`RedrawWindow(RDW_INVALIDATE)` -> `WM_PAINT` path. The second one
is a no-op inside per-context render (the dirty flag was already
cleared by the first pass) but still burns a frame of
`begin_render` / `pre_present_notify` / present work.

Clear the worker's dirty flag from the `REDRAW_REQUESTED_MSG`
handler so the next tick sees it false and skips the redundant
invalidate. Also drop the `present_after_input` fallback (and the
`last_input_timestamp` / `mark_input_received` plumbing that fed
it) since rio already drives redraws explicitly via `request_redraw`
on every event that can mutate the terminal — the fallback was
driving another 60 fps stream of invalidations during any input
window, stacking on top of the custom-message paints.
…handshake

Tracing the WndProc for a held 'a' in corvus-skk hiragana
direct-input showed a very sharp pattern: WM_IME_STARTCOMPOSITION
was delivered immediately after WM_KEYDOWN, but the paired
WM_IME_COMPOSITION only arrived ~95–100 ms later, with the main
thread completely idle in between. The OS auto-repeat (~30 Hz)
kept queuing WM_KEYDOWN messages with accumulating repeat counts
(lparam low word 2, 4, …) while we were stuck waiting, so the
user only saw ~10 cps. Forwarding the IME messages to
`DefWindowProc` runs a synchronous handshake with the TIP — that
handshake is the 100 ms wait.

Return 0 for both START and ENDCOMPOSITION the way wezterm does
for ENDCOMPOSITION. The composition result still arrives via
`WM_IME_COMPOSITION`, just without the handshake stall, and the
per-keystroke cycle drops to ~31 ms — matching the OS auto-repeat
cadence (confirmed on a corvus-skk hiragana trace: 21.502086 s
START → 21.502169 s COMPOSITION, i.e. 0.08 ms vs. the prior
~98 ms).

Drop the `Ime::Enabled` / `Ime::Disabled` dispatches that START /
END used to send: the app's handler only toggled an unread
`enabled` flag, so for IMEs that fire this whole trio every
keystroke we were paying one full event-handler round-trip per
press for nothing.

The pre-commit `Ime::Preedit("")` in the GCS_RESULTSTR branch is
intentionally kept: most IMEs (MS-IME, Google Japanese Input,
ATOK) fire WM_IME_COMPOSITION on confirm with only GCS_RESULTSTR
— no GCS_COMPSTR — so the GCS_COMPSTR branch below never runs and
the app's `ime.preedit` would otherwise stay `Some(...)` from the
last preedit update. `process_key_event` short-circuits while a
preedit is active, so without this clear every keystroke after a
commit is silently dropped. For direct-input IMEs (corvus-skk
hiragana) that never had a preedit, the app's handler skips
damage/redraw because `None != None` is false — the extra event
is one no-op handler call, paid only on commit (not auto-repeat).

This branch has not been deployed

No deployments
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.

1 participant