Allow doubleclick-and-drag and tripleclick-and-drag#8166
Open
hallyhaa wants to merge 5 commits into
Open
Conversation
Implements egui issue emilk#2550: when the user double-clicks and keeps the button held while dragging, the selection now extends word-by-word, and triple-click-and-drag extends line-by-line. Plain click-and-drag still selects character-by-character. egui only computes a click `count` on release, but to know the drag granularity we need that count while the button is still held. So `PointerState` now computes a `press_click_count` at press time using the same double-/triple-click logic the release path uses. `TextCursorState` gains a `SelectionMode` (Char/Word/Line) and an anchor unit so dragging back and forth across the anchor re-derives the selection correctly. `pointer_interaction` picks the mode from `press_click_count` on the start of a drag and, while dragging in Word/Line mode, takes the union of the anchor word/line and the word/line under the pointer. `LabelSelectionState` carries the same mode and anchor so label drag respects word/line granularity too. The single-galley case is fully correct; cross-galley label drags snap each galley's moving end to a word/line boundary. Existing single/double/triple-click behavior is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Fix 1: In multi-widget label selection, a word/line drag anchored in one galley failed to keep the anchor unit selected when dragged upward into a previous label. `selection.secondary` was set once at drag start to the anchor unit's MIN, which is only correct for forward/downward drags. `cursor_for` now recomputes `selection.secondary` every frame it processes the anchor galley (matched via the stored `drag_anchor` widget Id) whenever the primary is in a different galley. The far end of the anchor unit is chosen by `anchor_secondary_index` based on whether the primary has been dragged before the anchor in document order (compared via screen position). The single-galley case still flows through `combine_units` unchanged. Fix 2: `extend_word_line_drag` ran an O(n) word/line scan every frame while dragging. A `last_drag_pointer` cache on `TextCursorState` now stores the pointer char index and resulting range from the previous frame, skipping the scan while the pointer stays on the same character. `begin_drag` clears the cache so a new drag never reuses a stale range. Adds unit tests for the secondary-flip direction logic and the cache. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Fix 1 (behavior regression): label shift-click now resets the drag-selection granularity to Char. Previously a shift-click in a label left `selection_mode` at whatever a prior word/line drag had set, so a later shift-click extended by whole words/lines instead of by character, diverging from `TextEdit` and from `main`. Fix 2 (performance): add a stationary-pointer cache (`last_drag_unit`) to `LabelSelectionState`, mirroring `TextCursorState::last_drag_pointer`. The label word/line drag path called `unit_bounds_at` every frame, running an O(n) scan and heap-allocating a line-sized `String` even when the pointer was stationary. The cache (widget Id + pointer char index -> unit bounds) skips the scan while the pointer stays put, and is cleared on `drag_started` so no stale reuse. Fix 3 (API hygiene): `SelectionMode` and `SelectionMode::from_click_count` were accidentally `pub`, enlarging egui's public API; they are now `pub(crate)`. The unused `TextCursorState::selection_mode()` accessor (no callers anywhere) is removed. The three drag-transient fields (`selection_mode`, `drag_anchor_unit`, `last_drag_pointer`) now carry `#[serde(skip)]` so ephemeral drag state is not persisted. Fix 4 (cleanup): the double-/triple-click-count arithmetic that was duplicated between the press and release paths in `input_state/mod.rs` is extracted into a single private `PointerState::compute_click_count` helper. Tests: add `test_from_click_count` and `test_shift_click_resets_to_char_mode`; existing cache test still holds. `cargo test -p egui`, clippy (`--all-features`), fmt, and both default / `--no-default-features` builds are clean. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Follow-up to the double-/triple-click-and-drag feature (egui issue emilk#2550). Edge case 1 (FIXED): cache not invalidated on mid-drag text change. `TextCursorState::last_drag_pointer` was keyed only on the pointer char index, so an IME composition or programmatic edit that changed the galley text while the pointer stayed on the same char returned a stale range. The cache key now also includes `text.len()`; a differing length is treated as a miss. A new unit test covers this. Edge case 2 (INVESTIGATED, no code change needed): stale selection mode when a drag starts without a hovered press. Confirmed unreachable: the `is_being_dragged` branch in `pointer_interaction` can only run for a widget whose id equals `dragged_id()`, which requires `potential_drag_id` to have been set from a press-time drag hit, on a frame where the widget is also in the `hovered` set and `any_pressed()` is true -- so the press branch is guaranteed to have run first and overwritten `selection_mode`/ `drag_anchor_unit`. (Labels never reach this branch; they always pass `is_being_dragged = false`.) Added an explanatory comment instead of dead defensive code. Edge case 3 (PRE-EXISTING egui LIMITATION, documented): anchor galley scrolled out of view. The cross-galley secondary-flip can only run on the frame the anchor galley is visited. If it scrolls out, `selection.secondary` freezes at its last valid value. Verified there is no crash, panic or cross-galley index misuse; this degrades no worse than `main`'s char-mode multi-widget selection, which has the identical failure mode. Documented precisely in a code comment; a proper fix needs a multi-widget-selection rewrite, out of scope. Edge case 4 (FIXED): one-frame flicker on fast direction reversal. The cross-galley secondary-flip decided drag direction from `selection.primary.pos`, which is one frame stale when the anchor galley is processed before the pointer's galley. It now uses the live pointer position from `pointer_interact_pos()`, removing the lag at its source. Tests: `cargo test -p egui` all pass (38 unit + 166 doctests). `cargo clippy -p egui --all-features`, `cargo fmt`, `cargo build -p egui` and `cargo build -p egui --no-default-features` all clean. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
egui's CONTRIBUTING.md asks for each type in its own file unless trivial. Move the `SelectionMode` enum and its impl out of `text_cursor_state.rs` into a new `selection_mode.rs`, and make the `select_word_at`, `select_line_at` and `range_bounds` helpers `pub(crate)` so the new module can use them. The `from_click_count` test moves along with it. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Preview available at https://egui-pr-preview.github.io/pr/8166-featureclick-drag-selection View snapshot changes at kitdiff |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix for the current behaviour where a double-click selects a word and a triple-click selects a line, but dragging with multi-click looses the initial selection and then extends the selection character by character.
The fix works for both
TextEditand selectableLabels (single- and multi-widget selection). Plain single-click drag, double/triple-click without dragging, and shift-click are all unchanged.How
The click count (1/2/3) is normally only known on pointer release, but a double-/triple-click-and-drag needs it while the button is still held. So
PointerStatenow also computes the click count at press time (press_click_count), reusing the same double/triple-click logic the release path already used.TextCursorState(shared byTextEditandLabel) gains aSelectionMode(Char/Word/Line) and an anchored unit, so the selection extends and shrinks in whole words/lines as the pointer moves back and forth.Known limitation
For multi-widget label selection, if the anchor widget (where the drag started) is scrolled out of view, the selection endpoint freezes at its last valid value. This is a limitation of egui's multi-widget text selection (char-mode selection on
mainhas the same failure mode).This PR addresses #2550
cargo test -p eguipasses (added unit tests for the new logic)cargo clippy -p egui --all-featuresis cleancargo fmtis cleanserdefeature