From 9ea38931a51616ac830e36528c52381e58c109a1 Mon Sep 17 00:00:00 2001 From: chengcheng84 Date: Mon, 6 Apr 2026 09:33:09 +0800 Subject: [PATCH 1/3] feat: add Windows title bar theme support --- src-tauri/src/lib.rs | 87 ++++++++++++++++++++++++++++++++++++ src/context/ThemeContext.tsx | 5 +++ 2 files changed, 92 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 64a8090..11bd1d7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3702,6 +3702,11 @@ pub fn run() { }; if let Some(main_window) = app.get_webview_window("main") { + #[cfg(target_os = "windows")] + { + windows_title_bar::apply_title_bar_theme(&main_window, false); + } + let has_notes_folder = app .state::() .app_config @@ -3792,6 +3797,7 @@ pub fn run() { install_cli, uninstall_cli, get_cli_status, + set_title_bar_theme, ]) .build(tauri::generate_context!()) .expect("error while building tauri application"); @@ -3814,3 +3820,84 @@ pub fn run() { } }); } + +#[cfg(target_os = "windows")] +mod windows_title_bar { + use tauri::WebviewWindow; + + #[allow(non_snake_case)] + mod dwm { + pub const DWMWA_USE_IMMERSIVE_DARK_MODE: u32 = 20; + pub const DWMWA_CAPTION_COLOR: u32 = 35; + pub const DWMWA_BORDER_COLOR: u32 = 34; + + extern "system" { + pub fn DwmSetWindowAttribute( + hwnd: isize, + attr: u32, + value: *const std::ffi::c_void, + size: u32, + ) -> i32; + } + } + + pub fn apply_title_bar_theme(window: &WebviewWindow, is_dark: bool) { + let Ok(hwnd) = window.hwnd() else { + return; + }; + let hwnd = hwnd.0 as isize; + + unsafe { + let set_attr = + |attr: u32, value: *const std::ffi::c_void, size: u32| { + let _ = dwm::DwmSetWindowAttribute(hwnd, attr, value, size); + }; + + let dark_mode: i32 = if is_dark { 1 } else { 0 }; + set_attr( + dwm::DWMWA_USE_IMMERSIVE_DARK_MODE, + &dark_mode as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); + + if is_dark { + let caption_color: u32 = 0x00_0E_0C_0B; + set_attr( + dwm::DWMWA_CAPTION_COLOR, + &caption_color as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); + set_attr( + dwm::DWMWA_BORDER_COLOR, + &caption_color as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); + } else { + let caption_color: u32 = 0x00_FA_FA_F9; + set_attr( + dwm::DWMWA_CAPTION_COLOR, + &caption_color as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); + set_attr( + dwm::DWMWA_BORDER_COLOR, + &caption_color as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); + } + } + } +} + +#[tauri::command] +fn set_title_bar_theme(app: AppHandle, is_dark: bool) -> Result<(), String> { + #[cfg(target_os = "windows")] + { + if let Some(window) = app.get_webview_window("main") { + windows_title_bar::apply_title_bar_theme(&window, is_dark); + } + } + let _ = app; + let _ = is_dark; + Ok(()) +} diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index ee48436..c2c065c 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -6,6 +6,7 @@ import { useCallback, type ReactNode, } from "react"; +import { invoke } from "@tauri-apps/api/core"; import { getSettings, updateSettings } from "../services/notes"; import type { ThemeSettings, @@ -270,6 +271,10 @@ export function ThemeProvider({ children }: ThemeProviderProps) { } else { root.classList.remove("dark"); } + + invoke("set_title_bar_theme", { isDark: resolvedTheme === "dark" }).catch( + () => {}, + ); }, [resolvedTheme]); // Save theme mode to backend From a57ec5446f9178ecaee9cea11f1512b24dd3f137 Mon Sep 17 00:00:00 2001 From: chengcheng84 Date: Mon, 6 Apr 2026 09:48:36 +0800 Subject: [PATCH 2/3] fix: review --- src-tauri/src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 11bd1d7..c15aa00 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3702,11 +3702,6 @@ pub fn run() { }; if let Some(main_window) = app.get_webview_window("main") { - #[cfg(target_os = "windows")] - { - windows_title_bar::apply_title_bar_theme(&main_window, false); - } - let has_notes_folder = app .state::() .app_config @@ -3893,11 +3888,16 @@ mod windows_title_bar { fn set_title_bar_theme(app: AppHandle, is_dark: bool) -> Result<(), String> { #[cfg(target_os = "windows")] { - if let Some(window) = app.get_webview_window("main") { - windows_title_bar::apply_title_bar_theme(&window, is_dark); + for (label, window) in app.webview_windows() { + if label == "main" || label.starts_with("preview-") { + windows_title_bar::apply_title_bar_theme(&window, is_dark); + } } } - let _ = app; - let _ = is_dark; + #[cfg(not(target_os = "windows"))] + { + let _ = app; + let _ = is_dark; + } Ok(()) } From 469aa7defcc3d22a4c8bacff3c8896287770bb82 Mon Sep 17 00:00:00 2001 From: erictli Date: Mon, 4 May 2026 21:05:08 -0400 Subject: [PATCH 3/3] fix: title bar follows custom theme colors and correct COLORREF byte order The hardcoded caption color literals were encoded as 0x00RRGGBB but DwmSetWindowAttribute expects COLORREF (0x00BBGGRR), so the title bar was a few bits off from --color-bg-secondary. With #134 letting users customize theme colors, the gap also widened beyond the defaults. Pass the resolved bg-secondary color from the frontend as an RGB triple and build the COLORREF correctly in Rust. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/src/lib.rs | 63 ++++++++++++++++-------------------- src/context/ThemeContext.tsx | 32 +++++++++++++++--- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c15aa00..3fc4553 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3836,17 +3836,21 @@ mod windows_title_bar { } } - pub fn apply_title_bar_theme(window: &WebviewWindow, is_dark: bool) { + pub fn apply_title_bar_theme(window: &WebviewWindow, is_dark: bool, rgb: (u8, u8, u8)) { let Ok(hwnd) = window.hwnd() else { return; }; let hwnd = hwnd.0 as isize; + // Windows COLORREF is little-endian 0x00BBGGRR + let (r, g, b) = rgb; + let caption_color: u32 = + ((b as u32) << 16) | ((g as u32) << 8) | (r as u32); + unsafe { - let set_attr = - |attr: u32, value: *const std::ffi::c_void, size: u32| { - let _ = dwm::DwmSetWindowAttribute(hwnd, attr, value, size); - }; + let set_attr = |attr: u32, value: *const std::ffi::c_void, size: u32| { + let _ = dwm::DwmSetWindowAttribute(hwnd, attr, value, size); + }; let dark_mode: i32 = if is_dark { 1 } else { 0 }; set_attr( @@ -3854,50 +3858,39 @@ mod windows_title_bar { &dark_mode as *const _ as *const std::ffi::c_void, std::mem::size_of::() as u32, ); - - if is_dark { - let caption_color: u32 = 0x00_0E_0C_0B; - set_attr( - dwm::DWMWA_CAPTION_COLOR, - &caption_color as *const _ as *const std::ffi::c_void, - std::mem::size_of::() as u32, - ); - set_attr( - dwm::DWMWA_BORDER_COLOR, - &caption_color as *const _ as *const std::ffi::c_void, - std::mem::size_of::() as u32, - ); - } else { - let caption_color: u32 = 0x00_FA_FA_F9; - set_attr( - dwm::DWMWA_CAPTION_COLOR, - &caption_color as *const _ as *const std::ffi::c_void, - std::mem::size_of::() as u32, - ); - set_attr( - dwm::DWMWA_BORDER_COLOR, - &caption_color as *const _ as *const std::ffi::c_void, - std::mem::size_of::() as u32, - ); - } + set_attr( + dwm::DWMWA_CAPTION_COLOR, + &caption_color as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); + set_attr( + dwm::DWMWA_BORDER_COLOR, + &caption_color as *const _ as *const std::ffi::c_void, + std::mem::size_of::() as u32, + ); } } } #[tauri::command] -fn set_title_bar_theme(app: AppHandle, is_dark: bool) -> Result<(), String> { +fn set_title_bar_theme( + app: AppHandle, + is_dark: bool, + r: u8, + g: u8, + b: u8, +) -> Result<(), String> { #[cfg(target_os = "windows")] { for (label, window) in app.webview_windows() { if label == "main" || label.starts_with("preview-") { - windows_title_bar::apply_title_bar_theme(&window, is_dark); + windows_title_bar::apply_title_bar_theme(&window, is_dark, (r, g, b)); } } } #[cfg(not(target_os = "windows"))] { - let _ = app; - let _ = is_dark; + let _ = (app, is_dark, r, g, b); } Ok(()) } diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index c2c065c..f6b3472 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -76,6 +76,21 @@ const defaultThemeColors: Record<"light" | "dark", Record export { defaultThemeColors }; +// Normalize any CSS color string (hex, rgb(), rgba(), hsl(), named) to an RGB +// triple by letting the browser parse it via getComputedStyle. +function parseCssColorToRgb(value: string): [number, number, number] | null { + if (typeof document === "undefined") return null; + const probe = document.createElement("div"); + probe.style.color = value; + probe.style.display = "none"; + document.body.appendChild(probe); + const computed = getComputedStyle(probe).color; + document.body.removeChild(probe); + const match = computed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); + if (!match) return null; + return [Number(match[1]), Number(match[2]), Number(match[3])]; +} + interface ThemeContextType { theme: ThemeMode; resolvedTheme: "light" | "dark"; @@ -271,10 +286,6 @@ export function ThemeProvider({ children }: ThemeProviderProps) { } else { root.classList.remove("dark"); } - - invoke("set_title_bar_theme", { isDark: resolvedTheme === "dark" }).catch( - () => {}, - ); }, [resolvedTheme]); // Save theme mode to backend @@ -463,6 +474,19 @@ export function ThemeProvider({ children }: ThemeProviderProps) { const value = activeColors[key] ?? defaults[key]; root.style.setProperty(`--color-${key}`, value); } + + // Sync the Windows title bar to match bg-secondary (no-op on other OSes). + const captionColor = + activeColors["bg-secondary"] ?? defaults["bg-secondary"]; + const rgb = parseCssColorToRgb(captionColor); + if (rgb) { + invoke("set_title_bar_theme", { + isDark: resolvedTheme === "dark", + r: rgb[0], + g: rgb[1], + b: rgb[2], + }).catch(() => {}); + } }, [resolvedTheme, customColorsLight, customColorsDark]); // Set a single custom color for a given mode