mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(combine): let the cards zoom to 500% (#2531)
The Combine grid stopped at 200%, too small to read a value off an invoice while reordering the pages of a statement. The main viewer already reached 6400%, so only the card grid was capped. - The range is 60–500%, and the buttons and keys step 60/80/100/125/150/175/200/250/300/400/500%. - MAX_SIDE, the thumbnail texture cap, goes 768 → 1536, so a card near the top re-renders sharper instead of upscaling a 768px texture. It only binds near the top: a card below it asks for less and costs no more than it did. Up there one thumbnail is a few megabytes, so TEXTURE_BUDGET and UPLOAD_BYTES_PER_FRAME keep the cache at a screenful plus one frame's uploads — and a screenful at the top of the range is a card or two, because a card is wider than most windows. - The control channel's range message is derived from the constant rather than repeating "60 and 200", so it cannot drift from the range again.
This commit is contained in:
@@ -34,9 +34,13 @@ const UPLOAD_BYTES_PER_FRAME: usize = 8 << 20;
|
||||
/// view are kept even over budget (at most a screenful of thumbnails of at most [`MAX_SIDE`]).
|
||||
const TEXTURE_BUDGET: usize = 32 << 20;
|
||||
/// The longest side of a thumbnail, in pixels.
|
||||
/// (Room for a Letter page at 200% on a screen of 1.5 pixels per point; sharper screens upscale a
|
||||
/// little at the largest sizes.)
|
||||
const MAX_SIDE: f32 = 768.0;
|
||||
/// (Room for a Letter page at 500% on a screen of 1.5 pixels per point; sharper screens upscale a
|
||||
/// little at the largest sizes.) This only binds near the top of the zoom range: a card below it
|
||||
/// asks for less than this and costs no more than it did. Near the top one thumbnail is a few
|
||||
/// megabytes, so [`TEXTURE_BUDGET`] and [`UPLOAD_BYTES_PER_FRAME`] keep the cache at a screenful
|
||||
/// plus one frame's uploads — and at the top of the range a screenful is a card or two, because a
|
||||
/// card is wider than most windows.
|
||||
const MAX_SIDE: f32 = 1536.0;
|
||||
/// Thumbnail sizes round up to this many pixels, so a slightly different layout reuses them.
|
||||
const SIZE_STEP: u32 = 16;
|
||||
/// The box a page is fitted in at 100%, and the room the card adds around it (for the hover bar
|
||||
@@ -50,8 +54,8 @@ const ABOVE: f32 = 32.0;
|
||||
const BAR_H: f32 = 34.0;
|
||||
/// The grid's zoom range, and the sizes its buttons and keys step through (the slider, the wheel
|
||||
/// and a pinch go anywhere between).
|
||||
pub(crate) const ZOOM_RANGE: std::ops::RangeInclusive<f32> = 0.6..=2.0;
|
||||
const ZOOM_STEPS: [f32; 7] = [0.6, 0.8, 1.0, 1.25, 1.5, 1.75, 2.0];
|
||||
pub(crate) const ZOOM_RANGE: std::ops::RangeInclusive<f32> = 0.6..=5.0;
|
||||
const ZOOM_STEPS: [f32; 11] = [0.6, 0.8, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0, 5.0];
|
||||
|
||||
/// The next size up (or down) from `zoom`; the end of the range stays put.
|
||||
pub(crate) fn zoom_step(zoom: f32, larger: bool) -> f32 {
|
||||
@@ -1371,10 +1375,10 @@ mod tests {
|
||||
// The box follows the zoom, within its range; nonsense is 100%.
|
||||
assert_eq!(paper_box(1.0), PAPER);
|
||||
assert_eq!(paper_box(2.0), PAPER * 2.0);
|
||||
assert_eq!(paper_box(99.0), PAPER * 2.0);
|
||||
assert_eq!(paper_box(99.0), PAPER * 5.0);
|
||||
assert_eq!(paper_box(-1.0), PAPER * 0.6);
|
||||
assert_eq!(paper_box(f32::NAN), PAPER);
|
||||
for zoom in [0.6, 1.0, 2.0] {
|
||||
for zoom in [0.6, 1.0, 2.0, 5.0] {
|
||||
let fit = paper_box(zoom);
|
||||
// Letter is a little wider than the box: it fills the width. A tall page fills the height.
|
||||
let letter = paper_size(612.0, 792.0, fit).unwrap();
|
||||
@@ -1394,12 +1398,12 @@ mod tests {
|
||||
fn zoom_buttons_step_through_round_sizes() {
|
||||
let mut z = 0.6;
|
||||
let mut up = vec![z];
|
||||
while z < 2.0 {
|
||||
while z < 5.0 {
|
||||
z = zoom_step(z, true);
|
||||
up.push(z);
|
||||
}
|
||||
assert_eq!(up, ZOOM_STEPS);
|
||||
assert_eq!(zoom_step(2.0, true), 2.0, "the largest stays put");
|
||||
assert_eq!(zoom_step(5.0, true), 5.0, "the largest stays put");
|
||||
assert_eq!(zoom_step(0.6, false), 0.6, "the smallest stays put");
|
||||
// From anywhere the slider left it, to the next round size.
|
||||
assert_eq!(zoom_step(1.1, true), 1.25);
|
||||
@@ -1408,6 +1412,29 @@ mod tests {
|
||||
assert_eq!(zoom_step(1.0000001, true), 1.25, "a hair over a size counts as that size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cards_zoom_far_enough_to_read_a_value() {
|
||||
// #2531: the grid stopped at 200%, which was not enough to read invoice values while
|
||||
// reordering the pages of a statement. It has to reach 500%.
|
||||
assert_eq!(*ZOOM_RANGE.end(), 5.0, "the grid stops at {}%, not 500%", ZOOM_RANGE.end() * 100.0);
|
||||
assert_eq!(paper_box(5.0), PAPER * 5.0, "500% is kept, not clamped to the old ceiling");
|
||||
// Every round size above the old 200% ceiling is a step the toolbar can reach.
|
||||
let mut z = *ZOOM_RANGE.start();
|
||||
let mut up = vec![z];
|
||||
while z < *ZOOM_RANGE.end() {
|
||||
z = zoom_step(z, true);
|
||||
up.push(z);
|
||||
}
|
||||
for want in [2.5, 3.0, 4.0, 5.0] {
|
||||
assert!(up.contains(&want), "no step reaches {want}%: {up:?}");
|
||||
}
|
||||
// A card stays sharp at the ceiling instead of upscaled from the old 768px cap. A Letter
|
||||
// page fills the height of the box, so at 500% it needs PAPER.y * 5.0 points of it, which
|
||||
// on a 1.5 pixels-per-point screen is this many pixels.
|
||||
let wanted = PAPER.y * 5.0 * 1.5;
|
||||
assert!(MAX_SIDE >= wanted, "MAX_SIDE {MAX_SIDE} is under the {wanted}px a card needs at 500%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrow_keys_step_by_file_and_by_row() {
|
||||
// The list: one at a time, staying put at both ends.
|
||||
|
||||
@@ -2586,7 +2586,8 @@ impl PdfCraftApp {
|
||||
("combine-zoom", _) => {
|
||||
let percent = value.trim_end_matches('%').parse::<f32>().map_err(|e| e.to_string())?;
|
||||
if !percent.is_finite() || !combine_grid::ZOOM_RANGE.contains(&(percent / 100.0)) {
|
||||
return Err("combine-zoom must be between 60 and 200".into());
|
||||
let (lo, hi) = (combine_grid::ZOOM_RANGE.start(), combine_grid::ZOOM_RANGE.end());
|
||||
return Err(format!("combine-zoom must be between {} and {}", lo * 100.0, hi * 100.0));
|
||||
}
|
||||
self.set_combine_zoom(percent / 100.0);
|
||||
}
|
||||
|
||||
@@ -403,7 +403,8 @@ fn thumbnails_show_the_first_page_each_file_takes() {
|
||||
assert!(matches!(thumbs[0], CombineThumb::Ready { page: 2, .. }), "\"3, 1\" starts with page 3: {:?}", thumbs[0]);
|
||||
assert!(matches!(thumbs[1], CombineThumb::Ready { page: 0, .. }));
|
||||
let CombineThumb::Ready { width, height, .. } = thumbs[1] else { unreachable!() };
|
||||
assert!(width > 0 && height > 0 && width <= 768 && height <= 768, "{width}×{height}");
|
||||
// Within MAX_SIDE (combine_grid), which is not visible from here.
|
||||
assert!(width > 0 && height > 0 && width <= 1536 && height <= 1536, "{width}×{height}");
|
||||
// A new range is a new thumbnail, never the old page.
|
||||
h.state_mut().combine_draft[0].range = "2".into();
|
||||
assert!(settle(&mut h, |app| matches!(app.combine_thumbnails()[0], CombineThumb::Ready { page: 1, .. })));
|
||||
@@ -541,7 +542,8 @@ fn long_thin_pages_get_thumbnails_within_the_size_limits() {
|
||||
assert!(settle(&mut h, |app| app.combine_thumbnails().iter().all(|t| *t != CombineThumb::None && *t != CombineThumb::Pending)));
|
||||
for t in h.state().combine_thumbnails() {
|
||||
match t {
|
||||
CombineThumb::Ready { width, height, .. } => assert!(width <= 768 && height <= 768, "{width}×{height}"),
|
||||
// Within MAX_SIDE (combine_grid), which is not visible from here.
|
||||
CombineThumb::Ready { width, height, .. } => assert!(width <= 1536 && height <= 1536, "{width}×{height}"),
|
||||
other => panic!("each strip gets a thumbnail: {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -728,15 +730,15 @@ fn the_toolbar_sizes_the_cards_within_limits_and_the_size_is_kept_in_the_setting
|
||||
assert_eq!(h.state().combine_zoom, 1.25);
|
||||
let after = card(&h, "a.pdf");
|
||||
assert!(after.width() > before.width() && after.height() > before.height(), "{before:?} → {after:?}");
|
||||
// Larger until it can't go further: 200%, and the button says so.
|
||||
for _ in 0..10 {
|
||||
// Larger until it can't go further: 500% (#2531), and the button says so.
|
||||
for _ in 0..12 {
|
||||
if disabled(&h, "Larger pages") {
|
||||
break;
|
||||
}
|
||||
h.get_by_label("Larger pages").click();
|
||||
h.run_steps(2);
|
||||
}
|
||||
assert_eq!(h.state().combine_zoom, 2.0);
|
||||
assert_eq!(h.state().combine_zoom, 5.0);
|
||||
assert!(disabled(&h, "Larger pages"));
|
||||
// The percentage resets it.
|
||||
h.get_by_label("Reset page size").click();
|
||||
@@ -758,17 +760,22 @@ fn the_toolbar_sizes_the_cards_within_limits_and_the_size_is_kept_in_the_setting
|
||||
fresh.restore(&saved);
|
||||
assert_eq!(fresh.combine_zoom, 0.6);
|
||||
fresh.restore(r#"{"combine_zoom": 99}"#);
|
||||
assert_eq!(fresh.combine_zoom, 2.0);
|
||||
assert_eq!(fresh.combine_zoom, 5.0);
|
||||
fresh.restore(r#"{"combine_zoom": "big"}"#);
|
||||
assert_eq!(fresh.combine_zoom, 2.0);
|
||||
assert_eq!(fresh.combine_zoom, 5.0);
|
||||
// Scriptable in percent, with a clear refusal outside the range.
|
||||
let app = h.state_mut();
|
||||
app.set_option("combine-zoom", "150%").unwrap();
|
||||
assert_eq!(app.combine_zoom, 1.5);
|
||||
for bad in ["59", "201", "big", "NaN", "inf", ""] {
|
||||
// #2531: 200% was not enough to read a value off a card; 500% has to be reachable.
|
||||
for pct in ["200", "300", "400", "500"] {
|
||||
app.set_option("combine-zoom", pct).unwrap();
|
||||
assert_eq!(app.combine_zoom, pct.parse::<f32>().unwrap() / 100.0);
|
||||
}
|
||||
for bad in ["59", "501", "big", "NaN", "inf", ""] {
|
||||
assert!(app.set_option("combine-zoom", bad).is_err(), "{bad:?}");
|
||||
}
|
||||
assert_eq!(app.combine_zoom, 1.5);
|
||||
assert_eq!(app.combine_zoom, 5.0, "a refused value leaves the size alone");
|
||||
// The list has no card size.
|
||||
h.get_by_label("List view").click();
|
||||
h.run_steps(2);
|
||||
@@ -780,7 +787,7 @@ fn the_slider_sizes_the_cards_from_smallest_to_largest() {
|
||||
let mut h = grid_of_pages(&[("a.pdf", 1), ("b.pdf", 1)]);
|
||||
let slider = h.get_by_label("Page size").rect();
|
||||
drag(&mut h, slider.center(), slider.right_center() + vec2(40.0, 0.0));
|
||||
assert_eq!(h.state().combine_zoom, 2.0);
|
||||
assert_eq!(h.state().combine_zoom, 5.0);
|
||||
let slider = h.get_by_label("Page size").rect();
|
||||
drag(&mut h, slider.center(), slider.left_center() - vec2(40.0, 0.0));
|
||||
assert_eq!(h.state().combine_zoom, 0.6);
|
||||
@@ -883,7 +890,38 @@ fn zooming_keeps_the_card_under_the_pointer_in_place_and_thumbnails_never_go_bla
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_and_arrow_keys_work_at_the_smallest_and_largest_sizes() {
|
||||
fn cards_at_the_top_zoom_render_a_sharper_thumbnail_within_the_texture_limit() {
|
||||
// #2531: raising the ceiling is only worth something if the thumbnail gets sharper with it.
|
||||
// The texture cap was 768px, which upscaled any card past 200%.
|
||||
let mut h = grid_of_pages(&[("a.pdf", 1)]);
|
||||
h.state_mut().set_option("combine-zoom", "100").unwrap();
|
||||
assert!(settle(&mut h, |app| ready(&app.combine_thumbnails()[0])));
|
||||
let CombineThumb::Ready { width: w0, height: h0, .. } = h.state().combine_thumbnails()[0] else { unreachable!() };
|
||||
let at_100 = w0.max(h0);
|
||||
|
||||
h.state_mut().set_option("combine-zoom", "500").unwrap();
|
||||
// The old thumbnail stands in until the sharper one arrives, so "ready" is not enough: wait
|
||||
// for the size to actually change.
|
||||
let mut redrawn = false;
|
||||
for _ in 0..400 {
|
||||
h.run_steps(1);
|
||||
if matches!(h.state().combine_thumbnails()[0], CombineThumb::Ready { width, height, .. } if width.max(height) != at_100) {
|
||||
redrawn = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let CombineThumb::Ready { width, height, .. } = h.state().combine_thumbnails()[0] else { unreachable!() };
|
||||
let at_500 = width.max(height);
|
||||
assert!(redrawn, "500% never re-rendered the thumbnail: {width}×{height}");
|
||||
assert!(at_500 <= 1536, "past the texture limit: {width}×{height}");
|
||||
assert!(at_500 >= at_100 * 4, "500% is not sharper than 100%: {at_100}px → {at_500}px");
|
||||
}
|
||||
|
||||
// 60% against a large size, rather than the largest: at 500% a card is wider than this viewport,
|
||||
// so only one fits across and "down moves a row" would have nothing to check.
|
||||
#[test]
|
||||
fn dragging_and_arrow_keys_work_at_the_smallest_and_a_large_size() {
|
||||
let mut columns = Vec::new();
|
||||
for zoom in ["60", "200"] {
|
||||
let files: Vec<(String, Vec<u8>)> = (0..24).map(|i| (format!("f{i:02}.pdf"), fixture(1))).collect();
|
||||
|
||||
@@ -2992,8 +2992,8 @@ milestone = "M4"
|
||||
status = "shipped"
|
||||
commands = ["page.combine"]
|
||||
tools = ["doc_combine"]
|
||||
evidence = ["crates/ui-egui/tests/combine_grid.rs::combine_shows_a_grid_by_default_and_the_view_is_kept_in_the_settings", "crates/ui-egui/tests/combine_grid.rs::cards_select_like_the_list_and_the_selection_survives_switching_views", "crates/ui-egui/tests/combine_grid.rs::dragging_a_card_moves_it_and_undo_puts_it_back", "crates/ui-egui/tests/combine_grid.rs::a_dragged_selection_moves_together_to_the_first_and_last_gaps", "crates/ui-egui/tests/combine_grid.rs::thumbnails_show_the_first_page_each_file_takes", "crates/ui-egui/tests/combine_grid.rs::many_files_render_only_what_is_in_view_a_few_at_a_time", "crates/ui-egui/tests/combine_grid.rs::long_thin_pages_get_thumbnails_within_the_size_limits", "crates/ui-egui/tests/combine_grid.rs::combine_follows_the_order_shown_in_the_grid", "crates/ui-egui/src/combine_grid.rs::render_threads_never_exceed_three_while_files_come_and_go", "crates/ui-egui/src/combine_grid.rs::textures_over_budget_go_longest_unseen_first_and_those_in_view_stay", "crates/ui-egui/src/combine_grid.rs::on_the_web_one_thumbnail_waits_at_a_time", "crates/ui-egui/tests/combine_grid.rs::a_file_whose_pages_cant_be_measured_says_there_is_no_preview", "crates/ui-egui/tests/combine_grid.rs::the_toolbar_sizes_the_cards_within_limits_and_the_size_is_kept_in_the_settings", "crates/ui-egui/tests/combine_grid.rs::ctrl_keys_and_ctrl_wheel_size_the_cards_and_leave_the_window_alone", "crates/ui-egui/tests/combine_grid.rs::zooming_keeps_the_card_under_the_pointer_in_place_and_thumbnails_never_go_blank", "crates/ui-egui/tests/combine_grid.rs::zooming_quickly_never_runs_more_than_three_render_threads", "crates/ui-egui/tests/combine_grid.rs::hovering_a_card_shows_its_trash_and_magnifier_and_only_then", "crates/ui-egui/tests/combine_grid.rs::the_card_trash_removes_only_that_file_and_undo_brings_it_back", "crates/ui-egui/tests/combine_grid.rs::the_magnifier_shows_the_file_large_and_flips_through_the_pages_it_adds", "crates/ui-egui/tests/combine_grid.rs::the_preview_closes_when_its_file_leaves_and_keeps_to_three_render_threads", "crates/ui-egui/tests/combine_grid.rs::a_right_click_on_a_card_offers_preview_and_remove", "crates/ui-egui/tests/combine_grid.rs::space_shows_the_selected_file_large_in_the_grid_and_the_list", "crates/ui-egui/tests/combine_grid.rs::expanding_a_file_shows_the_pages_it_adds_in_order_with_their_numbers", "crates/ui-egui/tests/combine_grid.rs::expand_all_and_collapse_all_from_the_toolbar_and_a_right_click", "crates/ui-egui/tests/combine_grid.rs::expanding_is_a_view_kept_across_views_and_zoom_and_never_an_undo_step", "crates/ui-egui/tests/combine_grid.rs::dragging_a_page_moves_that_page_even_between_another_files_pages", "crates/ui-egui/tests/combine_grid.rs::combining_with_files_expanded_gives_the_same_document", "crates/ui-egui/tests/combine_grid.rs::a_page_click_selects_that_page_and_the_arrows_step_rows_of_cards", "crates/ui-egui/tests/combine_grid.rs::a_page_cards_magnifier_opens_the_preview_on_that_page", "crates/ui-egui/tests/combine_grid.rs::a_long_file_expanded_renders_only_its_pages_in_view_within_three_threads", "crates/ui-egui/tests/combine_grid.rs::collapsing_brings_the_files_card_into_view", "crates/ui-egui/tests/combine_grid.rs::a_file_too_long_to_spread_out_offers_no_expand", "crates/ui-egui/src/combine_grid.rs::the_next_page_of_a_file_reuses_its_open_document_until_it_closes", "crates/ui-egui/tests/combine_grid.rs::removing_pages_from_the_hover_bar_the_toolbar_and_delete_never_touches_the_file", "crates/ui-egui/tests/combine_grid.rs::a_split_file_counts_once_and_combines_with_one_bookmark", "crates/ui-egui/src/combine_ui.rs::a_page_moved_between_another_files_pages_splits_its_file_and_parts_rejoin", "crates/ui-egui/src/combine_ui.rs::removing_pages_drops_emptied_parts_and_joins_what_meets", "crates/ui-egui/src/combine_ui.rs::page_lists_become_the_shortest_range", "crates/ui-egui/tests/combine_grid.rs::pages_picked_out_of_sight_or_before_an_undo_are_let_go_of", "crates/ui-egui/tests/combine_grid.rs::a_files_parts_join_when_what_was_between_them_goes_and_one_file_cannot_be_combined", "crates/organize/src/tests.rs::a_page_shown_twice_gets_its_own_annotations", "crates/engine/src/tests.rs::a_page_with_a_form_field_shown_twice_keeps_one_field", "crates/ui-egui/tests/combine_grid.rs::files_picked_by_automation_let_go_of_the_pages_picked"]
|
||||
notes = "Combine files shows a card per file (the default; Grid/List toggle kept in the settings, --combine-view grid|list): the first page the file contributes (its page range's first page), stacked when it contributes more, and its name shortened in the middle. Cards are sized from 60% to 200% (toolbar slider, buttons and percentage reset, Ctrl/⌘ +/−/0, Ctrl/⌘ with the wheel or a pinch over the grid; kept in the settings, --combine-zoom 60..200), keeping the card under the pointer in place. Hovering a card shows its own actions along the top of its page: remove that file only (whatever else is selected; undoable) and a magnifier that shows the file large, flipping through the pages it adds (buttons, Left/Right; Esc, close or a click outside closes it), also on the card's right-click menu, and Space shows the selected file large (the way there by keyboard). Cards select (click, Ctrl, Shift, arrows by card and by row), drag to reorder (one undo step; the grid scrolls at its edges) and share the list's toolbar. Thumbnails render off the UI thread, only for cards in view, sized to both sides of the page and at most 768 px; at most three render threads at once, counted as threads until they exit (no watchdog replacements; a page stuck for good in one step that can't be interrupted keeps its thread, and so its place, for the rest of the run); textures out of view are dropped over a 32 MB budget (those in view are kept). Locked, unreadable and bad-range files show why instead of a page, and a file whose page size can't be read (e.g. opened with an RC4 owner password) says No preview; page ranges are edited in the list. On the web a thumbnail, and the page shown large, render on the UI thread (one per frame, and only one waits at a time), so a very slow or very large page stalls that frame. A file that adds more than one page (up to 2000) expands into its pages (hover bar, right-click, or Expand all / Collapse all in the toolbar): one card per page it adds, in its range's order, each saying Page N and the file's name, on a tinted band; a click on a page selects that page, Up/Down step rows of cards, dragging a file's card moves the whole file (a drop among a file's pages goes to its nearer end), and its magnifier opens the preview on that page; expanding is a view, not an undo step, and changes nothing in the combined file; a page's render reuses its file's open document for a moment rather than reading the file again. With a file expanded, single pages are selected (click, Ctrl, Shift), dragged anywhere (a page moved between another file's pages splits its file into parts, shown as rows in the list with their own page ranges; parts side by side join again) and removed (a page card's trash, the toolbar, Delete), each one undo step, never changing the files themselves; a split file still counts and combines as one file."
|
||||
evidence = ["crates/ui-egui/tests/combine_grid.rs::combine_shows_a_grid_by_default_and_the_view_is_kept_in_the_settings", "crates/ui-egui/tests/combine_grid.rs::cards_select_like_the_list_and_the_selection_survives_switching_views", "crates/ui-egui/tests/combine_grid.rs::dragging_a_card_moves_it_and_undo_puts_it_back", "crates/ui-egui/tests/combine_grid.rs::a_dragged_selection_moves_together_to_the_first_and_last_gaps", "crates/ui-egui/tests/combine_grid.rs::thumbnails_show_the_first_page_each_file_takes", "crates/ui-egui/tests/combine_grid.rs::many_files_render_only_what_is_in_view_a_few_at_a_time", "crates/ui-egui/tests/combine_grid.rs::long_thin_pages_get_thumbnails_within_the_size_limits", "crates/ui-egui/tests/combine_grid.rs::combine_follows_the_order_shown_in_the_grid", "crates/ui-egui/src/combine_grid.rs::render_threads_never_exceed_three_while_files_come_and_go", "crates/ui-egui/src/combine_grid.rs::textures_over_budget_go_longest_unseen_first_and_those_in_view_stay", "crates/ui-egui/src/combine_grid.rs::on_the_web_one_thumbnail_waits_at_a_time", "crates/ui-egui/tests/combine_grid.rs::a_file_whose_pages_cant_be_measured_says_there_is_no_preview", "crates/ui-egui/tests/combine_grid.rs::the_toolbar_sizes_the_cards_within_limits_and_the_size_is_kept_in_the_settings", "crates/ui-egui/src/combine_grid.rs::the_cards_zoom_far_enough_to_read_a_value", "crates/ui-egui/tests/combine_grid.rs::ctrl_keys_and_ctrl_wheel_size_the_cards_and_leave_the_window_alone", "crates/ui-egui/tests/combine_grid.rs::zooming_keeps_the_card_under_the_pointer_in_place_and_thumbnails_never_go_blank", "crates/ui-egui/tests/combine_grid.rs::zooming_quickly_never_runs_more_than_three_render_threads", "crates/ui-egui/tests/combine_grid.rs::hovering_a_card_shows_its_trash_and_magnifier_and_only_then", "crates/ui-egui/tests/combine_grid.rs::the_card_trash_removes_only_that_file_and_undo_brings_it_back", "crates/ui-egui/tests/combine_grid.rs::the_magnifier_shows_the_file_large_and_flips_through_the_pages_it_adds", "crates/ui-egui/tests/combine_grid.rs::the_preview_closes_when_its_file_leaves_and_keeps_to_three_render_threads", "crates/ui-egui/tests/combine_grid.rs::a_right_click_on_a_card_offers_preview_and_remove", "crates/ui-egui/tests/combine_grid.rs::space_shows_the_selected_file_large_in_the_grid_and_the_list", "crates/ui-egui/tests/combine_grid.rs::expanding_a_file_shows_the_pages_it_adds_in_order_with_their_numbers", "crates/ui-egui/tests/combine_grid.rs::expand_all_and_collapse_all_from_the_toolbar_and_a_right_click", "crates/ui-egui/tests/combine_grid.rs::expanding_is_a_view_kept_across_views_and_zoom_and_never_an_undo_step", "crates/ui-egui/tests/combine_grid.rs::dragging_a_page_moves_that_page_even_between_another_files_pages", "crates/ui-egui/tests/combine_grid.rs::combining_with_files_expanded_gives_the_same_document", "crates/ui-egui/tests/combine_grid.rs::a_page_click_selects_that_page_and_the_arrows_step_rows_of_cards", "crates/ui-egui/tests/combine_grid.rs::a_page_cards_magnifier_opens_the_preview_on_that_page", "crates/ui-egui/tests/combine_grid.rs::a_long_file_expanded_renders_only_its_pages_in_view_within_three_threads", "crates/ui-egui/tests/combine_grid.rs::collapsing_brings_the_files_card_into_view", "crates/ui-egui/tests/combine_grid.rs::a_file_too_long_to_spread_out_offers_no_expand", "crates/ui-egui/src/combine_grid.rs::the_next_page_of_a_file_reuses_its_open_document_until_it_closes", "crates/ui-egui/tests/combine_grid.rs::removing_pages_from_the_hover_bar_the_toolbar_and_delete_never_touches_the_file", "crates/ui-egui/tests/combine_grid.rs::a_split_file_counts_once_and_combines_with_one_bookmark", "crates/ui-egui/src/combine_ui.rs::a_page_moved_between_another_files_pages_splits_its_file_and_parts_rejoin", "crates/ui-egui/src/combine_ui.rs::removing_pages_drops_emptied_parts_and_joins_what_meets", "crates/ui-egui/src/combine_ui.rs::page_lists_become_the_shortest_range", "crates/ui-egui/tests/combine_grid.rs::pages_picked_out_of_sight_or_before_an_undo_are_let_go_of", "crates/ui-egui/tests/combine_grid.rs::a_files_parts_join_when_what_was_between_them_goes_and_one_file_cannot_be_combined", "crates/organize/src/tests.rs::a_page_shown_twice_gets_its_own_annotations", "crates/engine/src/tests.rs::a_page_with_a_form_field_shown_twice_keeps_one_field", "crates/ui-egui/tests/combine_grid.rs::files_picked_by_automation_let_go_of_the_pages_picked"]
|
||||
notes = "Combine files shows a card per file (the default; Grid/List toggle kept in the settings, --combine-view grid|list): the first page the file contributes (its page range's first page), stacked when it contributes more, and its name shortened in the middle. Cards are sized from 60% to 500% (toolbar slider, buttons and percentage reset, Ctrl/⌘ +/−/0, Ctrl/⌘ with the wheel or a pinch over the grid; the buttons step 60/80/100/125/150/175/200/250/300/400/500%, kept in the settings, --combine-zoom 60..500), keeping the card under the pointer in place. 200% used to be the ceiling, which was too small to read values off an invoice while reordering the pages of a statement (#2531). Hovering a card shows its own actions along the top of its page: remove that file only (whatever else is selected; undoable) and a magnifier that shows the file large, flipping through the pages it adds (buttons, Left/Right; Esc, close or a click outside closes it), also on the card's right-click menu, and Space shows the selected file large (the way there by keyboard). Cards select (click, Ctrl, Shift, arrows by card and by row), drag to reorder (one undo step; the grid scrolls at its edges) and share the list's toolbar. Thumbnails render off the UI thread, only for cards in view, sized to both sides of the page and at most 1536 px (room for a Letter page at 500% on a 1.5 pixels-per-point screen); at most three render threads at once, counted as threads until they exit (no watchdog replacements; a page stuck for good in one step that can't be interrupted keeps its thread, and so its place, for the rest of the run); textures out of view are dropped over a 32 MB budget (those in view are kept). Locked, unreadable and bad-range files show why instead of a page, and a file whose page size can't be read (e.g. opened with an RC4 owner password) says No preview; page ranges are edited in the list. On the web a thumbnail, and the page shown large, render on the UI thread (one per frame, and only one waits at a time), so a very slow or very large page stalls that frame. A file that adds more than one page (up to 2000) expands into its pages (hover bar, right-click, or Expand all / Collapse all in the toolbar): one card per page it adds, in its range's order, each saying Page N and the file's name, on a tinted band; a click on a page selects that page, Up/Down step rows of cards, dragging a file's card moves the whole file (a drop among a file's pages goes to its nearer end), and its magnifier opens the preview on that page; expanding is a view, not an undo step, and changes nothing in the combined file; a page's render reuses its file's open document for a moment rather than reading the file again. With a file expanded, single pages are selected (click, Ctrl, Shift), dragged anywhere (a page moved between another file's pages splits its file into parts, shown as rows in the list with their own page ranges; parts side by side join again) and removed (a page card's trash, the toolbar, Delete), each one undo step, never changing the files themselves; a split file still counts and combines as one file."
|
||||
|
||||
[[feature]]
|
||||
id = "organize.combine-options"
|
||||
|
||||
Reference in New Issue
Block a user