11import AppKit
2+ import ApplicationServices
23import SwiftUI
34
45final class OverlayPanel {
56 private var window : NSPanel ?
67 private var hostingView : NSHostingView < AnyView > ?
8+ private var targetDisplayID : CGDirectDisplayID ?
79
810 @ MainActor
911 func show (
1012 appState : AppState ,
13+ targetApp : NSRunningApplication ? ,
1114 onCancel : @escaping ( ) -> Void ,
1215 onConfirm : @escaping ( ) -> Void
1316 ) {
1417 let initialLayout = OverlayLayout ( appState : appState )
18+ targetDisplayID = OverlayTargetScreenResolver . displayID ( for : targetApp )
1519
1620 if window == nil {
1721 let panel = NSPanel (
@@ -61,6 +65,7 @@ final class OverlayPanel {
6165 window ? . close ( )
6266 window = nil
6367 hostingView = nil
68+ targetDisplayID = nil
6469 }
6570
6671 @ MainActor
@@ -78,15 +83,116 @@ final class OverlayPanel {
7883
7984 @ MainActor
8085 private func frame ( for size : CGSize , window : NSWindow ) -> NSRect {
81- let screen = window . screen ?? NSScreen . main
86+ let screen = OverlayTargetScreenResolver . screen ( with : targetDisplayID )
87+ ?? NSScreen . main
88+ ?? window . screen
8289 let visibleFrame = screen ? . visibleFrame ?? NSRect ( x : 0 , y : 0 , width : size . width , height : size . height )
8390 return OverlayPanelPlacement . frame ( for : size , in : visibleFrame )
8491 }
8592}
8693
94+ @ MainActor
95+ enum OverlayTargetScreenResolver {
96+ static func displayID ( for app : NSRunningApplication ? ) -> CGDirectDisplayID ? {
97+ guard let windowFrame = focusedWindowFrame ( for : app ) else {
98+ return NSScreen . main . flatMap ( displayID ( for : ) )
99+ }
100+
101+ let screens = NSScreen . screens
102+ // AX window geometry and CG display bounds share the same global coordinate space.
103+ let displayFrames = screens . map { screen in
104+ displayID ( for : screen ) . map ( CGDisplayBounds ) ?? . null
105+ }
106+ guard let index = OverlayPanelPlacement . targetDisplayIndex (
107+ for : windowFrame ,
108+ in : displayFrames
109+ ) else {
110+ return NSScreen . main . flatMap ( displayID ( for : ) )
111+ }
112+ return displayID ( for : screens [ index ] )
113+ }
114+
115+ static func screen ( with displayID : CGDirectDisplayID ? ) -> NSScreen ? {
116+ guard let displayID else { return nil }
117+ return NSScreen . screens . first { self . displayID ( for : $0 ) == displayID }
118+ }
119+
120+ private static func displayID ( for screen : NSScreen ) -> CGDirectDisplayID ? {
121+ let key = NSDeviceDescriptionKey ( " NSScreenNumber " )
122+ guard let number = screen . deviceDescription [ key ] as? NSNumber else { return nil }
123+ return CGDirectDisplayID ( number . uint32Value )
124+ }
125+
126+ private static func focusedWindowFrame ( for app : NSRunningApplication ? ) -> CGRect ? {
127+ guard let app , AXIsProcessTrusted ( ) else { return nil }
128+ let application = AXUIElementCreateApplication ( app . processIdentifier )
129+
130+ for attribute in [ kAXFocusedWindowAttribute , kAXMainWindowAttribute ] {
131+ guard let frame = windowFrame ( from : application , attribute : attribute as CFString ) else {
132+ continue
133+ }
134+ return frame
135+ }
136+ return nil
137+ }
138+
139+ private static func windowFrame ( from application : AXUIElement , attribute : CFString ) -> CGRect ? {
140+ var windowValue : CFTypeRef ?
141+ guard AXUIElementCopyAttributeValue ( application , attribute , & windowValue ) == . success ,
142+ let rawWindow = windowValue ,
143+ CFGetTypeID ( rawWindow ) == AXUIElementGetTypeID ( ) else {
144+ return nil
145+ }
146+
147+ let window = rawWindow as! AXUIElement
148+ var positionValue : CFTypeRef ?
149+ var sizeValue : CFTypeRef ?
150+ guard AXUIElementCopyAttributeValue (
151+ window ,
152+ kAXPositionAttribute as CFString ,
153+ & positionValue
154+ ) == . success ,
155+ AXUIElementCopyAttributeValue (
156+ window ,
157+ kAXSizeAttribute as CFString ,
158+ & sizeValue
159+ ) == . success ,
160+ let rawPosition = positionValue ,
161+ let rawSize = sizeValue ,
162+ CFGetTypeID ( rawPosition ) == AXValueGetTypeID ( ) ,
163+ CFGetTypeID ( rawSize ) == AXValueGetTypeID ( ) else {
164+ return nil
165+ }
166+
167+ var position = CGPoint . zero
168+ var size = CGSize . zero
169+ guard AXValueGetValue ( rawPosition as! AXValue , . cgPoint , & position ) ,
170+ AXValueGetValue ( rawSize as! AXValue , . cgSize , & size ) ,
171+ position . x . isFinite ,
172+ position . y . isFinite ,
173+ size . width . isFinite ,
174+ size . height . isFinite ,
175+ size . width > 0 ,
176+ size . height > 0 else {
177+ return nil
178+ }
179+ return CGRect ( origin : position , size : size )
180+ }
181+ }
182+
87183enum OverlayPanelPlacement {
88184 static let bottomMargin : CGFloat = 24
89185
186+ static func targetDisplayIndex ( for windowFrame : CGRect , in displayFrames : [ CGRect ] ) -> Int ? {
187+ displayFrames . indices
188+ . map { index in
189+ ( index : index , overlap : overlapArea ( windowFrame , displayFrames [ index ] ) )
190+ }
191+ . filter { $0 . overlap > 0 }
192+ . max { lhs , rhs in lhs . overlap < rhs . overlap } ?
193+ . index
194+ }
195+
90196 static func frame ( for size : CGSize , in visibleFrame : CGRect ) -> CGRect {
91197 CGRect (
92198 x : visibleFrame . midX - size . width / 2 ,
@@ -95,4 +201,10 @@ enum OverlayPanelPlacement {
95201 height : size . height
96202 )
97203 }
204+
205+ private static func overlapArea ( _ lhs : CGRect , _ rhs : CGRect ) -> CGFloat {
206+ let intersection = lhs . intersection ( rhs )
207+ guard ! intersection . isNull else { return 0 }
208+ return intersection . width * intersection . height
209+ }
98210}
0 commit comments