gardesk/ers / 4ade8a7

Browse files

Add nswindow_overlay module: NSWindow + CAShapeLayer border with sharingType=.none

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4ade8a7b9a0b9e8cb47791a6b2ed0f1e6de242dc
Parents
ecafbb8
Tree
9a810ea

1 changed file

StatusFile+-
A src/nswindow_overlay.rs 235 0
src/nswindow_overlay.rsadded
@@ -0,0 +1,235 @@
1
+//! NSWindow-backed overlay border.
2
+//!
3
+//! Replaces the SLS-only window approach. The reason is screenshot
4
+//! exclusion on macOS Tahoe: `screencaptureui` enumerates windows via
5
+//! `_SLSCopyWindowsWithOptionsAndTagsAndSpaceOptions` +
6
+//! `_CGSGetWindowTags` and ignores the sharing-state of raw SLS-only
7
+//! windows. NSWindow.sharingType = .none is the only documented and
8
+//! verified-honored exclusion mechanism (verified empirically on Tahoe
9
+//! with `screencapture -l <wid>`: SLS overlays capture, NSWindows with
10
+//! `.none` sharingType return "could not create image from window").
11
+//!
12
+//! We use a CAShapeLayer for the rounded-rect border so updates stay
13
+//! declarative — no NSView subclassing required.
14
+
15
+use objc2::rc::Retained;
16
+use objc2::runtime::AnyObject;
17
+use objc2::{ClassType, MainThreadMarker, MainThreadOnly, msg_send};
18
+use objc2_app_kit::{
19
+    NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSColor, NSScreen, NSWindow,
20
+    NSWindowCollectionBehavior, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask,
21
+};
22
+use objc2_core_foundation::{CGPoint, CGRect, CGSize};
23
+use objc2_quartz_core::{CALayer, CAShapeLayer};
24
+use std::ptr;
25
+
26
+const NS_FLOATING_WINDOW_LEVEL: isize = 3;
27
+
28
+/// Top-left Y in CG global coordinates becomes bottom-left Y in Cocoa
29
+/// global coordinates by subtracting from the primary screen height.
30
+fn cg_to_cocoa_frame(cg: CGRect, mtm: MainThreadMarker) -> CGRect {
31
+    let screens = NSScreen::screens(mtm);
32
+    let primary_height = if screens.count() > 0 {
33
+        screens.objectAtIndex(0).frame().size.height
34
+    } else {
35
+        0.0
36
+    };
37
+    let cocoa_y = primary_height - cg.origin.y - cg.size.height;
38
+    CGRect::new(
39
+        CGPoint::new(cg.origin.x, cocoa_y),
40
+        CGSize::new(cg.size.width, cg.size.height),
41
+    )
42
+}
43
+
44
+/// Initialize NSApplication. Must be called once from the main thread.
45
+pub fn init_application() -> MainThreadMarker {
46
+    let mtm = MainThreadMarker::new().expect("init_application must run on the main thread");
47
+    let app = NSApplication::sharedApplication(mtm);
48
+    app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
49
+    mtm
50
+}
51
+
52
+/// One NSWindow + CAShapeLayer pair drawing a rounded-rect border.
53
+pub struct OverlayWindow {
54
+    window: Retained<NSWindow>,
55
+    border_layer: Retained<CAShapeLayer>,
56
+    pub bounds_cg: CGRect,
57
+    pub border_width: f64,
58
+    pub radius: f64,
59
+    mtm: MainThreadMarker,
60
+}
61
+
62
+impl OverlayWindow {
63
+    /// Create an NSWindow border overlay covering `target_bounds_cg + border_width`.
64
+    pub fn new(
65
+        target_bounds_cg: CGRect,
66
+        border_width: f64,
67
+        radius: f64,
68
+        color: (f64, f64, f64, f64),
69
+        mtm: MainThreadMarker,
70
+    ) -> Option<Self> {
71
+        let outer_cg = CGRect::new(
72
+            CGPoint::new(
73
+                target_bounds_cg.origin.x - border_width,
74
+                target_bounds_cg.origin.y - border_width,
75
+            ),
76
+            CGSize::new(
77
+                target_bounds_cg.size.width + 2.0 * border_width,
78
+                target_bounds_cg.size.height + 2.0 * border_width,
79
+            ),
80
+        );
81
+        let cocoa_frame = cg_to_cocoa_frame(outer_cg, mtm);
82
+
83
+        let style = NSWindowStyleMask::Borderless;
84
+        let window: Retained<NSWindow> = unsafe {
85
+            msg_send![
86
+                NSWindow::alloc(mtm),
87
+                initWithContentRect: cocoa_frame,
88
+                styleMask: style,
89
+                backing: NSBackingStoreType::Buffered,
90
+                defer: false
91
+            ]
92
+        };
93
+        window.setOpaque(false);
94
+        window.setHasShadow(false);
95
+        window.setIgnoresMouseEvents(true);
96
+        window.setLevel(NS_FLOATING_WINDOW_LEVEL);
97
+        unsafe { window.setReleasedWhenClosed(false) };
98
+        window.setSharingType(NSWindowSharingType::None);
99
+        window.setCollectionBehavior(
100
+            NSWindowCollectionBehavior::CanJoinAllSpaces
101
+                | NSWindowCollectionBehavior::Stationary
102
+                | NSWindowCollectionBehavior::IgnoresCycle
103
+                | NSWindowCollectionBehavior::FullScreenAuxiliary,
104
+        );
105
+        // Clear background.
106
+        let clear = unsafe { NSColor::clearColor() };
107
+        unsafe { window.setBackgroundColor(Some(&clear)) };
108
+
109
+        let content_view = window.contentView()?;
110
+        content_view.setWantsLayer(true);
111
+        let host_layer: Retained<CALayer> = unsafe {
112
+            let layer: Option<Retained<CALayer>> = msg_send![&*content_view, layer];
113
+            layer?
114
+        };
115
+
116
+        let border_layer = unsafe { CAShapeLayer::new() };
117
+        let path_rect = inset_for_stroke(outer_cg.size, border_width);
118
+        unsafe {
119
+            let path = objc2_core_graphics::CGPath::with_rounded_rect(
120
+                path_rect, radius, radius, ptr::null(),
121
+            );
122
+            let path_ref: *mut AnyObject =
123
+                objc2_core_foundation::CFRetained::as_ptr(&path).as_ptr() as *mut AnyObject;
124
+            let _: () = msg_send![&*border_layer, setPath: path_ref];
125
+
126
+            let _: () = msg_send![&*border_layer, setFillColor: ptr::null::<AnyObject>()];
127
+            let stroke = make_cgcolor(color, mtm);
128
+            let stroke_ref: *mut AnyObject =
129
+                objc2_core_foundation::CFRetained::as_ptr(&stroke).as_ptr() as *mut AnyObject;
130
+            let _: () = msg_send![&*border_layer, setStrokeColor: stroke_ref];
131
+            border_layer.setLineWidth(border_width * 2.0);
132
+            border_layer.setFrame(CGRect::new(
133
+                CGPoint::new(0.0, 0.0),
134
+                CGSize::new(outer_cg.size.width, outer_cg.size.height),
135
+            ));
136
+            host_layer.addSublayer(&border_layer);
137
+        }
138
+
139
+        window.orderFrontRegardless();
140
+
141
+        Some(OverlayWindow {
142
+            window,
143
+            border_layer,
144
+            bounds_cg: target_bounds_cg,
145
+            border_width,
146
+            radius,
147
+            mtm,
148
+        })
149
+    }
150
+
151
+    /// NSWindow's windowNumber, usable as a wid for tracking.
152
+    pub fn wid(&self) -> u32 {
153
+        self.window.windowNumber() as u32
154
+    }
155
+
156
+    pub fn set_bounds(&mut self, target_bounds_cg: CGRect) {
157
+        let outer_cg = CGRect::new(
158
+            CGPoint::new(
159
+                target_bounds_cg.origin.x - self.border_width,
160
+                target_bounds_cg.origin.y - self.border_width,
161
+            ),
162
+            CGSize::new(
163
+                target_bounds_cg.size.width + 2.0 * self.border_width,
164
+                target_bounds_cg.size.height + 2.0 * self.border_width,
165
+            ),
166
+        );
167
+        let cocoa_frame = cg_to_cocoa_frame(outer_cg, self.mtm);
168
+        self.window.setFrame_display(cocoa_frame, true);
169
+        // Update the border path to match new size.
170
+        unsafe {
171
+            let path = objc2_core_graphics::CGPath::with_rounded_rect(
172
+                inset_for_stroke(outer_cg.size, self.border_width),
173
+                self.radius,
174
+                self.radius,
175
+                ptr::null(),
176
+            );
177
+            let path_ref: *mut AnyObject =
178
+                objc2_core_foundation::CFRetained::as_ptr(&path).as_ptr() as *mut AnyObject;
179
+            let _: () = msg_send![&*self.border_layer, setPath: path_ref];
180
+            self.border_layer.setFrame(CGRect::new(
181
+                CGPoint::new(0.0, 0.0),
182
+                CGSize::new(outer_cg.size.width, outer_cg.size.height),
183
+            ));
184
+        }
185
+        self.bounds_cg = target_bounds_cg;
186
+    }
187
+
188
+    pub fn set_color(&self, color: (f64, f64, f64, f64)) {
189
+        unsafe {
190
+            let stroke = make_cgcolor(color, self.mtm);
191
+            let stroke_ref: *mut AnyObject =
192
+                objc2_core_foundation::CFRetained::as_ptr(&stroke).as_ptr() as *mut AnyObject;
193
+            let _: () = msg_send![&*self.border_layer, setStrokeColor: stroke_ref];
194
+        }
195
+    }
196
+
197
+    pub fn order_above(&self, target_wid: u32) {
198
+        self.window
199
+            .orderWindow_relativeTo(NSWindowOrderingMode::Above, target_wid as isize);
200
+    }
201
+
202
+    pub fn order_out(&self) {
203
+        self.window.orderOut(None);
204
+    }
205
+
206
+    pub fn set_alpha(&self, alpha: f64) {
207
+        self.window.setAlphaValue(alpha);
208
+    }
209
+}
210
+
211
+impl Drop for OverlayWindow {
212
+    fn drop(&mut self) {
213
+        self.window.close();
214
+    }
215
+}
216
+
217
+fn inset_for_stroke(size: CGSize, border_width: f64) -> CGRect {
218
+    // CAShapeLayer strokes centered on the path. To get the stroke
219
+    // exactly inside the layer bounds we inset by half the line width.
220
+    let half = border_width;
221
+    CGRect::new(
222
+        CGPoint::new(half, half),
223
+        CGSize::new(
224
+            (size.width - 2.0 * half).max(0.0),
225
+            (size.height - 2.0 * half).max(0.0),
226
+        ),
227
+    )
228
+}
229
+
230
+fn make_cgcolor(
231
+    rgba: (f64, f64, f64, f64),
232
+    _mtm: MainThreadMarker,
233
+) -> objc2_core_foundation::CFRetained<objc2_core_graphics::CGColor> {
234
+    unsafe { objc2_core_graphics::CGColor::new_srgb(rgba.0, rgba.1, rgba.2, rgba.3) }
235
+}