diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs
index b3a1fd06b..fd062e504 100644
--- a/apps/vectorcraft/src/main.rs
+++ b/apps/vectorcraft/src/main.rs
@@ -97,7 +97,7 @@ use titlebar::opted_into_system_title_bar;
#[cfg(windows)]
use titlebar::set_taskbar_icon;
#[cfg(all(test, feature = "wgpu"))]
-use titlebar::{app_draws_frame, opted_into_app_title_bar, start_frame};
+use titlebar::{app_draws_frame, opted_into_app_title_bar, start_frame, system_title_bar_applies};
use titlebar::{custom_titlebar, native_options, titlebar_from_env};
struct App {
diff --git a/apps/vectorcraft/src/tests.rs b/apps/vectorcraft/src/tests.rs
index 555d829e0..e81dd7815 100644
--- a/apps/vectorcraft/src/tests.rs
+++ b/apps/vectorcraft/src/tests.rs
@@ -245,6 +245,79 @@ fn the_start_frame_applies_the_app_title_bar_preference() {
assert!(!super::custom_titlebar_from(&both, true), "System Title Bar takes precedence over the app title bar preference");
}
+/// `VECTORCRAFT_TITLEBAR` chooses the frame for the run it is set in, with System Title Bar off and
+/// on. The saved preference still decides while the variable is unset.
+#[test]
+fn the_titlebar_variable_takes_precedence_over_system_title_bar() {
+ use craft_window::Frame;
+ assert!(super::system_title_bar_applies(true, false));
+ assert!(!super::system_title_bar_applies(true, true));
+ assert!(!super::system_title_bar_applies(false, false));
+ assert!(!super::system_title_bar_applies(false, true));
+ // VECTORCRAFT_TITLEBAR=app with System Title Bar on: VectorCraft draws its own title bar.
+ let frame = super::start_frame(Frame::Desktop, Some(true), false, false);
+ assert_eq!(super::custom_titlebar_from(&frame, super::system_title_bar_applies(true, true)), !cfg!(target_os = "macos"));
+ // VECTORCRAFT_TITLEBAR=desktop with System Title Bar off keeps the desktop's frame.
+ #[cfg(not(any(target_os = "macos", target_os = "windows")))]
+ {
+ let frame = super::start_frame(Frame::App(craft_window::Layout::default()), Some(false), false, false);
+ assert!(!super::custom_titlebar_from(&frame, super::system_title_bar_applies(false, true)));
+ }
+}
+
+/// `VECTORCRAFT_TITLEBAR` read from the environment at startup, with System Title Bar off and on.
+/// The variable is process-wide and read once, so each value runs in a separate test process.
+#[test]
+fn the_titlebar_variable_decides_the_frame_before_the_window_opens() {
+ const CHILD: &str = "VECTORCRAFT_TEST_TITLEBAR_CHILD";
+ if std::env::var_os(CHILD).is_none() {
+ for value in ["app", "system", "desktop", "unknown"] {
+ // Linux and the BSDs: hold `spawning` while the child starts, so it does not keep a
+ // copy of a GPU start test's locked start file (see `CHILD_SPAWN`).
+ #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))]
+ let spawning = super::spawning();
+ let child = std::process::Command::new(std::env::current_exe().unwrap())
+ .args(["--exact", "tests::the_titlebar_variable_decides_the_frame_before_the_window_opens", "--nocapture"])
+ .env(CHILD, "1")
+ .env("VECTORCRAFT_TITLEBAR", value)
+ .stdin(std::process::Stdio::null())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn();
+ #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))]
+ drop(spawning);
+ let output = child.unwrap().wait_with_output().unwrap();
+ assert!(
+ output.status.success(),
+ "VECTORCRAFT_TITLEBAR={value}\n{}\n{}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+ return;
+ }
+ let value = std::env::var("VECTORCRAFT_TITLEBAR").unwrap();
+ let not_read = cfg!(any(target_os = "macos", target_os = "windows"));
+ assert_eq!(super::titlebar_from_env(), !not_read && value != "unknown", "VECTORCRAFT_TITLEBAR={value}");
+ let off = ui_state_with_prefs(serde_json::json!({"systemTitleBar": false}));
+ let on = ui_state_with_prefs(serde_json::json!({"systemTitleBar": true}));
+ for (saved, opted_into_system) in [(None, false), (Some(&off), false), (Some(&on), true)] {
+ let custom = super::custom_titlebar(saved);
+ let expected = match value.as_str() {
+ _ if not_read => !cfg!(target_os = "macos") && !opted_into_system,
+ "app" => true,
+ "system" | "desktop" => false,
+ // Not a value the variable takes: System Title Bar decides when it is on. Otherwise the
+ // `craft-window` probe decides, the same with System Title Bar off as with no saved
+ // preferences.
+ _ if opted_into_system => false,
+ _ => super::custom_titlebar(None),
+ };
+ assert_eq!(custom, expected, "{value}, preference {opted_into_system}");
+ assert_eq!(super::native_options(custom).viewport.decorations, Some(!custom), "{value}, preference {opted_into_system}");
+ }
+}
+
/// 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/apps/vectorcraft/src/titlebar.rs b/apps/vectorcraft/src/titlebar.rs
index 6e7451ed9..17c847040 100644
--- a/apps/vectorcraft/src/titlebar.rs
+++ b/apps/vectorcraft/src/titlebar.rs
@@ -41,7 +41,8 @@ pub(crate) const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
/// Whether the user asked for the system title bar in Preferences › User Interface › System Title
/// Bar. Read from the saved preferences before the window opens; a missing or unreadable value is
-/// `false` (the default). This preference forces the desktop/native frame back on.
+/// `false` (the default). This preference forces the desktop/native frame back on unless
+/// `VECTORCRAFT_TITLEBAR` is set ([`system_title_bar_applies`]).
pub(crate) fn opted_into_system_title_bar(saved: Option<&vectorcraft_ui_egui::UiState>) -> bool {
saved.map(|ui| ui.engine_prefs.get("systemTitleBar").and_then(serde_json::Value::as_bool).unwrap_or(false)).unwrap_or(false)
}
@@ -87,12 +88,18 @@ fn probed_choice() -> &'static craft_window::Choice {
}
/// Whether `VECTORCRAFT_TITLEBAR` chose this window's frame (Linux and the BSDs), so Preferences ›
-/// User Interface › VectorCraft Title Bar on Every Desktop does not apply while it is set. Always
-/// `false` on macOS and Windows.
+/// User Interface › System Title Bar and VectorCraft Title Bar on Every Desktop do not apply while
+/// it is set. Always `false` on macOS and Windows.
pub(crate) fn titlebar_from_env() -> bool {
cfg!(not(any(target_os = "macos", target_os = "windows"))) && probed_choice().overridden().is_some()
}
+/// Whether System Title Bar decides this start: `opted_into_system` unless `VECTORCRAFT_TITLEBAR`
+/// is set (`from_env`), which chooses the frame for the run it is set in.
+pub(crate) fn system_title_bar_applies(opted_into_system: bool, from_env: bool) -> bool {
+ opted_into_system && !from_env
+}
+
/// Whether this start draws its own title bar, given the frame the shared `craft-window` crate
/// decided and the user's System Title Bar preference.
///
@@ -102,7 +109,8 @@ pub(crate) fn titlebar_from_env() -> bool {
/// ([`craft_window::Frame::App`]) only where the compositor leaves it to the app (GNOME's Mutter),
/// or when `VECTORCRAFT_TITLEBAR` overrides the probe. VectorCraft draws its own title bar exactly
/// when `craft-window` says the app owns the frame **and** the user has not opted into the system
-/// title bar; the preference always forces the native frame back on.
+/// title bar; the preference forces the native frame back on unless `VECTORCRAFT_TITLEBAR` is set
+/// ([`system_title_bar_applies`]).
///
/// macOS and Windows keep today's behavior and are decided by [`CUSTOM_TITLEBAR`] rather than the
/// probe: `craft-window` returns `Frame::Desktop` for both (the OS always draws a frame), but
@@ -124,7 +132,8 @@ pub(crate) fn custom_titlebar_from(_frame: &craft_window::Frame, opted_into_syst
/// Whether this start draws its own title bar. Probes the shared `craft-window` crate once (its
/// override variable is `VECTORCRAFT_TITLEBAR`) and combines its decision with the saved System
-/// Title Bar preference. Call once at startup, before [`native_options`].
+/// Title Bar preference, which does not apply while the variable is set. Call once at startup,
+/// before [`native_options`].
///
/// On Linux, Preferences › Performance › Linux Display Server › X11 opens the window through
/// XWayland (below). A GNOME/Wayland probe would say `Frame::App` there, but the window really runs
@@ -133,7 +142,7 @@ pub(crate) fn custom_titlebar_from(_frame: &craft_window::Frame, opted_into_syst
/// and so does VectorCraft Title Bar on Every Desktop ([`app_draws_frame`]).
pub(crate) fn custom_titlebar(saved: Option<&vectorcraft_ui_egui::UiState>) -> bool {
let choice = probed_choice();
- let opted_into_system = opted_into_system_title_bar(saved);
+ let opted_into_system = system_title_bar_applies(opted_into_system_title_bar(saved), titlebar_from_env());
// macOS and Windows never read VectorCraft Title Bar on Every Desktop (`custom_titlebar_from`
// ignores the frame there).
let opted_into_app = cfg!(not(any(target_os = "macos", target_os = "windows"))) && opted_into_app_title_bar(saved);
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
index 4b931aaef..802bb695d 100644
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -196,8 +196,8 @@ pub struct VectorcraftApp {
/// A URL to open through egui next frame (when the host has no `open_url` service).
pending_url: Option,
/// `VECTORCRAFT_TITLEBAR` chose this window's frame (Linux and the BSDs), so Preferences › User
- /// Interface › VectorCraft Title Bar on Every Desktop does not apply while it is set. `false`
- /// unless the host sets it.
+ /// Interface › System Title Bar and VectorCraft Title Bar on Every Desktop do not apply while
+ /// it is set. `false` unless the host sets it.
pub titlebar_from_env: bool,
/// 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.
diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs
index 733aaf200..d5f391433 100644
--- a/crates/ui-egui/src/prefs_dialog.rs
+++ b/crates/ui-egui/src/prefs_dialog.rs
@@ -36,8 +36,8 @@ pub fn open(app: &mut VectorcraftApp, category: Option<&str>) {
fields.insert("__smartGuides".into(), json!(app.ui.view.smart_guides));
fields.insert("__snapToGrid".into(), json!(app.ui.view.snap_to_grid));
fields.insert("__taskBar".into(), json!(app.ui.task_bar));
- // Read-only: whether VECTORCRAFT_TITLEBAR chose this window's frame (VectorCraft Title Bar on
- // Every Desktop's note).
+ // Read-only: whether VECTORCRAFT_TITLEBAR chose this window's frame (System Title Bar's dimmed
+ // row and VectorCraft Title Bar on Every Desktop's note).
fields.insert("__titlebarFromEnv".into(), json!(app.titlebar_from_env));
fields.insert("__search".into(), json!(""));
fields.insert("__focusSearch".into(), json!(true));
@@ -483,7 +483,8 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
// Windows and Linux only: macOS always has the system's title bar, the web has none.
PrefKind::Bool if sp.key == "systemTitleBar" => {
if cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))) {
- bool_row(ui, d, sp.key, sp.label);
+ // VECTORCRAFT_TITLEBAR chooses the frame while it is set: the checkbox is dimmed.
+ ui.add_enabled_ui(!d.bool("__titlebarFromEnv"), |ui| bool_row(ui, d, sp.key, sp.label));
// The window's decorations are chosen once, when the app starts. On Linux and the
// BSDs the note goes under the next row, which also applies at the next start.
if !APP_TITLE_BAR_SHOWN {
@@ -499,7 +500,7 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
let from_env = d.bool("__titlebarFromEnv");
let system = d.bool("systemTitleBar");
ui.add_enabled_ui(!from_env && !system, |ui| bool_row(ui, d, sp.key, sp.label));
- if system {
+ if system && !from_env {
let note = tl!("System Title Bar takes precedence over this option while it is on.");
ui.label(egui::RichText::new(note).color(t.text_dim).size(11.0));
}
diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs
index 11fd0f418..9ef8f3ed7 100644
--- a/crates/ui-egui/src/prefs_dialog/tests.rs
+++ b/crates/ui-egui/src/prefs_dialog/tests.rs
@@ -336,7 +336,8 @@ fn app_title_bar_preference_shows_and_persists() {
/// VectorCraft Title Bar on Every Desktop does not take clicks while System Title Bar is on or
/// VECTORCRAFT_TITLEBAR is set. With the variable set its note names the variable in place of the
-/// next-launch note, and with System Title Bar on a note says that System Title Bar takes precedence.
+/// next-launch note, and with System Title Bar on and the variable unset a note says that System
+/// Title Bar takes precedence.
#[test]
fn app_title_bar_is_dimmed_while_something_takes_precedence() {
fn texts(s: &egui::Shape, out: &mut Vec) {
@@ -364,7 +365,7 @@ fn app_title_bar_is_dimmed_while_something_takes_precedence() {
let mut shown = vec![];
out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown));
assert_eq!(shown.iter().any(|t| t == note), supported && from_env, "{shown:?}");
- assert_eq!(shown.iter().any(|t| t == system_note), supported && system, "{shown:?}");
+ assert_eq!(shown.iter().any(|t| t == system_note), supported && system && !from_env, "{shown:?}");
// The next-launch note stays under this row on Linux and the BSDs, where it also covers
// System Title Bar, and under System Title Bar on Windows.
let restart_shown = if supported { !from_env } else { cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))) };
@@ -392,6 +393,42 @@ fn app_title_bar_is_dimmed_while_something_takes_precedence() {
}
}
+/// While VECTORCRAFT_TITLEBAR chooses the frame, System Title Bar does not take clicks.
+#[test]
+fn system_title_bar_is_dimmed_while_the_environment_variable_chooses_the_frame() {
+ let supported = cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos")));
+ for from_env in [false, true] {
+ let mut a = app();
+ a.titlebar_from_env = from_env;
+ open(&mut a, Some("User Interface"));
+ let ctx = egui::Context::default();
+ crate::theme::install_fonts(&ctx);
+ 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 label = out.shapes.iter().find_map(|c| match &c.shape {
+ egui::Shape::Text(t) if t.galley.text() == "System Title Bar" => Some(t.visual_bounding_rect()),
+ _ => None,
+ });
+ assert_eq!(label.is_some(), supported);
+ if let Some(r) = label {
+ for pressed in [true, false] {
+ let input = egui::RawInput {
+ events: vec![
+ egui::Event::PointerMoved(r.center()),
+ egui::Event::PointerButton { pos: r.center(), button: egui::PointerButton::Primary, pressed, modifiers: Default::default() },
+ ],
+ ..Default::default()
+ };
+ let mut out = ctx.run_ui(input, |ui| show(&mut a, ui.ctx()));
+ out.textures_delta.clear();
+ }
+ assert_eq!(a.ui.dialog.as_ref().unwrap().bool("systemTitleBar"), !from_env, "clicked with the variable {from_env}");
+ }
+ }
+}
+
/// Performance › Linux Display Server (#2028): on Linux it shows the drawing-tablet note, and
/// X11 (XWayland) When a Pen Is Attached is applied by OK and saved under the key the desktop
/// app reads before the window opens.
diff --git a/docs/control-protocol.md b/docs/control-protocol.md
index 290619b2a..66d90cc33 100644
--- a/docs/control-protocol.md
+++ b/docs/control-protocol.md
@@ -338,7 +338,9 @@ when the app starts, off by default) turns off the desktop's frame and uses the
sessions where the desktop would draw the frame: X11 sessions, Wayland compositors that draw server-side decorations
such as KDE Plasma, and a window opened through XWayland. System Title Bar and `VECTORCRAFT_TITLEBAR` (`app`, or
`system` or `desktop`) take precedence over it. The checkbox is dimmed while either applies, and a note under it names
-each one that applies.
+each one that applies. `VECTORCRAFT_TITLEBAR` also takes precedence over System Title Bar for the run it is set in:
+`app` keeps the app bar as the title bar with System Title Bar on. While the variable is set, the Preferences dialog dims
+System Title Bar and its `__titlebarFromEnv` field is `true`.
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);
diff --git a/docs/development.md b/docs/development.md
index 3204c445b..a4077abc6 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -118,7 +118,7 @@ A file opened from the file manager (double-click, Open With, a drop on the exec
## Linux: Wayland and X11
-On GNOME/Wayland, enable **Preferences › User Interface › System Title Bar** and restart to use winit's font-free Adwaita window controls (`wayland-csd-adwaita-notitle`); the existing custom title bar remains the Linux default. On X11, on a Wayland compositor that draws server-side decorations such as KDE Plasma, and in a window opened through XWayland, enable **Preferences › User Interface › VectorCraft Title Bar on Every Desktop** and restart to use the app bar as the title bar instead of the desktop's frame. `VECTORCRAFT_TITLEBAR=app`, `=system` or `=desktop` chooses the frame for one run and takes precedence over VectorCraft Title Bar on Every Desktop.
+On GNOME/Wayland, enable **Preferences › User Interface › System Title Bar** and restart to use winit's font-free Adwaita window controls (`wayland-csd-adwaita-notitle`); the existing custom title bar remains the Linux default. On X11, on a Wayland compositor that draws server-side decorations such as KDE Plasma, and in a window opened through XWayland, enable **Preferences › User Interface › VectorCraft Title Bar on Every Desktop** and restart to use the app bar as the title bar instead of the desktop's frame. `VECTORCRAFT_TITLEBAR=app`, `=system` or `=desktop` chooses the frame for one run and takes precedence over System Title Bar and VectorCraft Title Bar on Every Desktop.
The window runs natively on Wayland (eframe's `wayland` feature) and on X11. On machines with two GPUs it renders on the one the desktop runs on unless Preferences say otherwise; see [Desktop graphics processor](#desktop-graphics-processor) (#502). The system clipboard (`apps/vectorcraft/src/clipboard.rs`) is arboard with its `wayland-data-control` feature:
diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md
index 0093bc209..5a37da7ba 100644
--- a/docs/target-app-parity.md
+++ b/docs/target-app-parity.md
@@ -821,7 +821,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`).
- Edit › Keyboard Shortcuts › Export Text… saves the set shown in the dialog as a UTF-8 text file, as the reference app's [Export keyboard shortcuts list as a text document](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/export-keyboard-shortcuts-list.html) page describes (`shortcuts.exportText {path?}`). The layout is VectorCraft's own: the set's name, then the Tools and Menu Commands rows in the dialog's order, each with its name, a tab and its shortcuts as the dialog shows them, and nothing after the tab for a row with no shortcut. Tests `shortcut_editor::tests::export_text_*` and `tests::picks::keyboard_shortcuts_export_text_writes_the_working_set_as_text`.
- On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one.
- - On Linux and the BSDs, Preferences › User Interface › VectorCraft Title Bar on Every Desktop (off by default) uses the app bar as the title bar from the next start in the sessions where the desktop would draw the window's frame (X11, Wayland compositors with server-side decorations such as KDE Plasma, a window opened through XWayland). System Title Bar and `VECTORCRAFT_TITLEBAR` take precedence over it.
+ - On Linux and the BSDs, Preferences › User Interface › VectorCraft Title Bar on Every Desktop (off by default) uses the app bar as the title bar from the next start in the sessions where the desktop would draw the window's frame (X11, Wayland compositors with server-side decorations such as KDE Plasma, a window opened through XWayland). System Title Bar and `VECTORCRAFT_TITLEBAR` take precedence over it, and `VECTORCRAFT_TITLEBAR` also takes precedence over System Title Bar for the run it is set in.
- Edit › Keyboard Shortcuts lists items of the menu bar and panel menus, as the reference app's [Customize keyboard shortcuts](https://web.archive.org/web/20221205131209id_/https://helpx.adobe.com/illustrator/using/customizing-keyboard-shortcuts.html) page describes for its Menu Commands. A menu command with no default shortcut that needs a value a key press does not give, such as File › Document Setup › Units (`document.setUnits {units}`), is listed only while the user has changed its shortcut. A command stays listed when a menu bar item runs it without a value or when it opens a dialog run without one, as the Variables panel's commands do.
- A shortcut on the key left of 1 runs whatever character that key types, and Edit › Keyboard Shortcuts, the menus in the window, the command palette and tool tips show it as that character once the key has been pressed: `"` on a Turkish Q layout, `~` on a US layout as before. Shortcuts saved as `~` keep working (#4559).
- On Windows a maximized window stays inside the work area with the taskbar on any edge (#3242, #3723).