From c4a677fc3ddbf3c0c49e3eb6805abded3fbdff83 Mon Sep 17 00:00:00 2001 From: samialabri Date: Tue, 20 Oct 2026 21:36:31 +0900 Subject: [PATCH] Title bar in bg on Windows: theme colours on the native caption Tauri 2 overlays web content on the title bar only on macOS, so Windows and Linux keep the native title bar and window controls. On Windows 11 the caption, title and border now take the theme's bg and text (DWM), re-applied on every theme change and when the OS setting changes under "System", so the caption and the app header read as one custom title bar. Linux (GTK) keeps its own colours. Co-Authored-By: Claude Opus 5.5 --- src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 1 + src-tauri/src/main.rs | 10 ++++++++ src-tauri/src/title_bar.rs | 52 ++++++++++++++++++++++++++++++++++++++ src/main.ts | 14 ++++++++++ 5 files changed, 78 insertions(+) create mode 100644 src-tauri/src/title_bar.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3ee7131..eee9a2a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -60,6 +60,7 @@ keyring = "4" windows-sys = { version = "0.61", features = [ "Win32_Security_Cryptography", "Win32_Foundation", + "Win32_Graphics_Dwm", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7b7f810..9ae358c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,4 +9,5 @@ pub mod ipc; pub mod private_file; pub mod qr; pub mod sync; +pub mod title_bar; pub mod words; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f9eec50..6126e6b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -425,6 +425,15 @@ async fn setup_from_file(app: State<'_, App>, request: tauri::ipc::Request<'_>) app.sync.setup_from_file(bytes) } +/// The theme's `bg` and `text` for the native title bar (Windows 11 only; see `title_bar`). +#[tauri::command] +fn set_title_bar_colors(window: tauri::WebviewWindow, bg: String, text: String) { + #[cfg(windows)] + kestrove_desktop::title_bar::paint(&window, &bg, &text); + #[cfg(not(windows))] + let _ = (window, bg, text); +} + #[tauri::command] fn clear_setup(app: State<'_, App>) { app.sync.clear_setup(); @@ -616,6 +625,7 @@ fn main() { }) .invoke_handler(tauri::generate_handler![ state, + set_title_bar_colors, create_account, unlock, lock, diff --git a/src-tauri/src/title_bar.rs b/src-tauri/src/title_bar.rs new file mode 100644 index 0000000..13f4bfb --- /dev/null +++ b/src-tauri/src/title_bar.rs @@ -0,0 +1,52 @@ +//! The window's title bar on Windows and Linux. +//! +//! Tauri 2 can only overlay web content on the title bar on macOS (`titleBarStyle: "Overlay"`), so Windows and +//! Linux keep the native title bar and its window controls. On Windows 11 its caption is painted in the theme's +//! `bg` with `text` for the title, so it reads as one bar with the app's own header below it. Windows 10 and +//! Linux (GTK) draw their own colours. + +/// `#rrggbb` (a design token) → a Win32 `COLORREF` (`0x00bbggrr`). +pub fn colorref(hex: &str) -> Option { + let hex = hex.trim().strip_prefix('#')?; + if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + let rgb = u32::from_str_radix(hex, 16).ok()?; + let (r, g, b) = (rgb >> 16, (rgb >> 8) & 0xff, rgb & 0xff); + Some(b << 16 | g << 8 | r) +} + +/// Paints the caption, its title and the window border. Errors (Windows 10 has no caption colour) are ignored. +#[cfg(windows)] +pub fn paint(window: &tauri::WebviewWindow, bg: &str, text: &str) { + use windows_sys::Win32::Graphics::Dwm::{ + DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, DWMWA_TEXT_COLOR, + }; + let (Ok(hwnd), Some(bg), Some(text)) = (window.hwnd(), colorref(bg), colorref(text)) else { return }; + for (attribute, color) in [(DWMWA_CAPTION_COLOR, bg), (DWMWA_BORDER_COLOR, bg), (DWMWA_TEXT_COLOR, text)] { + // SAFETY: a live window handle and a COLORREF-sized value, as DwmSetWindowAttribute expects. + unsafe { + DwmSetWindowAttribute(hwnd.0, attribute as u32, (&color as *const u32).cast(), size_of::() as u32); + } + } +} + +#[cfg(test)] +mod tests { + use super::colorref; + + #[test] + fn token_hex_to_colorref() { + assert_eq!(colorref("#f4f6f4"), Some(0x00f4f6f4)); + assert_eq!(colorref(" #0B1210 "), Some(0x0010120b)); + assert_eq!(colorref("#13201b"), Some(0x001b2013)); + } + + #[test] + fn rejects_other_formats() { + assert_eq!(colorref("f4f6f4"), None); + assert_eq!(colorref("#fff"), None); + assert_eq!(colorref("rgb(1, 2, 3)"), None); + assert_eq!(colorref("#gg0000"), None); + } +} diff --git a/src/main.ts b/src/main.ts index 18b4cfd..4a39d90 100644 --- a/src/main.ts +++ b/src/main.ts @@ -81,8 +81,22 @@ function applyTheme(t: Theme) { else document.documentElement.dataset.theme = t; // The native title bar and window controls follow too. void getCurrentWindow().setTheme(t === "system" ? null : t).catch(() => undefined); + paintTitleBar(); } +/// Windows and Linux keep the native title bar (Tauri overlays it only on macOS); on Windows 11 its caption +/// takes the theme's `bg` and `text`, so it and the header below read as one custom title bar. +function paintTitleBar() { + if (isMac) return; + const css = getComputedStyle(document.documentElement); + const bg = css.getPropertyValue("--bg").trim(); + const text = css.getPropertyValue("--text").trim(); + void invoke("set_title_bar_colors", { bg, text }).catch(() => undefined); +} + +// "System" follows the OS setting while the app runs. +matchMedia("(prefers-color-scheme: dark)").addEventListener("change", paintTitleBar); + function setTheme(t: Theme) { try { localStorage.setItem("theme", t);