//! P5-47 — TDS Custom Window Chrome (opt-in, runtime-reversible). //! //! When the user opts into custom window chrome, the web client renders its own //! `` (folds/TDS styled) and we strip the OS-native window frame so //! the two don't stack. This is entirely opt-in; the default is the untouched //! native frame. On Windows and macOS the window is built with the native frame //! and `set_custom_chrome` toggles it at runtime. On Linux the saved choice //! decides the frame when the window is built, because on native Wayland a //! runtime change doesn't reach the compositor (it then applies next launch). //! `set_custom_chrome` reports whether the frame is stripped *now*, and the web //! client shows its titlebar only then. //! //! Everything here goes through the cross-platform Tauri v2 window API (plus a //! Windows-only `window_vibrancy` dance in `set_custom_chrome`, since Mica and a //! frameless window can't coexist). Each command resolves the "main" window and //! silently no-ops if it isn't present (e.g. during teardown); the `Result`s are //! intentionally ignored since a failed chrome tweak should never surface as an //! error to the user. use std::sync::atomic::{AtomicBool, Ordering}; use tauri::{AppHandle, Manager}; /// Whether the native frame is **actually** stripped right now, which is not /// always what the user last asked for: on Linux / native Wayland GTK only /// negotiates decorations with the compositor when the window is created, so a /// runtime `set_decorated` leaves the compositor's frame in place (KDE drew its /// buttons around the Lotus titlebar). There the choice is applied when the /// window is built (from the persisted file) and takes effect next launch. pub struct ChromeState { active: AtomicBool, } impl ChromeState { /// `built_undecorated`: the main window was created without a frame. pub fn new(built_undecorated: bool) -> Self { Self { active: AtomicBool::new(built_undecorated), } } } /// Where the custom-chrome choice is kept natively, so the main window can be /// built with the right frame before the web client has loaded. fn custom_chrome_file(app: &AppHandle) -> Option { app.path() .app_config_dir() .ok() .map(|dir| dir.join("custom-chrome")) } /// The saved choice; defaults to off (native frame). pub fn read_custom_chrome(app: &AppHandle) -> bool { custom_chrome_file(app) .and_then(|path| std::fs::read_to_string(path).ok()) .is_some_and(|value| value.trim() == "1") } fn write_custom_chrome(app: &AppHandle, enabled: bool) { let Some(path) = custom_chrome_file(app) else { return; }; if let Some(dir) = path.parent() { let _ = std::fs::create_dir_all(dir); } let _ = std::fs::write(path, if enabled { "1" } else { "0" }); } /// Whether `set_decorations` takes effect on a live window here. False only on /// Linux with GTK on native Wayland. Pure, for tests. pub(crate) fn runtime_toggle_works(linux: bool, native_wayland: bool) -> bool { !(linux && native_wayland) } /// Whether the main window should be built without a frame: only on Linux, /// where a runtime toggle may not apply. Windows keeps building with the native /// frame and toggling at runtime (Mica needs it there, see below). pub fn build_undecorated(app: &AppHandle) -> bool { cfg!(target_os = "linux") && read_custom_chrome(app) } /// Apply the custom-chrome choice. `enabled` = custom chrome on, which means /// the OS decorations must come **off** (`set_decorations(!enabled)`). The /// choice is saved for the next launch; where the frame can change at runtime /// (Windows, macOS, Linux on X11) it changes now. /// /// Returns whether the native frame is stripped **now**, so the web client /// shows its own titlebar only then (never two titlebars, never none). #[tauri::command] pub fn set_custom_chrome(app: AppHandle, enabled: bool) -> bool { write_custom_chrome(&app, enabled); let native_wayland = crate::gpu_workarounds::gtk_native_wayland(&|k: &str| std::env::var(k).ok()); let can_toggle = runtime_toggle_works(cfg!(target_os = "linux"), native_wayland); let state = app.try_state::(); if !can_toggle { return state .map(|s| s.active.load(Ordering::Relaxed)) .unwrap_or(false); } if let Some(window) = app.get_webview_window("main") { // Windows: the Mica backdrop (applied at startup in lib.rs) and a // frameless window are a known-bad combo — stripping WS_CAPTION under a // system backdrop glitches the whole surface (black/blank window). Drop // the backdrop before undecorating, and restore it together with the // native frame when custom chrome turns off. #[cfg(target_os = "windows")] if enabled { let _ = window_vibrancy::clear_mica(&window); } let _ = window.set_decorations(!enabled); // Re-assert the DWM shadow so a frameless window keeps its drop shadow // and resize borders on Windows (no-op / harmless elsewhere). let _ = window.set_shadow(true); #[cfg(target_os = "windows")] if !enabled { let _ = window_vibrancy::apply_mica(&window, Some(true)); } if let Some(state) = &state { state.active.store(enabled, Ordering::Relaxed); } } state .map(|s| s.active.load(Ordering::Relaxed)) .unwrap_or(false) } /// Minimize the main window (custom titlebar min button). #[tauri::command] pub fn window_minimize(app: AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.minimize(); } } /// Toggle maximize/restore the main window (custom titlebar max button and /// drag-region double-click). #[tauri::command] pub fn window_toggle_maximize(app: AppHandle) { if let Some(window) = app.get_webview_window("main") { if window.is_maximized().unwrap_or(false) { let _ = window.unmaximize(); } else { let _ = window.maximize(); } } } /// Begin an OS-level window drag from the custom titlebar drag region. The web /// side also marks the drag area with `data-tauri-drag-region`; this command is /// the explicit fallback so behaviour is identical across platforms. #[tauri::command] pub fn window_start_drag(app: AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.start_dragging(); } } /// Close from the custom titlebar. Mirrors the app's close-to-tray behaviour /// (see the `CloseRequested` handler in `lib.rs`): we `hide()` the window rather /// than exiting, so the tray keeps the app running and the tray menu remains the /// single explicit quit path. #[tauri::command] pub fn window_close(app: AppHandle) { // Same rules as the OS close button (tray / quit / first-close dialog, // cinny-desktop #5). crate::handle_close_request(&app); } #[cfg(test)] mod tests { use super::runtime_toggle_works; #[test] fn only_linux_on_native_wayland_needs_a_restart() { assert!(!runtime_toggle_works(true, true)); assert!(runtime_toggle_works(true, false)); // Linux on X11 / XWayland assert!(runtime_toggle_works(false, false)); // Windows / macOS assert!(runtime_toggle_works(false, true)); } }