From 7437bde378f43856eada40af25b983812e615e8a Mon Sep 17 00:00:00 2001 From: bmh129 Date: Tue, 13 Oct 2026 02:04:13 -0500 Subject: [PATCH] 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 <11478682+bmh129@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 42 ++-- ROADMAP.md | 1 + apps/pdfcraft/src/decorations.rs | 70 +++++++ apps/pdfcraft/src/main.rs | 35 +++- apps/pdfcraft/src/text_scale.rs | 2 +- crates/ui-egui/src/chrome.rs | 31 ++- crates/ui-egui/src/i18n/ar.tsv | 5 + crates/ui-egui/src/i18n/bg.tsv | 5 + crates/ui-egui/src/i18n/cs.tsv | 5 + crates/ui-egui/src/i18n/de.tsv | 5 + crates/ui-egui/src/i18n/el.tsv | 5 + crates/ui-egui/src/i18n/es.tsv | 5 + crates/ui-egui/src/i18n/fr.tsv | 5 + crates/ui-egui/src/i18n/hu.tsv | 5 + crates/ui-egui/src/i18n/it.tsv | 5 + crates/ui-egui/src/i18n/ja.tsv | 5 + crates/ui-egui/src/i18n/kk.tsv | 5 + crates/ui-egui/src/i18n/lt.tsv | 5 + crates/ui-egui/src/i18n/pl.tsv | 5 + crates/ui-egui/src/i18n/pt-br.tsv | 5 + crates/ui-egui/src/i18n/ru.tsv | 5 + crates/ui-egui/src/i18n/sl.tsv | 5 + crates/ui-egui/src/i18n/te.tsv | 5 + crates/ui-egui/src/i18n/tr.tsv | 5 + crates/ui-egui/src/i18n/uk.tsv | 5 + crates/ui-egui/src/i18n/zh-hans.tsv | 5 + crates/ui-egui/src/i18n/zh-hant.tsv | 5 + crates/ui-egui/src/js_ui.rs | 5 + crates/ui-egui/src/lib.rs | 49 +++++ crates/ui-egui/src/window_controls.rs | 288 ++++++++++++++++++++++++++ crates/ui-egui/tests/ui.rs | 46 ++++ docs/development.md | 1 + 32 files changed, 642 insertions(+), 33 deletions(-) create mode 100644 apps/pdfcraft/src/decorations.rs create mode 100644 crates/ui-egui/src/window_controls.rs diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index a3da9f86..28785f76 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -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" diff --git a/ROADMAP.md b/ROADMAP.md index 9b1f46c3..a035c9bf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/apps/pdfcraft/src/decorations.rs b/apps/pdfcraft/src/decorations.rs new file mode 100644 index 00000000..f21faf3d --- /dev/null +++ b/apps/pdfcraft/src/decorations.rs @@ -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 { + 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::(&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 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, + ) { + } +} diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 16aa8fd9..a1d2ad99 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -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, create_images: bool, integrated: bool, + /// The window buttons the app draws, when the desktop leaves the frame to it. + window_controls: Option, } 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() }; diff --git a/apps/pdfcraft/src/text_scale.rs b/apps/pdfcraft/src/text_scale.rs index 8bb8ea51..76e0d020 100644 --- a/apps/pdfcraft/src/text_scale.rs +++ b/apps/pdfcraft/src/text_scale.rs @@ -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 { +pub(crate) fn gsettings(args: &[&str]) -> Option { 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()?; diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 1f2d2371..78018781 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -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")), diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 08b78f1a..222865d7 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -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 السماح diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 25828d13..17a0890f 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -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 Разреши diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c99248b1..e6e29c8b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a49e5f87..38ef665d 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index ea65545a..106278f0 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -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 Να επιτραπεί diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ae62fca6..5afbe4f2 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 02492cb4..79389837 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 756d144f..3d6e1e3a 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f853a638..3575ab37 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index fa3d3e19..5e61802c 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 許可 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index b50c64f0..8e4a0904 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -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 Рұқсат беру diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index caffba40..ae61e396 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index a300c85c..310a990f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f43aa0f9..623d69ce 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 5c8cf255..7f628088 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 Разрешить diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index a35d51b7..3feaad35 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 1beb1105..d15365f3 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -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 అనుమతించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 82ad855f..44fd05f0 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a178110b..648dc855 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 Дозволити diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 805bb1cc..391c8eda 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -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 允许 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 422f669c..da45921c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -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 允許 diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index ef22083e..fe474622 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -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 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 93e708c1..ea3da009 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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, + /// 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, pub password_prompt: Option, pub unlock_prompt: Option, 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() { diff --git a/crates/ui-egui/src/window_controls.rs b/crates/ui-egui/src/window_controls.rs new file mode 100644 index 00000000..eacf854c --- /dev/null +++ b/crates/ui-egui/src/window_controls.rs @@ -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