From f201b82cf155361b4a007fecb62ccd0b46bf8a74 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:55:24 +0200 Subject: [PATCH] chore: bring #408 onto main as tested in the combined build; 0.24.18 The branch is main plus exactly the combined test build's state after this PR (conflicts with the PRs merged before it resolved as tested), version 0.24.18. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011zPmivSAQeivGUdBjTsaMu --- docs/SHELL-PANEL-CURSOR-FINDINGS.md | 2 +- docs/architecture/01-overview.md | 2 +- docs/architecture/02-tick-loop.md | 5 +- docs/architecture/06-input.md | 2 +- docs/architecture/07-cursor.md | 6 +- src/config_ui/webview_recover.h | 8 --- src/cursor_blanker.cpp | 8 +-- src/cursor_blanker.h | 2 - src/cursor_sprite.cpp | 1 - src/cursor_sprite.h | 1 - src/gain_learner.h | 6 -- src/input_router.cpp | 11 +--- src/input_router.h | 9 +-- src/installer_state.h | 25 +------- src/java_bridge.h | 1 - src/keybind_rules.h | 16 ----- src/main.cpp | 27 +++------ src/render_model.h | 1 - src/tick_stats.h | 12 +--- src/transform.cpp | 7 --- src/transform.h | 15 ----- src/transform_model.cpp | 27 +++------ src/tray_app/flyout_model.h | 46 +------------- src/tray_app/flyout_window.cpp | 6 -- src/version.h | 4 +- src/zoom_ladder.h | 17 ------ tests/test_flyout.cpp | 94 ----------------------------- tests/test_gain_learner.cpp | 4 -- tests/test_installer_state.cpp | 23 ------- tests/test_keybind_rules.cpp | 18 ++++++ tests/test_transform.cpp | 58 ------------------ tests/test_tray_status.cpp | 7 --- tests/test_webview_recover.cpp | 9 --- tests/test_zoom_ladder.cpp | 9 --- 34 files changed, 55 insertions(+), 434 deletions(-) diff --git a/docs/SHELL-PANEL-CURSOR-FINDINGS.md b/docs/SHELL-PANEL-CURSOR-FINDINGS.md index d4dd4eb5..193daa94 100644 --- a/docs/SHELL-PANEL-CURSOR-FINDINGS.md +++ b/docs/SHELL-PANEL-CURSOR-FINDINGS.md @@ -54,7 +54,7 @@ transform: it drifts off the view centre by speed x tick x level. which marshals every tick write onto the system-wide mouse input thread. Field: hitches on zoom, worst when zooming with the picker open (the public write, 3-9 ms, landed on that thread too). - **Shipped: freeze.** While a panel is open the pointer is pinned with a 1px `ClipCursor`; the hand's - motion arrives as ballistics-cooked raw input (the Inspect machinery, `cookActive`), and the + motion arrives as ballistics-cooked raw input (the Inspect machinery), and the transform model moves the clip (which moves the pointer) right after writing the view, on the tick thread. Owner-verified: no wobble, no flicker, normal hitch-free zoom. diff --git a/docs/architecture/01-overview.md b/docs/architecture/01-overview.md index 40a9314d..9da975d8 100644 --- a/docs/architecture/01-overview.md +++ b/docs/architecture/01-overview.md @@ -89,7 +89,7 @@ build. See [11](11-build-test-release.md). | Tray | `tray_host.*`, `tray_ipc.h`, `tray_items.*`, `tray_status.h`, `tray_app/` | | Settings host | `config_ui/` (`main.cpp`, `ini_edit.*`, `mpo.h`, `wind_watchdog.h`, `webview_recover.h`) | | Logging and diagnostics | `logging.*`, `test_telemetry.h`, `hitch_record.*`, `tick_span.h` | -| Installer support | `installer_state.h`, `webview2_probe.h`, `version.h` | +| Installer support | `installer_state.h` (ParseVersion), `webview2_probe.h`, `version.h` | Other top-level folders: `ui/` (Svelte settings app and Playwright tests), `tests/` (doctest), `tools/` (deploy, release and measurement scripts, [12](12-instrumentation.md)), `installer/` diff --git a/docs/architecture/02-tick-loop.md b/docs/architecture/02-tick-loop.md index ef5910da..d3b57bbd 100644 --- a/docs/architecture/02-tick-loop.md +++ b/docs/architecture/02-tick-loop.md @@ -11,8 +11,7 @@ All state that feeds the view is read and written on the tick thread, in one pas across threads makes the view and the cursor sample different instants, which shows as a visible beat (the wobble class in [../NATIVE-MAGNIFIER-STOMP.md](../NATIVE-MAGNIFIER-STOMP.md)). -`RunTick` never sleeps or waits; the caller paces it (see [Pacing](#pacing)). That is also why it -is safe to call from the `WM_TIMER` branch in `WndProc` during a modal loop. +`RunTick` never sleeps or waits; the caller paces it (see [Pacing](#pacing)). ## The phases of a tick @@ -106,7 +105,7 @@ to 1x. `StripUiOnlyKeys` (`src/config.cpp`) drops `uiTheme`, `uiPalette`, `showA (`t.lastCoreIni`). An identical fingerprint skips the reload. The fingerprint is seeded at startup; an empty one would make the first Settings write of a session reload. -A real reload re-binds the hook's buttons and swallowed keys (`g_input.setButtons`/`setKeys`), +A real reload re-binds the hook's buttons and swallowed keys (`g_input.setButtonBinds`/`setKeys`), re-registers the hotkeys, invalidates the foreground cache, and rebuilds `ZoomController` and `CursorMapper` with the mapper's centre kept. Engine-shaped keys (`model`) need a restart: they decide which models exist. diff --git a/docs/architecture/06-input.md b/docs/architecture/06-input.md index d1e91f3b..92838665 100644 --- a/docs/architecture/06-input.md +++ b/docs/architecture/06-input.md @@ -44,7 +44,7 @@ Bound inputs are eaten so they never also fire in the focused app. - **Balanced down/up.** A DOWN of a bound input is swallowed and recorded (`g_swallowedDown`, `g_kbSwallowedDown`); an UP is swallowed only if its DOWN was. Swallowing an UP whose DOWN the system saw leaves the input held system-wide (the stuck side-button bug, issue #113). -- **No stranded keys.** Records are cleared on every remap (`setButtons`, `setKeys`), and teardown +- **No stranded keys.** Records are cleared on every remap (`setButtonBinds`, `setKeys`), and teardown runs `ReleaseSwallowedButtons`/`ReleaseSwallowedKeys`, which synthesize the missing UP. - **Decide once per press.** A key bind is swallowed only when a bind on that key has all its modifiers held (`keyBindMatches`). A VK-only test once ate a plain F1 system-wide for a Ctrl+F1 diff --git a/docs/architecture/07-cursor.md b/docs/architecture/07-cursor.md index ed174060..d7ebb99a 100644 --- a/docs/architecture/07-cursor.md +++ b/docs/architecture/07-cursor.md @@ -156,9 +156,9 @@ Inspect and the hide-cursor hotkey (`cursorVisibility=never`) hide DWM's pointer session; the Inspect crosshair is then a window of Wind's. - **`CursorBlanker`** (`src/cursor_blanker.*`) swaps the 14 system cursors for transparent ones and - keeps the originals. It first reloads the user's scheme, so a previously killed Wind's blanks are - never captured as originals. `MagShowSystemCursor(FALSE)` covers app-custom cursors. The swaps - run in order on the `Wind cursor swaps` worker thread (#363): the restore is a full + restores them by reloading the user's scheme (no cached originals). The constructor reloads it + first, so a previously killed Wind's blanks never outlive it. `MagShowSystemCursor(FALSE)` covers + app-custom cursors. The swaps run in order on the `Wind cursor swaps` worker thread (#363): the restore is a full `SPI_SETCURSORS` scheme reload (8 ms median, up to 90 ms under load) and the blank 14 `SetSystemCursor` calls, which froze the 1x landing frame on the tick thread. The zoom-out repaint nudge rides on the worker after the restore. `restoreSync()` waits (shutdown); the crash filter diff --git a/src/config_ui/webview_recover.h b/src/config_ui/webview_recover.h index d0abbd5a..7f26a1bb 100644 --- a/src/config_ui/webview_recover.h +++ b/src/config_ui/webview_recover.h @@ -35,12 +35,4 @@ inline WvRecovery DecideWvRecovery(int kind, WvRecoverBudget& b, unsigned long l return r; } -// The page's unsaved edits travel as a JSON object of string values; only something shaped like one -// is ever echoed back into the page. -inline bool LooksLikeJsonObject(const char* s, unsigned long long len) { - unsigned long long a = 0, z = len; - while (a < z && (s[a] == ' ' || s[a] == '\n' || s[a] == '\r' || s[a] == '\t')) ++a; - while (z > a && (s[z - 1] == ' ' || s[z - 1] == '\n' || s[z - 1] == '\r' || s[z - 1] == '\t')) --z; - return z - a >= 2 && s[a] == '{' && s[z - 1] == '}'; -} } // namespace wind diff --git a/src/cursor_blanker.cpp b/src/cursor_blanker.cpp index 7e8d2185..932010a0 100644 --- a/src/cursor_blanker.cpp +++ b/src/cursor_blanker.cpp @@ -33,14 +33,8 @@ static void RestoreAll() { CursorBlanker::CursorBlanker() : st_(std::make_shared()) { // If a previous Wind was hard-killed while cursors were blanked, the desktop still has blank - // shared cursors; reload the user's scheme FIRST or the blanks get captured as "originals". + // shared cursors; reload the user's scheme so they do not outlive the crash. RestoreAll(); - for (UINT id : kStandardIds) { - HCURSOR shared = LoadCursorW(nullptr, MAKEINTRESOURCEW(id)); - if (!shared) continue; - HCURSOR copy = CopyCursor(shared); - if (copy) originals_[shared] = copy; - } worker_ = std::thread([st = st_] { run(st); }); } diff --git a/src/cursor_blanker.h b/src/cursor_blanker.h index dffaa44c..5c3dc945 100644 --- a/src/cursor_blanker.h +++ b/src/cursor_blanker.h @@ -6,7 +6,6 @@ #include #include #include -#include namespace wind { // Swaps the system cursor set for blanks while the real pointer is hidden (Inspect, the hide-cursor // hotkey), and back. @@ -41,7 +40,6 @@ class CursorBlanker { void post(std::function op); bool runSync(std::function op); static void run(std::shared_ptr st); - std::unordered_map originals_; bool blanked_ = false; std::shared_ptr st_; std::thread worker_; diff --git a/src/cursor_sprite.cpp b/src/cursor_sprite.cpp index f7f5d979..362c6861 100644 --- a/src/cursor_sprite.cpp +++ b/src/cursor_sprite.cpp @@ -151,7 +151,6 @@ void CursorSprite::keepOnTop() { // synchronous DWM z-order transaction that hitches a fullscreen game (issue #148; same fix // as the render overlay's calm-topmost). The per-tick displaced() walk reclaims immediately. if (displaced()) { - lastTopmostMs_ = GetTickCount64(); SetWindowPos(hwnd_, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } } diff --git a/src/cursor_sprite.h b/src/cursor_sprite.h index 478bab7b..ae17fbf9 100644 --- a/src/cursor_sprite.h +++ b/src/cursor_sprite.h @@ -55,6 +55,5 @@ class CursorSprite { int scale_ = 1; bool visible_ = false; bool crosshairMode_ = false; // window currently holds the crosshair pixels - unsigned long long lastTopmostMs_ = 0; // last HWND_TOPMOST re-assert (throttled) }; } diff --git a/src/gain_learner.h b/src/gain_learner.h index 650bc37b..80803b6c 100644 --- a/src/gain_learner.h +++ b/src/gain_learner.h @@ -78,12 +78,6 @@ class GainLearner { return gain_[lo] + (gain_[hi] - gain_[lo]) * (t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t)); } - bool warmedUp() const { - int have = 0; - for (int i = 0; i < kBins; ++i) if (n_[i] > 0) ++have; - return have >= 2; - } - void reset() { for (int i = 0; i < kBins; ++i) { gain_[i] = 1.0; n_[i] = 0; } } // Text round-trip, so a restart starts from the last learned curve instead of raw passthrough diff --git a/src/input_router.cpp b/src/input_router.cpp index 667e62ed..f0346cf1 100644 --- a/src/input_router.cpp +++ b/src/input_router.cpp @@ -33,7 +33,7 @@ static bool g_hookOk = false; // result of SetWindowsHookExW, publi // Only an UP whose DOWN we swallowed may be swallowed too, so the system's down/up view stays // balanced and a button can never be left believed-held. Reset on remap so a stale flag from a // previous binding can't cause a later UP to be wrongly swallowed. ATOMIC: touched by three -// contexts - the hook thread (MouseProc), the tick thread (setButtons on hot-reload), and the +// contexts - the hook thread (MouseProc), the tick thread (setButtonBinds on hot-reload), and the // teardown caller (ReleaseSwallowedButtons via stop()) - so plain bools would be a data race. static wind::SwallowLedger<6> g_swallowedDown; // index = button id 1..5 (#285: 3/4/5 = L/R/M) // Which direction a pressed bound button is holding (1 in, 2 out, 0 none) and that bind's modifiers, @@ -143,15 +143,6 @@ void InputRouter::setButtonState(int buttonId, bool down) { } PublishButtonHeld(state_); } -bool InputRouter::isZoomButton(int xbuttonId) const { - return xbuttonId == inButtonId_.load(std::memory_order_relaxed) - || xbuttonId == inButtonId2_.load(std::memory_order_relaxed) - || xbuttonId == outButtonId_.load(std::memory_order_relaxed) - || xbuttonId == outButtonId2_.load(std::memory_order_relaxed); -} -void InputRouter::setButtons(int inButtonId, int inButtonId2, int outButtonId, int outButtonId2) { - setButtonBinds(inButtonId, 0, inButtonId2, 0, outButtonId, 0, outButtonId2, 0); -} void InputRouter::setButtonBinds(int inButtonId, int inMods, int inButtonId2, int inMods2, int outButtonId, int outMods, int outButtonId2, int outMods2) { inButtonMods_.store(inMods, std::memory_order_relaxed); diff --git a/src/input_router.h b/src/input_router.h index 97380a93..62b14c5f 100644 --- a/src/input_router.h +++ b/src/input_router.h @@ -47,8 +47,6 @@ class InputRouter { // Map an XBUTTON id (1 = XBUTTON1, 2 = XBUTTON2) to the in/out held state, using the // configured zoom buttons. Shared by the WH_MOUSE_LL hook and main's WM_INPUT path. void setButtonState(int xbuttonId, bool down); - // Whether the id is one of the configured zoom buttons (used to decide swallowing). - bool isZoomButton(int xbuttonId) const; // Whether the hook should swallow the configured zoom buttons (set in start()). bool swallowEnabled() const { return swallow_; } // --- Keyboard binds (WH_KEYBOARD_LL hook) ------------------------------------------------- @@ -148,11 +146,10 @@ class InputRouter { // (Raw Input still delivers the transition even though the hook swallows the legacy message, so // both writing would race/double-count). WM_INPUT button writes are only the WIND_NOHOOK fallback. bool hookActive() const { return hookActive_.load(std::memory_order_relaxed); } - // Live-rebind the configured zoom buttons (called from the tick thread on hot-reload). - // Atomic so the hook thread's reads in setButtonState/isZoomButton stay race-free, and the + // Live-rebind the configured zoom buttons (called from the tick thread on hot-reload), with a + // modifier mask per slot; button ids 3/4/5 = left/right/middle click (#285). + // Atomic so the hook thread's reads in setButtonState stay race-free, and the // held flags are cleared so a stale press of the previous button does not stick. - void setButtons(int inButtonId, int inButtonId2, int outButtonId, int outButtonId2); - // #285: the same with a modifier mask per slot, and button ids 3/4/5 = left/right/middle click. void setButtonBinds(int inButtonId, int inMods, int inButtonId2, int inMods2, int outButtonId, int outMods, int outButtonId2, int outMods2); // The slot a button press belongs to, given the modifiers held (hook thread). False = not a bind. diff --git a/src/installer_state.h b/src/installer_state.h index 573f24ed..6bf6fe33 100644 --- a/src/installer_state.h +++ b/src/installer_state.h @@ -1,7 +1,5 @@ #pragma once -// Pure decision logic shared between the installer's NSIS script and the test suite. -// NSIS reimplements these rules in its own dialect; these definitions are what the rules -// are checked against, so a change here is a change the installer must follow. +// Version parsing used by the WebView2 runtime probe. // NO : this header is compiled into the desktop-free test binary. #include @@ -14,8 +12,6 @@ struct Version { bool valid = false; }; -enum class InstallState { Fresh, Upgrade, Reinstall, Downgrade }; - // "1.2.3" or "1.2.3.4" (the build field is read and discarded: ARP writes three parts, // VERSIONINFO writes four, and they must compare equal). Anything else is invalid. inline Version ParseVersion(const std::string& s) { @@ -46,23 +42,4 @@ inline Version ParseVersion(const std::string& s) { return v; } -inline int CompareVersion(const Version& a, const Version& b) { - if (a.major != b.major) return a.major < b.major ? -1 : 1; - if (a.minor != b.minor) return a.minor < b.minor ? -1 : 1; - if (a.patch != b.patch) return a.patch < b.patch ? -1 : 1; - return 0; -} - -// `found` is whatever DisplayVersion the ARP key held, which is "" on a clean machine and -// can be junk left by a half-removed install. Either way there is nothing to upgrade from. -inline InstallState ClassifyInstall(const std::string& found, const std::string& ours) { - const Version f = ParseVersion(found); - const Version o = ParseVersion(ours); - if (!f.valid || !o.valid) return InstallState::Fresh; - const int c = CompareVersion(f, o); - if (c < 0) return InstallState::Upgrade; - if (c == 0) return InstallState::Reinstall; - return InstallState::Downgrade; -} - } // namespace wind diff --git a/src/java_bridge.h b/src/java_bridge.h index de00982d..c2669dd6 100644 --- a/src/java_bridge.h +++ b/src/java_bridge.h @@ -22,7 +22,6 @@ class JavaBridge { bool ensure(HWND javaWindow, DWORD wakeTid, UINT wakeMsg, bool log); // The caret of the focused Java text component, screen px. False when there is none. bool caret(HWND javaWindow, RECT& out); - bool loaded() const { return mod_ != nullptr; } // The last successful read: which bridge call answered and the caret index (trackLog). const char* lastSrc() const { return lastSrc_; } int lastIndex() const { return lastIndex_; } diff --git a/src/keybind_rules.h b/src/keybind_rules.h index bdf8f6bc..87d0ada6 100644 --- a/src/keybind_rules.h +++ b/src/keybind_rules.h @@ -92,20 +92,4 @@ inline BindVerdict CheckClickBind(int button, int mods) { return BindVerdict::Ok; } -inline const char* BindVerdictName(BindVerdict v) { - switch (v) { - case BindVerdict::Ok: return "ok"; - case BindVerdict::NeverBindable: return "never"; - case BindVerdict::ModifierAsKey: return "modifier"; - case BindVerdict::NotAlone: return "notalone"; - case BindVerdict::ShiftTypes: return "shifttypes"; - case BindVerdict::AltGrTypes: return "altgr"; - case BindVerdict::SystemReserved: return "system"; - case BindVerdict::WindowsReserved: return "windows"; - case BindVerdict::NeedsModifier: return "needsmod"; - case BindVerdict::CtrlAlone: return "ctrlalone"; - case BindVerdict::ShiftAlone: return "shiftalone"; - } - return "?"; -} } // namespace wind diff --git a/src/main.cpp b/src/main.cpp index 5607fea7..0e518a8d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -258,10 +258,8 @@ static VirtualBounds QueryVirtualBounds() { } // --- Per-tick state ------------------------------------------------------------------------- -// All the state one magnifier tick mutates, in one struct so the tick can run from BOTH the -// main loop AND a WM_TIMER. The tray context menu's TrackPopupMenu spins its own modal message -// loop that owns the thread until it closes; without a timer-driven tick the lens froze for -// the duration. The timer (set around the menu) dispatches WM_TIMER into WndProc, which ticks. +// All the state one magnifier tick mutates, in one struct. The main loop owns the only tick; +// WndProc reaches it through g_tick for the hotkey messages. struct TickState { IMagnifierModel* model; // CURRENT engine (hybrid swaps at zoom-in) IMagnifierModel* mRender = nullptr; // hybrid: the two engines (null when not hybrid) @@ -414,8 +412,6 @@ struct TickState { bool dbgPrevOutHeld = false; double dbgInHeldSec = 0.0; double dbgOutHeldSec = 0.0; - bool dbgInOverstayLogged = false; // one overstay WARN per stuck episode - bool dbgOutOverstayLogged = false; std::atomic quickZoomHotkey{false}; // set by WM_HOTKEY (hotkey-mode quick zoom), consumed in RunTick bool cursorHidden = false; // runtime-only override (no ini write, no hot-reload) double outlineIdleSec = 0.0; // seconds the cursor has been still (drives the outline idle fade) @@ -784,8 +780,7 @@ static void RegisterHideCursorHotkey(HWND hwnd, int vk, int mods); static void RegisterQuickZoomHotkey(HWND hwnd, int vk, int mods); // One magnifier tick: advance zoom, hot-reload config, then pan/draw via the render engine. -// Pure of any pacing wait - the caller paces. Safe to call from the main loop or from a -// WM_TIMER during a modal loop. +// Pure of any pacing wait - the caller paces. // Test telemetry (issue #225): the proving-ground harness sets WIND_TESTLOG= and every // tick appends one CSV sample. Disabled (one branch per tick) in normal runs. static wind::TestTelemetry g_testlog; @@ -2578,12 +2573,12 @@ static void RunTick(TickState& t) { // Diagnostics (issue #113): log the side-button held-state timeline so the intermittent stuck can // be diagnosed from the log. On every rise/fall, dump the hook + Raw Input event counters and the // held duration; a stuck shows as a rise with no matching fall (and the next event only on - // re-click). Also WARN once if a hold overstays 6 s (well past any hold-to-zoom, which caps in + // re-click). Also WARN once if a hold overstays (first line at 10 s, then every 5 s; well past any hold-to-zoom, which caps in // ~2 s) - that line, with static counters, pinpoints a stuck episode. Edges/overstay only, so // Log() is never hit on the per-frame path. { auto& st = g_input.state(); - auto snap = [&](const char* tag, bool held, bool& prev, double& secs, bool& warned) { + auto snap = [&](const char* tag, bool held, bool& prev, double& secs) { if (held != prev) { // Edge: `secs` still holds the accumulated duration (meaningful on a fall; ~0 on a rise). wind::Log(wind::LogLevel::Info, "input", @@ -2594,7 +2589,6 @@ static void RunTick(TickState& t) { st.dbgRawDown[1].load(), st.dbgRawUp[1].load(), st.dbgRawDown[2].load(), st.dbgRawUp[2].load(), g_input.hookActive() ? 1 : 0, lvl); - warned = false; // arm the overstay warning for the next episode prev = held; // Re-fire every 5 s while a hold overstays, not once. A stuck hold has no falling edge, // so a single sample can never show HOW FAST the runaway zoom is climbing - and that @@ -2610,7 +2604,6 @@ static void RunTick(TickState& t) { st.dbgHookDown[2].load(), st.dbgHookUp[2].load(), st.dbgHookDbl[2].load(), st.dbgRawDown[1].load(), st.dbgRawUp[1].load(), st.dbgRawDown[2].load(), st.dbgRawUp[2].load(), lvl); - warned = true; } // Accumulate AFTER edge handling so a fall reports the pre-reset duration; cleared at 0 when up. secs = held ? (secs + dt) : 0.0; @@ -2620,8 +2613,8 @@ static void RunTick(TickState& t) { // detector could not see, which is why episodes of #167 never left a STUCK? line in the // log despite being hit repeatedly. inHeld/outHeld are the same values that drive the // zoom, so the diagnostic now reports what actually happened rather than half of it. - snap("in", inHeld, t.dbgPrevInHeld, t.dbgInHeldSec, t.dbgInOverstayLogged); - snap("out", outHeld, t.dbgPrevOutHeld, t.dbgOutHeldSec, t.dbgOutOverstayLogged); + snap("in", inHeld, t.dbgPrevInHeld, t.dbgInHeldSec); + snap("out", outHeld, t.dbgPrevOutHeld, t.dbgOutHeldSec); } // Frame-pacing diagnostics: a 2 s window of loop-interval stats (dt = time between ticks = @@ -2734,10 +2727,6 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (g_tick) g_tick->quickZoomHotkey.store(true); // RunTick consumes it (rising-edge via MOD_NOREPEAT) return 0; } - // Keep ticking while a modal loop (the tray menu) owns the thread. The tray sets a timer - // around TrackPopupMenu; its WM_TIMER lands here so the lens doesn't freeze. (No other - // WM_TIMER exists in this process.) - if (msg == WM_TIMER) { if (g_tick) RunTick(*g_tick); return 0; } if (msg == WM_INPUT) { // One syscall, not two: RAWINPUT for mouse/keyboard always fits the fixed buffer, so the // size-query round trip per packet (hundreds/s while panning) bought nothing. @@ -3123,7 +3112,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { ts.mRender = model.get(); ts.mTransform = model2.get(); ts.hwnd = hwnd; // so RunTick can re-register the hide-cursor hotkey - g_tick = &ts; // so the WM_TIMER tick (during the tray menu's modal loop) can run + g_tick = &ts; // so WndProc's hotkey messages can reach the tick state { // Restore the learned gain curve so the first locked session after a restart pans at the // learned desktop speed instead of raw passthrough ("default to the last read value"). // Corrupt or missing = fresh learner, which re-warms from live use in seconds. diff --git a/src/render_model.h b/src/render_model.h index 5df273de..17902acd 100644 --- a/src/render_model.h +++ b/src/render_model.h @@ -41,7 +41,6 @@ class RenderModel : public IMagnifierModel { int zorderBand_; bool hdrTonemap_; int gpuPriority_; - bool primed_ = false; bool visible_ = false; // the overlay is revealed (setActive(true) .. setActive(false)) public: // Whether the overlay is on screen. The colour filter follows the VISIBLE engine, not the diff --git a/src/tick_stats.h b/src/tick_stats.h index 1c6483e1..d725f41c 100644 --- a/src/tick_stats.h +++ b/src/tick_stats.h @@ -63,7 +63,7 @@ inline double MedianMs(const float* v, int n) { // cancel in a mean, while the median sits on whichever half of the pair is more common - which // is how the tray read "145 fps" on a 144Hz panel (field 2026-08-28). A lone stall barely moves // a 256-sample mean (~0.07ms), and a SUSTAINED slowdown moving it is the figure being honest. -// The median stays the anchor for the sparkline scale and the LateCount threshold, where +// The median stays the anchor for the sparkline scale, where // robustness against stalls is exactly what is wanted. inline double MeanMs(const float* v, int n) { if (!v || n <= 0) return 0.0; @@ -75,14 +75,4 @@ inline double MeanMs(const float* v, int n) { // Frames per second implied by an interval. 0 in, 0 out - never a divide by zero into the UI. inline double FpsFromMs(double ms) { return ms > 0.0001 ? 1000.0 / ms : 0.0; } -// How many of the samples were LATE: over 1.5x the median. The same 1.5x threshold the pan-wake -// harness uses, so the tray and the diagnostics agree about what counts as a stall. -inline int LateCount(const float* v, int n) { - if (!v || n <= 0) return 0; - const double thr = MedianMs(v, n) * 1.5; - int late = 0; - for (int i = 0; i < n; ++i) if ((double)v[i] > thr) ++late; - return late; -} - } // namespace wind diff --git a/src/transform.cpp b/src/transform.cpp index 225d2c39..53909e1b 100644 --- a/src/transform.cpp +++ b/src/transform.cpp @@ -15,13 +15,6 @@ OffsetF ComputeOffsetF(double centerX, double centerY, double level, int screenW return OffsetF{ x, y }; } -OffsetF ComputeFixedPointOffset(double centerX, double centerY, double level) { - if (level < 1.0) level = 1.0; - // T(p) = (p - off) * level. Solving T(center) == center gives off = center * (1 - 1/level). - const double k = 1.0 - 1.0 / level; - return OffsetF{ centerX * k, centerY * k }; -} - static int iround(double v) { int lower = (int)(v >= 0 ? v : v - 1); double frac = v - lower; diff --git a/src/transform.h b/src/transform.h index dadf7485..2d370b61 100644 --- a/src/transform.h +++ b/src/transform.h @@ -6,21 +6,6 @@ struct OffsetF { double x; double y; }; // Used by the own GPU renderer, which pans sub-pixel. OffsetF ComputeOffsetF(double centerX, double centerY, double level, int screenW, int screenH); -// Source top-left that makes `center` the FIXED POINT of the fullscreen transform: T(center)==center. -// -// The render model draws the cursor into its own frame, so it can centre the view on the cursor. The -// transform model cannot: DWM composites the cursor AND layered windows OUTSIDE the fullscreen -// magnification (measured - a layered window at desktop P stays at screen P, unscaled, while the -// content magnifies around it). So whatever draws the cursor lands at the unmagnified point L, while -// the item a click at L hits is drawn at T(L). Centring makes T(L) the screen centre, so the two -// diverge by (centre - L): zero at the centre, growing toward the edges. That is the click drift. -// -// Anchoring the transform at the cursor instead makes the content under the cursor be the content AT -// the cursor, so an unmagnified cursor always sits on exactly what it clicks - regardless of what DWM -// does or does not magnify. The result always lies in [0, screen - screen/level], so unlike the -// centred offset it NEVER clamps, which is what removes the dead zones at the screen edges. -OffsetF ComputeFixedPointOffset(double centerX, double centerY, double level); - // The two forms of the fullscreen-magnifier transform, both derived from the sub-pixel source // top-left (srcLeft/srcTop, screen px) the CursorMapper already clamps. off* is the whole-pixel // offset the public MagSetFullscreenTransform takes; tx* is the screen-space translation diff --git a/src/transform_model.cpp b/src/transform_model.cpp index ec3b0ccd..d2c18d22 100644 --- a/src/transform_model.cpp +++ b/src/transform_model.cpp @@ -51,15 +51,15 @@ static bool NudgeBlocked() { // has ended (zoom can be bound to a mouse button, so the transition often lands inside the window // and a still pointer would otherwise stay invisible until the hand moved). A press held past the // window is a drag and is nudged as usual (NudgeBlockedByClick). -static void NudgePointer(POINT& np) { +static void NudgePointer() { if (NudgeBlocked()) { g_nudgeOwed.store(true, std::memory_order_relaxed); return; } g_nudgeOwed.store(false, std::memory_order_relaxed); + POINT np; if (GetCursorPos(&np)) { SetCursorPos(np.x + 1, np.y); SetCursorPos(np.x, np.y); } } static void DeliverOwedNudge() { if (!NudgeDue(g_nudgeOwed.load(std::memory_order_relaxed), NudgeBlocked())) return; - POINT np; - NudgePointer(np); + NudgePointer(); } void TransformModel::resetTransformState() { nativePrimed_ = false; // a rebuilt context needs its own public prime (#369) @@ -119,8 +119,7 @@ void TransformModel::teardownMag() { // Released mid-session (shutdown, model swap): the pointer leaves DWM's composition for the // hardware plane, which Windows repaints only on the next cursor EVENT, so nudge it a pixel // and back (the same trick the zoom-out uses). - POINT np; - NudgePointer(np); + NudgePointer(); } lensFailed_ = false; // a fresh context may build the lens QueryPerformanceCounter(&b); @@ -365,8 +364,7 @@ void TransformModel::setActive(bool active) { // hand is still (measured: no composed pointer in any ramp frame). Nudge a pixel and back. if (host_.createCursorLens()) { host_.setCursorLens(true); - POINT np; - NudgePointer(np); + NudgePointer(); } QueryPerformanceCounter(&z2); lastEnter_.ensureMagMs = double(z2.QuadPart - z1.QuadPart) * 1000.0 / zf.QuadPart; @@ -406,8 +404,7 @@ void TransformModel::setActive(bool active) { // blanker's worker (#363: the scheme reload froze the 1x landing frame for 8-90 ms), so // the nudge rides along on the worker too. blanker_->restore([] { - POINT np; - NudgePointer(np); + NudgePointer(); }); step(1); // system cursor restore queued (it was the whole teardown cost) } @@ -439,8 +436,7 @@ void TransformModel::setActive(bool active) { // change a game makes). The hardware plane repaints only on the next cursor EVENT, so // nudge the pointer a pixel and back. host_.setCursorLens(false); - POINT np; - NudgePointer(np); + NudgePointer(); } RECT full{ 0, 0, mon_.w, mon_.h }; host_.setInputTransform(false, full, full); // input mapping back to identity at 1x @@ -765,8 +761,7 @@ void TransformModel::present(const MapResult& r, double level, const Config& cfg writeTransform((float)applyLevel, m.offX + offJitter, m.offY, m.txX + txJitter, m.txY, fastPan_ && !prime); if (prime) nativePrimed_ = true; if (NudgeAfterWrite(dwmCentreOn_, true)) { - POINT np; - NudgePointer(np); + NudgePointer(); } forceWrite_ = false; } @@ -866,8 +861,7 @@ void TransformModel::present(const MapResult& r, double level, const Config& cfg if (ok && NudgeAfterPublish(applyLevel, ixPubLevel_)) { // A scale-changing publish stops DWM drawing the composed pointer until the next // cursor event: give it one, a pixel and back. - POINT np; - NudgePointer(np); + NudgePointer(); } if (ok) ixPubLevel_ = applyLevel; if (ok) { @@ -953,8 +947,7 @@ void TransformModel::present(const MapResult& r, double level, const Config& cfg // A restored-but-still pointer stays invisible until a cursor EVENT, so the nudge // follows the restore on the blanker's worker, as at the zoom-out. blanker_->restore([] { - POINT np; - NudgePointer(np); + NudgePointer(); }); cursorHidden_ = false; } diff --git a/src/tray_app/flyout_model.h b/src/tray_app/flyout_model.h index 11a9b223..e622c326 100644 --- a/src/tray_app/flyout_model.h +++ b/src/tray_app/flyout_model.h @@ -1,6 +1,6 @@ #pragma once // The tray flyout's PURE model (no , so it is unit-tested): where the window opens -// (taskbar on any edge, any monitor, any DPI), the layout of its rows in DIPs, hit-testing, the +// (at the click point, any monitor, any DPI), the layout of its rows in DIPs, hit-testing, the // slider specs and value text, and the view model built from the ini, the tray layout and Wind's // shared status. flyout_draw.cpp paints a View; flyout_window.cpp owns the window. // Spec: docs/specs/2026-10-01-tray-flyout-design.md (issue #313). @@ -31,55 +31,13 @@ inline int ToDip(int px, int dpi) { return dpi > 0 ? (px * 96 + dpi / 2) / dpi : // ---------------------------------------------------------------- placement -enum class Edge { Bottom, Top, Left, Right }; -struct Placement { int x = 0, y = 0; Edge edge = Edge::Bottom; }; +struct Placement { int x = 0, y = 0; }; inline int ClampInt(int v, int lo, int hi) { if (hi < lo) return lo; // not enough room: keep the top/left edge on screen return v < lo ? lo : (v > hi ? hi : v); } -// Which edge the taskbar is on, from the part of the monitor the work area does not cover. With no -// visible taskbar (auto-hide, or an icon on another monitor) it falls back to the edge nearest the -// tray icon, which is where the shell will slide the bar in. -inline Edge DetectEdge(const IRect& monitor, const IRect& work, const IRect& icon) { - const int dl = work.l - monitor.l, dt = work.t - monitor.t; - const int dr = monitor.r - work.r, db = monitor.b - work.b; - const int m = (std::max)((std::max)(dl, dr), (std::max)(dt, db)); - if (m > 0) { - if (db == m) return Edge::Bottom; - if (dt == m) return Edge::Top; - if (dr == m) return Edge::Right; - return Edge::Left; - } - const int cx = (icon.l + icon.r) / 2, cy = (icon.t + icon.b) / 2; - const int toL = cx - monitor.l, toR = monitor.r - cx, toT = cy - monitor.t, toB = monitor.b - cy; - const int n = (std::min)((std::min)(toL, toR), (std::min)(toT, toB)); - if (n == toB) return Edge::Bottom; - if (n == toT) return Edge::Top; - if (n == toR) return Edge::Right; - return Edge::Left; -} - -// Top-left of a w x h window (pixels) next to the tray icon: just inside the work area on the -// taskbar's side, centred on the icon along the taskbar, clamped to the work area, `gap` px from -// every edge. All arguments are physical pixels of the icon's monitor. -inline Placement PlaceFlyout(const IRect& icon, const IRect& monitor, const IRect& work, - int w, int h, int gap) { - Placement p; - p.edge = DetectEdge(monitor, work, icon); - const int cx = (icon.l + icon.r) / 2, cy = (icon.t + icon.b) / 2; - switch (p.edge) { - case Edge::Bottom: p.x = cx - w / 2; p.y = work.b - h - gap; break; - case Edge::Top: p.x = cx - w / 2; p.y = work.t + gap; break; - case Edge::Left: p.x = work.l + gap; p.y = cy - h / 2; break; - case Edge::Right: p.x = work.r - w - gap; p.y = cy - h / 2; break; - } - p.x = ClampInt(p.x, work.l + gap, work.r - w - gap); - p.y = ClampInt(p.y, work.t + gap, work.b - h - gap); - return p; -} - // Top-left of a w x h window opened at the click point, the way the old TrackPopupMenu tray menu // was (owner decision: "same position as the old tray menu", including when the icon lives in the // overflow flyout). Owner decision: the window opens UP AND TO THE LEFT of the pointer, diff --git a/src/tray_app/flyout_window.cpp b/src/tray_app/flyout_window.cpp index b4c79eba..62a957ec 100644 --- a/src/tray_app/flyout_window.cpp +++ b/src/tray_app/flyout_window.cpp @@ -782,12 +782,6 @@ bool OpenFlyout() { bool FlyoutIsOpen() { return g_f != nullptr; } -void FlyoutRefresh() { - if (!g_f || g_f->closing) return; - RebuildView(*g_f); - Render(*g_f); -} - void CloseFlyout() { if (!g_f) return; g_f->closing = true; diff --git a/src/version.h b/src/version.h index 67f18d8c..11c7be65 100644 --- a/src/version.h +++ b/src/version.h @@ -4,7 +4,7 @@ #define WIND_VER_MAJOR 0 #define WIND_VER_MINOR 24 -#define WIND_VER_PATCH 17 +#define WIND_VER_PATCH 18 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.24.17" +#define WIND_VERSION_STR "0.24.18" diff --git a/src/zoom_ladder.h b/src/zoom_ladder.h index c34f16c4..17a4faef 100644 --- a/src/zoom_ladder.h +++ b/src/zoom_ladder.h @@ -115,23 +115,6 @@ inline double SnapSmoothLevel(double want, double centreX, double centreY, int w return want; } -// THE EASE-OUT MAY NOT CROSS A ROUNDING STEP (field 2026-10-07). After the zoom key is released the -// zoom decelerates; snapping there hopped between clean levels (read as settling), and not snapping -// let the slow zoom cross one of DWM's whole-pixel rounding steps, which jumps the image by up to -// the level in px (read as the view and cursor shifting once the zoom settled). Within one rounding -// cell (same scratch size n and same rounded origin on both axes) the image moves continuously, so -// the ease-out follows the request while it stays in the cell and stops just before it would leave. -inline bool SameRoundingCell(double z1, double z2, double centreX, double centreY, int w, int h) { - auto cell = [](double z, int extent, double centre, double& n, double& o) { - n = std::floor(extent / z + 0.5); - o = std::floor(LadderOrigin(centre, z, extent) + 0.5); - }; - double n1x, o1x, n2x, o2x, n1y, o1y, n2y, o2y; - cell(z1, w, centreX, n1x, o1x); cell(z2, w, centreX, n2x, o2x); - cell(z1, h, centreY, n1y, o1y); cell(z2, h, centreY, n2y, o2y); - return n1x == n2x && o1x == o2x && n1y == n2y && o1y == o2y; -} - // TRUNCATED EASE-OUT (field 2026-10-07). After a release the user's ease-out runs, snapped to // clean levels like a held zoom; once it moves less per frame than clean levels are apart, the // ladder could only hop or jump, so the zoom stops there on the level on screen. The fast part of diff --git a/tests/test_flyout.cpp b/tests/test_flyout.cpp index 4b4c335e..22e1117a 100644 --- a/tests/test_flyout.cpp +++ b/tests/test_flyout.cpp @@ -18,100 +18,6 @@ TEST_CASE("StepToward: linear, clamps at the target, instant for a zero duration static const IRect kMon{0, 0, 1920, 1080}; -TEST_CASE("placement: taskbar on the bottom opens above the icon, centred, inside the work area") { - const IRect work{0, 0, 1920, 1040}; // 40 px bar at the bottom - const IRect icon{1700, 1044, 1724, 1068}; - const Placement p = PlaceFlyout(icon, kMon, work, 300, 285, 8); - CHECK(p.edge == Edge::Bottom); - CHECK(p.x == 1712 - 150); - CHECK(p.y == 1040 - 285 - 8); -} - -TEST_CASE("placement: bottom taskbar, icon at the far right is clamped to the work area") { - const IRect work{0, 0, 1920, 1040}; - const IRect icon{1890, 1044, 1914, 1068}; - const Placement p = PlaceFlyout(icon, kMon, work, 300, 285, 8); - CHECK(p.x == 1920 - 300 - 8); - CHECK(p.y == 1040 - 285 - 8); -} - -TEST_CASE("placement: taskbar on top opens below the icon") { - const IRect work{0, 40, 1920, 1080}; - const IRect icon{1700, 8, 1724, 32}; - const Placement p = PlaceFlyout(icon, kMon, work, 300, 285, 8); - CHECK(p.edge == Edge::Top); - CHECK(p.y == 40 + 8); - CHECK(p.x == 1712 - 150); -} - -TEST_CASE("placement: taskbar on the left opens to its right, centred on the icon") { - const IRect work{48, 0, 1920, 1080}; - const IRect icon{12, 500, 36, 524}; - const Placement p = PlaceFlyout(icon, kMon, work, 300, 285, 8); - CHECK(p.edge == Edge::Left); - CHECK(p.x == 48 + 8); - CHECK(p.y == 512 - 142); -} - -TEST_CASE("placement: taskbar on the right opens to its left, clamped at the bottom") { - const IRect work{0, 0, 1872, 1080}; - const IRect icon{1884, 1060, 1908, 1080}; - const Placement p = PlaceFlyout(icon, kMon, work, 300, 285, 8); - CHECK(p.edge == Edge::Right); - CHECK(p.x == 1872 - 300 - 8); - CHECK(p.y == 1080 - 285 - 8); -} - -TEST_CASE("placement: a secondary monitor to the left (negative coordinates)") { - const IRect mon{-1920, 0, 0, 1080}; - const IRect work{-1920, 0, 0, 1040}; - const IRect icon{-300, 1044, -276, 1068}; - const Placement p = PlaceFlyout(icon, mon, work, 300, 285, 8); - CHECK(p.edge == Edge::Bottom); - CHECK(p.x == -288 - 150); - CHECK(p.y == 1040 - 285 - 8); -} - -TEST_CASE("placement: a secondary monitor above and to the right") { - const IRect mon{1920, -1080, 3840, 0}; - const IRect work{1920, -1080, 3840, -40}; - const IRect icon{3800, -36, 3824, -12}; - const Placement p = PlaceFlyout(icon, mon, work, 300, 285, 8); - CHECK(p.edge == Edge::Bottom); - CHECK(p.x == 3840 - 300 - 8); - CHECK(p.y == -40 - 285 - 8); -} - -TEST_CASE("placement: DPI 100 to 250 percent, sizes scaled, always fully inside the work area") { - for (int dpi : { 96, 120, 144, 168, 192, 240 }) { - const int w = ScalePx(300, dpi), h = ScalePx(285, dpi), gap = ScalePx(8, dpi); - const int barH = ScalePx(48, dpi); - const IRect mon{0, 0, 3840, 2160}; - const IRect work{0, 0, 3840, 2160 - barH}; - const IRect icon{3700, 2160 - barH + 8, 3700 + ScalePx(24, dpi), 2160 - 8}; - const Placement p = PlaceFlyout(icon, mon, work, w, h, gap); - CHECK(p.x >= work.l + gap); - CHECK(p.x + w <= work.r - gap); - CHECK(p.y >= work.t + gap); - CHECK(p.y + h <= work.b - gap); - } -} - -TEST_CASE("placement: auto-hide taskbar (work area == monitor) uses the edge nearest the icon") { - const IRect work = kMon; - CHECK(DetectEdge(kMon, work, IRect{1700, 1056, 1724, 1080}) == Edge::Bottom); - CHECK(DetectEdge(kMon, work, IRect{1700, 0, 1724, 24}) == Edge::Top); - CHECK(DetectEdge(kMon, work, IRect{0, 500, 24, 524}) == Edge::Left); - CHECK(DetectEdge(kMon, work, IRect{1896, 500, 1920, 524}) == Edge::Right); -} - -TEST_CASE("placement: a window larger than the work area keeps its top-left on screen") { - const IRect work{0, 0, 200, 200}; - const Placement p = PlaceFlyout(IRect{100, 190, 120, 200}, IRect{0, 0, 200, 220}, work, 300, 285, 8); - CHECK(p.x == 8); - CHECK(p.y == 8); -} - TEST_CASE("dismissal: the icon click that deactivated the flyout must not reopen it") { CHECK(IgnoreIconClick(1000, 900, false)); // 100 ms after the deactivation close CHECK_FALSE(IgnoreIconClick(1000, 600, false)); // 400 ms: a genuine new click diff --git a/tests/test_gain_learner.cpp b/tests/test_gain_learner.cpp index c87ad2ad..185452b9 100644 --- a/tests/test_gain_learner.cpp +++ b/tests/test_gain_learner.cpp @@ -10,7 +10,6 @@ using namespace wind; TEST_CASE("unlearned state is raw passthrough") { GainLearner g; CHECK(g.gainFor(10, 7.0) == doctest::Approx(1.0)); - CHECK(!g.warmedUp()); } TEST_CASE("a learned ratio is replayed at the same speed") { @@ -20,9 +19,7 @@ TEST_CASE("a learned ratio is replayed at the same speed") { // Interpolation trades exact replay for continuity: a query off the bin centre // blends toward the neighbour, so a few percent of drift is by design. CHECK(g.gainFor(10.0, 7.0) == doctest::Approx(2.5).epsilon(0.06)); - CHECK(g.warmedUp() == false); // one bin is not enough to call it warmed for (int i = 0; i < 100; ++i) g.observe(100.0, 400.0, 7.0); // faster speed, higher gain - CHECK(g.warmedUp()); CHECK(g.gainFor(100.0, 7.0) == doctest::Approx(4.0).epsilon(0.06)); } @@ -78,7 +75,6 @@ TEST_CASE("the learned curve round-trips through text") { CHECK(h.deserialize(buf)); CHECK(h.gainFor(10.0, 7.0) == doctest::Approx(g.gainFor(10.0, 7.0))); CHECK(h.gainFor(150.0, 7.0) == doctest::Approx(g.gainFor(150.0, 7.0))); - CHECK(h.warmedUp()); } TEST_CASE("a corrupt persistence file is rejected whole, leaving a fresh learner") { diff --git a/tests/test_installer_state.cpp b/tests/test_installer_state.cpp index 3c662020..82812534 100644 --- a/tests/test_installer_state.cpp +++ b/tests/test_installer_state.cpp @@ -27,26 +27,3 @@ TEST_CASE("ParseVersion rejects junk") { CHECK_FALSE(ParseVersion(".1.2.3").valid); CHECK_FALSE(ParseVersion("1.2.3.4.5").valid); } - -TEST_CASE("CompareVersion orders by major then minor then patch") { - CHECK(CompareVersion(ParseVersion("1.0.0"), ParseVersion("0.9.9")) == 1); - CHECK(CompareVersion(ParseVersion("0.1.0"), ParseVersion("0.1.0")) == 0); - CHECK(CompareVersion(ParseVersion("0.1.2"), ParseVersion("0.1.10")) == -1); -} - -// ARP writes three parts and VERSIONINFO writes four; the same release must not read as -// an upgrade over itself just because one source carried a trailing build field. -TEST_CASE("CompareVersion ignores the build field so three and four parts agree") { - CHECK(CompareVersion(ParseVersion("0.1.0"), ParseVersion("0.1.0.0")) == 0); -} - -TEST_CASE("ClassifyInstall calls an absent previous version a fresh install") { - CHECK(ClassifyInstall("", "0.2.0") == InstallState::Fresh); - CHECK(ClassifyInstall("garbage", "0.2.0") == InstallState::Fresh); -} - -TEST_CASE("ClassifyInstall separates upgrade, reinstall and downgrade") { - CHECK(ClassifyInstall("0.1.0", "0.2.0") == InstallState::Upgrade); - CHECK(ClassifyInstall("0.2.0", "0.2.0") == InstallState::Reinstall); - CHECK(ClassifyInstall("0.3.0", "0.2.0") == InstallState::Downgrade); -} diff --git a/tests/test_keybind_rules.cpp b/tests/test_keybind_rules.cpp index 487e24f0..271c1f33 100644 --- a/tests/test_keybind_rules.cpp +++ b/tests/test_keybind_rules.cpp @@ -6,6 +6,24 @@ #include using namespace wind; +// Fixture spelling of each verdict; only this test needs the names. +static inline const char* BindVerdictName(BindVerdict v) { + switch (v) { + case BindVerdict::Ok: return "ok"; + case BindVerdict::NeverBindable: return "never"; + case BindVerdict::ModifierAsKey: return "modifier"; + case BindVerdict::NotAlone: return "notalone"; + case BindVerdict::ShiftTypes: return "shifttypes"; + case BindVerdict::AltGrTypes: return "altgr"; + case BindVerdict::SystemReserved: return "system"; + case BindVerdict::WindowsReserved: return "windows"; + case BindVerdict::NeedsModifier: return "needsmod"; + case BindVerdict::CtrlAlone: return "ctrlalone"; + case BindVerdict::ShiftAlone: return "shiftalone"; + } + return "?"; +} + // Runs every case in the shared list (also read by the UI mirror's tests). TEST_CASE("keybind safety rules match the shared case list (#285)") { // The cwd varies (repo root from build.bat, a build dir elsewhere), so also try the folder diff --git a/tests/test_transform.cpp b/tests/test_transform.cpp index 95c70e96..aff836de 100644 --- a/tests/test_transform.cpp +++ b/tests/test_transform.cpp @@ -31,64 +31,6 @@ TEST_CASE("ComputeOffsetF clamps level below 1.0 to 1x (whole screen, origin)") CHECK(a.y == doctest::Approx(0.0)); } -// --- Fixed-point offset (transform model) --- -// The transform model must anchor the magnification AT the cursor, because DWM composites the cursor -// and layered windows OUTSIDE the fullscreen magnification. Only then does an unmagnified cursor sit -// on exactly the content a click at that point hits. - -// T(p) = (p - off) * level, the mapping DWM applies to desktop content. -static double T(double p, double off, double level) { return (p - off) * level; } - -TEST_CASE("ComputeFixedPointOffset makes the cursor the fixed point: T(center) == center") { - static const double levels[] = { 1.0, 1.5, 2.143, 4.0, 8.0, 20.0 }; - static const double pts[] = { 0.0, 1.0, 507.0, 1920.0, 2600.0, 3840.0 }; - for (double level : levels) { - for (double c : pts) { - wind::OffsetF o = wind::ComputeFixedPointOffset(c, c, level); - CHECK(T(c, o.x, level) == doctest::Approx(c)); - CHECK(T(c, o.y, level) == doctest::Approx(c)); - } - } -} - -TEST_CASE("ComputeFixedPointOffset never leaves the legal source range, so it never clamps") { - const double sw = 3840.0, sh = 2160.0; - static const double levels[] = { 1.0, 2.0, 3.7, 8.0, 20.0 }; - static const double fracs[] = { 0.0, 0.13, 0.5, 0.87, 1.0 }; - for (double level : levels) { - for (double frac : fracs) { - wind::OffsetF o = wind::ComputeFixedPointOffset(sw * frac, sh * frac, level); - CHECK(o.x >= 0.0); - CHECK(o.y >= 0.0); - CHECK(o.x <= doctest::Approx(sw - sw / level)); // max legal source top-left - CHECK(o.y <= doctest::Approx(sh - sh / level)); - } - } -} - -TEST_CASE("ComputeFixedPointOffset is identity at 1x and clamps a sub-1 level up to 1") { - wind::OffsetF a = wind::ComputeFixedPointOffset(1234.0, 567.0, 1.0); - CHECK(a.x == doctest::Approx(0.0)); - CHECK(a.y == doctest::Approx(0.0)); - wind::OffsetF b = wind::ComputeFixedPointOffset(1234.0, 567.0, 0.25); // treated as 1.0 - CHECK(b.x == doctest::Approx(0.0)); - CHECK(b.y == doctest::Approx(0.0)); -} - -TEST_CASE("centred offset does NOT fix the cursor, which is the click-drift bug") { - // Same cursor, same level: centring puts the cursor's content at the screen centre, so the - // content under an unmagnified cursor is a different point. Drift is zero only at the centre. - const double sw = 3840.0, sh = 2160.0, level = 2.0; - wind::OffsetF mid = wind::ComputeOffsetF(sw / 2, sh / 2, level, (int)sw, (int)sh); - CHECK(T(sw / 2, mid.x, level) == doctest::Approx(sw / 2)); // no drift at the centre - - wind::OffsetF off = wind::ComputeOffsetF(700.0, 400.0, level, (int)sw, (int)sh); - CHECK(T(700.0, off.x, level) != doctest::Approx(700.0)); // drifts away from the centre - // ...whereas the fixed-point offset does not, anywhere. - wind::OffsetF fp = wind::ComputeFixedPointOffset(700.0, 400.0, level); - CHECK(T(700.0, fp.x, level) == doctest::Approx(700.0)); -} - // --- MagSetInputTransform rects (issue #185; POINTER-HITTEST-FINDINGS.md) ------------------- TEST_CASE("input-transform rects: 4x on the primary matches the native-Magnifier-measured shape") { // Native at 400% on 3840x2160 published src extents of exactly 960x540 (field-sampled). diff --git a/tests/test_tray_status.cpp b/tests/test_tray_status.cpp index 0c214521..775c63ef 100644 --- a/tests/test_tray_status.cpp +++ b/tests/test_tray_status.cpp @@ -83,13 +83,6 @@ TEST_CASE("the fps figure uses the mean so jitter pairs cancel to the true rate" CHECK(MeanMs(nullptr, 0) == doctest::Approx(0.0)); } -TEST_CASE("late frames are counted at 1.5x the median, matching the pan-wake harness") { - float v[10] = {7,7,7,7,7,7,7,7,7,25}; - CHECK(LateCount(v, 10) == 1); - float clean[5] = {7,7,7,7,7}; - CHECK(LateCount(clean, 5) == 0); -} - TEST_CASE("fps never divides by zero") { CHECK(FpsFromMs(0.0) == doctest::Approx(0.0)); CHECK(FpsFromMs(-1.0) == doctest::Approx(0.0)); diff --git a/tests/test_webview_recover.cpp b/tests/test_webview_recover.cpp index 9e7a2522..6f4f2288 100644 --- a/tests/test_webview_recover.cpp +++ b/tests/test_webview_recover.cpp @@ -22,12 +22,3 @@ TEST_CASE("an engine that dies on every start gives up after three in a minute") CHECK(DecideWvRecovery(0, b, 59999) == WvRecovery::GiveUp); CHECK(DecideWvRecovery(0, b, 60001) == WvRecovery::Recreate); // the first one aged out } -TEST_CASE("only a JSON object is echoed back as the page's draft") { - auto ok = [](const char* s) { return LooksLikeJsonObject(s, std::strlen(s)); }; - CHECK(ok("{\"colorWarmPct\":\"80\"}")); - CHECK(ok(" {} ")); - CHECK_FALSE(ok("")); - CHECK_FALSE(ok("null")); - CHECK_FALSE(ok("[1]")); - CHECK_FALSE(ok("{")); -} diff --git a/tests/test_zoom_ladder.cpp b/tests/test_zoom_ladder.cpp index 440e874a..1cb49afd 100644 --- a/tests/test_zoom_ladder.cpp +++ b/tests/test_zoom_ladder.cpp @@ -77,15 +77,6 @@ TEST_CASE("a slow (easing) zoom is barely snapped: the snap never exceeds the fr } } -TEST_CASE("rounding cells: a tiny step usually stays inside, a long glide leaves") { - int same = 0, total = 0; - for (double z = 3.0; z < 20.0; z *= 1.003, ++total) - if (SameRoundingCell(z, z * 1.00005, 1920.0, 1080.0, 3840, 2160)) ++same; - CHECK(same > total * 8 / 10); - CHECK(SameRoundingCell(4.0, 6.0, 1920.0, 1080.0, 3840, 2160) == false); - CHECK(SameRoundingCell(5.0, 5.0, 1920.0, 1080.0, 3840, 2160) == true); -} - TEST_CASE("the ease-out runs while it moves faster than the clean-level spacing, then stops") { CHECK(EaseOutShouldStop(5.0, 5.0 / 1.02) == false); // 2 % per frame: still gliding CHECK(EaseOutShouldStop(5.0, 5.0 / 1.001) == true); // 0.1 % per frame: the tail, stop