1#[cfg(all(feature = "qa-probes", debug_assertions))]
9use serde::Serialize;
10use tauri::{WebviewWindow, WebviewWindowBuilder, Wry};
11
12#[cfg(all(feature = "qa-probes", debug_assertions))]
13use crate::error::ShellError;
14
15pub fn style<'a>(
17 b: WebviewWindowBuilder<'a, Wry, tauri::AppHandle<Wry>>,
18 frameless: bool,
19) -> WebviewWindowBuilder<'a, Wry, tauri::AppHandle<Wry>> {
20 if !frameless {
21 return b;
22 }
23 #[cfg(target_os = "macos")]
24 {
25 b.title_bar_style(tauri::TitleBarStyle::Overlay)
26 .hidden_title(true)
27 }
28 #[cfg(not(target_os = "macos"))]
29 {
30 b.decorations(false)
31 }
32}
33
34#[derive(Default, Clone)]
51pub struct TrafficLights {
52 #[cfg(target_os = "macos")]
53 inner: std::sync::Arc<macos::Inner>,
54}
55
56impl std::fmt::Debug for TrafficLights {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("TrafficLights").finish_non_exhaustive()
59 }
60}
61
62impl TrafficLights {
63 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn set_offset(&self, window: &WebviewWindow<Wry>, dx: f64, dy: f64) {
71 #[cfg(target_os = "macos")]
72 {
73 let inner = std::sync::Arc::clone(&self.inner);
74 macos::dispatch(window, move |window| macos::apply(window, dx, dy, &inner));
75 }
76 #[cfg(not(target_os = "macos"))]
77 let _ = (window, dx, dy);
78 }
79 pub fn set_inset(&self, window: &WebviewWindow<Wry>, x: f64, y: f64) {
84 #[cfg(target_os = "macos")]
85 {
86 let inner = std::sync::Arc::clone(&self.inner);
87 macos::dispatch(window, move |window| {
88 macos::apply_inset(window, x, y, &inner)
89 });
90 }
91 #[cfg(not(target_os = "macos"))]
92 let _ = (window, x, y);
93 }
94 pub fn reapply(&self, window: &WebviewWindow<Wry>) {
96 #[cfg(target_os = "macos")]
97 {
98 let inner = std::sync::Arc::clone(&self.inner);
99 macos::dispatch(window, move |window| macos::reapply(window, &inner));
100 }
101 #[cfg(not(target_os = "macos"))]
102 let _ = window;
103 }
104 pub fn handle_window_event(&self, window: &WebviewWindow<Wry>, event: &tauri::WindowEvent) {
107 if matches!(
108 event,
109 tauri::WindowEvent::Resized(_)
110 | tauri::WindowEvent::Focused(true)
111 | tauri::WindowEvent::ScaleFactorChanged { .. }
112 ) {
113 self.reapply(window);
114 }
115 }
116}
117
118#[cfg(all(feature = "qa-probes", debug_assertions))]
121#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
122pub struct ButtonFrame {
123 pub x: f64,
124 pub y: f64,
125 pub width: f64,
126 pub height: f64,
127 pub hidden: bool,
128}
129
130#[cfg(all(feature = "qa-probes", debug_assertions))]
131impl ButtonFrame {
132 pub fn new(origin: (f64, f64), size: (f64, f64), hidden: bool) -> Self {
134 Self {
135 x: origin.0,
136 y: origin.1,
137 width: size.0,
138 height: size.1,
139 hidden,
140 }
141 }
142}
143
144#[cfg(all(feature = "qa-probes", debug_assertions))]
147#[derive(Debug, Clone, Copy, PartialEq)]
148pub struct ButtonFrames {
149 pub close: ButtonFrame,
150 pub miniaturize: ButtonFrame,
151 pub zoom: ButtonFrame,
152 pub window_visible: bool,
154}
155
156#[cfg(all(feature = "qa-probes", debug_assertions))]
157impl ButtonFrames {
158 pub fn buttons(&self) -> [ButtonFrame; 3] {
160 [self.close, self.miniaturize, self.zoom]
161 }
162
163 pub fn to_json(&self) -> String {
166 serde_json::json!({ "buttons": self.buttons(), "windowVisible": self.window_visible })
167 .to_string()
168 }
169}
170
171#[cfg(all(feature = "qa-probes", debug_assertions))]
184pub fn button_frames(window: &WebviewWindow<Wry>) -> Result<ButtonFrames, ShellError> {
185 #[cfg(target_os = "macos")]
186 {
187 macos::frames(window)
188 }
189 #[cfg(not(target_os = "macos"))]
190 {
191 let _ = window;
192 Err(ShellError::Unsupported(
193 "window button frames exist only on macOS".into(),
194 ))
195 }
196}
197
198#[cfg(target_os = "macos")]
199mod macos {
200 use std::sync::Mutex;
201
202 use objc2_app_kit::{NSWindow, NSWindowButton};
203 use objc2_foundation::NSPoint;
204 use tauri::{WebviewWindow, Wry};
205
206 #[cfg(all(feature = "qa-probes", debug_assertions))]
207 use crate::error::ShellError;
208
209 const KINDS: [NSWindowButton; 3] = [
210 NSWindowButton::CloseButton,
211 NSWindowButton::MiniaturizeButton,
212 NSWindowButton::ZoomButton,
213 ];
214
215 #[derive(Default)]
216 pub struct Inner {
217 defaults: Mutex<Option<[NSPoint; 3]>>,
218 offset: Mutex<(f64, f64)>,
219 inset: Mutex<Option<(f64, f64)>>,
221 }
222
223 extern "C" {
224 fn pthread_main_np() -> std::os::raw::c_int;
226 }
227
228 pub fn dispatch<F>(window: &WebviewWindow<Wry>, f: F)
231 where
232 F: FnOnce(&WebviewWindow<Wry>) + Send + 'static,
233 {
234 use tauri::Manager;
235 if window
236 .app_handle()
237 .try_state::<crate::ShellState>()
238 .is_some()
239 {
240 let _ = crate::windows::dispatch_native(window, move |w| {
241 f(w);
242 Ok(())
243 });
244 } else if unsafe { pthread_main_np() } == 1 {
245 f(window);
246 } else {
247 let w = window.clone();
248 let _ = window.run_on_main_thread(move || f(&w));
249 }
250 }
251
252 fn ns_window(window: &WebviewWindow<Wry>) -> Option<&NSWindow> {
253 let ptr = window.ns_window().ok()?;
254 if ptr.is_null() {
255 return None;
256 }
257 Some(unsafe { &*(ptr as *const NSWindow) })
259 }
260
261 #[cfg(all(feature = "qa-probes", debug_assertions))]
263 pub fn frames(window: &WebviewWindow<Wry>) -> Result<super::ButtonFrames, ShellError> {
264 use tauri::Manager;
265 if window
266 .app_handle()
267 .try_state::<crate::ShellState>()
268 .is_some()
269 {
270 return crate::windows::dispatch_native(window, read).map_err(ShellError::Other);
271 }
272 if unsafe { pthread_main_np() } == 1 {
273 return read(window).map_err(ShellError::Other);
274 }
275 let (tx, rx) = std::sync::mpsc::channel();
276 let target = window.clone();
277 window
278 .run_on_main_thread(move || {
279 let _ = tx.send(read(&target));
280 })
281 .map_err(|e| ShellError::Other(format!("main thread unavailable: {e}")))?;
282 rx.recv_timeout(std::time::Duration::from_secs(5))
283 .map_err(|_| ShellError::Timeout("main thread did not answer within 5 s".into()))?
284 .map_err(ShellError::Other)
285 }
286
287 #[cfg(all(feature = "qa-probes", debug_assertions))]
288 fn read(window: &WebviewWindow<Wry>) -> Result<super::ButtonFrames, String> {
289 let win = ns_window(window).ok_or("no NSWindow for this window")?;
290 let mut out = [super::ButtonFrame::new((0.0, 0.0), (0.0, 0.0), false); 3];
291 for (slot, (name, kind)) in out.iter_mut().zip([
292 ("close", KINDS[0]),
293 ("miniaturize", KINDS[1]),
294 ("zoom", KINDS[2]),
295 ]) {
296 let button = win
297 .standardWindowButton(kind)
298 .ok_or_else(|| format!("window has no {name} button"))?;
299 let frame = button.frame();
300 *slot = super::ButtonFrame::new(
301 (frame.origin.x, frame.origin.y),
302 (frame.size.width, frame.size.height),
303 button.isHidden(),
304 );
305 }
306 Ok(super::ButtonFrames {
307 close: out[0],
308 miniaturize: out[1],
309 zoom: out[2],
310 window_visible: win.isVisible(),
311 })
312 }
313
314 pub fn apply(window: &WebviewWindow<Wry>, dx: f64, dy: f64, inner: &Inner) {
315 let Some(win) = ns_window(window) else { return };
316 let mut buttons = Vec::with_capacity(3);
317 for k in KINDS {
318 match win.standardWindowButton(k) {
319 Some(b) => buttons.push(b),
320 None => return,
321 }
322 }
323 let base = {
324 let mut d = inner.defaults.lock().unwrap_or_else(|e| e.into_inner());
325 *d.get_or_insert_with(|| {
326 let mut o = [NSPoint { x: 0.0, y: 0.0 }; 3];
327 for (i, b) in buttons.iter().enumerate() {
328 o[i] = b.frame().origin;
329 }
330 o
331 })
332 };
333 for (i, b) in buttons.iter().enumerate() {
334 b.setFrameOrigin(NSPoint {
336 x: base[i].x + dx,
337 y: base[i].y - dy,
338 });
339 }
340 *inner.offset.lock().unwrap_or_else(|e| e.into_inner()) = (dx, dy);
341 *inner.inset.lock().unwrap_or_else(|e| e.into_inner()) = None;
342 }
343
344 pub fn apply_inset(window: &WebviewWindow<Wry>, x: f64, y: f64, inner: &Inner) {
347 let Some(win) = ns_window(window) else { return };
348 let mut buttons = Vec::with_capacity(3);
349 for k in KINDS {
350 match win.standardWindowButton(k) {
351 Some(b) => buttons.push(b),
352 None => return,
353 }
354 }
355 let base = {
356 let mut d = inner.defaults.lock().unwrap_or_else(|e| e.into_inner());
357 *d.get_or_insert_with(|| {
358 let mut o = [NSPoint { x: 0.0, y: 0.0 }; 3];
359 for (i, b) in buttons.iter().enumerate() {
360 o[i] = b.frame().origin;
361 }
362 o
363 })
364 };
365 for (i, b) in buttons.iter().enumerate() {
366 let Some(container) = (unsafe { b.superview() }) else {
369 return;
370 };
371 let container_height = container.frame().size.height;
372 let button_height = b.frame().size.height;
373 b.setFrameOrigin(NSPoint {
375 x: x + (base[i].x - base[0].x),
376 y: container_height - y - button_height,
377 });
378 }
379 *inner.inset.lock().unwrap_or_else(|e| e.into_inner()) = Some((x, y));
380 }
381
382 pub fn reapply(window: &WebviewWindow<Wry>, inner: &Inner) {
383 if inner
384 .defaults
385 .lock()
386 .unwrap_or_else(|e| e.into_inner())
387 .is_some()
388 {
389 let inset = *inner.inset.lock().unwrap_or_else(|e| e.into_inner());
390 match inset {
391 Some((x, y)) => apply_inset(window, x, y, inner),
392 None => {
393 let (dx, dy) = *inner.offset.lock().unwrap_or_else(|e| e.into_inner());
394 apply(window, dx, dy, inner);
395 }
396 }
397 }
398 }
399}
400
401#[cfg(all(test, feature = "qa-probes", debug_assertions))]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn button_frame_maps_an_appkit_frame() {
407 let f = ButtonFrame::new((7.0, 6.5), (14.0, 16.0), true);
408 assert_eq!(
409 f,
410 ButtonFrame {
411 x: 7.0,
412 y: 6.5,
413 width: 14.0,
414 height: 16.0,
415 hidden: true,
416 }
417 );
418 }
419
420 #[test]
421 fn button_frames_json_keeps_the_viewright_shape_and_order() {
422 let frames = ButtonFrames {
423 close: ButtonFrame::new((7.0, 6.0), (14.0, 16.0), false),
424 miniaturize: ButtonFrame::new((27.0, 6.0), (14.0, 16.0), false),
425 zoom: ButtonFrame::new((47.0, 6.0), (14.0, 16.0), true),
426 window_visible: true,
427 };
428 let v: serde_json::Value = serde_json::from_str(&frames.to_json()).unwrap();
429 assert_eq!(v["windowVisible"], true);
430 let b = v["buttons"].as_array().unwrap();
431 assert_eq!(b.len(), 3);
432 assert_eq!(b[0]["x"], 7.0);
433 assert_eq!(b[1]["x"], 27.0);
434 assert_eq!(b[2]["x"], 47.0);
435 assert_eq!(b[0]["hidden"], false);
436 assert_eq!(b[2]["hidden"], true);
437 for button in b {
438 for key in ["x", "y", "width", "height", "hidden"] {
439 assert!(button.get(key).is_some(), "{key}");
440 }
441 }
442 assert_eq!(frames.buttons()[1], frames.miniaturize);
443 }
444}