#if os(macOS) import CoreGraphics import Foundation /// Where the mini window sits on screen. Four corners and nothing in /// between, like FaceTime's and iOS's picture in picture: a window that /// can rest anywhere ends up resting over whatever you were about to /// click, and a corner is a place you can find again without looking. public enum MiniWindowCorner: String, CaseIterable, Identifiable, Sendable { case bottomRight, bottomLeft, topRight, topLeft public var id: String { rawValue } public var title: String { switch self { case .bottomRight: "Bottom right" case .bottomLeft: "Bottom left" case .topRight: "Top right" case .topLeft: "Top left" } } var right: Bool { self == .bottomRight || self == .topRight } var top: Bool { self == .topRight || self == .topLeft } /// The window's frame in this corner of `visible`, which is a screen's /// `visibleFrame`: already clear of the menu bar and the Dock, so the /// inset is only breathing room. AppKit's origin is bottom-left. public func frame(size: CGSize, in visible: CGRect, inset: CGFloat = MiniWindowRules.inset) -> CGRect { CGRect( x: right ? visible.maxX - inset - size.width : visible.minX + inset, y: top ? visible.maxY - inset - size.height : visible.minY + inset, width: size.width, height: size.height) } /// The corner a dragged window belongs to: whichever side of the /// screen's middle its own middle landed on. public static func nearest(to frame: CGRect, in visible: CGRect) -> MiniWindowCorner { let right = frame.midX >= visible.midX let top = frame.midY >= visible.midY switch (top, right) { case (false, true): return .bottomRight case (false, false): return .bottomLeft case (true, true): return .topRight case (true, false): return .topLeft } } } /// Whether a dragged mini window settles in a corner or stays where it /// was dropped. Corners by default, for the reason MiniWindowCorner /// gives; "anywhere" is for the person who knows which part of their /// screen they never look at. public enum MiniWindowPlacement: String, CaseIterable, Identifiable, Sendable { case corners, anywhere public var id: String { rawValue } public var title: String { switch self { case .corners: "Snap to corners" case .anywhere: "Anywhere I put it" } } } public enum MiniWindowRules { /// 16:9 at the width of a small picture in picture: big enough to /// read a face, small enough to leave the app you went to usable. /// The size it opens at until someone resizes it. public static let size = CGSize(width: 320, height: 180) public static let inset: CGFloat = 20 public static let aspect: CGFloat = 16 / 9 /// Narrower than this and Mute, Leave and Collapse no longer fit /// across the top. public static let minimumWidth: CGFloat = 200 /// Wider than this share of the screen and it is no longer a mini /// window, it is a second call window covering the app you went to. public static let maximumShare: CGFloat = 0.4 /// The size for a width someone chose, held to 16:9 and to the limits /// of the screen it is on now — which may be a smaller one than the /// width was chosen on. public static func size(width: CGFloat, in visible: CGRect) -> CGSize { let widest = maximumSize(in: visible).width let chosen = min(max(width.rounded(), minimumWidth), widest) return CGSize(width: chosen, height: chosen / aspect) } public static func minimumSize() -> CGSize { CGSize(width: minimumWidth, height: minimumWidth / aspect) } /// Never below the minimum, even on a screen too small for 40% of it /// to be that wide. public static func maximumSize(in visible: CGRect) -> CGSize { let widest = max(minimumWidth, (visible.width * maximumShare).rounded(.down)) return CGSize(width: widest, height: widest / aspect) } /// `frame` moved the least distance that puts all of it inside /// `visible`. Its size is kept; one bigger than the screen is pinned /// to the top left, where the controls are. public static func clamp(_ frame: CGRect, into visible: CGRect) -> CGRect { var out = frame out.origin.x = min(max(frame.minX, visible.minX), visible.maxX - frame.width) out.origin.x = max(out.origin.x, visible.minX) out.origin.y = max(min(frame.minY, visible.maxY - frame.height), visible.minY) out.origin.y = min(out.origin.y, visible.maxY - frame.height) return out } /// Where a freely placed window is, relative to its screen's visible /// frame. Kept this way rather than in global coordinates so that /// moving a display in System Settings → Displays → Arrange does not /// throw the window off it. public static func offset(of frame: CGRect, in visible: CGRect) -> CGPoint { CGPoint(x: frame.minX - visible.minX, y: frame.minY - visible.minY) } /// A remembered offset put back on a screen, clamped: the display may /// have changed resolution, or been swapped for a smaller one. public static func frame(offset: CGPoint, size: CGSize, in visible: CGRect) -> CGRect { clamp( CGRect( origin: CGPoint(x: visible.minX + offset.x, y: visible.minY + offset.y), size: size), into: visible) } } /// Resizing the mini window from its edges and corners, always at /// 16:9. Done by the window's own surface rather than by AppKit's /// `.resizable`, whose edge handling on a borderless panel is not /// something to rely on, and which could not be held to this aspect /// and these limits in one place that tests can reach. public enum MiniWindowResize { public struct Handle: OptionSet, Hashable, Sendable { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } public static let left = Handle(rawValue: 1) public static let right = Handle(rawValue: 2) public static let top = Handle(rawValue: 4) public static let bottom = Handle(rawValue: 8) } /// How close to an edge, inside the window, a press starts a resize. public static let grip: CGFloat = 8 /// The edges a press at `point` (in the window's own coordinates, /// origin bottom-left) takes hold of. Two at a corner, none in the /// middle, where a press is a click or a move. public static func handle(at point: CGPoint, in size: CGSize) -> Handle { var handle: Handle = [] if point.x <= grip { handle.insert(.left) } if point.x >= size.width - grip { handle.insert(.right) } if point.y <= grip { handle.insert(.bottom) } if point.y >= size.height - grip { handle.insert(.top) } return handle } /// The frame after dragging `handle` by (dx, dy) from `start`. The /// opposite edge or corner stays put; an edge on its own grows the /// window evenly either side of its middle. At a corner, whichever /// direction asks for the bigger change wins, so the corner follows /// the pointer as closely as 16:9 allows. public static func frame( from start: CGRect, dragging handle: Handle, dx: CGFloat, dy: CGFloat, in visible: CGRect ) -> CGRect { let horizontal: CGFloat? = handle.contains(.right) ? start.width + dx : handle.contains(.left) ? start.width - dx : nil let vertical: CGFloat? = handle.contains(.top) ? (start.height + dy) * MiniWindowRules.aspect : handle.contains(.bottom) ? (start.height - dy) * MiniWindowRules.aspect : nil let proposed: CGFloat switch (horizontal, vertical) { case (let h?, let v?): proposed = abs(h - start.width) >= abs(v - start.width) ? h : v case (let h?, nil): proposed = h case (nil, let v?): proposed = v case (nil, nil): return start } let size = MiniWindowRules.size(width: proposed, in: visible) let x: CGFloat = handle.contains(.left) ? start.maxX - size.width : handle.contains(.right) ? start.minX : start.midX - size.width / 2 let y: CGFloat = handle.contains(.bottom) ? start.maxY - size.height : handle.contains(.top) ? start.minY : start.midY - size.height / 2 return CGRect(origin: CGPoint(x: x, y: y), size: size) } } /// The screen edge a collapsed mini window sits against. public enum MiniWindowEdge: String, Sendable { case left, right, top, bottom } /// The mini window collapsed to a tab: small, flush against the nearest /// edge of the screen, out of the way of whatever the person went to /// the other app for. public enum MiniWindowTab { public static let size = CGSize(width: 156, height: 40) /// How far from its edge a dragged tab has to be let go to count as /// pulled out, rather than slid along the edge to somewhere else. public static let pullOut: CGFloat = 48 /// The edge `frame` is closest to. Ties go left and right before top /// and bottom, because the corners are where a snapped window lives /// and a tab beside it reads as belonging to the side it came from. public static func nearestEdge(to frame: CGRect, in visible: CGRect) -> MiniWindowEdge { let distances: [(MiniWindowEdge, CGFloat)] = [ (.right, visible.maxX - frame.maxX), (.left, frame.minX - visible.minX), (.bottom, frame.minY - visible.minY), (.top, visible.maxY - frame.maxY), ] return distances.min { $0.1 < $1.1 }!.0 } /// The tab for a window at `frame`: against its nearest edge, level /// with the window's middle along that edge, and on screen. public static func dock(_ frame: CGRect, in visible: CGRect) -> (MiniWindowEdge, CGRect) { let edge = nearestEdge(to: frame, in: visible) let x: CGFloat let y: CGFloat switch edge { case .left: (x, y) = (visible.minX, frame.midY - size.height / 2) case .right: (x, y) = (visible.maxX - size.width, frame.midY - size.height / 2) case .bottom: (x, y) = (frame.midX - size.width / 2, visible.minY) case .top: (x, y) = (frame.midX - size.width / 2, visible.maxY - size.height) } return ( edge, MiniWindowRules.clamp(CGRect(origin: CGPoint(x: x, y: y), size: size), into: visible) ) } public enum Release: Equatable { /// Back against an edge — the same one, or another it was slid to. case dock(MiniWindowEdge, CGRect) /// Pulled out: open the mini window again. case expand } /// What letting go of a dragged tab means. public static func release(_ frame: CGRect, in visible: CGRect) -> Release { let away = min( frame.minX - visible.minX, visible.maxX - frame.maxX, frame.minY - visible.minY, visible.maxY - frame.maxY) if away > pullOut { return .expand } let (edge, docked) = dock(frame, in: visible) return .dock(edge, docked) } /// A window of `size` opened from a tab: centred where the tab is, /// and on screen. public static func expanded(from tab: CGRect, size: CGSize, in visible: CGRect) -> CGRect { MiniWindowRules.clamp( CGRect( x: tab.midX - size.width / 2, y: tab.midY - size.height / 2, width: size.width, height: size.height), into: visible) } /// Who the call is with, in the few words a tab has room for: the /// other person's name when there is one, a count when there are /// more, and "Just you" when nobody else has come. public static func caption(others: [String]) -> String { switch others.count { case 0: "Just you" case 1: others[0] default: "\(others.count + 1) people" } } } /// The two preferences, read and written in one place so the Settings /// window and the mini window cannot disagree about a key or a default. /// /// On by default, because the person who asked for it switches apps /// during most calls, and a setting nobody knows about that is off is a /// feature that does not exist. public struct MiniWindowPreferences { public static let enabledKey = "miniWindow.enabled" public static let cornerKey = "miniWindow.corner" public static let placementKey = "miniWindow.placement" public static let widthKey = "miniWindow.width" public static let positionsKey = "miniWindow.positions" public static let defaultEnabled = true public static let defaultCorner = MiniWindowCorner.bottomRight public static let defaultPlacement = MiniWindowPlacement.corners private let defaults: UserDefaults public init(defaults: UserDefaults = .standard) { self.defaults = defaults } public var enabled: Bool { get { defaults.object(forKey: Self.enabledKey) as? Bool ?? Self.defaultEnabled } nonmutating set { defaults.set(newValue, forKey: Self.enabledKey) } } public var corner: MiniWindowCorner { get { defaults.string(forKey: Self.cornerKey).flatMap(MiniWindowCorner.init(rawValue:)) ?? Self.defaultCorner } nonmutating set { defaults.set(newValue.rawValue, forKey: Self.cornerKey) } } public var placement: MiniWindowPlacement { get { defaults.string(forKey: Self.placementKey).flatMap(MiniWindowPlacement.init(rawValue:)) ?? Self.defaultPlacement } nonmutating set { defaults.set(newValue.rawValue, forKey: Self.placementKey) } } /// The width last resized to. Only the width: the height follows /// from 16:9, so there is nothing to disagree about. public var width: CGFloat { get { (defaults.object(forKey: Self.widthKey) as? Double).map { CGFloat($0) } ?? MiniWindowRules.size.width } nonmutating set { defaults.set(Double(newValue), forKey: Self.widthKey) } } /// Where the window was left in "anywhere" mode, per display, as an /// offset from that display's visible frame (see /// MiniWindowRules.offset). Keyed by the display's ID as a string, and /// kept as one JSON string so that Settings can clear it through /// AppStorage. public func position(on display: String) -> CGPoint? { guard let pair = positions[display], pair.count == 2 else { return nil } return CGPoint(x: pair[0], y: pair[1]) } public func setPosition(_ offset: CGPoint, on display: String) { var all = positions all[display] = [Double(offset.x), Double(offset.y)] guard let data = try? JSONEncoder().encode(all) else { return } defaults.set(String(decoding: data, as: UTF8.self), forKey: Self.positionsKey) } private var positions: [String: [Double]] { guard let text = defaults.string(forKey: Self.positionsKey), let all = try? JSONDecoder().decode([String: [Double]].self, from: Data(text.utf8)) else { return [:] } return all } public func forgetPositions() { defaults.removeObject(forKey: Self.positionsKey) } } /// What the mini window remembers for one call and forgets when it /// ends: whether it was collapsed to a tab, and any position it was /// ⌥-dragged to while the setting is "Snap to corners". Every call /// starts expanded — a tab left over from yesterday's call would be a /// call nobody can see they are in. public struct MiniWindowCallMemory: Equatable { public var collapsed = false /// ⌥-dropped positions by display, as offsets. One-offs: they win /// over the corner for the rest of this call and are never saved. public private(set) var oneOff: [String: CGPoint] = [:] public init() {} /// Where a newly shown window of `size` goes on `display`. public func frame( size: CGSize, in visible: CGRect, display: String, preferences: MiniWindowPreferences ) -> CGRect { if let offset = oneOff[display] { return MiniWindowRules.frame(offset: offset, size: size, in: visible) } if preferences.placement == .anywhere, let offset = preferences.position(on: display) { return MiniWindowRules.frame(offset: offset, size: size, in: visible) } return preferences.corner.frame(size: size, in: visible) } /// A drag ended with the window at `frame` on `display`. Returns /// where it settles. /// /// - "Anywhere I put it": exactly there (clamped on screen), saved for /// that display. /// - "Snap to corners" with ⌥ held: exactly there, for this call only. /// - "Snap to corners": the nearest corner, which becomes the setting, /// and any one-off on that display is dropped. public mutating func dropped( _ frame: CGRect, in visible: CGRect, display: String, option: Bool, preferences: MiniWindowPreferences ) -> CGRect { let placed = MiniWindowRules.clamp(frame, into: visible) switch (preferences.placement, option) { case (.anywhere, _): preferences.setPosition(MiniWindowRules.offset(of: placed, in: visible), on: display) return placed case (.corners, true): oneOff[display] = MiniWindowRules.offset(of: placed, in: visible) return placed case (.corners, false): oneOff[display] = nil let corner = MiniWindowCorner.nearest(to: frame, in: visible) preferences.corner = corner return corner.frame(size: frame.size, in: visible) } } /// A resize ended with the window at `frame`. It settles as a drop /// would, except that a window ⌥-placed for this call stays placed /// rather than snapping: resizing it is not asking for a corner. public mutating func resized( _ frame: CGRect, in visible: CGRect, display: String, preferences: MiniWindowPreferences ) -> CGRect { dropped( frame, in: visible, display: display, option: oneOff[display] != nil, preferences: preferences) } } /// When the mini window comes and goes. Kept apart from the windows it /// moves, like KnockWatch, because this is the part with rules in it and /// the windows need a real app to exist. /// /// The rules: /// - It appears only when the app stops being frontmost during a call, /// with the setting on and the call window on screen. A minimised or /// closed call window is one the person already put away themselves. /// - ⌘H wins. Hiding the app is asking for all of it to go, so a hidden /// app shows nothing, and a mini window already up is put away before /// the hide so that unhiding brings back the ordinary window. AppKit's /// order between "will hide" and "resigned active" is not something to /// rely on, so both orders end the same way. /// - Coming back to the app, by any route, restores the call window. /// - The call ending puts it away too — someone who pressed Leave in the /// mini window is done, and the lobby is waiting for them underneath. public struct MiniWindowPolicy: Equatable { public struct Conditions: Equatable { public var enabled: Bool public var inCall: Bool public var mainWindowVisible: Bool public var mainWindowMiniaturized: Bool public var mainWindowFullScreen: Bool public init( enabled: Bool, inCall: Bool, mainWindowVisible: Bool, mainWindowMiniaturized: Bool = false, mainWindowFullScreen: Bool = false ) { self.enabled = enabled self.inCall = inCall self.mainWindowVisible = mainWindowVisible self.mainWindowMiniaturized = mainWindowMiniaturized self.mainWindowFullScreen = mainWindowFullScreen } } public enum Action: Equatable { case none /// Show the mini window. `hideMain` is false for a full-screen call /// window: hiding one leaves its Space black and empty, and it is /// already out of the way in a Space of its own. case show(hideMain: Bool) /// Close the mini window and put the call window back as it was. /// `activate` is false when the app is not coming forward — the call /// ended from the mini window, or the app is being hidden — so the /// window returns where it was in the stack, behind whatever the /// person is doing, rather than jumping on top of it. case restore(activate: Bool) } public private(set) var showing = false /// Whether the main window was hidden, as opposed to left alone in /// its full-screen Space. public private(set) var mainHidden = false public private(set) var appHidden = false public init() {} public mutating func resignedActive(_ conditions: Conditions) -> Action { guard !showing, !appHidden, conditions.enabled, conditions.inCall, conditions.mainWindowVisible, !conditions.mainWindowMiniaturized else { return .none } showing = true mainHidden = !conditions.mainWindowFullScreen return .show(hideMain: mainHidden) } public mutating func becameActive() -> Action { appHidden = false return restore(activate: true) } public mutating func willHide() -> Action { appHidden = true return restore(activate: false) } public mutating func didUnhide() { appHidden = false } public mutating func callEnded() -> Action { restore(activate: false) } private mutating func restore(activate: Bool) -> Action { guard showing else { return .none } showing = false mainHidden = false return .restore(activate: activate) } } /// What one press on the mini window's surface is (2026-10-05, Paul: /// "Sometimes I can't click the mini window to either bring the call to /// the front or move it around"): apart from any view, so tests hold it. /// A press that ends within `slack` of where it began is a click, which /// brings the call forward (the tab: opens the window; twice, the call). /// One that travels further is a drag: from an edge or corner of the /// expanded window it resizes, from anywhere else it moves. The tab has /// no edges to resize by. Controls are SwiftUI buttons over the surface /// and never reach it. public enum MiniWindowGesture { public enum Surface: String, Sendable { case window, tab } public enum Outcome: Equatable, Sendable { case click(count: Int) case move(option: Bool) case resize(MiniWindowResize.Handle) } /// How far a press may wander and still be a click: a shaky hand's. public static let slack: CGFloat = 4 /// The edges a press at `point` takes hold of: none on the tab, and none /// on a window too small for its grips to leave a middle to move by. public static func grab(at point: CGPoint, in size: CGSize, surface: Surface) -> MiniWindowResize.Handle { guard surface == .window, size.width > 4 * MiniWindowResize.grip, size.height > 4 * MiniWindowResize.grip else { return [] } return MiniWindowResize.handle(at: point, in: size) } public static func dragged(dx: CGFloat, dy: CGFloat) -> Bool { hypot(dx, dy) > slack } /// The press's outcome once the button is up. public static func outcome( grabbed: MiniWindowResize.Handle, dragged: Bool, clickCount: Int, option: Bool ) -> Outcome { guard dragged else { return .click(count: max(1, clickCount)) } return grabbed.isEmpty ? .move(option: option) : .resize(grabbed) } } #endif