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 <[email protected]>
This commit is contained in:
@@ -60,6 +60,7 @@ keyring = "4"
|
|||||||
windows-sys = { version = "0.61", features = [
|
windows-sys = { version = "0.61", features = [
|
||||||
"Win32_Security_Cryptography",
|
"Win32_Security_Cryptography",
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
"Win32_Graphics_Dwm",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_Security_Authorization",
|
"Win32_Security_Authorization",
|
||||||
"Win32_Storage_FileSystem",
|
"Win32_Storage_FileSystem",
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ pub mod ipc;
|
|||||||
pub mod private_file;
|
pub mod private_file;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
pub mod title_bar;
|
||||||
pub mod words;
|
pub mod words;
|
||||||
|
|||||||
@@ -425,6 +425,15 @@ async fn setup_from_file(app: State<'_, App>, request: tauri::ipc::Request<'_>)
|
|||||||
app.sync.setup_from_file(bytes)
|
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]
|
#[tauri::command]
|
||||||
fn clear_setup(app: State<'_, App>) {
|
fn clear_setup(app: State<'_, App>) {
|
||||||
app.sync.clear_setup();
|
app.sync.clear_setup();
|
||||||
@@ -616,6 +625,7 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
state,
|
state,
|
||||||
|
set_title_bar_colors,
|
||||||
create_account,
|
create_account,
|
||||||
unlock,
|
unlock,
|
||||||
lock,
|
lock,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -81,8 +81,22 @@ function applyTheme(t: Theme) {
|
|||||||
else document.documentElement.dataset.theme = t;
|
else document.documentElement.dataset.theme = t;
|
||||||
// The native title bar and window controls follow too.
|
// The native title bar and window controls follow too.
|
||||||
void getCurrentWindow().setTheme(t === "system" ? null : t).catch(() => undefined);
|
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) {
|
function setTheme(t: Theme) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("theme", t);
|
localStorage.setItem("theme", t);
|
||||||
|
|||||||
Reference in New Issue
Block a user