use crate::window_state; use log::info; use rand::random; use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent}; use tokio::sync::mpsc; const DEFAULT_WINDOW_WIDTH: f64 = 1100.0; const DEFAULT_WINDOW_HEIGHT: f64 = 600.0; const MIN_WINDOW_WIDTH: f64 = 300.0; const MIN_WINDOW_HEIGHT: f64 = 300.0; pub const MAIN_WINDOW_PREFIX: &str = "main_"; const OTHER_WINDOW_PREFIX: &str = "other_"; const MAIN_WINDOW_STATE_KEY: &str = "main"; #[derive(Default, Debug)] pub struct CreateWindowConfig<'s> { pub url: &'s str, pub label: &'s str, pub title: &'s str, pub state_key: Option, pub inner_size: Option<(f64, f64)>, pub position: Option<(f64, f64)>, pub restore_position: Option, pub navigation_tx: Option>, pub close_tx: Option>, pub data_dir_key: Option, pub initialization_script: Option, pub hidden: bool, pub hide_titlebar: bool, pub use_native_titlebar: bool, } pub fn create_window( handle: &AppHandle, config: CreateWindowConfig, ) -> tauri::Result> { info!("Create new window label={}", config.label); let state_key = config.state_key.clone().unwrap_or_else(|| config.label.to_string()); let restore_position = config.restore_position.unwrap_or(true); let mut inner_size = config.inner_size; let mut position = config.position; let mut maximized = false; window_state::apply_saved_state( handle, &state_key, &mut inner_size, &mut position, &mut maximized, restore_position, ); let mut win_builder = tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into())) .title(config.title) .resizable(true) .visible(!config.hidden) .fullscreen(false) .maximized(maximized) .min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT); if let Some(script) = config.initialization_script { win_builder = win_builder.initialization_script(script); } if let Some(key) = config.data_dir_key { #[cfg(not(target_os = "macos"))] { use std::fs; let safe_key = format!("{:x}", md5::compute(key.as_bytes())); let dir = handle.path().app_data_dir()?.join("window-sessions").join(safe_key); fs::create_dir_all(&dir)?; win_builder = win_builder.data_directory(dir); } // macOS doesn't support `data_directory()` so must use this fn instead #[cfg(target_os = "macos")] { let hash = md5::compute(key.as_bytes()); let mut id = [0u8; 16]; id.copy_from_slice(&hash[..16]); // Take the first 16 bytes of the hash win_builder = win_builder.data_store_identifier(id); } } if let Some((w, h)) = inner_size { win_builder = win_builder.inner_size(w, h); } else { win_builder = win_builder.inner_size(600.0, 600.0); } if let Some((x, y)) = position { win_builder = win_builder.position(x, y); } else { win_builder = win_builder.center(); } if let Some(tx) = config.navigation_tx { win_builder = win_builder.on_navigation(move |url| { let url = url.to_string(); let tx = tx.clone(); tauri::async_runtime::block_on(async move { tx.send(url).await.unwrap(); }); true }); } if config.hide_titlebar && !config.use_native_titlebar { #[cfg(target_os = "macos")] { use tauri::TitleBarStyle; win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay); } #[cfg(not(target_os = "macos"))] { win_builder = win_builder.decorations(false); } } if let Some(w) = handle.webview_windows().get(config.label) { info!("Webview with label {} already exists. Focusing existing", config.label); w.set_focus()?; return Ok(w.to_owned()); } let win = win_builder.build()?; window_state::track_window(&win, &state_key); if let Some(tx) = config.close_tx { win.on_window_event(move |event| match event { WindowEvent::CloseRequested { .. } => { let tx = tx.clone(); tauri::async_runtime::spawn(async move { tx.send(()).await.unwrap(); }); } _ => {} }); } Ok(win) } pub fn create_main_window( handle: &AppHandle, url: &str, initialization_script: Option, use_native_titlebar: bool, ) -> tauri::Result> { let mut counter = 0; let label = loop { let label = format!("{MAIN_WINDOW_PREFIX}{counter}"); match handle.webview_windows().get(label.as_str()) { None => break Some(label), Some(_) => counter += 1, } } .expect("Failed to generate label for new window"); let config = CreateWindowConfig { url, label: label.as_str(), title: "Yaak", state_key: Some(MAIN_WINDOW_STATE_KEY.to_string()), inner_size: Some((DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)), position: Some(( // Offset by random amount so it's easier to differentiate 100.0 + random::() * 20.0, 100.0 + random::() * 20.0, )), restore_position: Some(counter == 0), initialization_script, hidden: true, hide_titlebar: true, use_native_titlebar, ..Default::default() }; create_window(handle, config) } pub fn create_child_window( parent_window: &WebviewWindow, url: &str, label: &str, title: &str, inner_size: (f64, f64), initialization_script: Option, use_native_titlebar: bool, ) -> tauri::Result> { let app_handle = parent_window.app_handle(); let state_key = label.to_string(); let label = format!("{OTHER_WINDOW_PREFIX}_{label}"); let scale_factor = parent_window.scale_factor()?; let current_pos = parent_window.inner_position()?.to_logical::(scale_factor); let current_size = parent_window.inner_size()?.to_logical::(scale_factor); // Position the new window in the middle of the parent let position = ( current_pos.x + current_size.width / 2.0 - inner_size.0 / 2.0, current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0, ); let config = CreateWindowConfig { label: label.as_str(), title, state_key: Some(state_key), url, inner_size: Some(inner_size), position: Some(position), initialization_script, hidden: true, hide_titlebar: true, use_native_titlebar, ..Default::default() }; let child_window = create_window(&app_handle, config)?; // NOTE: These listeners will remain active even when the windows close. Unfortunately, // there's no way to unlisten to events for now, so we just have to be defensive. { let parent_window = parent_window.clone(); let child_window = child_window.clone(); child_window.clone().on_window_event(move |e| match e { // When the new window is destroyed, bring the other up behind it WindowEvent::Destroyed => { if let Some(w) = parent_window.get_webview_window(child_window.label()) { w.set_focus().unwrap(); } } _ => {} }); } { let parent_window = parent_window.clone(); let child_window = child_window.clone(); parent_window.clone().on_window_event(move |e| match e { // When the parent window is closed, close the child WindowEvent::CloseRequested { .. } => child_window.close().unwrap(), // When the parent window is focused, bring the child above WindowEvent::Focused(focus) => { if *focus { if let Some(w) = parent_window.get_webview_window(child_window.label()) { w.set_focus().unwrap(); }; } } _ => {} }); } Ok(child_window) }