diff --git a/crates/qbz-ui/ui/app.slint b/crates/qbz-ui/ui/app.slint index a86c049c6..f3ac4bf35 100644 --- a/crates/qbz-ui/ui/app.slint +++ b/crates/qbz-ui/ui/app.slint @@ -59,9 +59,11 @@ export component AppWindow inherits Window { // winit attributes hook instead. `no-frame` is the authoritative switch // (the adapter re-applies it at every realization — same lesson as the // miniplayer, see MiniPlayerWindow below); the persisted pref is seeded - // into AppearanceState BEFORE the first show, and changing it takes an - // app restart (Wayland negotiates decorations at surface creation). - no-frame: !AppearanceState.is-macos && !AppearanceState.use-system-title-bar; + // into `system-title-bar-active` BEFORE the first show; the settings + // toggle edits the pref (`use-system-title-bar`) and Rust decides per + // platform whether it reaches this applied state live (Linux) or only + // on the next launch (macOS — attributes fixed at surface creation). + no-frame: !AppearanceState.is-macos && !AppearanceState.system-title-bar-active; // Edge-resize for the frameless mode (the winit backend only engages it // on undecorated windows, so this is inert under system chrome). resize-border-width: 6px; diff --git a/crates/qbz-ui/ui/settings/AppearanceSettings.slint b/crates/qbz-ui/ui/settings/AppearanceSettings.slint index 0a7e669fa..948cdda63 100644 --- a/crates/qbz-ui/ui/settings/AppearanceSettings.slint +++ b/crates/qbz-ui/ui/settings/AppearanceSettings.slint @@ -693,7 +693,11 @@ export component AppearanceSettings inherits VerticalLayout { } } } - SettingRow { + // Tiling-WM row: Linux-only — on macOS the native traffic lights are + // always present (overlay or system bar), so a "hide completely" + // frameless mode does not exist there (same gating idiom as the + // WINDOW CONTROLS rows below). + if !AppearanceState.is-macos: SettingRow { label: @tr("Hide title bar"); description: @tr("Frameless window without window controls or header drag (for tiling window manager users)"); enabled: !AppearanceState.use-system-title-bar; diff --git a/crates/qbz-ui/ui/shell/HeaderBar.slint b/crates/qbz-ui/ui/shell/HeaderBar.slint index 94ed6aa2f..825163454 100644 --- a/crates/qbz-ui/ui/shell/HeaderBar.slint +++ b/crates/qbz-ui/ui/shell/HeaderBar.slint @@ -582,7 +582,7 @@ export component HeaderBar inherits Rectangle { // the native traffic lights (overlay attributes from the winit hook) and // only needs the drag surface + a left inset under the lights. property chrome-drag-enabled: - !AppearanceState.use-system-title-bar && !AppearanceState.hide-title-bar; + !AppearanceState.system-title-bar-active && !AppearanceState.hide-title-bar; property chrome-controls: root.chrome-drag-enabled && !AppearanceState.is-macos && AppearanceState.show-window-controls; @@ -592,7 +592,7 @@ export component HeaderBar inherits Rectangle { // Left inset: the native traffic-light band on macOS (~70px logical; 78px // keeps a comfortable gap) or the drawn cluster when positioned left. property chrome-left-inset: - AppearanceState.is-macos && !AppearanceState.use-system-title-bar ? 78px + AppearanceState.is-macos && !AppearanceState.system-title-bar-active ? 78px : (root.chrome-controls && root.wc-on-left ? root.wc-cluster-width + 6px : 0px); // Custom-chrome drag surface. Declared FIRST among the header's elements @@ -938,7 +938,7 @@ export component HeaderBar inherits Rectangle { if AppearanceState.show-purchases && !OfflineState.offline && AppearanceState.nav-tb-purchases && !AppearanceState.hide-title-bar - && !AppearanceState.use-system-title-bar: VerticalLayout { + && !AppearanceState.system-title-bar-active: VerticalLayout { alignment: center; NavTab { icon: @image-url("../assets/icons/shopping-bag.svg"); @@ -1044,7 +1044,7 @@ export component HeaderBar inherits Rectangle { if AppearanceState.show-purchases && !OfflineState.offline && AppearanceState.nav-tb-purchases && !AppearanceState.hide-title-bar - && !AppearanceState.use-system-title-bar: VerticalLayout { + && !AppearanceState.system-title-bar-active: VerticalLayout { alignment: center; CompactDirectBtn { icon: @image-url("../assets/icons/shopping-bag.svg"); diff --git a/crates/qbz-ui/ui/shell/Sidebar.slint b/crates/qbz-ui/ui/shell/Sidebar.slint index 610dccf89..c5108b0f5 100644 --- a/crates/qbz-ui/ui/shell/Sidebar.slint +++ b/crates/qbz-ui/ui/shell/Sidebar.slint @@ -782,7 +782,7 @@ export component Sidebar inherits Rectangle { if AppearanceState.show-purchases && !OfflineState.offline && !(AppearanceState.nav-tb-purchases && !AppearanceState.hide-title-bar - && !AppearanceState.use-system-title-bar): SidebarDirectRow { + && !AppearanceState.system-title-bar-active): SidebarDirectRow { icon: @image-url("../assets/icons/shopping-bag.svg"); label: @tr("Purchases"); active: NavState.view == ContentView.purchases diff --git a/crates/qbz-ui/ui/state.slint b/crates/qbz-ui/ui/state.slint index 5f6a32e36..ff4a5ac8d 100644 --- a/crates/qbz-ui/ui/state.slint +++ b/crates/qbz-ui/ui/state.slint @@ -3002,6 +3002,14 @@ export global AppearanceState { // macOS = overlay (native traffic lights over the header). The .slint // default is the safe Linux one. in-out property use-system-title-bar: true; + // APPLIED chrome state (what the window was actually created with, vs the + // persisted pref above which the settings toggle edits). All chrome + // bindings (no-frame, header drag/inset/controls, Purchases placement) + // read THIS one. Rust decides per platform how the pref propagates: + // Linux mirrors the pref here live (hot apply, today's behavior); macOS + // leaves it at the startup value (the overlay attributes are fixed at + // window creation — restart to apply). + in-out property system-title-bar-active: true; // Tiling-WM variant: frameless without drawn controls / header drag. in-out property hide-title-bar: false; // Phase 2 (deferred): rounded corners matching the desktop decoration. diff --git a/crates/qbz/src/main.rs b/crates/qbz/src/main.rs index 73769469b..4b9cd5107 100644 --- a/crates/qbz/src/main.rs +++ b/crates/qbz/src/main.rs @@ -6675,6 +6675,13 @@ fn main() -> Result<(), Box> { // creation (decorations negotiate then on Wayland), and the macOS // attributes hook reads the same pref straight from ui_prefs. appearance.set_use_system_title_bar(prefs.use_system_title_bar); + // Applied chrome state — what this window is actually created with. + // The chrome bindings (no-frame, header drag/inset, Purchases + // placement) read THIS; the settings toggle edits the pref above. + // On Linux the appearance-bool handler mirrors pref -> active live; + // on macOS it does not (overlay attributes are fixed at creation), + // so this seed is the value for the whole session there. + appearance.set_system_title_bar_active(prefs.use_system_title_bar); appearance.set_hide_title_bar(prefs.hide_title_bar); appearance.set_show_window_controls(prefs.show_window_controls); appearance.set_wc_position_index(if prefs.wc_position == "left" { 0 } else { 1 }); @@ -8938,12 +8945,29 @@ fn main() -> Result<(), Box> { let chrome_weak = window.as_weak(); appearance.on_appearance_bool(move |key, value| match key.as_str() { "use-system-title-bar" => { - // Startup-time choice: `AppWindow.no-frame` / the macOS overlay - // attributes are fixed at surface creation (Wayland negotiates - // decorations then), so this applies on the next launch. + // The toggle only edits the PREF (`use-system-title-bar`); + // whether it reaches the applied chrome state + // (`system-title-bar-active`, which no-frame / the header + // drag+inset / Purchases placement read) is decided HERE, + // per platform. let mut prefs = crate::ui_prefs::load(); prefs.use_system_title_bar = value; crate::ui_prefs::save(&prefs); + // Linux: mirror live (today's hot path — no-frame drives + // winit set_decorations on the next properties update, and + // the drawn controls/drag follow). + #[cfg(not(target_os = "macos"))] + if let Some(w) = chrome_weak.upgrade() { + w.global::() + .set_system_title_bar_active(value); + } + // macOS: persist-only, restart to apply. The overlay + // attributes (titlebar_transparent / fullsize_content_view) + // are fixed at window creation; flipping the header bindings + // live desyncs them from the real chrome (traffic lights + // overlapped by content, or system bar + overlay inset at + // once), so `system-title-bar-active` keeps its startup + // value there. crate::toast::info_weak( &chrome_weak, qbz_i18n::t("Title bar mode changed — restart QBZ to apply"),