Browse / github.com/storytold/pdfcraft / patch
M3: Preferences can keep the page size showing (#4908)
Always Show Page Dimensions in Status Bar It would be great to have an option to always display the current PDF page dimensions in the status bar, similar to Adobe Acrobat's "Always show document page size" feature. Suggested functionality: - Display the page width and height in the bottom-left corner of the document window. - Allow users to choose measurement units (inches, millimeters, centimeters, or points). - Automatically update the displayed dimensions when navigating between pages with different sizes. - Include an option in Preferences to enable or disable this feature. Why this is useful: For users working in printing, pr
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- AdrianTM (imported from pull request #4910, open)
- Base
1dd2737b47a9c1c9562f0af563053d1bf510e4bf- Size
- +126 −19 in 14 files
- Review
- pending
- Shared
- 2026-10-28
- Id
2337676cc3868cfa3ca1105c
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md apps/pdfcraft/src/main.rs crates/ui-egui/src/app.rs crates/ui-egui/src/app/control.rs crates/ui-egui/src/app/prefs.rs crates/ui-egui/src/canvas.rs crates/ui-egui/src/canvas/bars.rs crates/ui-egui/src/lib.rs crates/ui-egui/src/workflows/js_ui.rs crates/ui-egui/tests/it/compare.rs crates/ui-egui/tests/it/control.rs crates/ui-egui/tests/it/preferences_layout.rs crates/ui-egui/tests/it/view.rs parity/acrobat-features.toml
@@ -745,3 +745,5 @@ Newest first: the date, what moved, and the new overall estimate. - **2026-10-18 (renderer, hayro patches 19/35):** A mesh shading (types 4–7) is sampled over its own triangles instead of the whole path: a BricsCAD plan with 310 one-triangle `sh` fills took 2 s per 1024 px tile at every zoom (and hit the 20 s watchdog on a busy machine); now about 70 ms, so zooming into dense topology drawings stays fluid. Estimates unchanged. - **2026-10-22 (PR #3227 integration):** M11: integrate upstream `80b7d215`; retain partial target-version compatibility and upstream content-boundary and image-size regression coverage. Scope and estimates unchanged.+- **2026-10-27 (M3, #4908):** Preferences gains "Always show the page size": the current page's size stays up at the bottom-left of the page view instead of only while the pointer is in that corner (same label and unit; off by default; saved with the settings; `set_option`/`--always-show-page-size`).+@@ -7,7 +7,7 @@ //! `--page N --zoom 150 --layout single|continuous|two-up|two-page --continuous on|off --panel comments|bookmarks|pages|fields|layers|attachments|none //! --theme light|dark|system --language auto|<code> --mode all|read|edit|convert|sign --tool <catalogue id> --left open|closed //! --organize on --fields on --dialog properties|shortcuts|about --palette <query> --home on-//! --cover on|off --default-layout single|continuous|two-up|two-page --default-continuous on|off --default-zoom fit-width|fit-page|<percent> --always-default-view on|off+//! --cover on|off --default-layout single|continuous|two-up|two-page --default-continuous on|off --default-zoom fit-width|fit-page|<percent> --always-default-view on|off --always-show-page-size on|off //! --page-gaps on|off --default-page-gaps on|off //! --combine-view grid|list --combine-zoom 60..200` //!@@ -93,6 +93,7 @@ impl PdfCraftApp { reload_changed_files: true, show_secured_notice: true, always_default_view: false,+ always_show_page_size: false, #[cfg(not(target_arch = "wasm32"))] watch: Default::default(), #[cfg(not(target_arch = "wasm32"))]@@ -193,6 +193,13 @@ impl PdfCraftApp { _ => return Err("always-default-view must be on or off".into()), }; }+ ("always-show-page-size", _) => {+ self.always_show_page_size = match value {+ "on" => true,+ "off" => false,+ _ => return Err("always-show-page-size must be on or off".into()),+ };+ } ("tools-side", _) => { self.side_panels.tools_side = match value { "left" => side_panels::ToolsSide::Left,@@ -94,6 +94,7 @@ impl PdfCraftApp { "default_layout": canvas::display_token(self.view_defaults.layout, self.view_defaults.continuous), "default_zoom": self.view_defaults.zoom_name(), "always_default_view": self.always_default_view,+ "always_show_page_size": self.always_show_page_size, "highlight_fields": self.view_defaults.highlight_fields, "units": self.units, "language": self.language,@@ -220,6 +221,9 @@ impl PdfCraftApp { if let Some(on) = v["always_default_view"].as_bool() { self.always_default_view = on; }+ if let Some(on) = v["always_show_page_size"].as_bool() {+ self.always_show_page_size = on;+ } if let Some(paper) = v .get("print_paper") .and_then(serde_json::Value::as_str)@@ -1251,6 +1251,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let prefs = &app.comment_prefs; let redact_mark = (app.redact_prefs.hover_fill, app.redact_prefs.opacity); let units = app.units;+ let always_page_size = app.always_show_page_size; let allowed = doc.allows_annotation(); let can_copy = doc.allows_copying(); let mut copy_selected = false;@@ -2431,7 +2432,8 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { }); } find_bar(view, info.pages.len(), avail, ui, &t);- page_size_hint(view, info, units, avail, unobstructed, ui, &t);+ let page_size = info.pages.get(view.current).map(|p| (p.width, p.height));+ page_size_hint(page_size, units, always_page_size, avail, unobstructed, ui, &t); if tool == QuickTool::EnhanceCamera { crate::camera_ui::bar(ui.ctx(), view, avail, &t, can_modify); }@@ -6,23 +6,26 @@ pub(super) fn in_size_hint_zone(area: Rect, p: Pos2) -> bool { } /// The current page's size, floating at the bottom-left of the document area while the pointer is-/// in that quadrant, as Acrobat does. Same formatter and unit as Document Properties.+/// in that quadrant, as Acrobat does, or all the time when `always` is set (#4908). Same+/// formatter and unit as Document Properties. pub(super) fn page_size_hint(- view: &DocView,- info: &DocInfo,+ size: Option<(f32, f32)>, units: crate::units::Unit,+ always: bool, area: Rect, unobstructed: bool, ui: &mut egui::Ui, t: &Tokens, ) {- let Some(page) = info.pages.get(view.current) else { return };- let inside = unobstructed && ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| in_size_hint_zone(area, p));- if !inside {+ let Some((width, height)) = size else { return };+ let inside = ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| in_size_hint_zone(area, p));+ // Under a dialog or the palette the label stays hidden, whatever the preference says.+ if !unobstructed || !(always || inside) { return; }- let text = units.size(f64::from(page.width), f64::from(page.height));- egui::Area::new(egui::Id::unique("page-size-hint"))+ let text = units.size(f64::from(width), f64::from(height));+ // One per document view: a side-by-side comparison draws two at once.+ egui::Area::new(ui.scope_id().with("page-size-hint")) .order(egui::Order::Foreground) .interactable(false) .pivot(egui::Align2::LEFT_BOTTOM)@@ -344,6 +344,9 @@ pub struct PdfCraftApp { /// Preferences: open every document at Default zoom and Default page display, whatever /// layout or zoom it asks for (#1618). pub always_default_view: bool,+ /// Preferences: keep the current page's size showing at the bottom-left of the page view,+ /// not only while the pointer is in that corner (#4908).+ pub always_show_page_size: bool, /// Open documents' files, watched for changes on disk. #[cfg(not(target_arch = "wasm32"))] pub(crate) watch: file_watch::FileWatch,@@ -447,6 +447,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok } }); ui.label(egui::RichText::new(tl!("For page sizes and margins.")).small().color(t.text_muted));+ ui.add(widgets::checkbox(&mut app.always_show_page_size, tl!("Always show the page size"))); // The web build has no file paths to reopen. #[cfg(not(target_arch = "wasm32"))] ui.add(widgets::checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed")));@@ -279,3 +279,14 @@ fn export_to_word_html_and_rtf() { } assert!(std::fs::read_to_string(dir.join("e.html")).unwrap().contains("Exported words")); }++/// #4908: with the page size always on, each half of a side-by-side comparison shows its own label.+#[test]+fn each_half_of_a_comparison_shows_its_own_page_size() {+ let mut h = side_by_side();+ let labels = |h: &Harness<'static, PdfCraftApp>| h.query_all_by_label_contains(" × ").count();+ assert_eq!(labels(&h), 0, "off by default, with the pointer away from the corners");+ h.state_mut().set_option("always-show-page-size", "on").unwrap();+ h.run_steps(4);+ assert_eq!(labels(&h), 2, "one label per half");+}@@ -1971,28 +1971,64 @@ fn click_and_drag_refuse_coordinates_that_overflow_like_move() { ok(&mut h, &c, "ui.drag", json!({ "from": [x, y], "to": [x + 10.0, y], "steps": 2 })); } +/// How many page-size labels (`612 × 792 pt`) are on screen.+fn size_labels(h: &Harness<'static, PdfCraftApp>) -> usize {+ h.query_all_by_label_contains(" × ").count()+}+ #[test] fn page_size_shows_while_the_pointer_is_in_the_bottom_left_quadrant() { let (mut h, _c) = harness(); let area = h.state().views[0].viewport_rect();- let shown = |h: &Harness<'static, PdfCraftApp>| {- let layer = egui::LayerId::new(egui::Order::Foreground, egui::Id::unique("page-size-hint"));- h.ctx.memory(|m| m.areas().visible_last_frame(&layer))- };+ let shown = size_labels; let away = area.center() + egui::vec2(300.0, -200.0); h.event(egui::Event::PointerMoved(away)); h.run_steps(3);- assert!(!shown(&h), "top-right: hidden");+ assert_eq!(shown(&h), 0, "top-right: hidden"); h.event(egui::Event::PointerMoved(area.left_bottom() + egui::vec2(80.0, -80.0))); h.run_steps(3);- assert!(shown(&h), "bottom-left: shown");+ assert_eq!(shown(&h), 1, "bottom-left: shown"); if let Ok(dir) = std::env::var("PDFCRAFT_SHOT_DIR") { let _gpu = crate::gpu(); h.render().unwrap().save(format!("{dir}/page-size-hint.png")).unwrap(); } h.event(egui::Event::PointerMoved(away)); h.run_steps(3);- assert!(!shown(&h), "pointer left: hidden again");+ assert_eq!(shown(&h), 0, "pointer left: hidden again");+}++/// #4908: with "Always show the page size" on, the label stays up wherever the pointer is.+#[test]+fn page_size_stays_up_when_always_show_is_on() {+ let (mut h, _c) = harness();+ let area = h.state().views[0].viewport_rect();+ let shown = size_labels;+ let away = area.center() + egui::vec2(300.0, -200.0);+ h.event(egui::Event::PointerMoved(away));+ h.run_steps(3);+ assert_eq!(shown(&h), 0, "off by default: hidden away from the corner");+ h.state_mut().set_option("always-show-page-size", "on").unwrap();+ h.run_steps(3);+ assert_eq!(shown(&h), 1, "on: shown with the pointer elsewhere");+ h.state_mut().set_option("always-show-page-size", "off").unwrap();+ h.run_steps(3);+ assert_eq!(shown(&h), 0, "off again: hidden");+ assert!(h.state_mut().set_option("always-show-page-size", "maybe").is_err());+}++/// The label stays under a dialog: ticking the preference in an open dialog does not bring it up.+#[test]+fn page_size_stays_hidden_under_a_dialog_even_when_always_show_is_on() {+ let (mut h, _c) = harness();+ h.state_mut().set_option("dialog", "preferences").unwrap();+ h.run_steps(4);+ h.state_mut().set_option("always-show-page-size", "on").unwrap();+ h.run_steps(4);+ assert!(h.state().dialog.is_some());+ assert_eq!(size_labels(&h), 0, "hidden under Preferences");+ h.state_mut().dialog = None;+ h.run_steps(4);+ assert_eq!(size_labels(&h), 1, "back once the dialog is closed"); } #[test]@@ -96,3 +96,25 @@ fn a_very_tall_window_does_not_leave_a_blank_gap_above_ok() { assert!(on_screen(ok, 2600.0) && on_screen(last, 2600.0), "{last:?} {ok:?}"); assert!(ok.min.y - last.max.y < 120.0, "a gap of {} pt between the last setting and OK", ok.min.y - last.max.y); }++/// #4908: the "Always show the page size" checkbox is in Preferences and turns the setting on and off.+#[test]+fn the_always_show_page_size_checkbox_sets_the_preference() {+ // Wide enough that the menu bar shows every menu: its overflow "⋯" has no glyph in a bare test font set.+ let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {+ let mut app = PdfCraftApp::new();+ app.set_option("language", "en").unwrap();+ app.set_option("dialog", "preferences").unwrap();+ app+ });+ h.run_steps(6);+ assert!(!h.state().always_show_page_size);+ h.get_by_label("Always show the page size").scroll_to_me();+ h.run_steps(4);+ h.get_by_label("Always show the page size").click();+ h.run_steps(3);+ assert!(h.state().always_show_page_size, "ticked");+ h.get_by_label("Always show the page size").click();+ h.run_steps(3);+ assert!(!h.state().always_show_page_size, "unticked");+}@@ -2257,3 +2257,18 @@ fn the_pages_panel_tools_are_off_without_page_editing_permission() { assert!(h.query_by_label("Delete the selected pages").is_none(), "its menu doesn't open"); assert_eq!(h.state().session.get(id).unwrap().info.pages.len(), 3); }++#[test]+fn always_show_the_page_size_survives_a_restart() {+ let mut app = PdfCraftApp::new();+ assert!(!app.always_show_page_size, "off by default");+ app.always_show_page_size = true;+ let mut restarted = PdfCraftApp::new();+ restarted.restore(&app.persist());+ assert!(restarted.always_show_page_size, "the choice is kept");+ restarted.restore(r#"{"always_show_page_size":"bogus"}"#);+ assert!(restarted.always_show_page_size, "garbage leaves it alone");+ let mut legacy = PdfCraftApp::new();+ legacy.restore("{}");+ assert!(!legacy.always_show_page_size, "a legacy file without the key keeps it off");+}@@ -1214,8 +1214,8 @@ milestone = "M3" status = "shipped" commands = [] tools = []-evidence = ["crates/ui-egui/tests/it/control.rs::page_size_shows_while_the_pointer_is_in_the_bottom_left_quadrant", "crates/ui-egui/src/canvas/tests.rs::page_size_hint_zone_is_the_bottom_left_quadrant"]-notes = "While the pointer is in the bottom-left quadrant of the page view, a small label shows the current page's size (after /Rotate and /UserUnit) in the Preferences unit, formatted as in Document Properties (#1855). It never takes clicks and hides when the pointer leaves. Acrobat's trigger is the corner itself and may differ in exact area and wording (not compared with real Acrobat); the size of the current page only, not of the page under the pointer; no headless tool (page sizes are in document info)."+evidence = ["crates/ui-egui/tests/it/control.rs::page_size_shows_while_the_pointer_is_in_the_bottom_left_quadrant", "crates/ui-egui/src/canvas/tests.rs::page_size_hint_zone_is_the_bottom_left_quadrant", "crates/ui-egui/tests/it/control.rs::page_size_stays_up_when_always_show_is_on", "crates/ui-egui/tests/it/preferences_layout.rs::the_always_show_page_size_checkbox_sets_the_preference", "crates/ui-egui/tests/it/view.rs::always_show_the_page_size_survives_a_restart"]+notes = "While the pointer is in the bottom-left quadrant of the page view, a small label shows the current page's size (after /Rotate and /UserUnit) in the Preferences unit, formatted as in Document Properties (#1855). It never takes clicks and hides when the pointer leaves. Acrobat's trigger is the corner itself and may differ in exact area and wording (not compared with real Acrobat); the size of the current page only, not of the page under the pointer; no headless tool (page sizes are in document info). Preferences ▸ Documents and view ▸ Always show the page size (and `--always-show-page-size on|off`, `set_option`) keeps the label up whatever the pointer does, in the same unit; it is saved with the settings and off by default (#4908). It is the same label in the same corner, not a separate status bar." [[feature]] id = "view.document-tabs"