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]