From da80135ab3df3b2c82e7f325d88647a81b8870be Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:09:09 +0200 Subject: [PATCH 1/4] fix(tracking): skip carets outside their element and container-sized focus (#278) Firefox in a zoomed iframe (a Claude artifact at high Ctrl+ zoom) reports the caret, through both GetGUIThreadInfo and UIA, outside the focused input (input 2226,997 798x74, caret 3097,1226 1x118), so the view showed empty page. A caret whose centre is outside its element is now skipped. Leaving a text box moves focus to the whole page, and centring on it dropped the view; focus rects covering half the monitor or more are skipped. Both logged under trackLog. Version 0.10.4. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG --- src/track_filter.h | 30 ++++++++++++++++++++++++++++++ src/version.h | 4 ++-- tests/test_track_filter.cpp | 20 ++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/track_filter.h create mode 100644 tests/test_track_filter.cpp diff --git a/src/track_filter.h b/src/track_filter.h new file mode 100644 index 0000000..3fea2ee --- /dev/null +++ b/src/track_filter.h @@ -0,0 +1,30 @@ +#pragma once +// Sanity filters for what the focus/caret watcher reports (issue #278). Pure; tests/test_track_filter.cpp. +namespace wind { + +struct TrackBox { long l, t, r, b; }; + +// Is the caret where its own focused element is? Firefox inside a zoomed iframe (a Claude artifact at +// high Ctrl+ page zoom, field 2026-09-29) reports the caret through BOTH GetGUIThreadInfo and UIA well +// outside the focused input (input 2226,997 798x74, caret 3097,1226 1x118), so following it showed +// empty page. A caret whose centre lies outside the element (plus a small slack for borders and +// padding) is not trusted. An empty element rect (unknown) trusts the caret. +inline bool CaretInsideElement(const TrackBox& caret, const TrackBox& elem, long slack = 8) { + if (elem.r <= elem.l || elem.b <= elem.t) return true; + const long cx = (caret.l + caret.r) / 2, cy = (caret.t + caret.b) / 2; + return cx >= elem.l - slack && cx <= elem.r + slack && cy >= elem.t - slack && cy <= elem.b + slack; +} + +// Is a focus rect a CONTAINER (the page, a pane, the window) rather than a control someone moved to? +// Leaving a text area moves focus to the whole document, and centring on it dropped the view to the +// middle of the page (field 2026-09-29: 3400x1912 on a 3840x2160 monitor). Half the monitor's area, +// counting only the part on the monitor, is the line. +inline bool IsContainerFocus(const TrackBox& rc, long monL, long monT, long monR, long monB) { + const long l = rc.l > monL ? rc.l : monL, r = rc.r < monR ? rc.r : monR; + const long t = rc.t > monT ? rc.t : monT, b = rc.b < monB ? rc.b : monB; + if (r <= l || b <= t) return false; + const double area = (double)(r - l) * (double)(b - t); + const double mon = (double)(monR - monL) * (double)(monB - monT); + return mon > 0 && area >= 0.5 * mon; +} +} // namespace wind diff --git a/src/version.h b/src/version.h index ae0c15f..1278d70 100644 --- a/src/version.h +++ b/src/version.h @@ -4,7 +4,7 @@ #define WIND_VER_MAJOR 0 #define WIND_VER_MINOR 10 -#define WIND_VER_PATCH 3 +#define WIND_VER_PATCH 4 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.10.3" +#define WIND_VERSION_STR "0.10.4" diff --git a/tests/test_track_filter.cpp b/tests/test_track_filter.cpp new file mode 100644 index 0000000..6c6e628 --- /dev/null +++ b/tests/test_track_filter.cpp @@ -0,0 +1,20 @@ +#include "doctest.h" +#include "../src/track_filter.h" +using namespace wind; + +TEST_CASE("caret inside its element is trusted; the field-recorded Firefox iframe caret is not") { + const TrackBox input{ 2226, 997, 3024, 1071 }; + CHECK(CaretInsideElement({ 2453, 991, 2478, 1077 }, input)); // the good reading + CHECK_FALSE(CaretInsideElement({ 3097, 1226, 3098, 1344 }, input)); // below and right of it + CHECK(CaretInsideElement({ 3020, 1000, 3022, 1068 }, input)); // at the right end, inside + CHECK(CaretInsideElement({ 5, 5, 6, 20 }, { 0, 0, 0, 0 })); // unknown element: trust +} +TEST_CASE("terminal caret beside its cell stays trusted") { + CHECK(CaretInsideElement({ 332, 1692, 343, 1737 }, { 322, 1692, 346, 1737 })); +} +TEST_CASE("page-sized focus is a container; controls and huge zoomed inputs partly off-screen are not") { + CHECK(IsContainerFocus({ 422, 122, 3822, 2034 }, 0, 0, 3840, 2160)); // the document + CHECK_FALSE(IsContainerFocus({ 2226, 997, 3024, 1071 }, 0, 0, 3840, 2160)); + CHECK_FALSE(IsContainerFocus({ 902, 912, 6146, 2106 }, 0, 0, 3840, 2160)); // 5244x1194, 41% on screen + CHECK_FALSE(IsContainerFocus({ 5000, 0, 6000, 100 }, 0, 0, 3840, 2160)); // off the monitor +} From 0017bc8d0482548258cf9b6a1778fb2df772b222 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:13:04 +0200 Subject: [PATCH 2/4] fix(tracking): wire the #278 filters into the focus tracker Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG --- src/focus_track.cpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/focus_track.cpp b/src/focus_track.cpp index f2e2070..9645f29 100644 --- a/src/focus_track.cpp +++ b/src/focus_track.cpp @@ -1,5 +1,6 @@ #include "focus_track.h" #include "logging.h" +#include "track_filter.h" #include #include #include @@ -181,11 +182,21 @@ void FocusTracker::run() { if (IsOwnOrTooltip(fg)) return; IUIAutomationElement* el = nullptr; if (uia) uia->GetFocusedElement(&el); + RECT b{}; // the focused element's bounds (empty = unknown) + if (el && FAILED(el->get_CurrentBoundingRectangle(&b))) b = RECT{}; // 1. A focus change: follow the focused control (if wanted). Its caret becomes the baseline. - if (focusChanged && wantFocus_.load() && el) { - RECT b{}; - if (SUCCEEDED(el->get_CurrentBoundingRectangle(&b)) && b.right > b.left && b.bottom > b.top) + // A container-sized focus (the page after leaving a text box, a pane, the window) is not + // something the user moved to, so it is skipped (issue #278). + if (focusChanged && wantFocus_.load() && el && b.right > b.left && b.bottom > b.top) { + MONITORINFO mi{ sizeof(mi) }; + const bool haveMon = GetMonitorInfoW(MonitorFromWindow(fg, MONITOR_DEFAULTTONEAREST), &mi) != 0; + if (haveMon && IsContainerFocus({ b.left, b.top, b.right, b.bottom }, + mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom)) { + if (log_.load()) wind::Log(wind::LogLevel::Info, "track", "focus skipped (container): %ld,%ld %ldx%ld", + b.left, b.top, b.right - b.left, b.bottom - b.top); + } else { publish(TrackKind::Focus, b.left, b.top, b.right, b.bottom, "uia-focus"); + } } // 2. The caret: published only when it moved within the same focus. if (wantCaret_.load()) { @@ -196,7 +207,14 @@ void FocusTracker::run() { if (log_.load()) wind::Log(wind::LogLevel::Info, "track", "caret baseline via %s: %ld,%ld", src, rc.left, rc.top); } else if (!EqualRect(&rc, &lastCaret)) { lastCaret = rc; - publish(TrackKind::Caret, rc.left, rc.top, rc.right, rc.bottom, src); + // A caret outside its own element is a bad report, not a place to look (Firefox + // in a zoomed iframe, issue #278): the view stays where it is. + if (!CaretInsideElement({ rc.left, rc.top, rc.right, rc.bottom }, { b.left, b.top, b.right, b.bottom })) { + if (log_.load()) wind::Log(wind::LogLevel::Info, "track", "caret skipped (outside its element %ld,%ld %ldx%ld) via %s: %ld,%ld", + b.left, b.top, b.right - b.left, b.bottom - b.top, src, rc.left, rc.top); + } else { + publish(TrackKind::Caret, rc.left, rc.top, rc.right, rc.bottom, src); + } } } } From e10fbb985ed8c29499306c5b3f73b64b2b2ab392 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:17:55 +0200 Subject: [PATCH 3/4] fix(tracking): limit the outside-caret skip to Gecko browsers (#278) Field: in the zoomed Firefox iframe both caret sources are wrong (the UIA caret and the element bounds point above the text, the Win32 caret below), so there is no source to fall back to. The skip now applies only to MozillaWindowClass windows; every other app keeps its 0.10.3 caret behaviour. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG --- src/focus_track.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/focus_track.cpp b/src/focus_track.cpp index 9645f29..b9fff2e 100644 --- a/src/focus_track.cpp +++ b/src/focus_track.cpp @@ -46,6 +46,12 @@ static bool IsOwnOrTooltip(HWND h) { return wcscmp(cls, L"tooltips_class32") == 0 || wcscmp(cls, L"Xaml_WindowedPopupClass") == 0; } +// A Gecko browser window (Firefox and every fork share this class). +static bool IsGeckoWindow(HWND h) { + wchar_t cls[64] = {}; GetClassNameW(h, cls, 64); + return wcscmp(cls, L"MozillaWindowClass") == 0; +} + // Classic Win32 caret of the foreground thread, in screen px. False when there is none. static bool Win32Caret(RECT& out) { HWND fg = GetForegroundWindow(); @@ -207,9 +213,13 @@ void FocusTracker::run() { if (log_.load()) wind::Log(wind::LogLevel::Info, "track", "caret baseline via %s: %ld,%ld", src, rc.left, rc.top); } else if (!EqualRect(&rc, &lastCaret)) { lastCaret = rc; - // A caret outside its own element is a bad report, not a place to look (Firefox - // in a zoomed iframe, issue #278): the view stays where it is. - if (!CaretInsideElement({ rc.left, rc.top, rc.right, rc.bottom }, { b.left, b.top, b.right, b.bottom })) { + // In a Gecko browser (Firefox, Zen, LibreWolf...: one window class), a caret + // outside its own element is a bad report, not a place to look (a zoomed iframe, + // issue #278; its UIA caret was wrong too, so there is no source to fall back + // to): the view stays where it is. Other apps are unchanged, so no case that + // worked before can lose tracking to this rule. + if (IsGeckoWindow(fg) && + !CaretInsideElement({ rc.left, rc.top, rc.right, rc.bottom }, { b.left, b.top, b.right, b.bottom })) { if (log_.load()) wind::Log(wind::LogLevel::Info, "track", "caret skipped (outside its element %ld,%ld %ldx%ld) via %s: %ld,%ld", b.left, b.top, b.right - b.left, b.bottom - b.top, src, rc.left, rc.top); } else { From 5474510869c64e131c1b9fad7e12920498d8e1db Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:28:42 +0200 Subject: [PATCH 4/4] docs(tracking): #278 findings Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG --- docs/TRACKING-FINDINGS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/TRACKING-FINDINGS.md b/docs/TRACKING-FINDINGS.md index 6eaf3ec..32beed5 100644 --- a/docs/TRACKING-FINDINGS.md +++ b/docs/TRACKING-FINDINGS.md @@ -48,3 +48,17 @@ highlighted option through UIA, so Wind follows only the terminal caret there. cursor's visible body (opaque bounds around the hotspot, re-measured on cursor change). - **Margin.** Edge mode has its own `mouseMarginPct` (default 0: the cursor reaches the view edge before the view moves; Settings slider 0-30%). `trackMarginPct` (15%) stays the caret/focus one. + +## Firefox in a zoomed iframe (issue #278, 2026-09-29) + +- A Claude artifact (an iframe) at high Ctrl+ page zoom in Zen: Firefox reports the caret wrongly + from BOTH sources. The Win32 caret sits below and right of the input (input 2226,997 798x74, + caret 3097,1226 1x118); the UIA caret range and the element bounds point above the real text. + Not reproducible on a plain page at the same zoom. There is no correct source to fall back to. +- Fix: in Gecko windows (`MozillaWindowClass`, Firefox and all forks) a caret whose centre is outside + its own element is skipped, so the view stays put. Limited to Gecko so no app that tracked + correctly before can lose tracking. A UIA fallback was tried and field-rejected (it lands above). +- Leaving a text box moves focus to the whole page (3400x1912); centring on it dropped the view. + Focus rects covering half the monitor or more are containers and are skipped (all apps). +- Diagnostic tool from this hunt: a recorder logging the foreground process, Win32 caret, UIA caret + and focus bounds every 100 ms; the Win32/UIA disagreement is what located the bad source.