feat(tracking): caret, keyboard focus and mouse edge tracking (#276) - #277
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
FocusTracker owns every accessibility call (WinEvents out of context, GetGUIThreadInfo, UI Automation on a COM MTA) on its own thread; the tick thread only flips setActive() and copies snapshot() under a mutex, never waiting on UIA. Caret resolution tries GetGUIThreadInfo first (classic Win32), then UIA TextPattern2::GetCaretRange, then TextPattern::GetSelection (Windows Terminal, which lacks TextPattern2); focus resolution uses the UIA focused element's own bounding rect. Events are coalesced 30ms before resolving, plus a 60Hz backstop poll while active. Adapted from the plan: RangeRect leaked the zero-element SAFEARRAY when widening an empty caret range (it reassigned the SAFEARRAY* without destroying the first one) - fixed by destroying it before reuse. Added a friend declaration for FocusHandler (needs FocusTracker's private active_/tid_, same as the existing FocusTrackImpl friend). Unnamed two unused WinEvent callback parameters to keep the W4 build warning-free. Added oleaut32.lib and uuid.lib to both app link lines (normal + uiaccess) for SafeArray* and UIA GUIDs; not part of the pure-logic test build, so build.bat test is unaffected (321 cases, 182306 assertions, all pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
RunTick now steps the ViewOwner state machine each tick, glides a detached view centre toward the caret/focus target or back to the pointer, and builds the frame with DetachedMap while an owner other than Mouse holds the view. suppressCursorSync and the transform hook write path are both gated on viewDetached so tracking never welds or hook-writes the pointer back onto the lens. The watcher thread starts and stops alongside the input router on every startup/shutdown path. Adapted from the plan: reused fsCover (already read once earlier in the tick for the pan-wall/launch-quiesce logic) for the game-session gate instead of introducing a new fsGameSession flag, since fsGame is only a same-tick alias of fsCover computed later in the function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
Adds the Tracking section (caret tracking on by default, keyboard-focus tracking off, and the shared centred/within-edges alignment select) to the config UI schema, after the Cursor section. Reuses the existing 'cursor' icon (no new icon needed). Playwright test confirms the defaults and that trackFocus writes through Apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
…toggle defaults; 0.10.3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
…carets; click quiet period Field test 2026-09-29: - Moving the mouse now places the pointer in the view (view centre) and the view stays; the old return glide chased a moving pointer and never arrived, felt as a wobbly, stuck view. A button press hands back without a warp. - The first caret after any focus change (Tab, click, page load, zoom-in) is a baseline; only a caret that moves within the same focus is followed, so a filled field no longer jumps the view to its end. - Caret/focus changes within 1 s of a mouse button are ignored (opening a page moved the view to the new focus). - trackLog logs every owner change and caret baselines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
The pointer roams freely inside the view and the view moves only when it reaches the margin band (trackMarginPct, 15%), just far enough; no weld, so clicks are native. Locked (mouselook) and Inspect keep the centred path. The MPO walls bound the source rect directly. When the mouse takes the view back from caret/focus in edge mode, the pointer is placed just inside the band, nearest to where it was. Settings: Tracking > Keep the mouse pointer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
…margin (#276) Pushing the free pointer into a screen edge or corner sent raw mickeys while the pointer could not move: the lock detector's mouselook tell. The false lock switched to the centred path and welded the pointer away from the corner (log: LOCKED/free flapping every ~20 ms). In edge mode that motion is now hidden from the tell (PointerPinnedAtEdge). Edge mode also gets its own margin, mouseMarginPct (default 5%, Settings slider), instead of the 15% caret one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
The band was measured to the hotspot, which sits at the arrow's tip: the body reached the right edge while the left kept a gap the width of the arrow. The visible body (opaque bounds around the hotspot, re-measured on cursor change) now counts on every side, so both edges are equal. Default margin 0: the cursor reaches the view edge before the view moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
trackGlideMode=1 (hidden, hot) glides with a critically damped spring that carries velocity across retargets, so typing becomes one continuous glide instead of a fresh ease per keystroke; 0 keeps the exponential ease. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
Field A/B 2026-09-29 (0/25/150/200 ms, old ease vs spring): the spring at 200 ms glides as you type without lagging. Caret and focus share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
…0.10.3 (#276) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #276.
Tracking modes for the magnifier view, all optional:
mouseMarginPct, default 0).Field decisions (details in
docs/TRACKING-FINDINGS.md): tracking never drags the pointer; a mouse move brings the pointer to the view; a button hands back without warping; only keyboard-driven caret moves are followed; 1 s click quiet period; edge-pinned motion is hidden from the lock detector so corners do not fling the pointer.Settings: new Tracking section (Follow the text cursor, Follow keyboard focus, Keep the text cursor and focus, Keep the mouse pointer, Mouse edge margin).
Version 0.10.3.
Verification
build.bat test: all doctests pass (new: view_target, view_glide incl. spring, detached_view, edge_pan)tools/release.ps1elevated: installer built, all installer checks passedInstaller is unsigned in CI; setup signs the uiAccess build per PC.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG