Title bar in bg on Windows: theme colours on the native caption
ci / build (ubuntu-22.04) (push) Canceled after 0s
ci / build (windows-2025) (push) Canceled after 0s
ci / deny (push) Canceled after 0s

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 <[email protected]>
This commit is contained in:
2026-10-20 21:36:31 +09:00
co-authored by Claude Opus 5.5
parent 19c4ce1899
commit c4a677fc3d
5 changed files with 78 additions and 0 deletions
+1
View File
@@ -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",
+1
View File
@@ -9,4 +9,5 @@ pub mod ipc;
pub mod private_file;
pub mod qr;
pub mod sync;
pub mod title_bar;
pub mod words;
+10
View File
@@ -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,
+52
View File
@@ -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<u32> {
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::<u32>() 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);
}
}