Skip to content

Commit

Permalink
Improve Response.dragged, drag_started and clicked (#3888)
Browse files Browse the repository at this point in the history
If a widgets sense both clicks and drags, we don't know wether or not a
mouse press on it will be a short click or a long drag.

With this PR, `response.dragged` and `response.drag_started` isn't true
until we know it is a drag and not a click.
If the widget ONLY senses drags, then we know as soon as someone presses
on it that it is a drag.
If it is sensitive to both clicks and drags, we don't know until the
mouse moves a bit, or stays pressed down long enough.

This PR also ensures that `response.clicked` and is only true for
widgets that senses clicks.
  • Loading branch information
emilk authored Jan 25, 2024
1 parent d190df7 commit a815923
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 152 deletions.
4 changes: 2 additions & 2 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,9 +718,9 @@ impl TopBottomPanel {
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
&& mouse_over_resize_line
{
ui.memory_mut(|mem| mem.interaction_mut().drag_id = Some(resize_id));
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
}
is_resizing = ui.memory(|mem| mem.interaction().drag_id == Some(resize_id));
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs();
let height =
Expand Down
8 changes: 2 additions & 6 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,8 @@ fn window_interaction(
id: Id,
rect: Rect,
) -> Option<WindowInteraction> {
{
let drag_id = ctx.memory(|mem| mem.interaction().drag_id);

if drag_id.is_some() && drag_id != Some(id) {
return None;
}
if ctx.memory(|mem| mem.dragging_something_else(id)) {
return None;
}

let mut window_interaction = ctx.memory(|mem| mem.window_interaction());
Expand Down
99 changes: 60 additions & 39 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,23 +956,21 @@ impl Context {
enabled: bool,
contains_pointer: bool,
) -> Response {
let hovered = contains_pointer && enabled; // can't even hover disabled widgets

let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id));

let mut response = Response {
// This is the start - we'll fill in the fields below:
let mut res = Response {
ctx: self.clone(),
layer_id,
id,
rect,
sense,
enabled,
contains_pointer,
hovered,
highlighted,
hovered: contains_pointer && enabled,
highlighted: self.frame_state(|fs| fs.highlight_this_frame.contains(&id)),
clicked: Default::default(),
double_clicked: Default::default(),
triple_clicked: Default::default(),
drag_started: false,
dragged: false,
drag_released: false,
is_pointer_button_down_on: false,
Expand All @@ -994,10 +992,10 @@ impl Context {
// Make sure anything that can receive focus has an AccessKit node.
// TODO(mwcampbell): For nodes that are filled from widget info,
// some information is written to the node twice.
self.accesskit_node_builder(id, |builder| response.fill_accesskit_node_common(builder));
self.accesskit_node_builder(id, |builder| res.fill_accesskit_node_common(builder));
}

let clicked_elsewhere = response.clicked_elsewhere();
let clicked_elsewhere = res.clicked_elsewhere();
self.write(|ctx| {
let input = &ctx.viewports.entry(ctx.viewport_id()).or_default().input;
let memory = &mut ctx.memory;
Expand All @@ -1007,41 +1005,53 @@ impl Context {
}

if sense.click
&& memory.has_focus(response.id)
&& memory.has_focus(res.id)
&& (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter))
{
// Space/enter works like a primary click for e.g. selected buttons
response.clicked[PointerButton::Primary as usize] = true;
res.clicked[PointerButton::Primary as usize] = true;
}

#[cfg(feature = "accesskit")]
if sense.click
&& input.has_accesskit_action_request(response.id, accesskit::Action::Default)
if sense.click && input.has_accesskit_action_request(res.id, accesskit::Action::Default)
{
response.clicked[PointerButton::Primary as usize] = true;
res.clicked[PointerButton::Primary as usize] = true;
}

if sense.click || sense.drag {
let interaction = memory.interaction_mut();

interaction.click_interest |= hovered && sense.click;
interaction.drag_interest |= hovered && sense.drag;

response.dragged = interaction.drag_id == Some(id);
response.is_pointer_button_down_on =
interaction.click_id == Some(id) || response.dragged;
interaction.click_interest |= contains_pointer && sense.click;
interaction.drag_interest |= contains_pointer && sense.drag;

res.is_pointer_button_down_on =
interaction.click_id == Some(id) || interaction.drag_id == Some(id);

if sense.click && sense.drag {
// 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.
res.dragged =
interaction.drag_id == Some(id) && input.pointer.is_decidedly_dragging();
res.drag_started = res.dragged && input.pointer.started_decidedly_dragging;
} else if sense.drag {
// We are just sensitive to drags, so we can mark ourself as dragged right away:
res.dragged = interaction.drag_id == Some(id);
// res.drag_started will be filled below if applicable
}

for pointer_event in &input.pointer.pointer_events {
match pointer_event {
PointerEvent::Moved(_) => {}

PointerEvent::Pressed { .. } => {
if hovered {
if contains_pointer {
let interaction = memory.interaction_mut();

if sense.click && interaction.click_id.is_none() {
// potential start of a click
interaction.click_id = Some(id);
response.is_pointer_button_down_on = true;
res.is_pointer_button_down_on = true;
}

// HACK: windows have low priority on dragging.
Expand All @@ -1056,51 +1066,62 @@ impl Context {
interaction.drag_id = Some(id);
interaction.drag_is_window = false;
memory.set_window_interaction(None); // HACK: stop moving windows (if any)
response.is_pointer_button_down_on = true;
response.dragged = true;

res.is_pointer_button_down_on = true;

// Again, only if we are ONLY sensitive to drags can we decide that this is a drag now.
if sense.click {
res.dragged = false;
res.drag_started = false;
} else {
res.dragged = true;
res.drag_started = true;
}
}
}
}

PointerEvent::Released { click, button } => {
response.drag_released = response.dragged;
response.dragged = false;
res.drag_released = res.dragged;
res.dragged = false;

if hovered && response.is_pointer_button_down_on {
if sense.click && res.hovered && res.is_pointer_button_down_on {
if let Some(click) = click {
let clicked = hovered && response.is_pointer_button_down_on;
response.clicked[*button as usize] = clicked;
response.double_clicked[*button as usize] =
let clicked = res.hovered && res.is_pointer_button_down_on;
res.clicked[*button as usize] = clicked;
res.double_clicked[*button as usize] =
clicked && click.is_double();
response.triple_clicked[*button as usize] =
res.triple_clicked[*button as usize] =
clicked && click.is_triple();
}
}
response.is_pointer_button_down_on = false;

res.is_pointer_button_down_on = false;
}
}
}
}

if response.is_pointer_button_down_on {
response.interact_pointer_pos = input.pointer.interact_pos();
if res.is_pointer_button_down_on {
res.interact_pointer_pos = input.pointer.interact_pos();
}

if input.pointer.any_down() && !response.is_pointer_button_down_on {
if input.pointer.any_down() && !res.is_pointer_button_down_on {
// We don't hover widgets while interacting with *other* widgets:
response.hovered = false;
res.hovered = false;
}

if memory.has_focus(response.id) && clicked_elsewhere {
if memory.has_focus(res.id) && clicked_elsewhere {
memory.surrender_focus(id);
}

if response.dragged() && !memory.has_focus(response.id) {
if res.dragged() && !memory.has_focus(res.id) {
// e.g.: remove focus from a widget when you drag something else
memory.stop_text_input();
}
});

response
res
}

/// Get a full-screen painter for a new or existing layer
Expand Down
12 changes: 12 additions & 0 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,9 @@ pub struct PointerState {
/// for it to be registered as a click.
pub(crate) has_moved_too_much_for_a_click: bool,

/// Did [`Self::is_decidedly_dragging`] go from `false` to `true` this frame?
pub(crate) started_decidedly_dragging: bool,

/// When did the pointer get click last?
/// Used to check for double-clicks.
last_click_time: f64,
Expand Down Expand Up @@ -667,6 +670,7 @@ impl Default for PointerState {
press_origin: None,
press_start_time: None,
has_moved_too_much_for_a_click: false,
started_decidedly_dragging: false,
last_click_time: std::f64::NEG_INFINITY,
last_last_click_time: std::f64::NEG_INFINITY,
last_move_time: std::f64::NEG_INFINITY,
Expand All @@ -678,6 +682,8 @@ impl Default for PointerState {
impl PointerState {
#[must_use]
pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> Self {
let was_decidedly_dragging = self.is_decidedly_dragging();

self.time = time;

self.pointer_events.clear();
Expand Down Expand Up @@ -798,6 +804,8 @@ impl PointerState {
self.last_move_time = time;
}

self.started_decidedly_dragging = self.is_decidedly_dragging() && !was_decidedly_dragging;

self
}

Expand Down Expand Up @@ -1137,6 +1145,7 @@ impl PointerState {
press_origin,
press_start_time,
has_moved_too_much_for_a_click,
started_decidedly_dragging,
last_click_time,
last_last_click_time,
pointer_events,
Expand All @@ -1156,6 +1165,9 @@ impl PointerState {
ui.label(format!(
"has_moved_too_much_for_a_click: {has_moved_too_much_for_a_click}"
));
ui.label(format!(
"started_decidedly_dragging: {started_decidedly_dragging}"
));
ui.label(format!("last_click_time: {last_click_time:#?}"));
ui.label(format!("last_last_click_time: {last_last_click_time:#?}"));
ui.label(format!("last_move_time: {last_move_time:#?}"));
Expand Down
24 changes: 24 additions & 0 deletions crates/egui/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ pub(crate) struct Interaction {
pub click_id: Option<Id>,

/// A widget interested in drags that has a mouse press on it.
///
/// Note that this is set as soon as the mouse is pressed,
/// so the widget may not yet be marked as "dragged",
/// as that can only happen after the mouse has moved a bit
/// (at least if the widget is interesated in both clicks and drags).
pub drag_id: Option<Id>,

pub focus: Focus,
Expand Down Expand Up @@ -698,12 +703,22 @@ impl Memory {
}

/// Is this specific widget being dragged?
///
/// Usually it is better to use [`crate::Response::dragged`].
///
/// A widget that sense both clicks and drags is only marked as "dragged"
/// when the mouse has moved a bit, but `is_being_dragged` will return true immediately.
#[inline(always)]
pub fn is_being_dragged(&self, id: Id) -> bool {
self.interaction().drag_id == Some(id)
}

/// Get the id of the widget being dragged, if any.
///
/// Note that this is set as soon as the mouse is pressed,
/// so the widget may not yet be marked as "dragged",
/// as that can only happen after the mouse has moved a bit
/// (at least if the widget is interesated in both clicks and drags).
#[inline(always)]
pub fn dragged_id(&self) -> Option<Id> {
self.interaction().drag_id
Expand All @@ -721,6 +736,15 @@ impl Memory {
self.interaction_mut().drag_id = None;
}

/// Is something else being dragged?
///
/// Returns true if we are dragging something, but not the given widget.
#[inline(always)]
pub fn dragging_something_else(&self, not_this: Id) -> bool {
let drag_id = self.interaction().drag_id;
drag_id.is_some() && drag_id != Some(not_this)
}

/// Forget window positions, sizes etc.
/// Can be used to auto-layout windows.
pub fn reset_areas(&mut self) {
Expand Down
Loading

0 comments on commit a815923

Please sign in to comment.