diff --git a/templates/clips/desktop/src-tauri/src/clips/mod.rs b/templates/clips/desktop/src-tauri/src/clips/mod.rs index 0feeab442..f0e6e0990 100644 --- a/templates/clips/desktop/src-tauri/src/clips/mod.rs +++ b/templates/clips/desktop/src-tauri/src/clips/mod.rs @@ -517,6 +517,9 @@ pub async fn show_bubble(app: AppHandle) -> Result<(), String> { // `getDisplayMedia`, which matches the other Clips chrome (popover, // toolbar, countdown) but NOT what users want for the camera bubble. let _ = win.show(); + // canJoinAllSpaces: bubble follows the user across every Mission Control + // space and every monitor — not just the one where the session started. + crate::util::set_window_can_join_all_spaces(&win); dlog!("[clips-tray] bubble shown at ({},{}) size {}", x, y, size); Ok(()) } diff --git a/templates/clips/desktop/src-tauri/src/lib.rs b/templates/clips/desktop/src-tauri/src/lib.rs index cbd26e4e0..835babf70 100644 --- a/templates/clips/desktop/src-tauri/src/lib.rs +++ b/templates/clips/desktop/src-tauri/src/lib.rs @@ -29,7 +29,7 @@ use state::{ DictationActive, DictationEnabled, LastTranscript, MeetingActive, PopoverShownAt, RecordingActive, TrayAnchor, TrayMeetings, VoiceWakePopover, }; -use util::{is_recording_active, set_capture_included}; +use util::{is_recording_active, set_capture_included, set_window_can_join_all_spaces}; // Embedded fallback icon — a tiny 16x16 solid purple PNG so the binary always // has *something* to display even if `icons/tray.png` is missing on disk. The @@ -185,6 +185,12 @@ pub fn run() { // popover boots. meetings_watcher::spawn_watcher(app.handle().clone()); + // Ensure the popover appears above every app on every Mission + // Control space so it's reachable before the user starts recording. + if let Some(window) = app.get_webview_window("popover") { + set_window_can_join_all_spaces(&window); + } + // Hide the popover on blur so it feels like a real menu-bar popover. // The 250ms guard is the important bit — during the tray-click // itself macOS briefly steals focus from the popover, which would diff --git a/templates/clips/desktop/src-tauri/src/util.rs b/templates/clips/desktop/src-tauri/src/util.rs index bcc3cf8d5..938ffdd8e 100644 --- a/templates/clips/desktop/src-tauri/src/util.rs +++ b/templates/clips/desktop/src-tauri/src/util.rs @@ -190,6 +190,45 @@ pub fn show_without_activation(window: &WebviewWindow) { } } +/// Set `NSWindowCollectionBehaviorCanJoinAllSpaces` so the window follows the +/// user across every Mission Control space and appears on any monitor — not +/// just the one where it was first shown. +#[cfg(target_os = "macos")] +pub fn set_window_can_join_all_spaces(window: &WebviewWindow) { + let win = window.clone(); + if let Err(err) = win.clone().run_on_main_thread(move || { + let label = win.label().to_string(); + let ns_window_ptr = match win.ns_window() { + Ok(p) => p, + Err(err) => { + eprintln!( + "[clips-tray] set_window_can_join_all_spaces({label}): ns_window() failed: {err}" + ); + return; + } + }; + if ns_window_ptr.is_null() { + return; + } + unsafe { + let obj = ns_window_ptr as *mut objc2::runtime::AnyObject; + // Read the current behavior so we don't discard flags Tauri + // already set (e.g. NSWindowCollectionBehaviorManaged = 4). + // NSWindowCollectionBehaviorCanJoinAllSpaces = 1 << 0 = 1. + let current: usize = objc2::msg_send![&*obj, collectionBehavior]; + let _: () = objc2::msg_send![&*obj, setCollectionBehavior: current | 1usize]; + } + dlog!("[clips-tray] set_window_can_join_all_spaces({label}): applied"); + }) { + eprintln!( + "[clips-tray] set_window_can_join_all_spaces: run_on_main_thread failed: {err}" + ); + } +} + +#[cfg(not(target_os = "macos"))] +pub fn set_window_can_join_all_spaces(_window: &WebviewWindow) {} + #[cfg(not(target_os = "macos"))] pub fn show_without_activation(window: &WebviewWindow) { // On non-macOS we just fall back to the standard show. Focus stealing diff --git a/templates/clips/desktop/src/styles.css b/templates/clips/desktop/src/styles.css index c3809af65..82ebdefe3 100644 --- a/templates/clips/desktop/src/styles.css +++ b/templates/clips/desktop/src/styles.css @@ -2049,9 +2049,6 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root { backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: - 0 10px 30px rgba(0, 0, 0, 0.35), - 0 2px 6px rgba(0, 0, 0, 0.3); color: white; cursor: grab; } @@ -2176,7 +2173,6 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root { backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45); user-select: none; -webkit-user-select: none; cursor: grab; @@ -2312,9 +2308,6 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root { border-radius: 50%; overflow: hidden; background: #000; - /* Drop shadow only — no brand ring. The bubble is the user's face; - a bright purple halo competes with what matters visually. */ - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.45); /* Position context for absolute children (close X, canvas). */ position: relative; /* Cursor hint — the drag region is here. */ @@ -2407,7 +2400,6 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root { z-index: 3; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); } .bubble-close:hover { @@ -2433,7 +2425,6 @@ body[data-clips-route]:not([data-clips-route="popover"]) #root { backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); z-index: 3; /* Cursor over the pill should not be a grab cursor (we're not dragging the bubble when clicking its controls). */