feat(appearance): phase 2 of match-system-window-chrome (transparent)
Rounded-corner window chrome now actually works. The toggle in
Settings > Appearance persists to the window_settings SQLite store and
gates the Tauri window transparency decision at startup, so on next
launch the main window is rebuilt with `transparent: true` and the
border-radius plus edge outline apply without painting white corners.
Flow:
* Toggle in Settings > Appearance -> windowChromeStore saves to
localStorage and calls v2_set_match_system_window_chrome which
writes match_system_window_chrome to window_settings.db.
* Toast reminds the user to restart.
* On next launch, should_use_main_window_transparency() reads the
setting and returns true, Tauri builds the window transparent.
* Setup stores the decision in MAIN_WINDOW_TRANSPARENT. A new command
v2_main_window_is_transparent reports it back to the frontend.
* +page.svelte only applies .app.match-chrome when the window is
actually transparent AND the custom titlebar is visible, so
stale-state toggling never produces white wedges.
* CSS sets html/body transparent via a global class on <html>, then
.app.match-chrome.floating paints the rounded corners plus a
12/28 rgba drop shadow and a 1px darkening outline.
* Maximized state clears the radius automatically.
Env vars QBZ_FORCE_TRANSPARENT_WINDOWS / QBZ_FORCE_OPAQUE_WINDOWS still
take precedence over the DB-persisted toggle.
cc
This commit is contained in:
1 parent
8502c2fdb3
commit
57e63e3ef5
8 files changed
+158
-6
No files matched your search
@@ -29,6 +29,22 @@ pub fn v2_set_use_system_titlebar(
|
||||
state.set_use_system_titlebar(value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_set_match_system_window_chrome(
|
||||
value: bool,
|
||||
state: State<'_, WindowSettingsState>,
|
||||
) -> Result<(), String> {
|
||||
state.set_match_system_window_chrome(value)
|
||||
}
|
||||
|
||||
/// Report whether the main window was built transparent for this session.
|
||||
/// The frontend uses this to decide whether to apply the rounded-corner
|
||||
/// CSS (which only looks right on a transparent window).
|
||||
#[tauri::command]
|
||||
pub fn v2_main_window_is_transparent() -> bool {
|
||||
crate::main_window_built_transparent()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_set_enable_tray(value: bool, state: State<'_, TraySettingsState>) -> Result<(), String> {
|
||||
state.set_enable_tray(value)?;
|
||||
|
||||
@@ -19,6 +19,10 @@ pub struct WindowSettings {
|
||||
pub window_height: f64,
|
||||
/// Whether the window was maximized when last closed
|
||||
pub is_maximized: bool,
|
||||
/// Match the active desktop's window chrome (rounded corners, edge).
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Default for WindowSettings {
|
||||
@@ -28,6 +32,7 @@ impl Default for WindowSettings {
|
||||
window_width: 1280.0,
|
||||
window_height: 800.0,
|
||||
is_maximized: false,
|
||||
match_system_window_chrome: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +81,10 @@ impl WindowSettingsStore {
|
||||
"ALTER TABLE window_settings ADD COLUMN is_maximized INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE window_settings ADD COLUMN match_system_window_chrome INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
|
||||
info!("[WindowSettings] Database initialized");
|
||||
|
||||
@@ -96,7 +105,8 @@ impl WindowSettingsStore {
|
||||
pub fn get_settings(&self) -> Result<WindowSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT use_system_titlebar, window_width, window_height, is_maximized
|
||||
"SELECT use_system_titlebar, window_width, window_height, is_maximized,
|
||||
match_system_window_chrome
|
||||
FROM window_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
@@ -104,6 +114,7 @@ impl WindowSettingsStore {
|
||||
let window_width: f64 = row.get(1)?;
|
||||
let window_height: f64 = row.get(2)?;
|
||||
let is_maximized: i32 = row.get(3)?;
|
||||
let match_chrome: i32 = row.get(4)?;
|
||||
let defaults = WindowSettings::default();
|
||||
let (w, h) = if is_valid_window_size(window_width, window_height) {
|
||||
(window_width, window_height)
|
||||
@@ -120,12 +131,23 @@ impl WindowSettingsStore {
|
||||
window_width: w,
|
||||
window_height: h,
|
||||
is_maximized: is_maximized != 0,
|
||||
match_system_window_chrome: match_chrome != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Failed to get window settings: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_match_system_window_chrome(&self, value: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE window_settings SET match_system_window_chrome = ?1 WHERE id = 1",
|
||||
params![if value { 1 } else { 0 }],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set match_system_window_chrome: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_use_system_titlebar(&self, value: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
@@ -229,6 +251,17 @@ impl WindowSettingsState {
|
||||
.ok_or("Window settings store not initialized")?;
|
||||
store.set_is_maximized(value)
|
||||
}
|
||||
|
||||
pub fn set_match_system_window_chrome(&self, value: bool) -> Result<(), String> {
|
||||
let guard = self
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock window settings store".to_string())?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or("Window settings store not initialized")?;
|
||||
store.set_match_system_window_chrome(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that window dimensions are within a sane range.
|
||||
@@ -261,3 +294,15 @@ pub fn set_use_system_titlebar(
|
||||
info!("[WindowSettings] Setting use_system_titlebar to {}", value);
|
||||
state.set_use_system_titlebar(value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_match_system_window_chrome(
|
||||
value: bool,
|
||||
state: tauri::State<WindowSettingsState>,
|
||||
) -> Result<(), String> {
|
||||
info!(
|
||||
"[WindowSettings] Setting match_system_window_chrome to {} (restart required)",
|
||||
value
|
||||
);
|
||||
state.set_match_system_window_chrome(value)
|
||||
}
|
||||
@@ -13,6 +13,16 @@ pub mod tauri_adapter;
|
||||
|
||||
pub mod auto_theme;
|
||||
pub mod desktop_theme;
|
||||
|
||||
/// Captures whether the main Tauri window was built transparent for this
|
||||
/// session. Written once during setup, read by a Tauri command so the
|
||||
/// frontend can decide whether to paint the matching border-radius CSS.
|
||||
static MAIN_WINDOW_TRANSPARENT: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub fn main_window_built_transparent() -> bool {
|
||||
MAIN_WINDOW_TRANSPARENT.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod autoconfig_graphics;
|
||||
|
||||
@@ -420,6 +430,17 @@ fn should_use_main_window_transparency() -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Opt-in via persisted settings: "match system window chrome" wants
|
||||
// rounded corners, which need a transparent window so the webview
|
||||
// background stops painting white outside the radius.
|
||||
if let Ok(store) = config::window_settings::WindowSettingsStore::new() {
|
||||
if let Ok(settings) = store.get_settings() {
|
||||
if settings.match_system_window_chrome {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stability-first default on Linux.
|
||||
false
|
||||
}
|
||||
@@ -776,6 +797,7 @@ pub fn run() {
|
||||
use_system_titlebar
|
||||
);
|
||||
let main_window_transparent = should_use_main_window_transparency();
|
||||
MAIN_WINDOW_TRANSPARENT.store(main_window_transparent, std::sync::atomic::Ordering::SeqCst);
|
||||
log::info!(
|
||||
"Main window transparency: {} (override with QBZ_FORCE_TRANSPARENT_WINDOWS=1 or QBZ_FORCE_OPAQUE_WINDOWS=1)",
|
||||
main_window_transparent
|
||||
@@ -1310,6 +1332,8 @@ pub fn run() {
|
||||
config::favorites_cache::is_track_favorite,
|
||||
commands_v2::v2_set_api_locale,
|
||||
commands_v2::v2_set_use_system_titlebar,
|
||||
commands_v2::v2_set_match_system_window_chrome,
|
||||
commands_v2::v2_main_window_is_transparent,
|
||||
commands_v2::v2_set_enable_tray,
|
||||
commands_v2::v2_set_minimize_to_tray,
|
||||
commands_v2::v2_set_close_to_tray,
|
||||
|
||||
@@ -4492,7 +4492,10 @@
|
||||
</div>
|
||||
<Toggle
|
||||
enabled={matchSystemWindowChromeState}
|
||||
onchange={(v) => setMatchSystemWindowChrome(v)}
|
||||
onchange={(v) => {
|
||||
setMatchSystemWindowChrome(v);
|
||||
showToast($t('settings.appearance.matchSystemChromeRestart'), 'info');
|
||||
}}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1009,7 +1009,8 @@
|
||||
"windowControlsColorPresetKlassy": "Klassy (auto-detect)",
|
||||
"windowControlsColorPresetPlasma": "Plasma (auto-detect)",
|
||||
"matchSystemChrome": "Match system window chrome",
|
||||
"matchSystemChromeDesc": "Round the window corners and add a subtle edge so QBZ blends with your desktop theme. Requires the custom title bar.",
|
||||
"matchSystemChromeDesc": "Round the window corners and add a subtle edge so QBZ blends with your desktop theme. Takes effect after restarting the app (the window is rebuilt transparent so the corners don't show white).",
|
||||
"matchSystemChromeRestart": "Restart QBZ to apply the window chrome change.",
|
||||
"windowControlsColorPresetCustom": "Custom",
|
||||
"windowControlsCustomColors": "Custom colors",
|
||||
"windowControlsClose": "Close",
|
||||
|
||||
@@ -1009,7 +1009,8 @@
|
||||
"windowControlsColorPresetKlassy": "Klassy (auto-detectar)",
|
||||
"windowControlsColorPresetPlasma": "Plasma (auto-detectar)",
|
||||
"matchSystemChrome": "Integrar con el tema del sistema",
|
||||
"matchSystemChromeDesc": "Redondea las esquinas de la ventana y agrega un borde sutil para que QBZ se integre con el tema del escritorio. Requiere la barra de título personalizada.",
|
||||
"matchSystemChromeDesc": "Redondea las esquinas de la ventana y agrega un borde sutil para que QBZ se integre con el tema del escritorio. Requiere reiniciar la app (la ventana se reconstruye transparente para que las esquinas no se vean blancas).",
|
||||
"matchSystemChromeRestart": "Reinicia QBZ para aplicar el cambio del chrome.",
|
||||
"windowControlsColorPresetCustom": "Personalizado",
|
||||
"windowControlsCustomColors": "Colores personalizados",
|
||||
"windowControlsClose": "Cerrar",
|
||||
|
||||
@@ -46,9 +46,38 @@ export function setMatchSystemWindowChrome(value: boolean): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, String(value));
|
||||
} catch {}
|
||||
// Persist on the Rust side too so `should_use_main_window_transparency`
|
||||
// can read it BEFORE window creation on next launch. Failure here is
|
||||
// non-fatal — localStorage covers the in-session state.
|
||||
void (async () => {
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
await invoke('v2_set_match_system_window_chrome', { value });
|
||||
} catch (e) {
|
||||
console.warn('[windowChrome] Failed to persist to Rust:', e);
|
||||
}
|
||||
})();
|
||||
notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hint from the Rust side: whether the Tauri window was actually built
|
||||
* transparent for this session. The frontend uses this to decide whether
|
||||
* to apply the border-radius (otherwise the webview paints the area
|
||||
* outside the radius white). Populated by the root layout at startup.
|
||||
*/
|
||||
let windowIsTransparent = false;
|
||||
|
||||
export function setWindowIsTransparent(value: boolean): void {
|
||||
if (windowIsTransparent === value) return;
|
||||
windowIsTransparent = value;
|
||||
notify();
|
||||
}
|
||||
|
||||
export function getWindowIsTransparent(): boolean {
|
||||
return windowIsTransparent;
|
||||
}
|
||||
|
||||
export function getCornerRadiusPx(): number {
|
||||
return cornerRadiusPx;
|
||||
}
|
||||
|
||||
+35
-2
@@ -111,6 +111,8 @@
|
||||
getMatchSystemWindowChrome,
|
||||
getCornerRadiusPx,
|
||||
setCornerRadiusPx,
|
||||
setWindowIsTransparent,
|
||||
getWindowIsTransparent,
|
||||
} from '$lib/stores/windowChromeStore';
|
||||
import { detectDesktopThemeCached } from '$lib/stores/windowControlsStore';
|
||||
|
||||
@@ -501,6 +503,7 @@
|
||||
let showTitleBar = $state(shouldShowTitleBar());
|
||||
let matchSystemChrome = $state(getMatchSystemWindowChrome());
|
||||
let chromeRadiusPx = $state(getCornerRadiusPx());
|
||||
let windowTransparent = $state(getWindowIsTransparent());
|
||||
let showWindowControls = $state(getShowWindowControls());
|
||||
|
||||
// Search Bar Location State
|
||||
@@ -4218,11 +4221,20 @@
|
||||
sidebarExpanded = getIsExpanded();
|
||||
});
|
||||
|
||||
// Declared upfront because subscribeTitleBar invokes the callback
|
||||
// immediately on subscription, which references this helper.
|
||||
const applyChromeClass = () => {
|
||||
if (typeof document === 'undefined') return;
|
||||
const active = matchSystemChrome && showTitleBar && windowTransparent;
|
||||
document.documentElement.classList.toggle('match-chrome-transparent', active);
|
||||
};
|
||||
|
||||
// Initialize and subscribe to title bar state changes
|
||||
initTitleBarStore();
|
||||
const unsubscribeTitleBar = subscribeTitleBar(() => {
|
||||
showTitleBar = shouldShowTitleBar();
|
||||
showWindowControls = getShowWindowControls();
|
||||
applyChromeClass();
|
||||
});
|
||||
|
||||
// Window chrome: subscribe to the "match system" toggle and fetch the
|
||||
@@ -4232,7 +4244,21 @@
|
||||
const unsubscribeWindowChrome = subscribeWindowChrome(() => {
|
||||
matchSystemChrome = getMatchSystemWindowChrome();
|
||||
chromeRadiusPx = getCornerRadiusPx();
|
||||
windowTransparent = getWindowIsTransparent();
|
||||
applyChromeClass();
|
||||
});
|
||||
// Ask the Rust side whether the main window was actually built
|
||||
// transparent. If not, the radius CSS is a no-op (white corners
|
||||
// otherwise). The backend captured the decision at setup time.
|
||||
void (async () => {
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const transparent = (await invoke('v2_main_window_is_transparent')) as boolean;
|
||||
setWindowIsTransparent(transparent);
|
||||
} catch (e) {
|
||||
console.warn('[windowChrome] could not query transparency:', e);
|
||||
}
|
||||
})();
|
||||
void detectDesktopThemeCached().then((info) => {
|
||||
if (info && typeof info.windowCornerRadiusPx === 'number') {
|
||||
setCornerRadiusPx(info.windowCornerRadiusPx);
|
||||
@@ -5011,7 +5037,7 @@
|
||||
class="app"
|
||||
class:no-titlebar={!showTitleBar}
|
||||
class:floating={isWindowFloating}
|
||||
class:match-chrome={matchSystemChrome && showTitleBar}
|
||||
class:match-chrome={matchSystemChrome && showTitleBar && windowTransparent}
|
||||
style="--chrome-radius: {chromeRadiusPx}px;"
|
||||
>
|
||||
<!-- macOS: drag region for window movement (overlay title bar has no native drag area) -->
|
||||
@@ -6195,7 +6221,14 @@
|
||||
/* Match system window chrome (Plasma / GNOME): apply the detected
|
||||
decoration radius and a thin edge outline so the window reads as
|
||||
its own surface against the desktop. Only takes effect in floating
|
||||
state (not maximized) and when the custom title bar is active. */
|
||||
state (not maximized) and when the custom title bar is active.
|
||||
Requires the Tauri window to have been built transparent (Phase 2:
|
||||
match_system_window_chrome persists to window_settings and gates the
|
||||
transparency path at startup). */
|
||||
:global(html.match-chrome-transparent),
|
||||
:global(html.match-chrome-transparent body) {
|
||||
background: transparent !important;
|
||||
}
|
||||
.app.match-chrome.floating {
|
||||
border-radius: var(--chrome-radius, 10px);
|
||||
box-shadow:
|
||||
|
||||
Reference in new issue
Block a user