Text selection: a drag after a double- or triple-click extends by whole words or lines (fixes #1685) (#1689)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Marco Arnulfo
2026-10-15 02:20:09 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent 44fbacfffd
commit f48b76c146
2 changed files with 59 additions and 10 deletions
+24 -10
View File
@@ -266,6 +266,16 @@ impl Selection {
}
}
/// The glyphs `clicks` quick clicks select around glyph `a`: two the word, three the line, more
/// the page.
fn click_unit(text: &PageText, clicks: u32, a: usize) -> Option<(usize, usize)> {
match clicks {
2 => text.word_at(a),
3 => text.line_at(a),
_ => text.glyphs.len().checked_sub(1).map(|last| (0, last)),
}
}
struct PageTex {
tag: u64,
tex: TextureHandle,
@@ -2840,11 +2850,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
// Multi-click highlights immediately on pointer press instead of release (#1024).
let clicks = if view.clicks > 2 && tool != QuickTool::Select { 1 } else { view.clicks };
if clicks >= 2 && over_text {
let span = text.nearest(vx, vy).and_then(|a| match clicks {
2 => text.word_at(a),
3 => text.line_at(a),
_ => text.glyphs.len().checked_sub(1).map(|last| (0, last)),
});
let span = text.nearest(vx, vy).and_then(|a| click_unit(&text, clicks, a));
if let Some((first, last)) = span {
view.selection = Some(Selection::run(i, first, last));
}
@@ -2862,10 +2868,22 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
};
}
}
let clicks = if view.clicks > 2 && tool != QuickTool::Select { 1 } else { view.clicks };
if resp.dragged()
&& let Some(sel) = view.selection.as_mut().filter(|s| s.page == i)
{
match &mut sel.span {
// After a multi-click the drag extends by its unit (#1685), as in
// Acrobat and browsers: whole words or lines, always keeping the one
// clicked, which holds the anchor whichever way the drag went.
Span::Run { anchor, head } if clicks >= 2 => {
if let Some(h) = text.nearest(vx, vy)
&& let Some((first, last)) = click_unit(&text, clicks, *anchor)
{
let (to_first, to_last) = click_unit(&text, clicks, h).unwrap_or((h, h));
(*anchor, *head) = if to_first < first { (last, to_first) } else { (first, to_last.max(last)) };
}
}
Span::Run { head, .. } => {
if let Some(h) = text.nearest(vx, vy) {
*head = h;
@@ -2901,11 +2919,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
// (a highlight made by the double-click left its word selected).
if release_clicks > 0 && clicks >= 2 && over_text {
if view.selection.is_none() {
let span = text.nearest(vx, vy).and_then(|a| match clicks {
2 => text.word_at(a),
3 => text.line_at(a),
_ => text.glyphs.len().checked_sub(1).map(|last| (0, last)),
});
let span = text.nearest(vx, vy).and_then(|a| click_unit(&text, clicks, a));
if let Some((first, last)) = span {
view.selection = Some(Selection::run(i, first, last));
}
@@ -314,3 +314,38 @@ fn a_selection_drag_held_at_the_bottom_edge_scrolls_the_view() {
h.run_steps(2);
assert_eq!(h.state().views[0].selected_text(), Some(held), "releasing keeps the selection");
}
/// Click at `at`, then press there again and drag to `to` before releasing: a double-click drag.
fn double_click_drag(h: &mut Harness<'static, PdfCraftApp>, at: Pos2, to: Pos2) {
click(h, at);
press_and_move(h, at, to);
h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(1);
}
/// Issue #1685: dragging after a double-click extends the selection a word at a time, and the
/// double-clicked word stays selected whichever way the drag goes.
#[test]
fn a_double_click_drag_extends_by_whole_words_both_ways() {
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");
// "The quick brown fox": the "u" of "quick" is glyph 5, the "o" of "brown" glyph 12.
let (u, o) = (glyph(&h, 5), glyph(&h, 12));
double_click_drag(&mut h, u, o);
let right = h.state().views[0].selected_text();
h.run_steps(60);
double_click_drag(&mut h, o, u);
let left = h.state().views[0].selected_text();
assert_eq!([right.as_deref(), left.as_deref()], [Some("quick brown"); 2], "dragging right, then left");
}
/// Issue #1685: after a triple-click the drag extends a whole line at a time.
#[test]
fn a_triple_click_drag_extends_by_whole_lines() {
let (mut h, at) = harness(|_| {});
// A glyph inside "jumps over the dog", on the second line.
let below = h.state().views[0].glyph_screen_pos(0, 23).expect("glyph on screen");
click(&mut h, at);
double_click_drag(&mut h, at, below);
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("The quick brown fox\njumps over the dog"));
}