Files
strans/tests/engine_test.c
Hojun-Cho 8ebe6b32df engine: a mode mark does not outlive its owner
The mark a mode switch leaves, from 6a6749e, is cleared at the top of
transition(), so the next key takes it away.  Nothing takes it away when
there is no next key.  reset() clears the pending text, the raw state, the okurigana split
and the search, and leaves modemark alone -- so a release or a lifecycle
reset flushes everything the popup was showing except the one thing that
is still drawn.

Then samedraw() sees a picture identical to the last one and redraw()
sends nothing, and win.c only ever unmaps from a Drawcmd (winshow, the
sole caller of both map and unmap).  So the popup stays.  Press the 한/영
key, type nothing, and alt-tab: 한 sits above every window, override
redirect and typed as a tooltip, until the next keystroke in any
strans-aware field -- which may be minutes.  Counted on a private X
server, viewable override-redirect windows:

			before	after
	at rest		0	0
	after Ctrl+S	1	1
	after letting go	1	0

reset() is where it belongs rather than the release arm, because the mark
is one more thing that is pending: it is drawn only when nothing else is
(snapshot), and it means the switch has not been typed on yet.  Clearing
it there is safe for the switch itself only because transition() zeroes it
at the top and setlang() runs after flush() -- reset() is reached through
that flush, before the new mark is set.  That ordering is not obvious, and
it is exactly what engine/direct-language-modes already asserts at
engine_test.c:584; breaking setlang so a switch marks nothing fails that
line and the new ones together.

Not a Wayland defect: wl.c's leave() calls hidepopup() unconditionally
after the release, so its popup was already coming down.  X11 was the
frontend that trusted the picture.

The new case goes in engine/direct-language-modes beside the mark
assertions that were already there, and drives imhandlekey rather than
transition, since the defect is on the owner-release path.  Without the
fix:

	engine_test.c:619: check failed: draindraw(&dc) > 0

that is, the engine published no picture at all.  Its second line needs
its own reason to exist, so it was checked by removing the fix and making
samedraw() answer 0 for everything: the picture then arrives and still
carries the mark,

	engine_test.c:620: want 0, got 1  (dc.pre.n)

which is the assertion that would otherwise have been decoration.  The two
setup lines are load-bearing too -- with setlang marking nothing they fail
rather than passing vacuously, since an empty picture equal to the last
one is never sent.

90 unit, check-live, check-stress and valgrind clean; the count is
unchanged because the case joined a test that already existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 07:48:26 +09:00

57 KiB