Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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::<i32>() as u32,
);
set_attr(
dwm::DWMWA_CAPTION_COLOR,
&caption_color as *const _ as *const std::ffi::c_void,
std::mem::size_of::<u32>() as u32,
);
set_attr(
dwm::DWMWA_BORDER_COLOR,
&caption_color as *const _ as *const std::ffi::c_void,
std::mem::size_of::<u32>() 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(())
}
29 changes: 29 additions & 0 deletions src/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -75,6 +76,21 @@ const defaultThemeColors: Record<"light" | "dark", Record<ThemeColorKey, string>

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";
Expand Down Expand Up @@ -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
Expand Down
Loading