From 12f8a77a00b6d37b691df251073a4ef1e352a6e3 Mon Sep 17 00:00:00 2001 From: rafi612 Date: Sat, 10 Oct 2026 14:07:33 +0200 Subject: [PATCH] =?UTF-8?q?User=20Interface=20=E2=80=BA=20System=20Title?= =?UTF-8?q?=20Bar:=20native=20decorations=20on=20Windows/Linux,=20file=20i?= =?UTF-8?q?n=20OS=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vectorcraft/src/main.rs | 67 ++++++++++++---- crates/engine/src/cmd/prefscmds.rs | 1 + crates/engine/src/lib.rs | 5 ++ crates/ui-egui/src/brand.rs | 9 +++ crates/ui-egui/src/chrome.rs | 119 +++++++++++++++++++++++++++- crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/lib.rs | 7 ++ crates/ui-egui/src/prefs_dialog.rs | 36 +++++++++ 17 files changed, 243 insertions(+), 21 deletions(-) diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 915daadd..0ce3d74f 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -400,6 +400,32 @@ fn adapter_summary(info: &eframe::wgpu::AdapterInfo) -> String { /// macOS keeps its traffic lights over the integrated title strip. const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos"); +/// Whether this start draws its own title bar: Windows and Linux do, unless Preferences › +/// User Interface › System Title Bar asks for the system's. Read from the saved preferences +/// before the window opens; a missing or unreadable value keeps the default. +fn custom_titlebar(saved: Option<&vectorcraft_ui_egui::UiState>) -> bool { + CUSTOM_TITLEBAR && !saved.map(|ui| ui.engine_prefs.get("systemTitleBar").and_then(serde_json::Value::as_bool).unwrap_or(false)).unwrap_or(false) +} + +fn native_options(custom_titlebar: bool) -> eframe::NativeOptions { + eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("VectorCraft") + .with_inner_size(window::DEFAULT_SIZE) + .with_min_inner_size(window::MIN_SIZE) + .with_drag_and_drop(true) + .with_decorations(!custom_titlebar) + .with_fullsize_content_view(true) + .with_titlebar_shown(false) + .with_title_shown(false) + .with_icon(app_icon()) + .with_app_id("ai.storyteller.vectorcraft"), + #[cfg(feature = "windows7")] + renderer: eframe::Renderer::Glow, + ..Default::default() + } +} + fn main() -> std::process::ExitCode { // First, so every start-up warning is recorded (`logging`). let logger = logging::install(); @@ -486,22 +512,8 @@ fn main() -> std::process::ExitCode { } #[cfg(feature = "wgpu")] let startup = std::sync::Arc::new(gpu::Startup::default()); - let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_title("VectorCraft") - .with_inner_size(window::DEFAULT_SIZE) - .with_min_inner_size(window::MIN_SIZE) - .with_drag_and_drop(true) - .with_decorations(!CUSTOM_TITLEBAR) - .with_fullsize_content_view(true) - .with_titlebar_shown(false) - .with_title_shown(false) - .with_icon(app_icon()) - .with_app_id("ai.storyteller.vectorcraft"), - #[cfg(feature = "windows7")] - renderer: eframe::Renderer::Glow, - ..Default::default() - }; + let custom_titlebar = custom_titlebar(saved.as_ref()); + let options = native_options(custom_titlebar); #[cfg(feature = "wgpu")] let options = { let mut options = options; @@ -575,7 +587,7 @@ fn main() -> std::process::ExitCode { { app.graphics_adapter = Some("OpenGL (Windows 7 compatibility)".into()); } - app.custom_titlebar = CUSTOM_TITLEBAR; + app.custom_titlebar = 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()); @@ -618,6 +630,27 @@ fn main() -> std::process::ExitCode { mod tests { use super::*; + fn ui_state_with_prefs(prefs: serde_json::Value) -> vectorcraft_ui_egui::UiState { + let mut ui = vectorcraft_ui_egui::UiState::default(); + ui.engine_prefs = prefs; + ui + } + + #[test] + fn the_system_title_bar_preference_keeps_the_window_decorations() { + // Preferences › User Interface › System Title Bar gives the window back its system + // decorations on Windows and Linux; macOS always has them. + assert_eq!(super::custom_titlebar(None), super::CUSTOM_TITLEBAR, "no preferences: the default"); + let off = ui_state_with_prefs(serde_json::json!({"systemTitleBar": false})); + assert_eq!(super::custom_titlebar(Some(&off)), super::CUSTOM_TITLEBAR); + let on = ui_state_with_prefs(serde_json::json!({"systemTitleBar": true})); + assert!(!super::custom_titlebar(Some(&on))); + assert_eq!(super::native_options(false).viewport.decorations, Some(true)); + if super::CUSTOM_TITLEBAR { + assert_eq!(super::native_options(true).viewport.decorations, Some(false)); + } + } + /// The file extensions the macOS bundle declares: its document types and its own exported type. fn plist_extensions(plist: &str) -> Vec<&str> { ["CFBundleTypeExtensions", "public.filename-extension"] diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index d1562c5e..0c764faa 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -282,6 +282,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("scaleCursorWithUi", "User Interface", "UI Scaling", "Scale Cursor Proportional to UI", bool), // `auto` or a language code the shell registers (`zh-hant`); the shell shows it as a dropdown. p!("interfaceLanguage", "User Interface", "Language", "Language", text), + p!("systemTitleBar", "User Interface", "", "System Title Bar", bool), // Performance p!("gpuPerformance", "Performance", "GPU Performance", "GPU Performance", bool), p!("animatedZoom", "Performance", "GPU Performance", "Animated Zoom", bool), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index ec72f1ee..8a964610 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -512,6 +512,10 @@ pub struct Prefs { /// UI language: `auto` (follow the system locale) or a language code such as `en`, `zh-hant`. /// The list of languages belongs to the shell (`ui-egui` i18n); an unknown code reads as `auto`. pub interface_language: String, + /// Windows and Linux: use the system's title bar and window buttons instead of the app bar + /// acting as the title bar (tiling window managers, desktops that draw their own decorations). + /// Read when the app starts. macOS always uses the system's. + pub system_title_bar: bool, // Performance pub gpu_performance: bool, pub animated_zoom: bool, @@ -726,6 +730,7 @@ impl Default for Prefs { ui_scaling: 1.0, scale_cursor_with_ui: false, interface_language: s("auto"), + system_title_bar: false, gpu_performance: true, animated_zoom: true, gpu_preference: s("automatic"), diff --git a/crates/ui-egui/src/brand.rs b/crates/ui-egui/src/brand.rs index 1d88dfb2..0a528569 100644 --- a/crates/ui-egui/src/brand.rs +++ b/crates/ui-egui/src/brand.rs @@ -36,6 +36,12 @@ pub fn paint_mark(ui: &Ui, r: Rect) { ui.painter().image(texture(ui.ctx()).id(), r, uv, Color32::WHITE); } +/// The mark's texture id (tests: find the painted marks among the shapes). +#[cfg(test)] +pub fn texture_id(ctx: &Context) -> egui::TextureId { + texture(ctx).id() +} + #[cfg(test)] mod tests { use super::*; @@ -55,6 +61,9 @@ mod tests { let ctx = Context::default(); crate::theme::install_fonts(&ctx); let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + // The system title bar hides the in-app mark (the OS draws its own icon), so paint the + // app-drawn bar here. + app.custom_titlebar = true; let mut frame = || { let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::chrome::app_bar(&mut app, ui)); let uploads = out.textures_delta.set.values().flat_map(|d| d.iter()).filter(|d| d.image.size() == [128, 128]).count(); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index be7d3f3c..653f24fb 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -18,9 +18,12 @@ pub const APP_BAR_HEIGHT: f32 = 44.0; /// The application bar: brand mark, Home, menus, then Discord, search and the workspace switcher /// at the right. With [`VectorcraftApp::custom_titlebar`] it is also the window's title bar /// ([`titlebar`]): the caption buttons take the right end and the rest of the bar drags the window. +/// With the system title bar (Windows and Linux, `system_title_bar`) the OS draws its own icon and +/// title, so the in-app bar shows Home, menus and workspace controls only: no brand mark. pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let custom = app.custom_titlebar; + let system = system_title_bar(app); let left = app.titlebar_inset.clamp(8.0, f32::from(i8::MAX)) as i8; let frame = egui::Frame::NONE.fill(t.app_bar).inner_margin(egui::Margin { left, right: if custom { 0 } else { 14 }, top: 0, bottom: 0 }); let bar = egui::Panel::top("app_bar").exact_size(APP_BAR_HEIGHT).frame(frame.stroke(Stroke::new(1.0, t.border))).show(ui, |ui| { @@ -28,10 +31,12 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { titlebar::drag_area(ui, ui.max_rect()); } ui.horizontal_centered(|ui| { - // Brand mark: the app icon. - let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); - crate::brand::paint_mark(ui, r); - ui.add_space(4.0); + // With the system title bar the OS draws its own icon: no in-app brand mark. + if !system { + let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); + crate::brand::paint_mark(ui, r); + ui.add_space(4.0); + } let on_home = menus::home_showing(app); if widgets::icon_button(ui, "house", tl!("Home"), on_home, 24.0).clicked() { app.run("app.home", json!({})).ok(); @@ -109,6 +114,35 @@ 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. +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")) +} + +/// The OS window title: the active document's name suffixed with the app name (a `*` prefix +/// marks unsaved changes, like the `*` in the document tabs), or just the app name with no +/// document open. +pub fn window_title(app: &VectorcraftApp) -> String { + app.session + .active() + .map(|d| format!("{}{} \u{2014} VectorCraft", if d.is_dirty() { "*" } else { "" }, d.title())) + .unwrap_or_else(|| "VectorCraft".into()) +} + +/// Keep the OS window title (and the taskbar / Alt-Tab entry) on the active file. Sends +/// `ViewportCommand::Title` only when the title changed since the last frame. +pub fn sync_window_title(app: &mut VectorcraftApp, ctx: &egui::Context) { + let want = window_title(app); + if app.last_window_title != want { + app.last_window_title = want.clone(); + ctx.send_viewport_cmd(egui::ViewportCommand::Title(want)); + } +} + /// An anchor button: icon, tooltip, command and the convert command's `to`. type AnchorButton<'a> = (&'a str, &'a str, &'a str, Option<&'a str>); @@ -965,4 +999,81 @@ mod tests { let off = hint(&mut app); assert!(off.contains(" to zoom into an area") && !off.contains("Hold"), "{off}"); } + + fn title_test_app(custom_titlebar: bool) -> crate::VectorcraftApp { + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.custom_titlebar = 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 + } + + #[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)); + } + + #[test] + fn window_title_with_no_document_is_the_app_name() { + assert_eq!(super::window_title(&title_test_app(true)), "VectorCraft"); + } + + #[test] + fn window_title_follows_the_active_file() { + let mut app = title_test_app(true); + app.run("file.new", json!({"width": 100, "height": 100, "name": "foo.svg"})).unwrap(); + assert_eq!(super::window_title(&app), "foo.svg \u{2014} VectorCraft"); + // Unsaved changes gain a `*` prefix, like the `*` in the document tabs. + app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + assert_eq!(super::window_title(&app), "*foo.svg \u{2014} VectorCraft"); + } + + #[test] + fn system_mode_hides_the_mark() { + for custom in [true, false] { + let mut app = title_test_app(custom); + app.run("file.new", json!({"width": 100, "height": 100, "name": "foo.svg"})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| super::app_bar(&mut app, ui)); + let uploads = out.textures_delta.set.values().flat_map(|d| d.iter()).filter(|d| d.image.size() == [128, 128]).count(); + out.textures_delta.clear(); + let marks: Vec = out + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Mesh(m) if m.texture_id == crate::brand::texture_id(&ctx) => Some(m.calc_bounds()), + _ => None, + }) + .collect(); + let system = super::system_title_bar(&app); + assert_eq!(system, !custom && !cfg!(target_os = "macos")); + assert_eq!(marks.is_empty(), system, "mark in system mode (custom={custom})"); + assert_eq!(uploads, if system { 0 } else { 1 }, "mark texture in system mode (custom={custom})"); + } + } + + #[test] + fn sync_window_title_sends_only_on_change() { + let mut app = title_test_app(true); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + super::sync_window_title(&mut app, &ctx); + assert_eq!(app.last_window_title, "VectorCraft"); + let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); + out.textures_delta.clear(); + let cmds = out.viewport_output.remove(&egui::ViewportId::ROOT).map(|v| v.commands).unwrap_or_default(); + assert!(cmds.iter().any(|c| matches!(c, egui::ViewportCommand::Title(t) if t == "VectorCraft")), "{cmds:?}"); + // Same document state again: the cached title stops a repeat command. + super::sync_window_title(&mut app, &ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); + out.textures_delta.clear(); + let cmds = out.viewport_output.remove(&egui::ViewportId::ROOT).map(|v| v.commands).unwrap_or_default(); + assert!(!cmds.iter().any(|c| matches!(c, egui::ViewportCommand::Title(_))), "repeat Title: {cmds:?}"); + app.run("file.new", json!({"width": 100, "height": 100, "name": "foo.svg"})).unwrap(); + super::sync_window_title(&mut app, &ctx); + assert_eq!(app.last_window_title, "foo.svg \u{2014} VectorCraft"); + } } diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 06aa8084..065bda2a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -673,3 +673,5 @@ Patchwork… Patchwork… Stained Glass… Vitráž… Texturizer… Texturovač… + System Title Bar Systémové záhlaví okna + Applies at next launch. Projeví se při příštím spuštění. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f703db8b..f4927b5e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3975,3 +3975,5 @@ axis Both Ambos Top Right Arriba a la derecha Bottom Right Abajo a la derecha Edge Borde + System Title Bar Barra de título del sistema + Applies at next launch. Se aplicará en el próximo inicio. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 04728393..6dc963a7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3987,3 +3987,5 @@ axis Both Les deux Bottom Right En bas à droite effect Soft Doux Edge Bord + System Title Bar Barre de titre du système + Applies at next launch. Prend effet au prochain démarrage. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 975c1a03..bbb37c63 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3977,3 +3977,5 @@ axis Both Entrambi effect Soft Morbido effect Regular Normale Edge Bordo + System Title Bar Barra del titolo di sistema + Applies at next launch. Si applica al prossimo avvio. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 93697179..40a9fe66 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3970,3 +3970,5 @@ effect Soft ソフト effect Horizontal 水平 effect Vertical 垂直 Edge エッジ + System Title Bar システムのタイトルバー + Applies at next launch. 次回の起動時に適用されます。 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 6b1ef85a..182b9d5e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2733,3 +2733,5 @@ Patchwork… Colcha de retalhos… Stained Glass… Vitral… Texturizer… Texturizador… + System Title Bar Barra de título do sistema + Applies at next launch. Aplica-se na próxima inicialização. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 83b26c87..3c04c962 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3973,3 +3973,5 @@ axis Both Оба Top Right Сверху справа Bottom Right Снизу справа Edge Край + System Title Bar Системная строка заголовка + Applies at next launch. Применится при следующем запуске. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5dab363e..34afec8f 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3958,3 +3958,5 @@ axis Both Обидві Top Right Вгорі праворуч Bottom Right Знизу праворуч Edge Край + System Title Bar Системний рядок заголовка + Applies at next launch. Набуде чинності після перезапуску. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b4ec898e..1dd56e99 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3606,3 +3606,5 @@ effect Soft 柔和 effect Horizontal 水平 effect Vertical 垂直 Edge 边缘 + System Title Bar 系统标题栏 + Applies at next launch. 下次启动时生效。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 5101d723..70696085 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3387,3 +3387,5 @@ axis Both 兩者 Bottom Right 右下 effect Soft 柔和 Edge 邊緣 + System Title Bar 系統標題列 + Applies at next launch. 下次啟動時套用。 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 6a4c67e6..d82fbbde 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -353,6 +353,9 @@ 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, + /// 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, /// The graphics adapter the window renders with ("name (backend)"), as the host reports it: /// shown in Help › About and `ui.inspect` for GPU bug reports. `None` when unknown. pub graphics_adapter: Option, @@ -462,6 +465,7 @@ impl VectorcraftApp { canvas_rect: None, hover_doc: None, custom_titlebar: false, + last_window_title: String::new(), graphics_adapter: None, place: Default::default(), system_paste: false, @@ -958,6 +962,9 @@ impl VectorcraftApp { } shortcut_editor::sync(&self.ui); prefs_dialog::apply_runtime(self, ctx); + // The OS title bar (and the taskbar / Alt-Tab entry) follows the active file; with the + // system title bar this is where the document name lives, as the in-app mark is hidden. + crate::chrome::sync_window_title(self, ctx); self.drain_control(ctx); if !self.synthetic.is_empty() { ctx.request_repaint(); diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index da7595de..72b649a5 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -261,6 +261,11 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { PrefKind::Bool if sp.key == "animatedZoom" => { ui.add_enabled_ui(d.bool("gpuPerformance"), |ui| bool_row(ui, d, sp.key, sp.label)); } + PrefKind::Bool if sp.key == "systemTitleBar" && cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))) => { + bool_row(ui, d, sp.key, sp.label); + // The window's decorations are chosen once, when the app starts. + ui.label(egui::RichText::new(tl!("Applies at next launch.")).color(t.text_dim).size(11.0)); + } PrefKind::Bool => bool_row(ui, d, sp.key, sp.label), PrefKind::Num { min, max, unit } => { labeled(ui, sp.label, |ui| { @@ -530,6 +535,37 @@ mod tests { assert_eq!(saved["engine_prefs"]["gpuPreference"], json!("highPerformance")); } + /// User Interface › System Title Bar: shown with its next-launch note, applied by OK and + /// saved with the UI state under the key the desktop app reads before the window opens. + #[test] + fn system_title_bar_preference_shows_and_persists() { + fn texts(s: &egui::Shape, out: &mut Vec) { + match s { + egui::Shape::Text(t) => out.push(t.galley.text().to_string()), + egui::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)), + _ => {} + } + } + let mut a = app(); + open(&mut a, Some("User Interface")); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + // The window sizes itself on the first frame and draws on the second. + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx())); + out.textures_delta.clear(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx())); + out.textures_delta.clear(); + let mut shown = vec![]; + out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown)); + assert!(shown.iter().any(|t| t == "System Title Bar"), "{shown:?}"); + assert!(shown.iter().any(|t| t == "Applies at next launch."), "{shown:?}"); + a.ui.dialog.as_mut().unwrap().fields.insert("systemTitleBar".into(), json!(true)); + confirm(&mut a).unwrap(); + assert!(a.session.prefs.system_title_bar); + let saved: Value = serde_json::from_slice(&serde_json::to_vec(&a.ui).unwrap()).unwrap(); + assert_eq!(saved["engine_prefs"]["systemTitleBar"], json!(true)); + } + /// Units › Numbers Without Units Are Points (#394): the preference reaches the fields when it /// changes, and its checkbox is dimmed unless a unit is Picas. #[test]