* Select tool: trigger multi-click text selection on pointer press (#1024) - Detect consecutive primary pointer presses on page text using press timing and distance thresholds, highlighting words, lines, or the whole page immediately on the down-stroke rather than waiting for pointer release. - Keep selection intact on release for multi-clicks while allowing single clicks to clear or shift-extend. - Add regression test `multiclick_selects_on_pointer_press_before_release` in `crates/ui-egui/tests/text_select.rs`. - Credit krinry in contributors/people.toml. * Count presses with egui's own double-click delay and distance --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
575f99f9be
commit
b90875466e
|
|
@ -75,6 +75,10 @@ sync_github_name = true
|
|||
[people.jay-tau]
|
||||
sync_github_name = true
|
||||
|
||||
[people.krinry]
|
||||
sync_github_name = true
|
||||
|
||||
|
||||
[people.arcanii]
|
||||
real_name = "Bryan Mark"
|
||||
display_name = "Arcanii"
|
||||
|
|
|
|||
|
|
@ -343,6 +343,8 @@ pub struct DocView {
|
|||
selection: Option<Selection>,
|
||||
/// Clicks in the current run on the text layer: two select a word, three the line, four the page.
|
||||
clicks: u32,
|
||||
last_press_time: f64,
|
||||
last_press_pos: Option<Pos2>,
|
||||
last_queue: Vec<RenderRequest>,
|
||||
viewport_w: f32,
|
||||
viewport_h: f32,
|
||||
|
|
@ -569,6 +571,8 @@ impl DocView {
|
|||
find: None,
|
||||
selection: None,
|
||||
clicks: 0,
|
||||
last_press_time: 0.0,
|
||||
last_press_pos: None,
|
||||
last_queue: Vec::new(),
|
||||
viewport_w: 800.0,
|
||||
viewport_h: 600.0,
|
||||
|
|
@ -2465,14 +2469,40 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
ui.ctx().set_cursor_icon(egui::CursorIcon::Text);
|
||||
}
|
||||
let press_here = ui.input(|inp| inp.pointer.press_origin()).is_some_and(|o| r.contains(o));
|
||||
let pressed = ui.input(|inp| inp.pointer.primary_pressed());
|
||||
if pressed && press_here && !over_link {
|
||||
let now = ui.input(|inp| inp.time);
|
||||
// The same limits egui uses to count clicks on release.
|
||||
let (max_delay, max_dist) = ui.ctx().options(|o| (o.input_options.max_double_click_delay, o.input_options.max_click_dist));
|
||||
let is_consecutive =
|
||||
now - view.last_press_time <= max_delay && view.last_press_pos.is_some_and(|pos| pos.distance(p) <= max_dist);
|
||||
view.clicks = if is_consecutive { view.clicks.saturating_add(1) } else { 1 };
|
||||
view.last_press_time = now;
|
||||
view.last_press_pos = Some(p);
|
||||
|
||||
// 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)),
|
||||
});
|
||||
if let Some((first, last)) = span {
|
||||
view.selection = Some(Selection::run(i, first, last));
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.drag_started() && press_here && !over_link {
|
||||
let origin = ui.input(|inp| inp.pointer.press_origin()).unwrap_or(p);
|
||||
let (ox, oy) = xf.screen_to_view(origin);
|
||||
view.selection = if column {
|
||||
Some(Selection { page: i, span: Span::Column { rect: [ox, oy, vx, vy] } })
|
||||
} else {
|
||||
text.nearest(ox, oy).map(|a| Selection::run(i, a, a))
|
||||
};
|
||||
if view.clicks < 2 {
|
||||
view.selection = if column {
|
||||
Some(Selection { page: i, span: Span::Column { rect: [ox, oy, vx, vy] } })
|
||||
} else {
|
||||
text.nearest(ox, oy).map(|a| Selection::run(i, a, a))
|
||||
};
|
||||
}
|
||||
}
|
||||
if resp.dragged()
|
||||
&& let Some(sel) = view.selection.as_mut().filter(|s| s.page == i)
|
||||
|
|
@ -2496,29 +2526,30 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
view.selection = None;
|
||||
}
|
||||
// Quick clicks widen the selection: two a word, three the line, four the page.
|
||||
// egui counts up to three, so four come from the run kept here. The markup and
|
||||
// Redact tools mark a double-clicked word at once, so they stop at the word.
|
||||
let clicks = if resp.triple_clicked() {
|
||||
// Release-based fallback for platforms or synthetic events that report double_clicked.
|
||||
let release_clicks = if resp.triple_clicked() {
|
||||
view.clicks.max(2).saturating_add(1)
|
||||
} else if resp.double_clicked() {
|
||||
2
|
||||
view.clicks.max(2)
|
||||
} else {
|
||||
u32::from(resp.clicked())
|
||||
0
|
||||
};
|
||||
if clicks > 0 {
|
||||
view.clicks = clicks;
|
||||
if release_clicks > 0 {
|
||||
view.clicks = release_clicks;
|
||||
}
|
||||
let clicks = if clicks > 2 && tool != QuickTool::Select { 1 } else { clicks };
|
||||
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)),
|
||||
});
|
||||
if let Some((first, last)) = span {
|
||||
view.selection = Some(Selection::run(i, first, last));
|
||||
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)),
|
||||
});
|
||||
if let Some((first, last)) = span {
|
||||
view.selection = Some(Selection::run(i, first, last));
|
||||
}
|
||||
}
|
||||
} else if resp.clicked() && !over_link {
|
||||
} else if resp.clicked() && view.clicks < 2 && !over_link {
|
||||
// ⇧-click extends a selection on this page to the click, keeping its
|
||||
// anchor (#527). Otherwise (no shift, no selection on this page, or a
|
||||
// column selection, which has no anchor glyph; a selection cannot yet
|
||||
|
|
|
|||
|
|
@ -139,3 +139,28 @@ fn shift_click_extends_the_selection_from_its_anchor() {
|
|||
shift_click(&mut h, n);
|
||||
assert_eq!(h.state().views[0].selected_text(), None);
|
||||
}
|
||||
|
||||
/// Issue #1024: multi-click selection triggers immediately on pointer press, before release.
|
||||
#[test]
|
||||
fn multiclick_selects_on_pointer_press_before_release() {
|
||||
let (mut h, at) = harness(|_| {});
|
||||
// First click: press and release.
|
||||
click(&mut h, at);
|
||||
// Second click: press and HOLD (do not release).
|
||||
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick"), "word highlights on press");
|
||||
// Release the second click: still selected.
|
||||
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick"), "word stays selected on release");
|
||||
|
||||
// Third click: press and HOLD.
|
||||
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("The quick brown fox"), "line highlights on press");
|
||||
// Release third click.
|
||||
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("The quick brown fox"), "line stays selected on release");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue