From 492e4611682484fdac750dfdfe9867208938f341 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:12:33 +0100 Subject: [PATCH] fix(mac): no tiny cursor at the Restart prompt with Handover off (0.5.1) loginwindow also owns the Restart and Shut Down prompts, and Wind took them for the lock screen, so Handover off gave the real cursor back. Now the session's locked state decides; a prompt below the overlay is simply drawn. Fixes #550 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kg7JN6EyAPMxrJa9AvGHEC --- mac/Sources/Wind/DrawZoom.swift | 25 +++++++++++++++++++++++-- mac/VERSION | 2 +- mac/docs/architecture.md | 2 ++ mac/scripts/backdrop_truth.py | 13 +++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/mac/Sources/Wind/DrawZoom.swift b/mac/Sources/Wind/DrawZoom.swift index 3dc2a3e2..86e83680 100644 --- a/mac/Sources/Wind/DrawZoom.swift +++ b/mac/Sources/Wind/DrawZoom.swift @@ -343,11 +343,25 @@ final class DrawZoom: NSObject, SCStreamOutput, SCStreamDelegate, @unchecked Sen private static let lockOwners: Set = { var s: Set = ["loginwindow", "ScreenSaverEngine"] #if WIND_TEST_CHANNEL - if let v = ProcessInfo.processInfo.environment["WIND_FAKE_LOCK"] { s.insert(v) } // a test window stands in for it + for k in ["WIND_FAKE_LOCK", "WIND_FAKE_LOGIN_DIALOG"] { if let v = ProcessInfo.processInfo.environment[k] { s.insert(v) } } // a test window stands in #endif return s }() + /// Owners whose windows are not always the lock screen. + private static let loginDialogOwners: Set = { + var s: Set = ["loginwindow"] + #if WIND_TEST_CHANNEL + if let v = ProcessInfo.processInfo.environment["WIND_FAKE_LOGIN_DIALOG"] { s.insert(v) } // a test window stands in for one + #endif + return s + }() + + /// The session really is locked, whatever windows are up. + private static var screenLocked: Bool { + (CGSessionCopyCurrentDictionary() as? [String: Any])?["CGSSessionScreenIsLocked"] as? Bool ?? false + } + /// Whether something is on screen the drawn zoom cannot show right: a window stacked above the /// overlay (it would show twice, unzoomed on top of its magnified copy), a window that opts out /// of screen capture (it would be hidden), or the lock screen. Every other window, prompts @@ -378,7 +392,14 @@ final class DrawZoom: NSObject, SCStreamOutput, SCStreamDelegate, @unchecked Sen if alpha < 0.05 || !big || owner == "Window Server" { continue } // the recording indicator is 28x28 if ignore.contains(owner) { continue } let id = w[kCGWindowNumber as String] as? Int ?? 0 - if lockOwners.contains(owner) { return (id, true, true) } + if lockOwners.contains(owner) { + // loginwindow also shows the Restart and Shut Down prompts with nothing locked. Taken for the + // lock screen with Handover off, they gave the real cursor back: the tiny cursor beside + // the magnified view (owner 2026-11-04). Below the overlay such a prompt is simply drawn; + // above it the drawn cursor cannot reach it, and the real one has to work. + if handoverAllowed || !loginDialogOwners.contains(owner) || screenLocked || aboveOverlay { return (id, true, true) } + continue + } if !handoverAllowed { continue } if sharing == 0 { return (id, false, false) } // Above the overlay (listed before it). With the overlay not on screen, judge by level: diff --git a/mac/VERSION b/mac/VERSION index 8f0916f7..4b9fcbec 100644 --- a/mac/VERSION +++ b/mac/VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 diff --git a/mac/docs/architecture.md b/mac/docs/architecture.md index 697c958d..080071b7 100644 --- a/mac/docs/architecture.md +++ b/mac/docs/architecture.md @@ -1085,6 +1085,8 @@ above the overlay, so it stays unmagnified on the zoomed view. It cannot be cove - `handover` (Settings, under Engine; greyed on Transform; Render too): off, nothing goes to macOS's zoom (owner 2026-11-04, the lock screen too). Protected windows and windows above the overlay stay as drawn, macOS's screenshot modes run under the overlay with their real crosshair unmagnified, and the lock screen shows unmagnified above the overlay: `DrawZoom.lockUp` gives the real cursor back meanwhile and restarts a capture it stopped. + loginwindow also owns the Restart and Shut Down prompts: only `CGSSessionScreenIsLocked`, or a window above the overlay, counts as the lock screen (owner + 2026-11-04: the prompt brought the tiny cursor). Tests never open the real prompt, which restarts by itself after 60 s. - No pixels reach Wind, so no xBR: `renderScaling` 0 is Nearest, 1 is `cubic` (the Scaling row is Render's only for now). - What is below reaches the screen only as far as a commit dirties the backdrop, so `keepAliveTick` nudges the backdrop layer itself 0.001 pt on every refresh the view does not move it. A nudge of the cursor layer alone, or a tick with the view still, redrew part of the view: a diff --git a/mac/scripts/backdrop_truth.py b/mac/scripts/backdrop_truth.py index ac93c8c0..b947c554 100644 --- a/mac/scripts/backdrop_truth.py +++ b/mac/scripts/backdrop_truth.py @@ -102,6 +102,19 @@ def torn(label): s = W.ctl("state"); W.check("handover off: the cursor drawn again after the lock screen", s["drawn"] and s.get("realCursorClear"), f"drawn {s['drawn']} clear {s.get('realCursorClear')}") W.set_config(handover=None); W.ctl("level 1"); time.sleep(0.5) +# Handover off and a loginwindow prompt with nothing locked (Restart, owner 2026-11-04): not the lock screen, so +# the drawn cursor stays and the real one stays out of sight. A test window stands in: never the real prompt, +# which restarts the Mac by itself after 60 s. +W.kill_all_wind() +os.environ.pop("WIND_FAKE_LOCK"); os.environ["WIND_FAKE_LOGIN_DIALOG"] = "fakeprompt" +subprocess.Popen([os.path.join(W.ROOT, "build/test/Wind.app/Contents/MacOS/Wind")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) +time.sleep(2); W.set_config(handover=False); time.sleep(0.8) +W.ctl("warp 960 500"); time.sleep(0.3); W.ctl(f"level {a.level}"); time.sleep(1.5) +p = subprocess.Popen([PROMPT, "4"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL); time.sleep(1.5) +s = W.ctl("state") +W.check("handover off: a login prompt with nothing locked keeps the drawn cursor", s["drawn"] and not s["handedOver"] and s.get("realCursorClear") and not s["realCursorVisible"], f"drawn {s['drawn']} clear {s.get('realCursorClear')} real {s['realCursorVisible']}") +p.wait(timeout=7); W.set_config(handover=None); W.ctl("level 1"); time.sleep(0.5) + # Switched from Render while zoomed, the capture running: it must end, and the dot with it (owner 2026-11-04). W.set_config(engine="draw"); time.sleep(1); W.ctl("level 3"); time.sleep(2) s = W.ctl("state"); W.check("Render captures, as the switch's starting point", s["drawCapturing"] and indicators() > 0, f"capturing {s['drawCapturing']}, {indicators()} indicators")