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. diff --git a/src/focus_track.cpp b/src/focus_track.cpp index f2e2070..b9fff2e 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 @@ -45,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(); @@ -181,11 +188,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 +213,18 @@ 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); + // 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 { + publish(TrackKind::Caret, rc.left, rc.top, rc.right, rc.bottom, src); + } } } } 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 +}