feat(window): adopt shared craft-window for Linux decorations (depends on craft-libs#217) (#3366)

* feat(window): adopt shared craft-window for Linux decorations

Decide the window frame on Linux/BSD via the shared craft-window crate
(storytold/craft-libs) instead of VectorCraft's unconditional
`CUSTOM_TITLEBAR = !macos`, so the app matches the rest of the Craft
suite per desktop.

- Probe `craft_window::Choice::probe(Prefix::new("VECTORCRAFT"))` once at
  startup; custom titlebar is on when the frame is `Frame::App(_)` and the
  user has not opted into the system title bar.
- Preserve the `systemTitleBar` preference: opting in forces the native
  (desktop) frame back on, as before.
- Keep macOS and Windows behavior exactly as today (decided by
  `CUSTOM_TITLEBAR`, not the probe): macOS integrated title strip,
  Windows app-drawn caption. craft-window returns `Frame::Desktop` for
  both; VectorCraft's overdraw is its own choice.
- The only behavioral change: KDE/Wayland now keeps KWin's native
  decorations (Frame::Desktop) instead of always drawing a custom
  titlebar; GNOME still draws its own (Frame::App).
- `VECTORCRAFT_TITLEBAR` override and the `systemTitleBar` pref both still
  work (the env var is craft-window's override for the `VECTORCRAFT` prefix).

NOT MERGEABLE YET: craft-window is open PR storytold/craft-libs#217; the
dep points at the PR branch and must be repointed to a merged `rev`
before this can merge.

* fix(window): native frame when X11 is forced; pin craft-window to merged rev

- When Preferences > Linux Display Server > X11 forces the window onto
  XWayland, treat the frame as the desktop's (native X11 decorations)
  rather than letting a GNOME/Wayland probe pick Frame::App and draw our
  own title bar on what is really an X11 window. An explicit
  VECTORCRAFT_TITLEBAR override still wins (checked via Choice::overridden).
- Repoint craft-window from the (now-merged) feature branch to the squash
  commit on storytold/craft-libs (craft-libs#217); regenerate Cargo.lock.
- The WAYLAND_SOCKET concern raised in review is handled in the merged
  craft-window crate itself (the probe skips when WAYLAND_SOCKET is set).

Verified: cargo build + test -p vectorcraft (131 tests) pass against the
merged dep; windows7 (--no-default-features) still compiles; rustfmt clean.

* fix(window): only force native frame when X11 libs are actually present

Follow-up to review: the XWayland title-bar override now also checks
xwayland::x11_libraries_found(), matching the launch path's own condition.
If X11 is requested but the libraries are missing, startup stays on
Wayland, so the frame decision must keep the probe's app-drawn result
rather than flipping to native. Verified: tests pass, rustfmt clean.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-10-21 07:51:10 -04:00
1 parent 0b94055cfe
commit cb0a7d9b31
4 files changed
+345 -286

No files matched your search

Generated
+243 -274
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -15,6 +15,9 @@ craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "ca50d63b77
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" }
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" }
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
# Decides who draws the window frame (desktop vs. app) so the suite shares one native titlebar per
# desktop. The `egui` feature adds `craft_window::decorate`. (craft-libs#217.)
craft-window = { git = "https://github.com/storytold/craft-libs", rev = "1f805378d454dac48b5c214029d254055fdf961b", features = ["egui"] }
# internal
vectorcraft-geom = { path = "crates/geom" }
vectorcraft-color = { path = "crates/color" }
+3
View File
@@ -20,6 +20,9 @@ windows7 = ["eframe/glow"]
vectorcraft-engine = { workspace = true }
vectorcraft-ui-egui = { workspace = true }
vectorcraft-text = { workspace = true }
# Shared across the Craft suite: decides who draws the window frame per desktop (compositor vs app)
# so VectorCraft matches the rest on Linux. `egui` adds `craft_window::decorate`. See #217.
craft-window = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wayland", "x11"] }
egui = { workspace = true }
serde_json = { workspace = true }
+96 -12
View File
@@ -624,13 +624,73 @@ fn adapter_summary(info: &eframe::wgpu::AdapterInfo) -> String {
/// Windows and Linux: no OS title bar; the app bar is the title bar (`vectorcraft_ui_egui::titlebar`).
/// macOS keeps its traffic lights over the integrated title strip.
///
/// Used on macOS and Windows (where VectorCraft keeps today's frame regardless of the
/// `craft-window` probe) and by the tests; Linux and the BSDs take the decision from the probe.
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
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.
/// 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.
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)
}
/// 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.
///
/// The suite-wide rule (`craft_window::Choice`) is: the desktop draws the frame
/// ([`craft_window::Frame::Desktop`]) on macOS, Windows, X11 and any Wayland compositor with
/// server-side decorations (KDE, Sway, Hyprland, COSMIC); the app draws it
/// ([`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.
///
/// 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
/// VectorCraft insets its content under the macOS traffic lights and draws its own caption buttons
/// on Windows — an overdraw on an OS-decorated window that is VectorCraft's choice, not
/// `craft-window`'s. Only Linux and the BSDs take the decision from the probe.
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn custom_titlebar_from(frame: &craft_window::Frame, opted_into_system: bool) -> bool {
matches!(frame, craft_window::Frame::App(_)) && !opted_into_system
}
/// macOS and Windows: today's behavior, independent of the `craft-window` probe (see
/// [`custom_titlebar_from`]). macOS never draws its own frame (`CUSTOM_TITLEBAR` is `false`);
/// Windows draws its own caption unless the user opted into the system title bar.
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn custom_titlebar_from(_frame: &craft_window::Frame, opted_into_system: bool) -> bool {
CUSTOM_TITLEBAR && !opted_into_system
}
/// 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`].
///
/// 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
/// on X11, whose window manager draws the frame — so when XWayland is being forced, keep the native
/// frame, the same as a real X11 session. An explicit `VECTORCRAFT_TITLEBAR` override still wins.
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)
let choice = craft_window::Choice::probe(craft_window::Prefix::new("VECTORCRAFT"));
let opted_into_system = opted_into_system_title_bar(saved);
#[cfg(target_os = "linux")]
{
let pref = saved.and_then(|ui| ui.engine_prefs.get("linuxDisplayServer")).and_then(serde_json::Value::as_str);
// `overridden() == None` means the probe decided (no VECTORCRAFT_TITLEBAR). When the window
// will actually open on X11 (XWayland) — the user asked for it *and* the X11 libraries are
// present, the same two conditions the launch path checks below — that run is effectively
// X11, so keep the native frame. If the libraries are missing, startup stays on Wayland, so
// the probe's own decision (app-drawn on GNOME) must stand.
let forced_to_x11 = xwayland::wanted(pref, |name| std::env::var_os(name), xwayland::pen_attached) && xwayland::x11_libraries_found();
if choice.overridden().is_none() && forced_to_x11 {
return custom_titlebar_from(&craft_window::Frame::Desktop, opted_into_system);
}
}
custom_titlebar_from(choice.frame(), opted_into_system)
}
fn native_options(custom_titlebar: bool) -> eframe::NativeOptions {
@@ -1043,17 +1103,41 @@ mod tests {
#[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");
use craft_window::{Frame, Layout};
// The System Title Bar preference (Preferences › User Interface) is read the same way on
// every platform; a missing or unreadable value is `false`.
assert!(!super::opted_into_system_title_bar(None), "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);
assert!(!super::opted_into_system_title_bar(Some(&off)));
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));
assert!(super::opted_into_system_title_bar(Some(&on)));
// The suite-wide mapping (Frame + preference -> whether VectorCraft draws its own title
// bar). The desktop frame (macOS, Windows, X11, KDE/Wayland, …) always keeps native
// decorations; the app frame (GNOME/Wayland, or a `VECTORCRAFT_TITLEBAR` override) draws
// our own title bar — unless the user opted into the system title bar, which forces the
// native frame back on.
let app = Frame::App(Layout::default());
let desktop = Frame::Desktop;
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
assert!(super::custom_titlebar_from(&app, false), "GNOME/Wayland: VectorCraft draws its own");
assert!(!super::custom_titlebar_from(&app, true), "System Title Bar preference forces native");
assert!(!super::custom_titlebar_from(&desktop, false), "KDE/X11/…: keep the compositor's frame");
assert!(!super::custom_titlebar_from(&desktop, true));
}
// macOS and Windows keep today's behavior, independent of the probed frame: the pref still
// forces the native frame back on where a custom one would otherwise be drawn.
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
assert_eq!(super::custom_titlebar_from(&desktop, false), super::CUSTOM_TITLEBAR);
assert_eq!(super::custom_titlebar_from(&app, false), super::CUSTOM_TITLEBAR, "the probe does not change macOS/Windows");
assert!(!super::custom_titlebar_from(&desktop, true));
}
assert_eq!(super::native_options(false).viewport.decorations, Some(true));
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.