// Multi-window support is desktop-only. Android Tauri apps run as a // single Activity, so `WebviewWindowBuilder` is not available there. // All four commands below have an Android stub that returns a clear // error so frontend invokes don't panic — the frontend itself is // expected to detect Android via `isAndroid()` and route the previously- // secondary content (preview overlay, transcript viewer, task float) // into routes inside the main window instead. #[cfg(not(target_os = "android"))] use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; #[cfg(not(target_os = "android"))] use crate::PreferencesScript; #[cfg(target_os = "android")] const ANDROID_MULTIWINDOW_ERR: &str = "Multi-window is not supported on Android; this command is desktop-only"; #[cfg(target_os = "android")] #[tauri::command] pub async fn open_task_window(_app: tauri::AppHandle) -> Result<(), String> { Err(ANDROID_MULTIWINDOW_ERR.into()) } #[cfg(target_os = "android")] #[tauri::command] pub async fn open_preview_window(_app: tauri::AppHandle) -> Result<(), String> { Err(ANDROID_MULTIWINDOW_ERR.into()) } #[cfg(target_os = "android")] #[tauri::command] pub async fn open_viewer_window(_app: tauri::AppHandle) -> Result<(), String> { Err(ANDROID_MULTIWINDOW_ERR.into()) } /// Open a floating always-on-top task window. #[cfg(not(target_os = "android"))] #[tauri::command] pub async fn open_task_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("tasks-float") { window.show().map_err(|e| e.to_string())?; window.set_focus().map_err(|e| e.to_string())?; app.emit("task-window-focus", ()) .map_err(|e| e.to_string())?; return Ok(()); } // On Linux we use native KWin/Mutter decorations so resize and drag // are handled by the compositor. Tauri's frameless path on Wayland // doesn't honour diagonal resize reliably and Tauri's own drag // region adds latency on webkit2gtk. macOS and Windows keep the // custom frameless chrome drawn by the Titlebar component. let use_native_decorations = cfg!(target_os = "linux"); let mut builder = WebviewWindowBuilder::new(&app, "tasks-float", WebviewUrl::App("/float".into())) .title("Magnotia Tasks") .inner_size(480.0, 520.0) .min_inner_size(360.0, 480.0) .always_on_top(true) .decorations(use_native_decorations) .resizable(true); // Inject preferences before Svelte mounts if let Some(script) = app.try_state::() { if !script.0.is_empty() { builder = builder.initialization_script(&script.0); } } builder.build().map_err(|e| e.to_string())?; Ok(()) } /// Open the always-on-top transcription preview window. Idempotent — an /// existing window is shown and focused; otherwise a new one is built. /// The preview is passive: it subscribes to the `transcription-result` /// event and the cross-window `preview-*` events that DictationPage fires /// as it moves through the phases of a dictation run. #[cfg(not(target_os = "android"))] #[tauri::command] pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("transcription-preview") { window.show().map_err(|e| e.to_string())?; // Intentionally NOT set_focus — the preview window is meant to // appear next to whatever the user is typing into; stealing focus // defeats the whole point. return Ok(()); } let use_native_decorations = cfg!(target_os = "linux"); // Preview is a transient helper overlay, not a primary surface. It // should (a) follow the user across virtual desktops — otherwise the // overlay vanishes the moment they switch workspace mid-dictation — // and (b) stay out of the Alt+Tab / taskbar lists. skip_taskbar covers // both on KWin (KWin's default Alt+Tab list reads _NET_WM_STATE_SKIP_TASKBAR); // visible_on_all_workspaces sets _NET_WM_STATE_STICKY via GTK on X11 // / XWayland so the overlay is pinned. Matches the ergonomic OpenWhispr // PR #183 shipped for KDE Plasma Wayland. // // Built hidden so we can set the Linux WindowTypeHint before the // window maps — GTK3 only honours the hint pre-realize. let mut builder = WebviewWindowBuilder::new( &app, "transcription-preview", WebviewUrl::App("/preview".into()), ) .title("Magnotia — Preview") .inner_size(420.0, 200.0) .min_inner_size(360.0, 140.0) .max_inner_size(520.0, 360.0) .always_on_top(true) .skip_taskbar(true) .visible_on_all_workspaces(true) .focused(false) .visible(false) .decorations(use_native_decorations) .resizable(true); if let Some(script) = app.try_state::() { if !script.0.is_empty() { builder = builder.initialization_script(&script.0); } } let window = builder.build().map_err(|e| e.to_string())?; // Defence-in-depth for non-KDE compositors (Hyprland, Sway, GNOME // Mutter) where SKIP_TASKBAR alone may not hide a window from the // alt-tab switcher. Classifying as Utility signals to the compositor // that this is an assistive auxiliary window — switchers and tilers // treat it accordingly. Noop on KWin (already handled by skip_taskbar) // but harmless. Must happen before show() per GTK3 docs. #[cfg(target_os = "linux")] { use gdk::WindowTypeHint; use gtk::prelude::GtkWindowExt; if let Ok(gtk_window) = window.gtk_window() { gtk_window.set_type_hint(WindowTypeHint::Utility); } } window.show().map_err(|e| e.to_string())?; Ok(()) } /// Open the transcript viewer window. #[cfg(not(target_os = "android"))] #[tauri::command] pub async fn open_viewer_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("transcript-viewer") { window.show().map_err(|e| e.to_string())?; window.set_focus().map_err(|e| e.to_string())?; return Ok(()); } // See note in open_task_window for the Linux-vs-other platform split. let use_native_decorations = cfg!(target_os = "linux"); let mut builder = WebviewWindowBuilder::new(&app, "transcript-viewer", WebviewUrl::App("/viewer".into())) .title("Magnotia - Transcription Editor") .inner_size(600.0, 700.0) .min_inner_size(560.0, 520.0) .decorations(use_native_decorations) .resizable(true); // Inject preferences before Svelte mounts if let Some(script) = app.try_state::() { if !script.0.is_empty() { builder = builder.initialization_script(&script.0); } } builder.build().map_err(|e| e.to_string())?; Ok(()) }