Skip to content

feat(tracking): caret, keyboard focus and mouse edge tracking (#276) - #277

Merged
Maxaubert merged 16 commits into
mainfrom
feat/276-edge-mode
Sep 29, 2026
Merged

Maxaubert merged 16 commits into
mainfrom
feat/276-edge-mode

Conversation

@Maxaubert

Copy link
Copy Markdown
Owner

Closes #276.

Tracking modes for the magnifier view, all optional:

  • Caret tracking (default on): the view follows the text cursor while typing. Sources: GetGUIThreadInfo (Win32), UIA TextPattern2 caret (browsers, VS Code), UIA selection (Windows Terminal).
  • Keyboard-focus tracking (default off): the view follows Tab/menu focus.
  • Mouse edge mode (default centred): the pointer roams freely and the view moves only when the cursor's visible body reaches the edge (mouseMarginPct, default 0).
  • Caret and focus glide with a critically damped spring, 200 ms (field A/B pick).

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)
  • Playwright settings suite: 60 passed
  • tools/release.ps1 elevated: installer built, all installer checks passed
  • Deployed signed UIAccess build on the dev PC and field-tested by the owner (caret, focus, edge mode, glide values)

Installer is unsigned in CI; setup signs the uiAccess build per PC.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG

Maxaubert and others added 16 commits September 28, 2026 21:33
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
@Maxaubert
Maxaubert merged commit f03b5ef into main Sep 29, 2026
1 check passed
@Maxaubert
Maxaubert deleted the feat/276-edge-mode branch September 29, 2026 07:55
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.

Tracking modes: caret tracking, keyboard-focus tracking, mouse edge mode

1 participant