feat(window-settings): add TitlebarMode enum and titlebar_mode column

Adds a 4-value mode enum (Qbz | System | Plasma | Hidden) to the existing
WindowSettings store, with column migration that infers mode from the
legacy use_system_titlebar flag. Setter is write-through so the legacy
column stays consistent for any remaining readers.

cc
This commit is contained in:
vicrodh committed 2026-05-03 14:44:41 -06:00
1 parent 773fa390eb
commit 045236f716
1 file changed
+126 -2
+126 -2
View File
@@ -9,9 +9,44 @@ use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::{Arc, Mutex};
/// Titlebar rendering mode. Controls which decoration source is active and
/// how the custom titlebar component renders (or whether it renders at all).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TitlebarMode {
/// Default — full custom titlebar with drag, resize, dblclick, controls.
Qbz,
/// OS-native chrome (CSD on Wayland, KWin SSD on X11, NSWindow on macOS).
System,
/// KDE-only — KWin SSD via Xwayland + stripped custom strip below for search+nav.
Plasma,
/// No titlebar — for tiling WMs (i3/Hyprland/sway/niri).
Hidden,
}
impl TitlebarMode {
/// Whether this mode requests OS decorations (decorations=true at window creation).
pub fn wants_decorations(self) -> bool {
matches!(self, TitlebarMode::System | TitlebarMode::Plasma)
}
/// Whether this mode requires GDK_BACKEND=x11 to coerce SSD on Wayland.
pub fn wants_xwayland(self) -> bool {
matches!(self, TitlebarMode::Plasma)
}
}
impl Default for TitlebarMode {
fn default() -> Self {
TitlebarMode::Qbz
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowSettings {
/// Use OS native window decorations instead of custom title bar
/// Use OS native window decorations (legacy — superseded by `titlebar_mode`).
/// Kept as a write-through shadow of `titlebar_mode in {System, Plasma}` for
/// any consumers that still read it; will be removed in a follow-up.
pub use_system_titlebar: bool,
/// Last saved window width (logical pixels, non-maximized)
pub window_width: f64,
@@ -23,6 +58,8 @@ pub struct WindowSettings {
/// Implies building the window transparent on Linux so the corners
/// can be seen through to the desktop. Requires restart to take effect.
pub match_system_window_chrome: bool,
/// Authoritative titlebar mode (replaces `use_system_titlebar`).
pub titlebar_mode: TitlebarMode,
}
impl Default for WindowSettings {
@@ -33,6 +70,7 @@ impl Default for WindowSettings {
window_height: 800.0,
is_maximized: false,
match_system_window_chrome: false,
titlebar_mode: TitlebarMode::Qbz,
}
}
}
@@ -86,6 +124,26 @@ impl WindowSettingsStore {
[],
);
// Add titlebar_mode column. Defaults to NULL so we can detect
// pre-existing rows and migrate from use_system_titlebar.
let _ = conn.execute(
"ALTER TABLE window_settings ADD COLUMN titlebar_mode TEXT",
[],
);
// One-time migration: if titlebar_mode is NULL but use_system_titlebar
// exists, derive mode. This runs at every open but only does work on
// rows where titlebar_mode is still NULL (idempotent).
let _ = conn.execute(
"UPDATE window_settings
SET titlebar_mode = CASE
WHEN use_system_titlebar = 1 THEN 'system'
ELSE 'qbz'
END
WHERE titlebar_mode IS NULL AND id = 1",
[],
);
info!("[WindowSettings] Database initialized");
Ok(Self { conn })
@@ -102,11 +160,32 @@ impl WindowSettingsStore {
Self::open_at(base_dir, "window_settings.db")
}
/// Lightweight read-only open for startup (before Tauri state).
/// Opens existing DB without creating tables/running migrations.
pub fn new_readonly() -> Result<Self, String> {
let db_path = dirs::data_dir()
.ok_or("Could not determine data directory")?
.join("qbz")
.join("window_settings.db");
if !db_path.exists() {
return Err("Window settings DB does not exist yet".to_string());
}
let conn = Connection::open_with_flags(
&db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| format!("Failed to open window settings database (readonly): {}", e))?;
Ok(Self { conn })
}
pub fn get_settings(&self) -> Result<WindowSettings, String> {
self.conn
.query_row(
"SELECT use_system_titlebar, window_width, window_height, is_maximized,
match_system_window_chrome
match_system_window_chrome, titlebar_mode
FROM window_settings WHERE id = 1",
[],
|row| {
@@ -115,6 +194,13 @@ impl WindowSettingsStore {
let window_height: f64 = row.get(2)?;
let is_maximized: i32 = row.get(3)?;
let match_chrome: i32 = row.get(4)?;
let mode_str: Option<String> = row.get(5)?;
let titlebar_mode = match mode_str.as_deref() {
Some("system") => TitlebarMode::System,
Some("plasma") => TitlebarMode::Plasma,
Some("hidden") => TitlebarMode::Hidden,
_ => TitlebarMode::Qbz,
};
let defaults = WindowSettings::default();
let (w, h) = if is_valid_window_size(window_width, window_height) {
(window_width, window_height)
@@ -132,6 +218,7 @@ impl WindowSettingsStore {
window_height: h,
is_maximized: is_maximized != 0,
match_system_window_chrome: match_chrome != 0,
titlebar_mode,
})
},
)
@@ -158,6 +245,26 @@ impl WindowSettingsStore {
Ok(())
}
pub fn set_titlebar_mode(&self, mode: TitlebarMode) -> Result<(), String> {
let mode_str = match mode {
TitlebarMode::Qbz => "qbz",
TitlebarMode::System => "system",
TitlebarMode::Plasma => "plasma",
TitlebarMode::Hidden => "hidden",
};
let use_system = mode.wants_decorations() as i64;
self.conn
.execute(
"UPDATE window_settings
SET titlebar_mode = ?1, use_system_titlebar = ?2
WHERE id = 1",
params![mode_str, use_system],
)
.map_err(|e| format!("Failed to set titlebar_mode: {}", e))?;
info!("Window titlebar_mode set to {}", mode_str);
Ok(())
}
/// Save the non-maximized window dimensions (called on resize while not maximized).
pub fn set_window_size(&self, width: f64, height: f64) -> Result<(), String> {
if !is_valid_window_size(width, height) {
@@ -262,6 +369,23 @@ impl WindowSettingsState {
.ok_or("Window settings store not initialized")?;
store.set_match_system_window_chrome(value)
}
pub fn set_titlebar_mode(&self, mode: TitlebarMode) -> Result<(), String> {
let store = self.store.lock().map_err(|e| format!("lock poisoned: {}", e))?;
store
.as_ref()
.ok_or_else(|| "Window settings store not initialized".to_string())?
.set_titlebar_mode(mode)
}
pub fn get_titlebar_mode(&self) -> Result<TitlebarMode, String> {
let store = self.store.lock().map_err(|e| format!("lock poisoned: {}", e))?;
let settings = store
.as_ref()
.ok_or_else(|| "Window settings store not initialized".to_string())?
.get_settings()?;
Ok(settings.titlebar_mode)
}
}
/// Check that window dimensions are within a sane range.