From de7ed00a1a7a21adc7defbe9b2a2458bebb773b3 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:49:35 +0800 Subject: [PATCH] M3: End shows the end of the document, the last page down to its bottom (view.page-navigation) (#6435) --- crates/ui-egui/src/canvas/navigation.rs | 19 +++++++++-- crates/ui-egui/src/commands.rs | 3 ++ crates/ui-egui/src/dialogs.rs | 8 +++-- crates/ui-egui/tests/it/viewer.rs | 42 +++++++++++++++++++++++++ parity/acrobat-features.toml | 4 +-- 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/canvas/navigation.rs b/crates/ui-egui/src/canvas/navigation.rs index 1a2a0343a6..49dc3336c6 100644 --- a/crates/ui-egui/src/canvas/navigation.rs +++ b/crates/ui-egui/src/canvas/navigation.rs @@ -108,6 +108,16 @@ impl DocView { self.current = page; } + /// The end of the document (End): the last page, scrolled to its bottom as far as the view + /// goes, as browsers and word processors do (#6434). [`go_to_page`](Self::go_to_page) shows a + /// page's top. + pub fn go_to_end(&mut self) { + self.go_to_page(usize::MAX); + // A fraction past a page's top scrolls as far as the content allows (`goto`, clamped when + // applied), so the whole page down to its bottom edge. + self.goto = Some((self.current, 1.0)); + } + /// Reveal a user-space rectangle without changing zoom, centred where the scroll bounds allow. pub(crate) fn reveal_rect(&mut self, page: usize, rect: [f32; 4], info: &DocInfo) { self.go_to_page(page); @@ -407,11 +417,14 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s view.find = None; } } - if key(Key::Home) { + // Home and End, and the keys listed with them for Go to first / last page: Shift+Ctrl + // (Shift+⌘ on macOS) with ↑ / ↓ or Page Up / Page Down (#6434). + let shift_cmd = |k| KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, k); + if key(Key::Home) || pressed(shift_cmd(Key::ArrowUp)) || pressed(shift_cmd(Key::PageUp)) { view.go_to_page(0); } - if key(Key::End) { - view.go_to_page(usize::MAX); + if key(Key::End) || pressed(shift_cmd(Key::ArrowDown)) || pressed(shift_cmd(Key::PageDown)) { + view.go_to_end(); } // Full screen: Space and Enter go forward a page and Backspace back, as in Acrobat (the // arrows and Page Up / Page Down already turn pages where nothing scrolls, below). diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 7c0df87782..73b097c706 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -30,6 +30,9 @@ pub(crate) const PASTE: Shortcut = Shortcut::cmd("V"); pub(crate) const SELECT_ALL: Shortcut = Shortcut::cmd("A"); pub(crate) const PAGE_PREV: Shortcut = Shortcut::cmd("←"); pub(crate) const PAGE_NEXT: Shortcut = Shortcut::cmd("→"); +/// Go to first / last page beside Home and End (`canvas::navigation`). +pub(crate) const FIRST_PAGE: Shortcut = Shortcut::cmd_shift("↑"); +pub(crate) const LAST_PAGE: Shortcut = Shortcut::cmd_shift("↓"); /// Whether shortcuts are written the macOS way (`⇧⌘S`) rather than `Ctrl+Shift+S`. egui knows the /// platform from the build target, and on the web from the browser's user agent. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7ce76c420e..c4a49acd67 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1498,7 +1498,8 @@ fn dialog_shortcuts(ui: &mut egui::Ui) -> bool { ui.label(egui::RichText::new(tl!("Keyboard shortcuts")).font(theme::semibold(18.0))); ui.add_space(8.0); use crate::commands::{ - ACTUAL_SIZE, COPY, FIND_NEXT, FIND_PREV, FIT_WIDTH, PAGE_LEVEL, PAGE_NEXT, PAGE_PREV, ROTATE_CCW, ROTATE_CW, SELECT_ALL, ZOOM_IN, ZOOM_OUT, + ACTUAL_SIZE, COPY, FIND_NEXT, FIND_PREV, FIRST_PAGE, FIT_WIDTH, LAST_PAGE, PAGE_LEVEL, PAGE_NEXT, PAGE_PREV, ROTATE_CCW, ROTATE_CW, + SELECT_ALL, ZOOM_IN, ZOOM_OUT, }; // Registered commands first (always in sync with the real bindings), then the // keys the document view handles itself. @@ -1528,7 +1529,10 @@ fn dialog_shortcuts(ui: &mut egui::Ui) -> bool { (key(FIT_WIDTH), tl!("Fit to width").to_string()), (pair(ZOOM_IN, ZOOM_OUT), scroll), (pair(ROTATE_CW, ROTATE_CCW), tl!("Rotate view").to_string()), - (format!("{} / {}", crate::i18n::key_name("Home"), crate::i18n::key_name("End")), tl!("First / last page").to_string()), + ( + format!("{} / {}, {}", crate::i18n::key_name("Home"), crate::i18n::key_name("End"), pair(FIRST_PAGE, LAST_PAGE)), + tl!("First / last page").to_string(), + ), (format!("← / →, {}", pair(PAGE_PREV, PAGE_NEXT)), tl!("Previous / next page").to_string()), (alt_arrows, format!("{} / {}", tl!("Previous view"), tl!("Next view"))), (named("V"), tl!("Select (V)").to_string()), diff --git a/crates/ui-egui/tests/it/viewer.rs b/crates/ui-egui/tests/it/viewer.rs index 9a06a0bc12..341e718c16 100644 --- a/crates/ui-egui/tests/it/viewer.rs +++ b/crates/ui-egui/tests/it/viewer.rs @@ -532,6 +532,48 @@ fn restart_page_labels(h: &mut Harness<'static, PdfCraftApp>) { h.run_steps(3); } +/// End shows the end of the document, the last page's bottom edge, when the page is taller than +/// the window; Home goes back to the first page's top (#6434). +#[test] +fn end_shows_the_bottom_of_the_last_page() { + use egui::Key; + let mut h = harness(); + h.state_mut().active = Some(0); + h.state_mut().views[0].fit = pdfcraft_ui_egui::canvas::Fit::Width; + h.run_steps(4); + let vp = h.state().views[0].viewport_rect(); + assert!(h.state().views[0].page_screen_rect(0).is_some_and(|r| r.height() > vp.height()), "a page is taller than the window"); + + h.key_press(Key::End); + h.run_steps(4); + let v = &h.state().views[0]; + assert_eq!(v.current, 4, "the last page"); + let last = v.page_screen_rect(4).expect("the last page is on screen"); + assert!(last.bottom() <= vp.bottom() + 1.0 && last.bottom() > vp.bottom() - 40.0, "its bottom edge is at the window's: {last:?} in {vp:?}"); + + h.key_press(Key::Home); + h.run_steps(4); + let v = &h.state().views[0]; + assert_eq!(v.current, 0, "the first page"); + let first = v.page_screen_rect(0).expect("the first page is on screen"); + assert!(first.top() >= vp.top() - 1.0 && first.top() < vp.top() + 40.0, "its top edge is at the window's: {first:?} in {vp:?}"); + + // The keys listed with them for Go to first / last page do the same. + let shift_cmd = egui::Modifiers::COMMAND | egui::Modifiers::SHIFT; + for (last, first) in [(Key::ArrowDown, Key::ArrowUp), (Key::PageDown, Key::PageUp)] { + h.key_press_modifiers(shift_cmd, last); + h.run_steps(4); + let v = &h.state().views[0]; + let bottom = v.page_screen_rect(4).map(|r| r.bottom()); + assert!(v.current == 4 && bottom.is_some_and(|b| (b - vp.bottom()).abs() < 40.0), "{last:?}: {bottom:?} in {vp:?}"); + h.key_press_modifiers(shift_cmd, first); + h.run_steps(4); + let v = &h.state().views[0]; + let top = v.page_screen_rect(0).map(|r| r.top()); + assert!(v.current == 0 && top.is_some_and(|t| (t - vp.top()).abs() < 40.0), "{first:?}: {top:?} in {vp:?}"); + } +} + #[test] fn page_label_input_keeps_current_page_when_submitted_unchanged() { let mut h = harness(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index fbb8d7fd7f..a3b6c0a198 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1154,8 +1154,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/it/control.rs::state_and_view_options", "crates/ui-egui/tests/it/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/it/viewer.rs::two_page_view_steps_a_spread_at_a_time", "crates/ui-egui/tests/it/viewer.rs::arrow_and_page_keys_move_through_a_scrolling_document", "crates/ui-egui/tests/it/viewer.rs::left_and_right_scroll_a_page_wider_than_the_window_before_turning_it"] -notes = "in two-page view, next and previous page move a whole spread (with or without a cover page); → / ← turn pages in every layout (on a page wider than the window, a plan zoomed in, they scroll sideways a line first and turn the page at its edge; ⌘→ / ⌘← turn at once), ↓ / ↑ scroll a line and Page Down / Page Up a screen (the page in single-page view), as in Acrobat; in single-page view ↓ / ↑ turn the page once there is nothing left to scroll that way, like the wheel" +evidence = ["crates/ui-egui/tests/it/control.rs::state_and_view_options", "crates/ui-egui/tests/it/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/it/viewer.rs::two_page_view_steps_a_spread_at_a_time", "crates/ui-egui/tests/it/viewer.rs::arrow_and_page_keys_move_through_a_scrolling_document", "crates/ui-egui/tests/it/viewer.rs::left_and_right_scroll_a_page_wider_than_the_window_before_turning_it", "crates/ui-egui/tests/it/viewer.rs::end_shows_the_bottom_of_the_last_page"] +notes = "in two-page view, next and previous page move a whole spread (with or without a cover page); → / ← turn pages in every layout (on a page wider than the window, a plan zoomed in, they scroll sideways a line first and turn the page at its edge; ⌘→ / ⌘← turn at once), ↓ / ↑ scroll a line and Page Down / Page Up a screen (the page in single-page view), as in Acrobat; in single-page view ↓ / ↑ turn the page once there is nothing left to scroll that way, like the wheel; Home shows the first page's top and End the end of the document, the last page down to its bottom edge; Shift+Ctrl (Shift+⌘ on macOS) with ↑ / ↓ or Page Up / Page Down do the same as Home and End (Shift+Ctrl+Page Up / Page Down used to turn one page, like Ctrl+Page Up / Page Down)" [[feature]] id = "view.goto-page-label"