mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
M3: the Pages panel follows the current page (#2785)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
2c4989413f
commit
b68b3c1119
@@ -165,6 +165,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
- **2026-10-15 (M7, Edit text caret):** In a text region that mixes sizes (a 24 pt title over 12 pt lines), the inline editor's caret on a small row was as tall as the region's first size and reached into the row below: egui draws the caret at least one row of the text field's font tall, and that font had the paragraph's first size. The field's font is now half the region's smallest size, so each row's own height decides. Kittest regression; checked in the app on a real document. Estimates unchanged.
|
||||
- **2026-10-16 (M3, #2781):** The Pages panel follows the current page, as Acrobat's page thumbnails do: going to a page whose thumbnail is out of sight (scrolling, Page Down, Go to page, a bookmark) scrolls the panel just enough to show it. A thumbnail already in view, or clicked, leaves the panel where it is, and a page change while the user scrolls the panel or drags thumbnails does not move it. Estimates unchanged.
|
||||
- **2026-10-18 (M3, #3149):** Page replacement cancels render demand collected for the previous geometry; all viewer raster upload paths reject empty or oversized textures before cache eviction. Synthetic regressions reproduce the reported 1880×2350 upload at a 2048 limit, retain cached images and unsaved edits, and resume bounded backdrops and tiles. Estimates unchanged.
|
||||
- **2026-10-18 (M8, #1350):** Integrated current main while preserving content-stream comment boundaries. Hidden-text cleanup retires unused named replacement descriptions on rewritten pages and forms, including inherited/shared resource holders, without removing descriptions still used by surviving content. Synthetic saved-byte and preservation regressions cover the cleanup. Estimates unchanged.
|
||||
- **2026-10-15 (M6, AcroForm XML text):** Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. Synthetic regressions verify exact values and unknown catalog data after incremental and full save/reopen. Nested-element and XFA interpretation are unchanged. Estimates unchanged.
|
||||
|
||||
@@ -554,6 +554,9 @@ pub struct DocView {
|
||||
pub pages_panel: Option<Rect>,
|
||||
/// The Pages-panel gap under the pointer (where dropped files go), when it can take pages.
|
||||
pub panel_gap: Option<usize>,
|
||||
/// The current page the Pages panel last followed: it scrolls to the current page's
|
||||
/// thumbnail only when the page changes.
|
||||
pub(crate) panel_followed: Option<usize>,
|
||||
/// How large the page grid draws its pages (1.0 = the usual thumbnails).
|
||||
grid_zoom: f32,
|
||||
/// A zoom asked for this frame (toolbar, pinch, keys); the grid applies it once it has laid
|
||||
@@ -770,6 +773,7 @@ impl DocView {
|
||||
grid_gap: None,
|
||||
pages_panel: None,
|
||||
panel_gap: None,
|
||||
panel_followed: None,
|
||||
grid_zoom: 1.0,
|
||||
grid_zoom_request: None,
|
||||
grid_anchor: None,
|
||||
|
||||
@@ -1064,6 +1064,11 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
|
||||
{
|
||||
delete_pages(view, info.pages.len());
|
||||
}
|
||||
// A new current page scrolls its thumbnail into view, as little as it takes (Acrobat), but
|
||||
// not while thumbnails are dragged: then the change counts as seen. A wheel turned or a
|
||||
// scroll bar dragged by the user already wins over a scroll to a place in egui.
|
||||
let follow = view.panel_followed != Some(view.current) && view.panel_drag.is_none();
|
||||
view.panel_followed = Some(view.current);
|
||||
let w = (ui.available_width() - 40.0).min(150.0);
|
||||
let count = info.pages.len();
|
||||
let mut rows: Vec<Rect> = Vec::with_capacity(info.pages.len());
|
||||
@@ -1108,7 +1113,14 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
|
||||
Stroke::new(if selected { 2.0 } else { 1.0 }, if selected { t.accent } else { t.border }),
|
||||
egui::StrokeKind::Outside,
|
||||
);
|
||||
ui.label(egui::RichText::new(&p.label).font(theme::medium(12.0)).color(if selected || picked { t.accent_text } else { t.text_muted }));
|
||||
let label = ui.label(egui::RichText::new(&p.label).font(theme::medium(12.0)).color(if selected || picked {
|
||||
t.accent_text
|
||||
} else {
|
||||
t.text_muted
|
||||
}));
|
||||
if follow && selected {
|
||||
ui.scroll_to_rect(rect.union(label.rect), None);
|
||||
}
|
||||
let resp = theme::hand(resp);
|
||||
if resp.clicked() {
|
||||
let m = ui.input(|i| i.modifiers);
|
||||
@@ -1116,6 +1128,8 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
|
||||
view.click_page(i, m, true);
|
||||
} else {
|
||||
view.clear_page_selection(Some(i));
|
||||
// A page clicked here is in view already: the panel stays put.
|
||||
view.panel_followed = Some(i);
|
||||
*nav = Some(Nav::Page(i));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,6 +1304,70 @@ fn pages_panel_thumbnails_have_a_context_menu() {
|
||||
assert_eq!(h.state().page_clipboard.as_ref().map(|c| c.pages.clone()), Some(vec![2]));
|
||||
}
|
||||
|
||||
/// Where the Pages panel draws page `n`'s thumbnail.
|
||||
fn thumbnail_rect(h: &Harness<'static, PdfCraftApp>, n: usize) -> egui::Rect {
|
||||
h.get_all_by_label(&format!("Page {n}")).find(|t| t.rect().left() > 1000.0).expect("the Pages panel thumbnail").rect()
|
||||
}
|
||||
|
||||
/// Whether `r` lies wholly in the part of the Pages panel in view.
|
||||
fn in_panel_view(h: &Harness<'static, PdfCraftApp>, r: egui::Rect) -> bool {
|
||||
h.state().views[0].pages_panel.expect("the Pages panel is shown").contains_rect(r)
|
||||
}
|
||||
|
||||
/// #2781: as in Acrobat, going to a page whose thumbnail is out of sight scrolls the Pages
|
||||
/// panel to show it.
|
||||
#[test]
|
||||
fn going_to_a_far_page_scrolls_its_thumbnail_into_view() {
|
||||
let mut h = pages_panel_harness(&[(200, 300); 30]);
|
||||
assert!(!in_panel_view(&h, thumbnail_rect(&h, 25)), "page 25 starts out of sight");
|
||||
h.state_mut().views[0].go_to_page(24);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, 24);
|
||||
assert!(in_panel_view(&h, thumbnail_rect(&h, 25)), "page 25's thumbnail is shown");
|
||||
}
|
||||
|
||||
/// #2781: a page whose thumbnail is already in view leaves the panel where it is, rather than
|
||||
/// centring the thumbnail on every page change.
|
||||
#[test]
|
||||
fn going_to_a_page_in_view_leaves_the_pages_panel_still() {
|
||||
// Landscape pages, so several thumbnails share the panel.
|
||||
let mut h = pages_panel_harness(&[(300, 200); 30]);
|
||||
h.state_mut().views[0].go_to_page(14);
|
||||
h.run_steps(4);
|
||||
let clip = h.state().views[0].pages_panel.expect("the Pages panel is shown");
|
||||
// The topmost thumbnail wholly in view, other than the current page's.
|
||||
let (n, before) =
|
||||
(1..=30).map(|n| (n, thumbnail_rect(&h, n))).find(|(n, r)| *n != 15 && in_panel_view(&h, *r)).expect("another thumbnail in view");
|
||||
assert!(
|
||||
(before.center().y - clip.center().y).abs() > 50.0,
|
||||
"page {n} at {before:?} is away from the middle of {clip:?}, where centring would move it"
|
||||
);
|
||||
h.state_mut().views[0].go_to_page(n - 1);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, n - 1);
|
||||
assert_eq!(thumbnail_rect(&h, n), before, "page {n}'s thumbnail stayed put");
|
||||
}
|
||||
|
||||
/// #2781: while the user drags thumbnails, a page change does not pull the panel away from
|
||||
/// where they are being dropped.
|
||||
#[test]
|
||||
fn a_page_change_while_thumbnails_are_dragged_does_not_scroll_the_pages_panel() {
|
||||
let mut h = pages_panel_harness(&[(200, 300); 30]);
|
||||
let first = thumbnail_rect(&h, 1);
|
||||
let at = first.center();
|
||||
h.hover_at(at);
|
||||
h.run_steps(1);
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.run_steps(1);
|
||||
h.hover_at(at + egui::vec2(0.0, 30.0));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().views[0].panel_drag, Some(vec![0]), "page 1 is being dragged");
|
||||
h.state_mut().views[0].go_to_page(24);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, 24);
|
||||
assert_eq!(thumbnail_rect(&h, 1), first, "the panel stays put under the drag");
|
||||
}
|
||||
|
||||
/// #444, #186: a mouse-wheel notch scrolls as far as the scroll speed preference says;
|
||||
/// trackpad (point) scrolling keeps its own speed. The choice is saved.
|
||||
#[test]
|
||||
|
||||
@@ -1262,8 +1262,8 @@ milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/ui.rs::theme_and_view_options_apply"]
|
||||
notes = ""
|
||||
evidence = ["crates/ui-egui/tests/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/viewer.rs::going_to_a_far_page_scrolls_its_thumbnail_into_view", "crates/ui-egui/tests/viewer.rs::going_to_a_page_in_view_leaves_the_pages_panel_still", "crates/ui-egui/tests/viewer.rs::a_page_change_while_thumbnails_are_dragged_does_not_scroll_the_pages_panel"]
|
||||
notes = "The panel follows the current page (#2781): a page whose thumbnail is out of sight scrolls it into view, as little as it takes; a thumbnail already in view, or clicked, leaves the panel still, and a page change while the user scrolls the panel or drags thumbnails does not move it."
|
||||
|
||||
[[feature]]
|
||||
id = "view.thumbnail-context-ops"
|
||||
|
||||
Reference in New Issue
Block a user