Browse / github.com/storytold/pdfcraft / patch
feat(ui): an optional page number indicator over the document (#4810)
Viewer: offer a persistent current-page / total-page indicator ## Request Provide an optional persistent viewer indicator such as `1 / 42`, preferably pinnable to the UI, for every PDF opened. ## Scope This is a display-only preference. It must not write headers or footers into the document. The existing Header & Footer page-number tokens are per-document content, and the existing page-number-box overlap report (#4082) is a separate layout issue.
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #4894, open)
- Base
f94668f1c8c78c8cf024a2b29375336607a1f76d- Size
- +52 −0 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
a46657b18b4d01517f697a09
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/canvas.rs crates/ui-egui/src/js_ui.rs crates/ui-egui/src/lib.rs
@@ -59,6 +59,27 @@ const MAX_CACHED_PAGES: usize = 24; /// `MAX_CACHED_PAGES` so raster tuning never silently retunes text memory. const MAX_CACHED_TEXT_PAGES: usize = 24; +/// The page indicator's text: `4 / 42`, or `iv (4 / 42)` when the page has a label that is not+/// its number (as Acrobat's page box does). Out of range (no pages) reads `0 / 0`.+fn page_indicator_text(current: usize, count: usize, label: &str) -> String {+ let number = if current < count { current + 1 } else { count };+ if label.is_empty() || label == number.to_string() || current >= count {+ format!("{number} / {count}")+ } else {+ format!("{label} ({number} / {count})")+ }+}++/// A small pill at the bottom centre of the document area; display only, it takes no input.+fn paint_page_indicator(ui: &egui::Ui, avail: Rect, text: &str) {+ let t = Tokens::get(ui.ctx());+ let painter = ui.painter().with_clip_rect(avail);+ let galley = painter.layout_no_wrap(text.to_owned(), theme::medium(12.0), t.text);+ let pill = Rect::from_center_size(pos2(avail.center().x, avail.bottom() - 22.0), galley.size() + vec2(20.0, 10.0));+ painter.rect(pill, CornerRadius::same(255), t.chrome.gamma_multiply(0.92), Stroke::new(1.0, t.border), egui::StrokeKind::Inside);+ painter.galley(pill.center() - galley.size() / 2.0, galley, t.text);+}+ fn rgba_bytes(width: usize, height: usize) -> usize { width.saturating_mul(height).saturating_mul(4) }@@ -4298,6 +4319,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.viewport_screen = out.inner_rect; view.auto_scroll.paint(ui, avail);+ if app.show_page_indicator {+ let label = info.pages.get(view.current).map_or("", |p| p.label.as_str());+ paint_page_indicator(ui, avail, &page_indicator_text(view.current, info.pages.len(), label));+ } if unobstructed && matches!(tool, QuickTool::Select | QuickTool::ColumnSelect) { copy_selected |= selection_actions(ui, avail, view, info, prefs, allowed && !comments_hidden, can_copy); }@@ -7842,6 +7867,17 @@ trailer << /Root 1 0 R >> assert_eq!((v.current, v.goto, v.goto_point), (1, Some((1, 0.0)), None)); } + #[test]+ fn the_page_indicator_reads_page_over_pages_with_the_label_when_it_differs() {+ assert_eq!(page_indicator_text(3, 42, ""), "4 / 42");+ assert_eq!(page_indicator_text(3, 42, "4"), "4 / 42");+ assert_eq!(page_indicator_text(3, 42, "iv"), "iv (4 / 42)");+ // No pages, or a page past the end: never beyond the last.+ assert_eq!(page_indicator_text(0, 0, ""), "0 / 0");+ assert_eq!(page_indicator_text(9, 3, "x"), "3 / 3");+ assert_eq!(page_indicator_text(usize::MAX, 3, ""), "3 / 3");+ }+ #[test] fn destinations_set_the_zoom_mode_and_the_point_to_show() { let info = pdfcraft_render::DocInfo {@@ -440,6 +440,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok .color(t.text_muted), ); ui.add(widgets::checkbox(&mut app.always_default_view, tl!("Always use my default zoom and page layout")));+ ui.add(widgets::checkbox(&mut app.show_page_indicator, tl!("Show the current page number over the document"))); ui.horizontal(|ui| { ui.label(tl!("Units")); for u in crate::units::Unit::ALL {@@ -740,6 +740,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: a `page / pages` indicator floating over the document, whatever the+ /// side panels show. Display only; nothing is written to the document.+ pub show_page_indicator: bool, /// Open documents' files, watched for changes on disk. #[cfg(not(target_arch = "wasm32"))] pub(crate) watch: file_watch::FileWatch,@@ -1160,6 +1163,7 @@ impl PdfCraftApp { reload_changed_files: true, show_secured_notice: true, always_default_view: false,+ show_page_indicator: false, #[cfg(not(target_arch = "wasm32"))] watch: Default::default(), #[cfg(not(target_arch = "wasm32"))]@@ -2642,6 +2646,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,+ "show_page_indicator": self.show_page_indicator, "highlight_fields": self.view_defaults.highlight_fields, "units": self.units, "language": self.language,@@ -2768,6 +2773,9 @@ impl PdfCraftApp { if let Some(on) = v["always_default_view"].as_bool() { self.always_default_view = on; }+ if let Some(on) = v["show_page_indicator"].as_bool() {+ self.show_page_indicator = on;+ } if let Some(paper) = v .get("print_paper") .and_then(serde_json::Value::as_str)@@ -3071,6 +3079,13 @@ impl PdfCraftApp { _ => return Err("always-default-view must be on or off".into()), }; }+ ("page-indicator", _) => {+ self.show_page_indicator = match value {+ "on" => true,+ "off" => false,+ _ => return Err("page-indicator must be on or off".into()),+ };+ } ("tools-side", _) => { self.side_panels.tools_side = match value { "left" => side_panels::ToolsSide::Left,