M3: draw a GNOME-style title bar on Wayland (#553) (#1530)

GNOME's Wayland compositor draws no window frames (it offers no
zxdg_decoration_manager_v1), so winit drew its own fallback title bar,
which ignores GNOME's theme, dark mode and button layout. PdfCraft now
asks the compositor at launch (apps/pdfcraft/src/decorations.rs) and,
where it draws no frames, turns winit's frame off and makes the tab strip
the header bar, as in GNOME's own apps
(crates/ui-egui/src/window_controls.rs):

- Minimize, maximize/restore and close in Adwaita's style (round buttons,
  symbolic glyphs drawn in code, no new assets), in the order and on the
  side named by org.gnome.desktop.wm.preferences button-layout, read
  through gsettings; Fedora's default `appmenu:close` shows only close.
  Hidden in full screen.
- Resize zones along the window's edges and L-shaped corners, above the
  rest of the UI so a press there resizes instead of reaching a
  scrollbar; a hairline outlines the window while it isn't maximized.
- Preferences > Use system title bar (shown only where the app draws the
  frame) gives the window back winit's frame at once, without a restart
  (part of #1001). Named as in VectorCraft and PhotoCraft, but GNOME draws
  no title bars for apps, so on GNOME this is winit's basic frame, not
  GNOME's own; the tooltip says so. Saved as `system_title_bar`;
  `system-title-bar` on the control channel.
- PDFCRAFT_TITLEBAR=app|system overrides the detection.

KDE, Sway, Hyprland, COSMIC and X11 offer server-side decorations and
keep the frame their desktop draws; macOS and Windows are unchanged.

Not done: right-clicking the header bar doesn't open GNOME's window menu
(egui has no command for it), the window has no shadow or rounded
corners, the resize zones lie just inside the window rather than outside
it, and Windows still uses its system frame (the rest of #1001).

The new labels (Minimize, Maximize, Restore, Use system title bar and its
tooltip) were added to all 21 translation catalogs. These translations
have not been checked by anyone fluent in those languages and should be
reviewed. The catalogs' hashes in ATTRIBUTION.toml are updated.

Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask
assets) and was checked by hand on Fedora 44 GNOME (light and dark,
maximized and not, the preference toggled live), but it has not been
exhaustively regression tested.

Co-authored-by: bmh129 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 03:04:13 -04:00
1 parent 7739685796
commit 7437bde378
32 files changed
+642 -33

No files matched your search

+21 -21
View File
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a05bbd3b58f64e8c2a06cb7803009c81de049716ef089fdd0abc56c7d0eb394b"
sha256 = "d0a69a35403e70b94c3adfef88b41ec719f4f6ab700d351c8f82a78c53b63ef6"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "30f26c55038c23956d79b6b85821890a55c82bc99108a0c982b679fccf918eeb"
sha256 = "caa23201b7f25dba9f8dc579771e13435e5e8c72a7efb9770e917a2a33003a39"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c92d6c1b4f92fe6cc0dda954da593e7c22821f524bff77c2cdf71796ba0ea8e3"
sha256 = "5ead9fccdca17810a3a98de3fa499d8a2469a1ff015c3d0e11880ca629295ea1"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "2a9b1347fe5eec8cf144c315592adb2999206c91c6be3c96115fa8871d2d18b0"
sha256 = "d48f9d68711850cae48af53f7691facee4209f7846e3eb2c064d97dad2ae2426"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b267b363ffd9a3d0b610f14ecf505543d53ce08c0603b78306be9a70a77d15ef"
sha256 = "6bc2edec99534d0df9b5e4a1c2e18dc32ad84c32e404c74dd437d86119b1d6ea"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "6ed7cd84692db0c02b322207206f9f1b79b47825c247e6bc4ea8e8231e3bdda2"
sha256 = "cb6c04bd36ba6ce0ebc808da2e47b65797e14d59466e93802adec4d4f5056e3b"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bc7823dca413e745370b5fb1640cfd80c8633dac959e3f9766a4e10271b323b7"
sha256 = "0368944155991c2ea304788418fb326f30e45649539a0cf49539194b026ca104"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a0d155d28ea5784bcd0bff539e3282e394ae74d08ee9ee2c156c3340009f74a8"
sha256 = "f4969c353f1424c6b9b5b110820c69dc59907590d013221511eae3f86c29024e"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ef3e873eadfaf5367e19834dcc892487b0eae1d089b11c0913835e0e24e9bd83"
sha256 = "4bb6fa4f3b6e236a4a353df79acd0c5528ea8354058eeaeb66c28bc0f7e02939"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "980062f140bbfe78ea077d1bc6ff66ea856c7e267d444054319d91b9b1fb915b"
sha256 = "adcbdfc683589b067e062a6200084bf9192935b297f5b616f8ed39db9c71db9b"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1433b66e2b40b590b6be1089d4201d6f74aac8116f67b79caca9048bfec444bf"
sha256 = "aebe63cd91df2d2f9d2bdc1da43b017823dbc8a32135e999b9ac7041277be0c8"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "fa5ecd1eb447cce1255098829a03af6909cd48fdaf3d4853c3ec322746b6f367"
sha256 = "3c8e3a28a5b751c6d6384809d4fd81c14c2ff5f561aaee5b58d24fbfa2ca4dc1"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "20ef278f871692830b2021996bd7038bf7ed5bfae959d8aaaddbdfb573a33d98"
sha256 = "637fcfdd0bf1898b413af7ccbc2eb4228bf656273de9f3ef9241bd128940aeab"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "38f753d18d4bac285d8b4eeb4cf5f84e97ae5fabbe7b63502fe46c4fe9cbdbde"
sha256 = "5285f8987bd8c265f73ff443906c1832a0886037592d334974cf539b9f972a3f"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "aae6a9f14b7b5d4ad702aaf2dc07046216ab1b4366c65215f52ff0cbb682ff04"
sha256 = "23779abc08be40ddfb305fd638fe280c854ac33473b2822006ba8bad399c0cad"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "42fe836106db7bce7a8e492b9642569df942a0695b330f8701cb85e0d6ea93ea"
sha256 = "c5129c40b202d8467d8d4cf0a189da8c2ba2af711998edb73f67e1b291289a04"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8c9d7fc2e7c040b40a4efbdad38183cc6e40adc5936f8faa1b23580b9adce704"
sha256 = "11ed2aa56dc5f8f6656eba717500c74bb8c8f931c08f9f9b210b5b33dd9c2d5a"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a18e085d8d9797a50d37bcb06daef2ce9675a2450d4db86c77faeeb6a66fd644"
sha256 = "a3863e2fed5ef584a87f65083a6b9fedf26d18ecebd574efce22bd7a01e3281c"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "448606a33ff4e953579ac38f7d614d2193b06771141b5cba005a35f6926a1d01"
sha256 = "cade94846289c2822b408a2ea23e6c8f8f321708f4f27fd563695a3c4fade6a1"
[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8e2d2bf1ec678cbdaf25a8c7f952b5bfb88d162a75bf8b627a742d823b131df7"
sha256 = "35b6b03dae01b9a2e98f6d1f682d0699d77772e6efec7e28e1f26039c57b151f"
[[asset]]
path = "assets/icons/accessibility.svg"
@@ -3351,4 +3351,4 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "fef63bd8be5ab43be8e48add9719b8418ddeb483f766408b34d280cdc08a3018"
sha256 = "2685fbb694cbdeb7632ecb5e7396e1209047c83319e28ceea032b3ec9fb2fc0e"
+1
View File
@@ -195,6 +195,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
Newest first: the date, what moved, and the new overall estimate.
- **2026-10-12 (M3, GNOME title bar, #553):** On GNOME's Wayland session, whose compositor draws no window frames (it offers no `zxdg_decoration_manager_v1`), PdfCraft no longer shows winit's fallback title bar: it turns the frame off and the tab strip becomes the header bar, as in GNOME's own apps. Minimize, maximize/restore and close are drawn in Adwaita's style, in the order and on the side GNOME's `button-layout` names (Fedora's default shows only close); the window's edges and corners resize it, and a hairline outlines it while it isn't maximized. Preferences ▸ Use system title bar (shown only there) gives the window back winit's frame at once, without a restart (part of #1001), and `PDFCRAFT_TITLEBAR=app|system` overrides the choice. KDE, Sway, Hyprland, COSMIC and X11 keep the frame their desktop draws. Not done: right-clicking the header bar doesn't open GNOME's window menu (egui can't ask for it), the window has no shadow or rounded corners, the resize edges lie just inside the window rather than outside it, and Windows still uses its system frame (the rest of #1001). Estimates unchanged.
- **2026-10-12 (M0, build label):** A local or patched build can carry a label, set at compile time with `PDFCRAFT_BUILD_LABEL`: `pdfcraft --version`, `pdfcraft-cli --version`, About and the log show `0.7.1 (titlebar)`, and `packaging/linux/package.sh` names the files `pdfcraft-0.7.1-titlebar-…` and makes the .rpm/.deb release `1.titlebar`, which upgrades over the release of the same version (and the next release over it). The version itself is unchanged, so the update check still compares it with published releases. Unlabelled builds are as before. Estimates unchanged.
- 2026-10-11: Organize Pages shows an editable, synchronized physical page-range selection (#1092), with bounded validation and UI/operation regression coverage; overall estimate unchanged.
+70
View File
@@ -0,0 +1,70 @@
//! Who draws the window frame on Linux.
//!
//! Wayland compositors that draw frames for their clients offer `zxdg_decoration_manager_v1`
//! (KDE, Sway, Hyprland, COSMIC). GNOME doesn't: it leaves the frame to the app, and winit then
//! drew an imitation of GNOME's. Without that protocol PdfCraft turns winit's frame off and draws
//! a header bar like GNOME's own apps (`pdfcraft_ui_egui::window_controls`), with the window
//! buttons GNOME's `button-layout` names. X11 window managers always draw the frame.
//!
//! `PDFCRAFT_TITLEBAR=app` or `=system` overrides the choice.
use pdfcraft_ui_egui::window_controls::Layout;
/// The window buttons to draw in the app, or `None` when the desktop draws the frame.
pub(crate) fn app_drawn() -> Option<Layout> {
let drawn = match std::env::var("PDFCRAFT_TITLEBAR").ok().as_deref().map(str::trim) {
Some("app") => true,
Some("system") => false,
_ => compositor_leaves_frame_to_app(),
};
drawn.then(button_layout)
}
/// GNOME's button order, or minimize, maximize and close on the right when it names none.
fn button_layout() -> Layout {
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))]
if let Some(value) = crate::text_scale::gsettings(&["get", "org.gnome.desktop.wm.preferences", "button-layout"]) {
let layout = Layout::parse(&value);
// A layout without a single button would leave no way to close the window by mouse.
if !layout.left.is_empty() || !layout.right.is_empty() {
return layout;
}
}
Layout::default()
}
/// Whether this is a Wayland session whose compositor draws no frames.
#[cfg(target_os = "linux")]
fn compositor_leaves_frame_to_app() -> bool {
use wayland_client::Connection;
use wayland_client::globals::registry_queue_init;
// No Wayland compositor: X11, whose window manager draws the frame (winit falls back to it).
let Ok(conn) = Connection::connect_to_env() else { return false };
let Ok((globals, _queue)) = registry_queue_init::<Registry>(&conn) else { return false };
let offered = globals.contents().with_list(|list| list.iter().any(|g| g.interface == "zxdg_decoration_manager_v1"));
if !offered {
log::info!("the Wayland compositor draws no window frames; PdfCraft draws its own title bar");
}
!offered
}
#[cfg(not(target_os = "linux"))]
fn compositor_leaves_frame_to_app() -> bool {
false
}
#[cfg(target_os = "linux")]
struct Registry;
#[cfg(target_os = "linux")]
impl wayland_client::Dispatch<wayland_client::protocol::wl_registry::WlRegistry, wayland_client::globals::GlobalListContents> for Registry {
fn event(
_: &mut Self,
_: &wayland_client::protocol::wl_registry::WlRegistry,
_: wayland_client::protocol::wl_registry::Event,
_: &wayland_client::globals::GlobalListContents,
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
) {
}
}
+26 -9
View File
@@ -32,6 +32,7 @@ use pdfcraft_ui_egui::PdfCraftApp;
#[cfg(target_os = "macos")]
mod apple_events;
mod compositor_gpu;
mod decorations;
mod logging;
#[cfg(target_os = "windows")]
mod motion;
@@ -199,6 +200,8 @@ fn main() -> eframe::Result {
}
let files = rejoin_split_paths(files, |p| std::path::Path::new(p).is_file());
let integrated = cfg!(target_os = "macos");
// GNOME on Wayland leaves the window frame to the app (src/decorations.rs).
let window_controls = if cfg!(any(target_os = "macos", target_os = "windows")) { None } else { decorations::app_drawn() };
migrate_legacy_folders();
// The log file lives in the settings folder; opened after the arguments (so `--version` leaves
// no file behind) and after the PrintCraft migration (which a fresh folder would block).
@@ -220,7 +223,7 @@ fn main() -> eframe::Result {
single_instance::Claim::Alone => None,
};
let choice = renderer_choice(std::env::var("PDFCRAFT_RENDERER").ok().as_deref());
let launch = Launch { files, options, control_file, create_images, integrated };
let launch = Launch { files, options, control_file, create_images, integrated, window_controls };
// Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one
// that launched us as well as later ones. Lives until the event loop returns.
#[cfg(target_os = "macos")]
@@ -230,7 +233,7 @@ fn main() -> eframe::Result {
let first = if choice == RendererChoice::Gl { eframe::Renderer::Glow } else { eframe::Renderer::Wgpu };
let result = eframe::run_native(
"PdfCraft",
native_options(integrated, first),
native_options(launch.integrated, launch.window_controls.is_some(), first),
app_creator(
launch.clone(),
Rc::clone(&started),
@@ -248,7 +251,7 @@ fn main() -> eframe::Result {
log::error!("the GPU renderer (wgpu) didn't start: {e}. Starting with OpenGL instead; set PDFCRAFT_RENDERER=gl to skip wgpu.");
eframe::run_native(
"PdfCraft",
native_options(integrated, eframe::Renderer::Glow),
native_options(launch.integrated, launch.window_controls.is_some(), eframe::Renderer::Glow),
app_creator(
launch,
started,
@@ -288,7 +291,7 @@ fn retry_with_gl(choice: RendererChoice, app_started: bool) -> bool {
}
/// The window and renderer settings for one run.
fn native_options(integrated: bool, renderer: eframe::Renderer) -> eframe::NativeOptions {
fn native_options(integrated: bool, app_frame: bool, renderer: eframe::Renderer) -> eframe::NativeOptions {
let mut viewport = egui::ViewportBuilder::default()
.with_title("PdfCraft")
.with_inner_size([1440.0, 920.0])
@@ -304,6 +307,10 @@ fn native_options(integrated: bool, renderer: eframe::Renderer) -> eframe::Nativ
if integrated {
viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false);
}
if app_frame {
// The tab strip is the title bar (pdfcraft_ui_egui::window_controls).
viewport = viewport.with_decorations(false);
}
let persistence_path = settings_dir().map(|d| d.join("app.ron"));
// Set explicitly, so the renderer never depends on which one eframe defaults to.
let mut native = eframe::NativeOptions { viewport, persistence_path, renderer, ..Default::default() };
@@ -321,6 +328,8 @@ struct Launch {
control_file: Option<String>,
create_images: bool,
integrated: bool,
/// The window buttons the app draws, when the desktop leaves the frame to it.
window_controls: Option<pdfcraft_ui_egui::window_controls::Layout>,
}
fn app_creator<'a>(
@@ -329,7 +338,7 @@ fn app_creator<'a>(
instance: Option<&'a single_instance::Server>,
#[cfg(target_os = "macos")] apple_events: &'a apple_events::AppleEvents,
) -> eframe::AppCreator<'a> {
let Launch { files, options, control_file, create_images, integrated } = launch;
let Launch { files, options, control_file, create_images, integrated, window_controls } = launch;
Box::new(move |cc| {
started.set(true);
let mut app = PdfCraftApp::new();
@@ -343,6 +352,7 @@ fn app_creator<'a>(
app.restore(&json);
}
app.integrated_titlebar = integrated;
app.window_controls = window_controls;
app.system_text_scale = text_scale::system();
// Windows' Animation effects setting, which Preferences ▸ Animation effects follows unless
// it is set to On or Off.
@@ -805,8 +815,15 @@ mod tests {
assert!(!retry_with_gl(RendererChoice::Wgpu, false));
assert!(!retry_with_gl(RendererChoice::Gl, false));
// Each run states its renderer rather than relying on eframe's default.
assert_eq!(native_options(false, eframe::Renderer::Wgpu).renderer, eframe::Renderer::Wgpu);
assert_eq!(native_options(false, eframe::Renderer::Glow).renderer, eframe::Renderer::Glow);
assert_eq!(native_options(false, false, eframe::Renderer::Wgpu).renderer, eframe::Renderer::Wgpu);
assert_eq!(native_options(false, false, eframe::Renderer::Glow).renderer, eframe::Renderer::Glow);
}
#[test]
fn the_desktop_frame_is_turned_off_only_when_the_app_draws_one() {
use super::native_options;
assert_eq!(native_options(false, false, eframe::Renderer::Glow).viewport.decorations, None);
assert_eq!(native_options(false, true, eframe::Renderer::Glow).viewport.decorations, Some(false));
}
#[test]
@@ -957,7 +974,7 @@ mod tests {
// than the device allows.
use eframe::wgpu;
let _gpu = gpu();
let native = super::native_options(false, eframe::Renderer::Wgpu);
let native = super::native_options(false, false, eframe::Renderer::Wgpu);
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &native.wgpu_options.wgpu_setup else {
panic!("default setup creates its own instance")
};
@@ -1024,7 +1041,7 @@ mod tests {
fn pipeline_creation_error_is_returned_and_device_remains_usable() {
use eframe::{egui_wgpu, wgpu};
let _gpu = gpu();
let mut native = super::native_options(false, eframe::Renderer::Wgpu);
let mut native = super::native_options(false, false, eframe::Renderer::Wgpu);
let egui_wgpu::WgpuSetup::CreateNew(setup) = &native.wgpu_options.wgpu_setup else { panic!("default setup creates its own instance") };
let instance = pollster::block_on(native.wgpu_options.wgpu_setup.new_instance());
let options = wgpu::RequestAdapterOptions { power_preference: setup.power_preference, ..Default::default() };
+1 -1
View File
@@ -27,7 +27,7 @@ fn parse(output: &str) -> f32 {
/// Run `gsettings` and return what it printed, or `None` if it is missing, fails or hangs.
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))]
fn gsettings(args: &[&str]) -> Option<String> {
pub(crate) fn gsettings(args: &[&str]) -> Option<String> {
use std::io::Read;
use std::process::{Command, Stdio};
let mut child = Command::new("gsettings").args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().ok()?;
+29 -2
View File
@@ -4,7 +4,7 @@ use egui::{Align, Align2, Color32, CornerRadius, Layout, Rect, Sense, Stroke, ve
use crate::canvas::{DocView, Fit, PageLayout};
use crate::theme::{self, ThemePreference, Tokens};
use crate::{Dialog, Mode, PdfCraftApp, PropsTab, RightPanel, icons, widgets};
use crate::{Dialog, Mode, PdfCraftApp, PropsTab, RightPanel, icons, widgets, window_controls};
/// Height of the tab strip when it doubles as the macOS title bar (#789). AppKit draws the traffic
/// lights itself, centred in the standard 28 pt title-bar band at the top of the window, and moving
@@ -46,14 +46,31 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
}
// GNOME on Wayland: the strip is the header bar, with the window buttons at its ends.
let fullscreen = ui.ctx().input(|i| i.viewport().fullscreen.unwrap_or(false));
let controls = app
.window_controls
.clone()
.filter(|_| app.draws_window_frame() && !fullscreen)
.unwrap_or_else(|| window_controls::Layout { left: Vec::new(), right: Vec::new() });
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if !controls.left.is_empty() {
ui.scope(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
window_controls::buttons(ui, &controls.left);
});
ui.add_space(window_controls::SIDE_GAP - 4.0);
}
if icons::button(ui, "house", button, app.active.is_none() && !app.combine_showing(), tl!("Home")).clicked() {
app.active = None;
app.combine_tab.focused = false;
}
// Keep the two icon buttons, Discord's text/padding and three gaps outside the scrolling tabs.
let controls_width = ui.fonts_mut(|f| f.layout_no_wrap("Discord".into(), theme::medium(13.0), t.text).size().x) + 2.0 * button + 50.0;
let controls_width = ui.fonts_mut(|f| f.layout_no_wrap("Discord".into(), theme::medium(13.0), t.text).size().x)
+ 2.0 * button
+ 50.0
+ window_controls::width(&controls.right);
// Open stays in sight after the tabs: its icon, text and padding, and the gaps around it.
let open_width = ui.fonts_mut(|f| f.layout_no_wrap(tl!("Open").into(), theme::medium(13.0), t.text).size().x) + 38.0 + 12.0;
let mut tabs_width = (ui.available_width() - controls_width - open_width).max(0.0);
@@ -194,6 +211,16 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
app.open_dialog();
}
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if !controls.right.is_empty() {
// Laid out left to right inside the right-to-left run, so they read in
// the desktop's order.
let size = egui::vec2(window_controls::width(&controls.right) - window_controls::SIDE_GAP, window_controls::BUTTON);
ui.allocate_ui_with_layout(size, Layout::left_to_right(Align::Center), |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
window_controls::buttons(ui, &controls.right);
});
ui.add_space(window_controls::SIDE_GAP - 4.0);
}
let (icon, label) = match app.theme_preference {
ThemePreference::System => ("monitor", tl!("Use system setting")),
ThemePreference::Light => ("sun", tl!("Light gray")),
+5
View File
@@ -2360,6 +2360,11 @@ animation effects Off معطّلة
The linked PDF opened, but its named destination was not applied. فُتح ملف PDF المرتبط، لكن لم تُطبَّق وجهته المسماة.
The linked PDF opened, but its page destination was out of range. فُتح ملف PDF المرتبط، لكن وجهة الصفحة خارج النطاق.
Local cross-PDF links require the desktop app تتطلب الروابط المحلية بين ملفات PDF تطبيق سطح المكتب
Minimize تصغير
Maximize تكبير
Restore استعادة
Use system title bar استخدام شريط عنوان النظام
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. إظهار شريط عنوان بسيط لأزرار النافذة بدلًا من شريط الألسنة. لا يرسم GNOME أشرطة العناوين للتطبيقات، لذلك لا يتبع هذا الشريط سمته.
# Absolute local GoToR confirmation (#1364).
Allow السماح
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2302,6 +2302,11 @@ animation effects Off Изключени
The linked PDF opened, but its named destination was not applied. Свързаният PDF се отвори, но именуваната му цел не беше приложена.
The linked PDF opened, but its page destination was out of range. Свързаният PDF се отвори, но целевата му страница е извън обхвата.
Local cross-PDF links require the desktop app Локалните връзки между PDF файлове изискват настолното приложение
Minimize Минимизиране
Maximize Максимизиране
Restore Възстановяване
Use system title bar Използване на системната заглавна лента
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Показване на обикновена заглавна лента за бутоните на прозореца вместо лентата с раздели. GNOME не рисува заглавни ленти за приложенията, затова тази не следва темата му.
# Absolute local GoToR confirmation (#1364).
Allow Разреши
Can't render this file because it contains an unexpected character in line 8 and column 16.
+5
View File
@@ -2413,6 +2413,11 @@ animation effects Off Vypnuto
The linked PDF opened, but its named destination was not applied. Odkazovaný PDF se otevřel, ale jeho pojmenovaný cíl nebyl použit.
The linked PDF opened, but its page destination was out of range. Odkazovaný PDF se otevřel, ale cílová stránka je mimo rozsah.
Local cross-PDF links require the desktop app Místní odkazy mezi soubory PDF vyžadují desktopovou aplikaci
Minimize Minimalizovat
Maximize Maximalizovat
Restore Obnovit
Use system title bar Použít systémové záhlaví okna
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Zobrazit jednoduché záhlaví s tlačítky okna místo pruhu karet. GNOME aplikacím záhlaví nekreslí, proto toto záhlaví nesleduje jeho motiv.
# Absolute local GoToR confirmation (#1364).
Allow Povolit
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2340,6 +2340,11 @@ animation effects Off Aus
The linked PDF opened, but its named destination was not applied. Die verknüpfte PDF wurde geöffnet, aber ihr benanntes Ziel wurde nicht angewendet.
The linked PDF opened, but its page destination was out of range. Die verknüpfte PDF wurde geöffnet, aber ihr Seitenziel liegt außerhalb des Bereichs.
Local cross-PDF links require the desktop app Lokale Verknüpfungen zwischen PDFs erfordern die Desktop-App
Minimize Minimieren
Maximize Maximieren
Restore Wiederherstellen
Use system title bar System-Titelleiste verwenden
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Eine einfache Titelleiste für die Fensterknöpfe statt der Tableiste anzeigen. GNOME zeichnet für Apps keine Titelleisten, daher folgt diese nicht seinem Design.
# Absolute local GoToR confirmation (#1364).
Allow Zulassen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+5
View File
@@ -2349,6 +2349,11 @@ animation effects Off Ανενεργά
Local cross-PDF links require the desktop app Οι τοπικοί σύνδεσμοι μεταξύ PDF απαιτούν την εφαρμογή υπολογιστή
The existing action will be preserved. Η υπάρχουσα ενέργεια θα διατηρηθεί.
This form's scripts are turned off because one ran too long. Reopen the document to run them again. Τα σενάρια αυτής της φόρμας είναι απενεργοποιημένα, επειδή ένα από αυτά εκτελούνταν για πολύ ώρα. Ανοίξτε ξανά το έγγραφο για να εκτελεστούν ξανά.
Minimize Ελαχιστοποίηση
Maximize Μεγιστοποίηση
Restore Επαναφορά
Use system title bar Χρήση γραμμής τίτλου συστήματος
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Εμφάνιση απλής γραμμής τίτλου για τα κουμπιά του παραθύρου αντί για τη γραμμή καρτελών. Το GNOME δεν σχεδιάζει γραμμές τίτλου για τις εφαρμογές, οπότε αυτή δεν ακολουθεί το θέμα του.
# Absolute local GoToR confirmation (#1364).
Allow Να επιτραπεί
Can't render this file because it contains an unexpected character in line 467 and column 9.
+5
View File
@@ -2469,6 +2469,11 @@ animation effects Off Desactivados
The linked PDF opened, but its page destination was out of range. El PDF vinculado se abrió, pero su página de destino está fuera de rango.
Local cross-PDF links require the desktop app Los enlaces locales entre PDF requieren la aplicación de escritorio
The existing action will be preserved. Se conservará la acción existente.
Minimize Minimizar
Maximize Maximizar
Restore Restaurar
Use system title bar Usar la barra de título del sistema
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Mostrar una barra de título básica para los botones de la ventana en lugar de la barra de pestañas. GNOME no dibuja barras de título para las aplicaciones, así que esta no sigue su tema.
# Absolute local GoToR confirmation (#1364).
Allow Permitir
Can't render this file because it contains an unexpected character in line 6 and column 60.
+5
View File
@@ -2339,6 +2339,11 @@ animation effects Off Désactivés
The linked PDF opened, but its named destination was not applied. Le PDF lié s'est ouvert, mais sa destination nommée n'a pas été appliquée.
The linked PDF opened, but its page destination was out of range. Le PDF lié s'est ouvert, mais sa page de destination est hors limites.
Local cross-PDF links require the desktop app Les liens locaux entre PDF nécessitent l'application de bureau
Minimize Réduire
Maximize Agrandir
Restore Restaurer
Use system title bar Utiliser la barre de titre du système
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Afficher une barre de titre simple pour les boutons de la fenêtre au lieu de la barre d’onglets. GNOME ne dessine pas de barre de titre pour les applications : celle-ci ne suit donc pas son thème.
# Absolute local GoToR confirmation (#1364).
Allow Autoriser
Can't render this file because it contains an unexpected character in line 6 and column 39.
+5
View File
@@ -2338,6 +2338,11 @@ animation effects Off Ki
The linked PDF opened, but its named destination was not applied. A hivatkozott PDF megnyílt, de a nevesített cél nem lett alkalmazva.
The linked PDF opened, but its page destination was out of range. A hivatkozott PDF megnyílt, de a céloldal a tartományon kívül esik.
Local cross-PDF links require the desktop app A PDF-ek közötti helyi hivatkozásokhoz az asztali alkalmazás szükséges
Minimize Minimalizálás
Maximize Maximalizálás
Restore Visszaállítás
Use system title bar Rendszer címsorának használata
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Egyszerű címsor megjelenítése az ablakgombokkal a lapsáv helyett. A GNOME nem rajzol címsort az alkalmazásoknak, ezért ez nem követi a témáját.
# Absolute local GoToR confirmation (#1364).
Allow Engedélyezés
Can't render this file because it contains an unexpected character in line 7 and column 44.
+5
View File
@@ -2348,6 +2348,11 @@ animation effects Off Disattivati
The linked PDF opened, but its named destination was not applied. Il PDF collegato è stato aperto, ma la sua destinazione con nome non è stata applicata.
The linked PDF opened, but its page destination was out of range. Il PDF collegato è stato aperto, ma la pagina di destinazione è fuori intervallo.
Local cross-PDF links require the desktop app I collegamenti locali tra PDF richiedono l'app desktop
Minimize Riduci a icona
Maximize Ingrandisci
Restore Ripristina
Use system title bar Usa la barra del titolo di sistema
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Mostra una barra del titolo semplice per i pulsanti della finestra invece della barra delle schede. GNOME non disegna le barre del titolo per le app, quindi questa non segue il suo tema.
# Absolute local GoToR confirmation (#1364).
Allow Consenti
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2339,6 +2339,11 @@ animation effects Off オフ
The linked PDF opened, but its named destination was not applied. リンク先の PDF を開きましたが、名前付き移動先は適用されませんでした。
The linked PDF opened, but its page destination was out of range. リンク先の PDF を開きましたが、移動先のページが範囲外です。
Local cross-PDF links require the desktop app PDF 間のローカルリンクにはデスクトップアプリが必要です
Minimize 最小化
Maximize 最大化
Restore 元のサイズに戻す
Use system title bar システムのタイトルバーを使う
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. タブバーの代わりに、ウィンドウボタン用のシンプルなタイトルバーを表示します。GNOME はアプリのタイトルバーを描画しないため、このタイトルバーは GNOME のテーマに従いません。
# Absolute local GoToR confirmation (#1364).
Allow 許可
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2337,6 +2337,11 @@ animation effects Off Өшірулі
The linked PDF opened, but its named destination was not applied. Байланысты PDF ашылды, бірақ оның аталған мақсаты қолданылмады.
The linked PDF opened, but its page destination was out of range. Байланысты PDF ашылды, бірақ оның мақсатты беті ауқымнан тыс.
Local cross-PDF links require the desktop app PDF файлдары арасындағы жергілікті сілтемелер үшін жұмыс үстелі қолданбасы қажет
Minimize Жию
Maximize Жаю
Restore Қалпына келтіру
Use system title bar Жүйелік тақырып жолағын пайдалану
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Терезе батырмалары үшін қойындылар жолағының орнына қарапайым тақырып жолағын көрсету. GNOME қолданбаларға тақырып жолағын салмайды, сондықтан бұл жолақ оның тақырыбына сәйкес келмейді.
# Absolute local GoToR confirmation (#1364).
Allow Рұқсат беру
Can't render this file because it contains an unexpected character in line 814 and column 9.
+5
View File
@@ -2583,6 +2583,11 @@ key Esc Esc
key Home Home
key End End
key Space Tarpas
Minimize Sumažinti
Maximize Išdidinti
Restore Atkurti
Use system title bar Naudoti sistemos pavadinimo juostą
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Rodyti paprastą pavadinimo juostą su lango mygtukais vietoj kortelių juostos. GNOME nepiešia programoms pavadinimo juostų, todėl ši neatitinka jo temos.
# Absolute local GoToR confirmation (#1364).
Allow Leisti
Can't render this file because it contains an unexpected character in line 393 and column 9.
+5
View File
@@ -2319,6 +2319,11 @@ animation effects Off Wyłączone
Local cross-PDF links require the desktop app Lokalne łącza między plikami PDF wymagają aplikacji komputerowej
The existing action will be preserved. Istniejąca akcja zostanie zachowana.
This form's scripts are turned off because one ran too long. Reopen the document to run them again. Skrypty tego formularza są wyłączone, ponieważ jeden z nich działał zbyt długo. Otwórz dokument ponownie, aby znów je uruchomić.
Minimize Zminimalizuj
Maximize Zmaksymalizuj
Restore Przywróć
Use system title bar Używaj systemowego paska tytułu
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Pokazuj prosty pasek tytułu z przyciskami okna zamiast paska kart. GNOME nie rysuje pasków tytułu dla aplikacji, więc ten nie pasuje do jego motywu.
# Absolute local GoToR confirmation (#1364).
Allow Zezwól
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2353,6 +2353,11 @@ animation effects Off Desativados
The linked PDF opened, but its named destination was not applied. O PDF vinculado foi aberto, mas seu destino nomeado não foi aplicado.
The linked PDF opened, but its page destination was out of range. O PDF vinculado foi aberto, mas sua página de destino está fora do intervalo.
Local cross-PDF links require the desktop app Links locais entre PDFs exigem o aplicativo para desktop
Minimize Minimizar
Maximize Maximizar
Restore Restaurar
Use system title bar Usar a barra de título do sistema
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Mostrar uma barra de título simples para os botões da janela em vez da barra de abas. O GNOME não desenha barras de título para os aplicativos, então esta não segue o tema dele.
# Absolute local GoToR confirmation (#1364).
Allow Permitir
Can't render this file because it contains an unexpected character in line 9 and column 16.
+5
View File
@@ -2252,6 +2252,11 @@ animation effects Off Выкл
The linked PDF opened, but its named destination was not applied. Связанный PDF открыт, но его именованное назначение не применено.
The linked PDF opened, but its page destination was out of range. Связанный PDF открыт, но страница назначения вне допустимого диапазона.
Local cross-PDF links require the desktop app Локальные ссылки между PDF работают только в настольном приложении
Minimize Свернуть
Maximize Развернуть
Restore Восстановить
Use system title bar Использовать системный заголовок окна
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Показывать простой заголовок окна с кнопками вместо полосы вкладок. GNOME не рисует заголовки для приложений, поэтому этот заголовок не следует его теме.
# Absolute local GoToR confirmation (#1364).
Allow Разрешить
Can't render this file because it contains an unexpected character in line 9 and column 16.
+5
View File
@@ -2344,6 +2344,11 @@ animation effects Off Izklopljeni
The linked PDF opened, but its named destination was not applied. Povezani PDF se je odprl, vendar njegov poimenovani cilj ni bil uporabljen.
The linked PDF opened, but its page destination was out of range. Povezani PDF se je odprl, vendar je ciljna stran zunaj obsega.
Local cross-PDF links require the desktop app Lokalne povezave med PDF-ji zahtevajo namizno aplikacijo
Minimize Pomanjšaj
Maximize Povečaj
Restore Obnovi
Use system title bar Uporabi sistemsko naslovno vrstico
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Prikaži preprosto naslovno vrstico za gumbe okna namesto vrstice zavihkov. GNOME aplikacijam ne riše naslovnih vrstic, zato ta ne sledi njegovi temi.
# Absolute local GoToR confirmation (#1364).
Allow Dovoli
Can't render this file because it contains an unexpected character in line 9 and column 16.
+5
View File
@@ -2252,6 +2252,11 @@ animation effects Off ఆఫ్
Local cross-PDF links require the desktop app PDFల మధ్య స్థానిక లింక్‌లకు డెస్క్‌టాప్ యాప్ అవసరం
The existing action will be preserved. ఉన్న చర్య అలాగే ఉంచబడుతుంది.
This form's scripts are turned off because one ran too long. Reopen the document to run them again. ఒక స్క్రిప్ట్ చాలా సేపు నడిచినందున ఈ ఫారమ్ స్క్రిప్ట్‌లు ఆపివేయబడ్డాయి. వాటిని మళ్లీ నడపడానికి పత్రాన్ని మళ్లీ తెరవండి.
Minimize కనిష్ఠీకరించు
Maximize గరిష్ఠీకరించు
Restore పునరుద్ధరించు
Use system title bar సిస్టమ్ శీర్షిక పట్టీని ఉపయోగించు
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. ట్యాబ్ పట్టీకి బదులుగా విండో బటన్‌ల కోసం సాధారణ శీర్షిక పట్టీని చూపించు. GNOME యాప్‌లకు శీర్షిక పట్టీలను గీయదు, కాబట్టి ఇది దాని థీమ్‌ను అనుసరించదు.
# Absolute local GoToR confirmation (#1364).
Allow అనుమతించు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+5
View File
@@ -2335,6 +2335,11 @@ animation effects Off Kapalı
The linked PDF opened, but its named destination was not applied. Bağlantılı PDF açıldı, ancak adlandırılmış hedefi uygulanmadı.
The linked PDF opened, but its page destination was out of range. Bağlantılı PDF açıldı, ancak sayfa hedefi aralık dışında.
Local cross-PDF links require the desktop app PDF'ler arası yerel bağlantılar masaüstü uygulamasını gerektirir
Minimize Küçült
Maximize Büyüt
Restore Geri yükle
Use system title bar Sistem başlık çubuğunu kullan
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Sekme çubuğu yerine pencere düğmeleri için basit bir başlık çubuğu göster. GNOME uygulamalar için başlık çubuğu çizmez, bu yüzden bu çubuk onun temasına uymaz.
# Absolute local GoToR confirmation (#1364).
Allow İzin ver
Can't render this file because it contains an unexpected character in line 813 and column 9.
+5
View File
@@ -2361,6 +2361,11 @@ animation effects Off Вимкнено
The linked PDF opened, but its named destination was not applied. Пов’язаний PDF відкрито, але його іменоване призначення не застосовано.
The linked PDF opened, but its page destination was out of range. Пов’язаний PDF відкрито, але сторінка призначення поза межами діапазону.
Local cross-PDF links require the desktop app Локальні посилання між PDF працюють лише в настільному застосунку
Minimize Згорнути
Maximize Розгорнути
Restore Відновити
Use system title bar Використовувати системний заголовок вікна
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Показувати простий заголовок вікна з кнопками замість панелі вкладок. GNOME не малює заголовки для програм, тому цей заголовок не відповідає його темі.
# Absolute local GoToR confirmation (#1364).
Allow Дозволити
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2303,6 +2303,11 @@ animation effects Off 关
The linked PDF opened, but its named destination was not applied. 已打开链接的 PDF,但未应用其命名目标。
The linked PDF opened, but its page destination was out of range. 已打开链接的 PDF,但其页面目标超出范围。
Local cross-PDF links require the desktop app PDF 之间的本地链接需要桌面应用
Minimize 最小化
Maximize 最大化
Restore 还原
Use system title bar 使用系统标题栏
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. 用简单的标题栏显示窗口按钮,而不是在标签栏中。GNOME 不为应用绘制标题栏,因此此标题栏不会跟随其主题。
# Absolute local GoToR confirmation (#1364).
Allow 允许
Can't render this file because it contains an unexpected character in line 9 and column 16.
+5
View File
@@ -2213,6 +2213,11 @@ animation effects Off 關
The linked PDF opened, but its page destination was out of range. 已開啟連結的 PDF,但其頁面目的地超出範圍。
Local cross-PDF links require the desktop app PDF 之間的本機連結需要桌面應用程式
The existing action will be preserved. 將保留現有動作。
Minimize 最小化
Maximize 最大化
Restore 還原
Use system title bar 使用系統標題列
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. 以簡單的標題列顯示視窗按鈕,而不是在分頁列中。GNOME 不為應用程式繪製標題列,因此此標題列不會跟隨其佈景主題。
# Absolute local GoToR confirmation (#1364).
Allow 允許
Can't render this file because it contains an unexpected character in line 9 and column 16.
+5
View File
@@ -395,6 +395,11 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
widgets::radio_value(ui, &mut app.animation_effects, choice, tl_ctx!("animation effects", choice.label()));
}
});
// Only where the app draws the title bar (GNOME on Wayland); elsewhere the desktop draws it.
if app.window_controls.is_some() {
ui.add(widgets::checkbox(&mut app.system_title_bar, tl!("Use system title bar")))
.on_hover_text(tl!("Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme."));
}
ui.add_space(8.0);
// Reading colours (Acrobat: Replace Document Colors): solid text and line-art paints are
// remapped while rendering, and the page clears to the background colour. The preference
+49
View File
@@ -131,6 +131,7 @@ pub mod ui_scale;
pub mod updates;
mod wheel_pager;
mod widgets;
pub mod window_controls;
#[cfg(all(test, not(target_arch = "wasm32")))]
mod open_worker_tests;
@@ -574,6 +575,14 @@ pub struct PdfCraftApp {
pub toast: Option<(String, f64)>,
/// Whether the macOS title bar is drawn by us (traffic lights over our tab strip).
pub integrated_titlebar: bool,
/// Set when the desktop leaves the window frame to the app (GNOME on Wayland): the tab strip
/// carries these window buttons and the window's edges resize it.
pub window_controls: Option<window_controls::Layout>,
/// Preferences ▸ Use system title bar: where `window_controls` is set, give the window back
/// winit's frame instead of drawing the header bar (#1001).
pub system_title_bar: bool,
/// Whether the window currently has the app's header bar (`None` until the first frame).
app_frame_shown: Option<bool>,
pub password_prompt: Option<PasswordPrompt>,
pub unlock_prompt: Option<UnlockPrompt>,
pub full_screen: bool,
@@ -912,6 +921,9 @@ impl PdfCraftApp {
pinned: Default::default(),
toast: None,
integrated_titlebar: false,
window_controls: None,
system_title_bar: false,
app_frame_shown: None,
password_prompt: None,
unlock_prompt: None,
full_screen: false,
@@ -1455,6 +1467,28 @@ impl PdfCraftApp {
}
}
/// Whether the app draws the window's title bar, buttons and resize edges: the desktop leaves
/// the frame to the app and Use system title bar is off.
pub fn draws_window_frame(&self) -> bool {
self.window_controls.is_some() && !self.system_title_bar
}
/// Follow Use system title bar at once: the window starts without winit's frame wherever the
/// app may draw its own, and gets it back (or loses it again) when the preference changes.
fn sync_window_frame(&mut self, ctx: &egui::Context) {
if self.window_controls.is_none() {
return;
}
let app_frame = self.draws_window_frame();
if self.app_frame_shown != Some(app_frame) {
// The window opens undecorated, so only a change from that needs a command.
if self.app_frame_shown.is_some() || !app_frame {
ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(!app_frame));
}
self.app_frame_shown = Some(app_frame);
}
}
/// Enter or leave full-screen reading (Acrobat: View ▸ Full Screen Mode, ⌘L).
pub fn set_full_screen(&mut self, ctx: &egui::Context, on: bool) {
self.full_screen = on;
@@ -2049,6 +2083,7 @@ impl PdfCraftApp {
// Kept only while the preference is on.
"last_session": self.reopen_last_session.then(|| self.session_to_save()),
"reload_changed_files": self.reload_changed_files,
"system_title_bar": self.system_title_bar,
"pinned_folders": self.pinned.folders,
"theme": self.theme_preference,
"scroll_speed": self.scroll_speed,
@@ -2131,6 +2166,9 @@ impl PdfCraftApp {
self.reopen_last_session = on;
}
self.last_session = last_session::LastSession::from_json(&v["last_session"]);
if let Some(on) = v["system_title_bar"].as_bool() {
self.system_title_bar = on;
}
if let Some(on) = v["reload_changed_files"].as_bool() {
self.reload_changed_files = on;
}
@@ -2496,6 +2534,13 @@ impl PdfCraftApp {
}
("author", _) => self.comment_prefs.author = value.to_string(),
("ui-scale", _) => self.ui_scale = ui_scale::parse(value)?,
("system-title-bar", _) => {
self.system_title_bar = match value {
"true" => true,
"false" => false,
_ => return Err("system-title-bar must be true or false".into()),
};
}
("flatten-fill-sign", _) => {
self.flatten_fill_sign_on_save = match value {
"true" => true,
@@ -2884,6 +2929,10 @@ impl eframe::App for PdfCraftApp {
self.finish_render_frame(&ctx);
return;
}
self.sync_window_frame(&ctx);
if self.draws_window_frame() {
window_controls::resize_edges(&ctx);
}
chrome::tab_strip(self, ui);
chrome::mode_bar(self, ui);
if self.active.is_some() {
+288
View File
@@ -0,0 +1,288 @@
//! Window buttons and resize edges for desktops that leave the window frame to the app.
//!
//! GNOME on Wayland has no server-side decorations (the compositor offers no
//! `zxdg_decoration_manager_v1`), so winit drew its own imitation frame. Instead the app turns the
//! frame off and the tab strip becomes the header bar, as in GNOME's own apps: it carries the
//! minimize, maximize and close buttons in the desktop's order (GNOME's `button-layout`), and
//! thin zones along the window's edges resize it. Desktops with server-side decorations (KDE,
//! Sway, Hyprland, X11) keep their own frame and never get here.
use crate::theme::Tokens;
use egui::viewport::ResizeDirection;
use egui::{Align2, Color32, CursorIcon, Pos2, Rect, Sense, Stroke, Vec2};
/// A window button.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Button {
Minimize,
Maximize,
Close,
}
/// Which buttons go on which end of the header bar, outermost last.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Layout {
pub left: Vec<Button>,
pub right: Vec<Button>,
}
impl Default for Layout {
/// What a desktop that names no layout gets: all three, on the right.
fn default() -> Self {
Self { left: Vec::new(), right: vec![Button::Minimize, Button::Maximize, Button::Close] }
}
}
impl Layout {
/// GNOME's `org.gnome.desktop.wm.preferences button-layout`: `left:right`, each a comma list
/// such as `appmenu:minimize,maximize,close` (Fedora's default is `appmenu:close`). Without a
/// colon everything is on the left, as in GTK. Names this app has no button for (`appmenu`,
/// `icon`, `menu`, `spacer`) are skipped, and a button named twice appears once. `gsettings`
/// prints the value quoted; the quotes are dropped.
pub fn parse(value: &str) -> Self {
let value = value.trim().trim_matches(|c| c == '\'' || c == '"');
let (left, right) = value.split_once(':').unwrap_or((value, ""));
let mut seen = Vec::new();
let mut side = |list: &str| {
let mut out = Vec::new();
for name in list.split(',') {
let button = match name.trim() {
"minimize" => Button::Minimize,
"maximize" => Button::Maximize,
"close" => Button::Close,
_ => continue,
};
if !seen.contains(&button) {
seen.push(button);
out.push(button);
}
}
out
};
let left = side(left);
let right = side(right);
Self { left, right }
}
}
/// The round button's diameter, as in Adwaita.
pub const BUTTON: f32 = 24.0;
/// Between two window buttons.
pub const GAP: f32 = 6.0;
/// Between the window buttons and the rest of the strip.
pub const SIDE_GAP: f32 = 10.0;
/// The width `buttons` take in the strip, including the gap to the rest of it.
pub fn width(buttons: &[Button]) -> f32 {
match buttons.len() {
0 => 0.0,
n => n as f32 * BUTTON + (n - 1) as f32 * GAP + SIDE_GAP,
}
}
/// Draw the window buttons for one end of the strip, in reading order, inside a horizontal
/// layout running left to right; the caller adds [`SIDE_GAP`] where the strip continues.
pub fn buttons(ui: &mut egui::Ui, buttons: &[Button]) {
let t = Tokens::get(ui.ctx());
let maximized = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
for (n, &button) in buttons.iter().enumerate() {
if n > 0 {
ui.add_space(GAP);
}
let label = match button {
Button::Minimize => tl!("Minimize"),
Button::Maximize if maximized => tl!("Restore"),
Button::Maximize => tl!("Maximize"),
Button::Close => tl!("Close"),
};
let (rect, response) = ui.allocate_exact_size(Vec2::splat(BUTTON), Sense::click());
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, label));
let response = response.on_hover_text(label);
if ui.is_rect_visible(rect) {
paint(ui.painter(), rect, button, maximized, &t, &response);
}
if response.clicked() {
let command = match button {
Button::Minimize => egui::ViewportCommand::Minimized(true),
Button::Maximize => egui::ViewportCommand::Maximized(!maximized),
// The same path as the desktop's close: unsaved documents still ask first.
Button::Close => egui::ViewportCommand::Close,
};
ui.ctx().send_viewport_cmd(command);
}
}
}
/// Adwaita's look: a round wash of the text colour, stronger on hover and press, and a 16-point
/// symbolic glyph drawn with lines (no icon files).
fn paint(painter: &egui::Painter, rect: Rect, button: Button, maximized: bool, t: &Tokens, response: &egui::Response) {
let alpha = if response.is_pointer_button_down_on() {
0.28
} else if response.hovered() {
0.16
} else {
0.10
};
painter.circle_filled(rect.center(), BUTTON / 2.0, t.text.gamma_multiply(alpha));
if response.has_focus() {
painter.circle_stroke(rect.center(), BUTTON / 2.0 - 0.5, Stroke::new(1.0, t.accent));
}
let stroke = Stroke::new(1.25, t.text);
// The glyph's 16-point square, its corners on pixel centres so lines stay crisp.
let c = rect.center();
let at = |x: f32, y: f32| Pos2::new((c.x - 8.0 + x).round() + 0.5, (c.y - 8.0 + y).round() + 0.5);
match button {
Button::Minimize => {
painter.line_segment([at(4.0, 11.0), at(11.0, 11.0)], stroke);
}
Button::Maximize if maximized => {
// Restore: a smaller window in front of another.
painter.rect_stroke(Rect::from_two_pos(at(4.0, 6.0), at(10.0, 12.0)), 1.0, stroke, egui::StrokeKind::Middle);
painter.line_segment([at(6.0, 6.0), at(6.0, 4.0)], stroke);
painter.line_segment([at(6.0, 4.0), at(12.0, 4.0)], stroke);
painter.line_segment([at(12.0, 4.0), at(12.0, 10.0)], stroke);
painter.line_segment([at(12.0, 10.0), at(10.0, 10.0)], stroke);
}
Button::Maximize => {
painter.rect_stroke(Rect::from_two_pos(at(4.0, 4.0), at(11.0, 11.0)), 1.0, stroke, egui::StrokeKind::Middle);
}
Button::Close => {
painter.line_segment([at(4.0, 4.0), at(11.0, 11.0)], stroke);
painter.line_segment([at(11.0, 4.0), at(4.0, 11.0)], stroke);
}
}
}
/// How far into the window an edge resizes it.
const EDGE: f32 = 5.0;
/// How far along an edge a corner reaches, so corners are easy to hit.
const CORNER: f32 = 16.0;
/// The resize zones of a window filling `rect`: edges `edge` deep, and each corner an L of two
/// strips reaching `corner` along both edges (a square would reach into the content).
fn zones(rect: Rect, edge: f32, corner: f32) -> Vec<(ResizeDirection, Rect)> {
let (l, r, t, b) = (rect.left(), rect.right(), rect.top(), rect.bottom());
// Never inverted, however small the window.
let zone = |x0: f32, x1: f32, y0: f32, y1: f32| Rect::from_min_max(Pos2::new(x0, y0), Pos2::new(x1.max(x0), y1.max(y0)));
vec![
(ResizeDirection::NorthWest, zone(l, l + corner, t, t + edge)),
(ResizeDirection::NorthWest, zone(l, l + edge, t + edge, t + corner)),
(ResizeDirection::NorthEast, zone(r - corner, r, t, t + edge)),
(ResizeDirection::NorthEast, zone(r - edge, r, t + edge, t + corner)),
(ResizeDirection::SouthWest, zone(l, l + corner, b - edge, b)),
(ResizeDirection::SouthWest, zone(l, l + edge, b - corner, b - edge)),
(ResizeDirection::SouthEast, zone(r - corner, r, b - edge, b)),
(ResizeDirection::SouthEast, zone(r - edge, r, b - corner, b - edge)),
(ResizeDirection::North, zone(l + corner, r - corner, t, t + edge)),
(ResizeDirection::South, zone(l + corner, r - corner, b - edge, b)),
(ResizeDirection::West, zone(l, l + edge, t + corner, b - corner)),
(ResizeDirection::East, zone(r - edge, r, t + corner, b - corner)),
]
}
fn cursor(direction: ResizeDirection) -> CursorIcon {
match direction {
ResizeDirection::North => CursorIcon::ResizeNorth,
ResizeDirection::South => CursorIcon::ResizeSouth,
ResizeDirection::East => CursorIcon::ResizeEast,
ResizeDirection::West => CursorIcon::ResizeWest,
ResizeDirection::NorthEast => CursorIcon::ResizeNorthEast,
ResizeDirection::SouthEast => CursorIcon::ResizeSouthEast,
ResizeDirection::NorthWest => CursorIcon::ResizeNorthWest,
ResizeDirection::SouthWest => CursorIcon::ResizeSouthWest,
}
}
/// Resize zones along the window's edges and a hairline border, while the window is neither
/// maximized nor full screen. The zones sit above everything else (in the foreground order), so a
/// press there resizes instead of reaching the scrollbar or panel under it.
pub fn resize_edges(ctx: &egui::Context) {
let (maximized, fullscreen) = ctx.input(|i| (i.viewport().maximized.unwrap_or(false), i.viewport().fullscreen.unwrap_or(false)));
if maximized || fullscreen {
return;
}
let screen = ctx.content_rect();
let t = Tokens::get(ctx);
egui::Area::new(egui::Id::new("window_resize_edges"))
.order(egui::Order::Foreground)
.fixed_pos(screen.min)
.pivot(Align2::LEFT_TOP)
.interactable(true)
.show(ctx, |ui| {
ui.set_min_size(Vec2::ZERO);
for (n, (direction, rect)) in zones(screen, EDGE, CORNER).into_iter().enumerate() {
if rect.width() <= 0.0 || rect.height() <= 0.0 {
continue;
}
let response = ui.interact(rect, ui.id().with(("edge", n)), Sense::drag()).on_hover_cursor(cursor(direction));
// Sensing only drags, a drag starts on the press itself.
if response.drag_started() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::BeginResize(direction));
}
}
// Without the desktop's frame or shadow, a hairline keeps the window apart from
// what's behind it.
ui.painter().rect_stroke(
screen,
0.0,
Stroke::new(1.0, Color32::from_black_alpha(if t.dark() { 160 } else { 60 })),
egui::StrokeKind::Inside,
);
});
}
#[cfg(test)]
mod tests {
use super::{Button, Layout, ResizeDirection, zones};
use egui::{Pos2, Rect};
#[test]
fn gnome_button_layouts_parse() {
let close = Layout { left: vec![], right: vec![Button::Close] };
assert_eq!(Layout::parse("'appmenu:close'"), close);
assert_eq!(Layout::parse("appmenu:close\n"), close);
assert_eq!(Layout::parse(":minimize,maximize,close"), Layout::default());
assert_eq!(
Layout::parse("close,minimize,maximize:appmenu"),
Layout { left: vec![Button::Close, Button::Minimize, Button::Maximize], right: vec![] }
);
// No colon: the left side, as GTK reads it.
assert_eq!(Layout::parse("close"), Layout { left: vec![Button::Close], right: vec![] });
// Unknown names, spaces and repeats.
assert_eq!(Layout::parse("icon,spacer: close , close,bogus"), close);
assert_eq!(Layout::parse(""), Layout { left: vec![], right: vec![] });
}
#[test]
fn widths_cover_the_buttons_and_their_gaps() {
assert_eq!(super::width(&[]), 0.0);
assert_eq!(super::width(&[Button::Close]), super::BUTTON + super::SIDE_GAP);
assert_eq!(super::width(&Layout::default().right), 3.0 * super::BUTTON + 2.0 * super::GAP + super::SIDE_GAP);
}
#[test]
fn resize_zones_line_the_edges_without_overlapping() {
let window = Rect::from_min_size(Pos2::ZERO, egui::vec2(800.0, 600.0));
let zones = zones(window, 5.0, 16.0);
let at = |x: f32, y: f32| zones.iter().filter(|(_, r)| r.contains(Pos2::new(x, y))).map(|(d, _)| *d).collect::<Vec<_>>();
assert_eq!(at(1.0, 1.0), [ResizeDirection::NorthWest]);
assert_eq!(at(2.0, 12.0), [ResizeDirection::NorthWest]);
assert_eq!(at(400.0, 2.0), [ResizeDirection::North]);
assert_eq!(at(799.0, 300.0), [ResizeDirection::East]);
assert_eq!(at(400.0, 599.0), [ResizeDirection::South]);
assert_eq!(at(1.0, 300.0), [ResizeDirection::West]);
assert_eq!(at(799.0, 599.0), [ResizeDirection::SouthEast]);
assert!(at(400.0, 300.0).is_empty(), "the middle of the window is left alone");
assert!(at(12.0, 12.0).is_empty(), "corners stay along the edges");
// Strips meet without overlapping.
for y in [0.5, 4.5, 5.5, 15.5, 16.5, 595.5] {
for x in [0.5, 4.5, 5.5, 15.5, 16.5, 795.5, 799.5] {
assert!(at(x, y).len() <= 1, "({x}, {y}): {:?}", at(x, y));
}
}
// A window smaller than its corners gets empty (not inverted) edge zones.
for (_, r) in super::zones(Rect::from_min_size(Pos2::ZERO, egui::vec2(20.0, 20.0)), 5.0, 16.0) {
assert!(r.width() >= 0.0 && r.height() >= 0.0);
}
}
}
+46
View File
@@ -882,6 +882,52 @@ fn preferences_selects_default_workspace_for_next_open() {
assert_eq!(h.state().mode, Mode::Read);
}
/// GNOME on Wayland: the tab strip carries the window buttons until Preferences ▸ Use system title
/// bar gives the window back its frame, at once and without a restart (#553, #1001). The choice
/// is saved; desktops that draw the frame themselves don't offer it.
#[test]
fn use_system_title_bar_swaps_the_header_bar_for_the_window_frame() {
use egui::accesskit::Role;
use pdfcraft_ui_egui::window_controls::Layout;
fn decorations(h: &Harness<'static, PdfCraftApp>) -> Option<bool> {
h.output().viewport_output.values().flat_map(|v| v.commands.iter()).find_map(|c| match c {
egui::ViewportCommand::Decorations(on) => Some(*on),
_ => None,
})
}
let mut h = harness(|app| app.window_controls = Some(Layout::default()));
// The window opened undecorated, so the first frame asks for nothing.
assert_eq!(decorations(&h), None);
assert!(h.query_by_label("Minimize").is_some(), "the header bar has the window buttons");
h.state_mut().set_option("dialog", "preferences").unwrap();
h.run_steps(2);
h.get_by_role_and_label(Role::CheckBox, "Use system title bar").scroll_to_me();
h.run_steps(4);
h.get_by_role_and_label(Role::CheckBox, "Use system title bar").click();
let mut asked = None;
for _ in 0..4 {
h.step();
asked = asked.or(decorations(&h));
}
assert!(h.state().system_title_bar);
assert_eq!(asked, Some(true), "the window gets its frame back");
assert!(h.query_by_label("Minimize").is_none(), "and the tab strip loses its buttons");
let saved = h.state().persist();
assert!(saved.contains(r#""system_title_bar":true"#), "{saved}");
let mut restored = PdfCraftApp::new();
restored.restore(&saved);
assert!(restored.system_title_bar);
restored.set_option("system-title-bar", "false").unwrap();
assert!(!restored.system_title_bar);
assert!(restored.set_option("system-title-bar", "maybe").is_err());
// Where the desktop draws the frame there is nothing to choose.
let mut h = harness(|app| app.set_option("dialog", "preferences").unwrap());
h.run_steps(2);
assert!(h.query_by_label("Use system title bar").is_none());
assert!(h.query_by_label("Minimize").is_none());
assert_eq!(decorations(&h), None);
}
/// Preferences ▸ Animation effects: the system's animation setting until the user chooses On or
/// Off. The choice is saved, and settings holding anything else leave it alone.
#[test]
+1
View File
@@ -116,6 +116,7 @@ driver's shader compilation, and explicit `PDFCRAFT_RENDERER=wgpu` still disable
| `WGPU_BACKEND` | Graphics backend; by default Windows uses Direct3D 12, falling back to OpenGL |
| `WGPU_VALIDATION_INDIRECT_CALL` | `1` turns wgpu's check of indirect draw arguments back on. PdfCraft draws nothing indirectly and turns it off, since the compute shader the check needs fails to compile on some drivers (Metal on an Intel Iris Pro), which lost the device at start-up |
| `PDFCRAFT_RENDERER` | `gl` starts with OpenGL (glow) and never loads wgpu; `wgpu` reports a wgpu failure instead of retrying with OpenGL; unset, wgpu is retried with OpenGL when it can't start (see [Renderer fallback](#renderer-fallback)) |
| `PDFCRAFT_TITLEBAR` | Linux: `app` draws PdfCraft's own title bar (window buttons in the tab strip, in the order of GNOME's `button-layout`, and resize edges); `system` keeps the frame winit or the desktop draws; unset, PdfCraft draws its own when the Wayland compositor draws no frames (GNOME), and leaves it to the desktop otherwise (KDE, Sway, Hyprland, X11). Where PdfCraft draws it, Preferences ▸ Use system title bar switches back to winit's frame without a restart; `system` also hides that option |
| `PDFCRAFT_BUILD_LABEL` | Build time: a label for a local or patched build (letters, digits, inner dots), shown after the version in `--version`, About and the log (`0.7.1 (titlebar)`). `packaging/linux/package.sh` also puts it in the file names and the .rpm/.deb release (`1.titlebar`), which upgrades over the release of the same version |
| `CRAFT_FONTS_DIR` | Build time: a [craft-fonts](https://github.com/storytold/craft-fonts) checkout to embed (Japanese fonts) |
| `PDFCRAFT_SYSTEM_FONTS` | `0` stops the desktop app from using an installed font for characters its embedded fonts lack (`cargo xtask screenshots` sets it) |