From 8c7e7041fd7142fb25be9f2c70169953644743db Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 18:31:22 -0800 Subject: [PATCH] Title bar: the app bar shows no brand mark under the desktop's own frame On Linux, when the desktop draws the window's frame (KDE, Sway, Hyprland, COSMIC and X11 sessions), the app bar painted the brand mark under the desktop's title bar unless System Title Bar was on. The mark followed the live preference instead of the frame the window has. The desktop app now records the frame it opened the window with (VectorcraftApp::desktop_frame), and system_title_bar reads it. A change to System Title Bar during a session no longer shows or hides the mark; it changes with the frame at the next start. Embedders without a window of their own keep the mark. --- apps/vectorcraft/src/main.rs | 1 + crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/chrome.rs | 14 +++++++----- crates/ui-egui/src/chrome/tests.rs | 35 ++++++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 4 ++++ docs/control-protocol.md | 3 ++- 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 8019276ec..112c71370 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -467,6 +467,7 @@ fn main() -> std::process::ExitCode { rs.device.set_device_lost_callback(move |reason, msg| loss.report(&ctx, format!("{reason:?}: {msg}"))); } app.custom_titlebar = custom_titlebar; + app.desktop_frame = !custom_titlebar; if let Some(port) = control_port { let rx = control_server::start(port, cc.egui_ctx.clone()); app = app.with_control(rx).with_automation_roots(roots.clone()); diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index a74582972..61057acb1 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -123,6 +123,7 @@ impl VectorcraftApp { canvas_rect: None, hover_doc: None, custom_titlebar: false, + desktop_frame: false, last_window_title: String::new(), fullscreen_for_mode: false, graphics_adapter: None, diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index ae8fbccc0..135958756 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -129,13 +129,15 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { } } -/// Whether the window shows the system's title bar instead of the app drawing its own: Windows -/// and Linux with Preferences › User Interface › System Title Bar. macOS always has system -/// decorations, but keeps the in-app brand mark, so it never counts as system mode here; nor -/// does the web build (the browser tab has no document title), nor an embedder that never -/// turned the preference on. +/// Whether the window shows the system's title bar instead of the app drawing its own +/// ([`VectorcraftApp::desktop_frame`]): Windows with Preferences › User Interface › System Title +/// Bar, and Linux whenever the desktop draws the frame, with or without the preference. The +/// preference is read when the app starts, so changing it later does not change this. macOS +/// always has system decorations, but keeps the in-app brand mark, so it never counts as system +/// mode here; nor does the web build (the browser tab has no document title), nor an embedder +/// without a window of its own. pub fn system_title_bar(app: &VectorcraftApp) -> bool { - !app.custom_titlebar && app.session.prefs.system_title_bar && !cfg!(any(target_os = "macos", target_arch = "wasm32")) + app.desktop_frame && !app.custom_titlebar && !cfg!(any(target_os = "macos", target_arch = "wasm32")) } /// The OS window title: the active document's name suffixed with the app name (a `*` prefix diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 00d790964..55650c46e 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -1230,16 +1230,51 @@ fn the_zoom_hint_follows_animated_zoom() { fn title_test_app(custom_titlebar: bool) -> crate::VectorcraftApp { let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); app.custom_titlebar = custom_titlebar; + app.desktop_frame = !custom_titlebar; // The preference that makes the shell launch without its own title bar. app.run("prefs.set", json!({"key": "systemTitleBar", "value": !custom_titlebar})).unwrap(); app } +/// How many brand marks one frame of the app bar paints. +fn brand_marks(app: &mut crate::VectorcraftApp) -> usize { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| super::app_bar(app, ui)); + out.textures_delta.clear(); + out.shapes.iter().filter(|c| matches!(&c.shape, egui::Shape::Mesh(m) if m.texture_id == crate::brand::texture_id(&ctx))).count() +} + #[test] fn without_the_preference_the_mark_stays() { // The default (e.g. the web build): no custom bar, no preference. let app = crate::VectorcraftApp::new(Session::new(), Default::default()); assert!(!super::system_title_bar(&app)); + // An embedder without a window of its own keeps the mark with the preference on too. + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("prefs.set", json!({"key": "systemTitleBar", "value": true})).unwrap(); + assert!(!super::system_title_bar(&app)); + assert_eq!(brand_marks(&mut app), 1); +} + +#[test] +fn a_desktop_frame_hides_the_mark_whatever_the_preference_says() { + // Linux on a desktop that draws the frame (KDE, X11): the window has the desktop's title bar + // with System Title Bar off. + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.desktop_frame = true; + app.run("prefs.set", json!({"key": "systemTitleBar", "value": false})).unwrap(); + let hidden = !cfg!(target_os = "macos"); + assert_eq!(super::system_title_bar(&app), hidden); + assert_eq!(brand_marks(&mut app), if hidden { 0 } else { 1 }); + // The preference applies at the next start: changing it now leaves the window's frame, and the mark, as they are. + let mut app = title_test_app(false); + app.run("prefs.set", json!({"key": "systemTitleBar", "value": false})).unwrap(); + assert_eq!(super::system_title_bar(&app), hidden); + let mut app = title_test_app(true); + app.run("prefs.set", json!({"key": "systemTitleBar", "value": true})).unwrap(); + assert!(!super::system_title_bar(&app)); + assert_eq!(brand_marks(&mut app), 1); } #[test] diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index fb8912455..9b054a683 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -198,6 +198,10 @@ pub struct VectorcraftApp { /// Windows and Linux: the window has no OS decorations, so the app bar is the title bar (drag, /// double-click to maximize, caption buttons) and invisible edge zones resize the window. pub custom_titlebar: bool, + /// The window has the desktop's own title bar: the OS or the window manager draws its frame, + /// icon and title. Set by the desktop app when it opens the window; `false` for embedders + /// without a window of their own. + pub desktop_frame: bool, /// Last window title sent to the OS (`ViewportCommand::Title`, see `chrome::sync_window_title`): /// sent again only when it changes, so idle frames don't spam the backend. pub(crate) last_window_title: String, diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 9357927f0..5f70424ce 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -328,7 +328,8 @@ not commands, so drive them with `ui.click` if needed. Close runs `app.quit` (th modified documents), and empty bar space and the 5 pt window edges move and resize the window. Preferences › User Interface › System Title Bar (`systemTitleBar`, read when the app starts) gives the window its OS decorations back instead: the OS title then shows the active document (`*name — VectorCraft` with unsaved changes) and the app bar -drops its brand mark. On macOS the window buttons are centred on the app bar; the web build is unchanged. +drops its brand mark. On Linux the app bar also drops its brand mark whenever the desktop draws the window's frame, with +System Title Bar on or off. On macOS the window buttons are centred on the app bar; the web build is unchanged. Fill/Stroke chips and panel shortcuts: the Control bar's and Properties' Fill and Stroke chips bring their proxy forward (`paint.toggleActive {fill}`) and open a popover with the Swatches panel (Shift-click: the Color panel's mixer);