1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Treat a press that leaves a widget as a drag (#8365)

A widget that senses both clicks and drags postpones the
click-versus-drag decision until the pointer moves past `max_click_dist`
or is held for `max_click_duration`. But a click has to be released *on*
the widget — so once the pointer leaves, the gesture can only be a drag,
and there is nothing left to wait for.

This matters for widgets thinner than `max_click_dist` (6px), such as
panel resize handles. The pointer leaves such a widget almost
immediately, which hands the hover to whatever is underneath, while
`dragged()` was not true yet. So a handle highlighting on `hovered() ||
dragged()` blinked out mid-gesture.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emil Ernerfeldt
2026-08-04 02:09:27 -07:00
committed by GitHub
parent 78c0e39d1d
commit 98eab50577
3 changed files with 222 additions and 4 deletions

View File

@@ -197,7 +197,19 @@ pub(crate) fn interact(
// This widget is sensitive to both clicks and drags.
// When the mouse first is pressed, it could be either,
// so we postpone the decision until we know.
input.pointer.is_decidedly_dragging()
//
// …unless a click is no longer possible at all: a click has to be
// released on the widget, and `hits.click` tells us whether the
// pointer is still somewhere a release would land on this widget.
// Note that this is not the same as being inside `interact_rect`:
// the hit-test also picks up widgets within `interact_radius`, and
// lets a widget on top take the hit.
//
// Deciding here means a thin drag handle (narrower than
// `max_click_dist`) doesn't spend the decision window as neither
// hovered nor dragged, which would make its highlight blink out.
let could_still_be_clicked = hits.click.is_some_and(|hit| hit.id == widget.id);
input.pointer.is_decidedly_dragging() || !could_still_be_clicked
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.senses_drag()

View File

@@ -309,6 +309,12 @@ impl Response {
///
/// In contrast to [`Self::contains_pointer`], this will be `false` whenever some other widget is being dragged.
/// `hovered` is always `false` for disabled widgets.
///
/// While a widget is being clicked or dragged it is the only hovered widget,
/// so this stays `true` even after the pointer moves off it. Together with
/// how [`Self::dragged`] resolves a press that leaves the widget, that means
/// `hovered() || dragged()` holds for a whole press-drag-release gesture,
/// which is what you want for highlighting something like a drag handle.
#[inline(always)]
pub fn hovered(&self) -> bool {
self.flags.contains(Flags::HOVERED)
@@ -403,11 +409,21 @@ impl Response {
/// To find out which button(s), use [`Self::dragged_by`].
///
/// If the widget is only sensitive to drags, this is `true` as soon as the pointer presses down on it.
/// If the widget also senses clicks, this won't be true until the pointer has moved a bit,
/// or the user has pressed down for long enough.
///
/// If the widget also senses clicks, the press could be either, so the
/// decision is postponed until whichever of these comes first:
/// * the pointer moves further than [`crate::InputOptions::max_click_dist`],
/// * it is held longer than [`crate::InputOptions::max_click_duration`],
/// * or it leaves the widget — a click has to be released on the widget, so
/// once the pointer is outside, the gesture can only be a drag. This is what
/// keeps a handle thinner than `max_click_dist` from spending the decision
/// window as neither hovered nor dragged.
///
/// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details.
///
/// If you want to avoid the delay, use [`Self::is_pointer_button_down_on`] instead.
/// While the decision is pending the pointer is still on the widget, so
/// [`Self::hovered`] is `true` throughout. If you want neither the delay nor
/// the distinction, use [`Self::is_pointer_button_down_on`].
///
/// If the widget is NOT sensitive to drags, this will always be `false`.
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
@@ -571,6 +587,9 @@ impl Response {
/// even when dragging outside the widget.
///
/// This could also be thought of as "is this widget being interacted with?".
///
/// Unlike [`Self::dragged`], this is `true` from the press frame onwards, with
/// no click-versus-drag decision window.
#[inline(always)]
pub fn is_pointer_button_down_on(&self) -> bool {
self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON)