diff --git a/mac/Sources/Wind/DrawZoom.swift b/mac/Sources/Wind/DrawZoom.swift index 1fc8893b..c734cbe4 100644 --- a/mac/Sources/Wind/DrawZoom.swift +++ b/mac/Sources/Wind/DrawZoom.swift @@ -345,11 +345,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 @@ -380,7 +394,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 75cc5805..c1567f58 100644 --- a/mac/docs/architecture.md +++ b/mac/docs/architecture.md @@ -1090,6 +1090,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")