diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 346f1d0fb..a9b8faf4d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1758,7 +1758,14 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.selection = Some(Selection { page: i, anchor: first, head: last }); } } else if resp.clicked() && !over_link { - view.selection = None; + // ⇧-click extends a selection on this page to the click, keeping its + // anchor (#527). Otherwise (no shift, or no selection on this page; a + // selection cannot yet span pages) a click clears the selection. + let shift = ui.input(|inp| inp.modifiers.shift); + match (view.selection.as_mut().filter(|s| shift && s.page == i), text.nearest(vx, vy)) { + (Some(sel), Some(h)) => sel.head = h, + _ => view.selection = None, + } } } } diff --git a/crates/ui-egui/tests/text_select.rs b/crates/ui-egui/tests/text_select.rs index 683f391c9..342668b55 100644 --- a/crates/ui-egui/tests/text_select.rs +++ b/crates/ui-egui/tests/text_select.rs @@ -85,3 +85,57 @@ fn quick_clicks_with_the_highlighter_mark_only_the_word() { assert_eq!(marks, [("Highlight", 1)], "one highlight, on the double-clicked word"); assert!(app.views[0].selected_text().is_none()); } + +/// Press at `from`, move to `to` and release there. +fn drag(h: &mut Harness<'static, PdfCraftApp>, from: Pos2, to: Pos2) { + h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); + 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: Modifiers::NONE }); + h.run_steps(1); +} + +/// Click at `pos` with Shift held. +fn shift_click(h: &mut Harness<'static, PdfCraftApp>, pos: Pos2) { + h.event(Event::ModifiersChanged(Modifiers::SHIFT)); + h.hover_at(pos); + h.run_steps(1); + for pressed in [true, false] { + h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT }); + } + h.run_steps(1); + h.event(Event::ModifiersChanged(Modifiers::NONE)); + h.run_steps(1); +} + +/// Issue #527: ⇧-click extends the text selection to the click, keeping its anchor. +#[test] +fn shift_click_extends_the_selection_from_its_anchor() { + let (mut h, _) = harness(|_| {}); + let glyph = |h: &Harness<'static, PdfCraftApp>, g: usize| h.state().views[0].glyph_screen_pos(0, g).expect("glyph on screen"); + // Drag over "quick" (glyphs 4..=8). + let (q, k) = (glyph(&h, 4), glyph(&h, 8)); + drag(&mut h, q, k); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick")); + h.run_steps(60); + // ⇧-click on the "n" of "brown": the selection runs from the anchor to it. + let n = glyph(&h, 14); + shift_click(&mut h, n); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick brown")); + h.run_steps(60); + // ⇧-click before the anchor: the anchor stays, the selection now runs back to the click. + let t = glyph(&h, 0); + shift_click(&mut h, t); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("The q")); + h.run_steps(60); + // A plain click still clears it. + click(&mut h, n); + assert_eq!(h.state().views[0].selected_text(), None); + h.run_steps(60); + // ⇧-click with nothing selected selects nothing. + shift_click(&mut h, n); + assert_eq!(h.state().views[0].selected_text(), None); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 04ea2600c..d711938d5 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1478,8 +1478,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word"] -notes = "Drag to select; with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). The markup and Redact tools mark a double-clicked word and stop there. Missing: paragraph selection, and ⌘A across the whole document (it selects the current page)." +evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word", "crates/ui-egui/tests/text_select.rs::shift_click_extends_the_selection_from_its_anchor"] +notes = "Drag to select; ⇧-click extends the selection to the click, keeping its anchor (same page only); with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). The markup and Redact tools mark a double-clicked word and stop there. Missing: selection across pages (dragging or ⇧-clicking onto another page), paragraph selection, and ⌘A across the whole document (it selects the current page)." [[feature]] id = "view.copy-text"