Files
pdfcraft/crates/ui-egui/tests/ui_group/column_select.rs
mtlcl410andClaude Opus 5.5 8356665d7b test(ui): wait for the text layer by state, not for a fixed second (#1277) (#2327)
Four ui_group harnesses waited at most one wall-clock second (200 x 5 ms)
for the page to render, and stopped as soon as nothing was pending, which
can be before the text layer is even asked for. Under load the text
layer came later and column-select, identity and update-check tests
failed. They now wait through support::wait_until for what they use (a
glyph on screen, the highlight in the document), with its 30-second
deadline. The highlighter quick-click test of #1277 itself did not fail
on this base in 256 runs.


Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:54:56 -04:00

149 lines
6.4 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Issue #740: column select, as in Acrobat. Alt-drag (Option-drag on macOS) with the Select tool,
//! or a plain drag with Edit ▸ Column select, selects only the text inside the rectangle drawn.
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
use egui_kittest::Harness;
use pdfcraft_ui_egui::PdfCraftApp;
/// One 300×200 page with a three-row table, "Name Qty / Apple 12 / Pear 7". Its columns sit far
/// enough apart that reading order takes one column, then the other: glyphs 0–3 "Name", 4–8
/// "Apple", 9–12 "Pear", 13–15 "Qty", 16–17 "12", 18 "7".
fn table() -> Vec<u8> {
let content =
"BT /F1 14 Tf 20 150 Td (Name) Tj 130 0 Td (Qty) Tj -130 -24 Td (Apple) Tj 130 0 Td (12) Tj -130 -24 Td (Pear) Tj 130 0 Td (7) Tj ET";
format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len()
)
.into_bytes()
}
fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("table.pdf", None, table()).expect("opens");
app.set_option("left", "closed").unwrap();
app.set_option("author", "Tester").unwrap();
setup(&mut app);
app
});
// Wait for the text layer itself (#1277): under load the first render takes longer than a
// fixed second, and nothing may be pending yet before the text layer is asked for.
crate::support::wait_until(&mut h, "the page's text layer", |app| app.views[0].glyph_screen_pos(0, 0).is_some() && !app.render_pending());
h
}
fn glyph(h: &Harness<'static, PdfCraftApp>, g: usize) -> Pos2 {
h.state().views[0].glyph_screen_pos(0, g).expect("glyph on screen")
}
fn text(h: &Harness<'static, PdfCraftApp>) -> Option<String> {
h.state().views[0].selected_text()
}
/// Press at `from`, move to `to` and release there, with `modifiers` held throughout.
fn drag(h: &mut Harness<'static, PdfCraftApp>, from: Pos2, to: Pos2, modifiers: Modifiers) {
h.event(Event::ModifiersChanged(modifiers));
h.hover_at(from);
h.run_steps(1);
h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers });
h.run_steps(1);
for k in 1..=4 {
h.hover_at(from + (to - from) * (k as f32 / 4.0));
h.run_steps(1);
}
h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers });
h.run_steps(1);
h.event(Event::ModifiersChanged(Modifiers::NONE));
h.run_steps(1);
}
/// Below-right of the top two rows: past the "y" of "Qty" (the wider cell), level with "12".
fn below_right_of_row_two(h: &Harness<'static, PdfCraftApp>) -> Pos2 {
egui::pos2(glyph(h, 15).x.max(glyph(h, 17).x) + 12.0, glyph(h, 17).y + 8.0)
}
/// From above-left of "Name" to below-right of "12": the top two rows of both columns.
fn top_two_rows(h: &Harness<'static, PdfCraftApp>) -> (Pos2, Pos2) {
(glyph(h, 0) - vec2(12.0, 12.0), below_right_of_row_two(h))
}
#[test]
fn alt_drag_selects_only_the_text_inside_the_rectangle() {
let mut h = harness(|_| {});
let (from, to) = top_two_rows(&h);
// A plain drag selects in reading order: everything from "Name" to "12".
drag(&mut h, from, to, Modifiers::NONE);
assert_eq!(text(&h).as_deref(), Some("Name\nApple\nPear\nQty\n12"));
h.run_steps(60);
// Alt (Option on macOS) at the start of the drag: only the rectangle, row by row.
drag(&mut h, from, to, Modifiers::ALT);
assert_eq!(text(&h).as_deref(), Some("Name\tQty\nApple\t12"));
// One column of numbers, which is what the issue asked for.
h.run_steps(60);
let (from, to) = (glyph(&h, 13) - vec2(8.0, 12.0), egui::pos2(glyph(&h, 15).x + 12.0, glyph(&h, 18).y + 12.0));
drag(&mut h, from, to, Modifiers::ALT);
assert_eq!(text(&h).as_deref(), Some("Qty\n12\n7"));
}
#[test]
fn the_column_select_tool_needs_no_modifier() {
let mut h = harness(|app| assert!(app.execute("edit.column_select")));
assert_eq!(h.state().quick_tool, pdfcraft_ui_egui::QuickTool::ColumnSelect);
let (from, to) = top_two_rows(&h);
drag(&mut h, from, to, Modifiers::NONE);
assert_eq!(text(&h).as_deref(), Some("Name\tQty\nApple\t12"));
// A rectangle around no text selects nothing.
h.run_steps(60);
let below = glyph(&h, 12) + vec2(0.0, 60.0);
drag(&mut h, below, below + vec2(60.0, 30.0), Modifiers::NONE);
assert_eq!(text(&h), None);
}
/// ⇧-click extends a reading-order selection; a column selection has no anchor glyph, so a
/// ⇧-click clears it like a plain click.
#[test]
fn shift_click_clears_a_column_selection() {
let mut h = harness(|_| {});
let (from, to) = top_two_rows(&h);
drag(&mut h, from, to, Modifiers::ALT);
assert!(text(&h).is_some());
h.run_steps(60);
let at = glyph(&h, 10);
h.event(Event::ModifiersChanged(Modifiers::SHIFT));
h.hover_at(at);
h.run_steps(1);
for pressed in [true, false] {
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT });
}
h.run_steps(1);
h.event(Event::ModifiersChanged(Modifiers::NONE));
h.run_steps(1);
assert_eq!(text(&h), None);
}
/// Alt-drag with the highlighter marks only the cells inside the rectangle: one quad per piece
/// of a line. The drag starts on text, since off text the highlighter draws an area highlight.
#[test]
fn alt_drag_with_the_highlighter_marks_only_the_column() {
let mut h = harness(|app| assert!(app.execute("comment.highlight")));
// Just above-left of the centre of "N", still on the glyph.
let (from, to) = (glyph(&h, 0) - vec2(2.0, 2.0), below_right_of_row_two(&h));
drag(&mut h, from, to, Modifiers::ALT);
h.run_steps(3);
let app = h.state();
let doc = app.session.get(app.views[0].id).unwrap();
let marks: Vec<(&str, usize)> = doc.info.annotations.iter().map(|a| (a.subtype.as_str(), a.quads.len())).collect();
assert_eq!(marks, [("Highlight", 4)], "Name, Qty, Apple and 12, each its own quad");
}