diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 64a8090..3fc4553 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3792,6 +3792,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 +3815,82 @@ 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, 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 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, + ); + 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, + 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, (r, g, b)); + } + } + } + #[cfg(not(target_os = "windows"))] + { + let _ = (app, is_dark, r, g, b); + } + Ok(()) +} diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index ee48436..f6b3472 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, @@ -75,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"; @@ -458,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