ADR 0016: Fix invisible caret; blink resets on activity, not on a fixed cycle¶
Status¶
Accepted
Context¶
User-reported regression, immediately after Phase 9 (ADR 0015) landed: the caret was completely invisible with the default configuration.
Root cause: updateAnimation()'s !m_animationsEnabled branch called
m_renderedCaretPos.clear() every frame. paintEvent only draws
carets when that array is non-empty. Since animations = false is the
shipped default, the caret never rendered at all outside the opt-in
animated path — a real, severe regression, not a cosmetic one.
Alongside the fix, a related request: the hard (non-animated) blink has two discrete states, and it was possible for the toggle to land at an inconvenient moment relative to a keystroke, making "force visible on input" feel unreliable. Wanted: solid-visible for the whole duration of active use, a normal resumed blink only once input stops.
Decisions¶
1. The disabled branch snaps to targets, it doesn't clear¶
Fixed to match every other "animations off" path in this file: compute
and store the exact current target for every cursor, same as the
steady-state result of the animated path once it's converged. Never
leaves the array in a state paintEvent's !isEmpty() check would
skip.
2. Two independent tick counters, not one shared one¶
m_caretTick keeps driving the animated fade phase, untouched.
A new m_idleTicks — ticks since the last cursor-moving action, reset
by a new resetCaretBlink() called from both keyPressEvent and
mousePressEvent — drives the hard blink's toggle instead. Splitting
these means resetting on activity can't also restart the fade's
sine phase (which would otherwise happen if both used the same
counter): each mode's timing stays exactly as tuned, independently.
resetCaretBlink() sets m_caretVisible = true and m_idleTicks = 0.
The blink timer's existing pre-increment-then-modulo check
(m_idleTicks % 17 == 0, ~500ms at the 30ms tick) means resetting to 0
does not immediately re-toggle on the very next tick (it becomes 1,
not a multiple of 17) — so a keystroke can never be immediately undone
by a coincidentally-timed blink. As long as input keeps arriving faster
than ~500ms apart, the toggle never fires at all; the first toggle
after activity stops always lands ~500ms later, the same interval the
blink already used — "resume blinking normally" falls out for free
rather than needing a separate grace period.
Consequences¶
Verified live: caret visible immediately on launch with the default config (was the reported bug); stays solid across a rapid sequence of keystrokes (200ms apart, all under the 500ms threshold); resumes toggling roughly 500ms after the last keystroke, sampled at ~600ms (invisible) and ~1150ms (visible again) of idle time. Full suite still green — this bug was in rendering-only state, untouched by any test.