//! macOS window tab preferences, applied to application-drawn project tabs. use crate::windows::{Launch, WindowsState}; use ide_core::{IdeError, IdeResult}; use ide_workspace::window_groups::{TabbingPreference, should_group_new_project}; use objc2::{MainThreadMarker, rc::Retained, runtime::ProtocolObject}; use objc2_app_kit::{NSWindow, NSWindowUserTabbingPreference}; use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; use tauri::{AppHandle, Manager, WebviewWindow}; fn tabbing_preference(app: &AppHandle) -> IdeResult { fn read(mtm: MainThreadMarker) -> TabbingPreference { match NSWindow::userTabbingPreference(mtm) { NSWindowUserTabbingPreference::Always => TabbingPreference::Always, NSWindowUserTabbingPreference::InFullScreen => TabbingPreference::InFullScreen, _ => TabbingPreference::Manually, } } if let Some(mtm) = MainThreadMarker::new() { return Ok(read(mtm)); } let (tx, rx) = std::sync::mpsc::sync_channel(1); app.run_on_main_thread(move || { let _ = tx.send(read(MainThreadMarker::new().expect("AppKit main thread"))); }) .map_err(|e| IdeError::internal(e.to_string()))?; rx.recv().map_err(|e| IdeError::internal(e.to_string())) } /// Capture before building the new window, which takes focus itself. pub fn new_project_tab_target(app: &AppHandle, launch: &Launch) -> IdeResult> { let windows = app.webview_windows(); let active = windows .values() .find(|w| w.is_focused().unwrap_or(false)) .cloned() .or_else(|| app.state::().last_focused().and_then(|label| windows.get(&label).cloned())); let active = active.and_then(|w| { let label = crate::windows::settings_parent(w.label()).unwrap_or(w.label()); windows.get(label).cloned() }); let active = active.filter(|w| { let host = app.state::(); host.project(w.label()).is_ok_and(|p| !p.light) || host.remote_dev.get(w.label()).is_some() }); let fullscreen = active.as_ref().is_some_and(|w| w.is_fullscreen().unwrap_or(false)); let light_edit = launch.project.is_none() && launch.remote.is_none(); Ok(should_group_new_project(tabbing_preference(app)?, fullscreen, active.is_some(), light_edit) .then_some(active) .flatten()) } struct FullscreenObserver(Retained>); impl Drop for FullscreenObserver { fn drop(&mut self) { let observer: &ProtocolObject = &self.0; unsafe { NSNotificationCenter::defaultCenter().removeObserver(observer.as_ref()) }; } } thread_local! { static FULLSCREEN_TRANSFERS: std::cell::RefCell> = std::cell::RefCell::default(); } pub fn forget_fullscreen(label: &str) { FULLSCREEN_TRANSFERS.with_borrow_mut(|observers| observers.remove(label)); } /// AppKit cannot enter another window's fullscreen Space until the old one has exited. pub fn transfer_fullscreen(from: &WebviewWindow, to: &WebviewWindow) -> tauri::Result<()> { let source = from.label().to_owned(); let to = to.clone(); from.with_webview(move |webview| unsafe { let native = &*webview.ns_window().cast::(); forget_fullscreen(&source); let label = source.clone(); let block = block2::RcBlock::new(move |_| { forget_fullscreen(&label); let app = to.app_handle(); let selected = app.state::().groups.lock().group(to.label()).map(|g| g.selected.clone()); if let Some(selected) = selected { // Exiting fullscreen can show the old window again; reapply group visibility. let _ = crate::window_groups::select(app, &selected, &selected); if selected == to.label() { let _ = to.set_fullscreen(true); } } }); let observer = NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock( Some(objc2_app_kit::NSWindowDidExitFullScreenNotification), Some(native), None, &block, ); FULLSCREEN_TRANSFERS.with_borrow_mut(|observers| observers.insert(source, FullscreenObserver(observer))); native.toggleFullScreen(None); }) }