diff --git a/.changeset/ca7a0a92.md b/.changeset/ca7a0a92.md new file mode 100644 index 0000000..9cd051f --- /dev/null +++ b/.changeset/ca7a0a92.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Refresh tile aspect ratio when a window is resized between overlay opens. Previously, resizing a window while the overlay was hidden would leave the next cmd-cmd showing the tile at its old aspect ratio (and capturing the live preview at the old dimensions) until something else added or removed a window. diff --git a/Sources/cmdcmd/Overlay.swift b/Sources/cmdcmd/Overlay.swift index 7dd2ef8..eb473a2 100644 --- a/Sources/cmdcmd/Overlay.swift +++ b/Sources/cmdcmd/Overlay.swift @@ -318,8 +318,25 @@ final class Overlay { let currentIDs = Set(allTiles.map { CGWindowID($0.scWindow.windowID) }) let addedIDs = newIDs.subtracting(currentIDs) let removedIDs = currentIDs.subtracting(newIDs) - guard !addedIDs.isEmpty || !removedIDs.isEmpty else { return } - Log.debug("reconcile: +\(addedIDs.count) -\(removedIDs.count) (was \(currentIDs.count), now \(newIDs.count))") + + // Refresh kept tiles' SCWindow so .frame reflects the current size. + // Without this, a window resized between prewarm and show keeps a + // stale frame and the tile renders at the old aspect ratio. + let candidateMap = Dictionary(uniqueKeysWithValues: candidates.map { (CGWindowID($0.windowID), $0) }) + var resized: [Tile] = [] + for t in allTiles { + let id = CGWindowID(t.scWindow.windowID) + guard !removedIDs.contains(id), let fresh = candidateMap[id] else { continue } + let oldSize = t.scWindow.frame.size + let newSize = fresh.frame.size + t.scWindow = fresh + if abs(oldSize.width - newSize.width) > 1 || abs(oldSize.height - newSize.height) > 1 { + resized.append(t) + } + } + + guard !addedIDs.isEmpty || !removedIDs.isEmpty || !resized.isEmpty else { return } + Log.debug("reconcile: +\(addedIDs.count) -\(removedIDs.count) ~\(resized.count) (was \(currentIDs.count), now \(newIDs.count))") let added: [Tile] = candidates.compactMap { w -> Tile? in let id = CGWindowID(w.windowID) @@ -382,12 +399,23 @@ final class Overlay { } let live = config.livePreviewsEnabled - Task { - await withTaskGroup(of: Void.self) { group in - for t in added { - group.addTask { - await t.snapshot() - if live { await t.start() } + if !added.isEmpty { + Task { + await withTaskGroup(of: Void.self) { group in + for t in added { + group.addTask { + await t.snapshot() + if live { await t.start() } + } + } + } + } + } + if !resized.isEmpty { + Task { + await withTaskGroup(of: Void.self) { group in + for t in resized { + group.addTask { await t.refreshAfterResize(live: live) } } } } diff --git a/Sources/cmdcmd/Tile.swift b/Sources/cmdcmd/Tile.swift index febc7cd..4c3ea77 100644 --- a/Sources/cmdcmd/Tile.swift +++ b/Sources/cmdcmd/Tile.swift @@ -54,7 +54,7 @@ final class Tile: NSObject, SCStreamOutput, SCStreamDelegate { return NSColor(srgbRed: r, green: g, blue: b, alpha: 1) } - let scWindow: SCWindow + var scWindow: SCWindow let ownerPID: pid_t let ignoreKey: String let layer: CALayer @@ -476,6 +476,25 @@ final class Tile: NSObject, SCStreamOutput, SCStreamDelegate { try? await s.stopCapture() } + /// Underlying window resized after capture started. Tear down the existing + /// stream (its config is fixed at the old dimensions) and rebuild from the + /// fresh `scWindow.frame`. + func refreshAfterResize(live: Bool) async { + if cancelled { return } + if let s = stream { + self.stream = nil + stopWatchdog() + try? await s.stopCapture() + } + if cancelled { return } + hasRenderedLiveFrame = false + loggedFirstLiveFrame = false + await snapshot() + if live && !cancelled { + await start() + } + } + func stopSync(group: DispatchGroup) { cancelled = true suppressFrames = true