Improve `Response.dragged`, `drag_started` and `clicked` (#3888)

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.
This commit is contained in:
Emil Ernerfeldt 2024-01-25 17:28:53 +01:00 committed by GitHub
parent d190df7d25
commit a815923717
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 281 additions and 154 deletions

View File

@ -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 =

View File

@ -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());

View File

@ -956,11 +956,8 @@ 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,
@ -968,11 +965,12 @@ impl Context {
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,
@ -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;
@ -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;
interaction.click_interest |= contains_pointer && sense.click;
interaction.drag_interest |= contains_pointer && sense.drag;
response.dragged = interaction.drag_id == Some(id);
response.is_pointer_button_down_on =
interaction.click_id == Some(id) || response.dragged;
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.
@ -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;
if hovered && response.is_pointer_button_down_on {
PointerEvent::Released { click, button } => {
res.drag_released = res.dragged;
res.dragged = false;
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

View File

@ -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,
@ -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,
@ -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();
@ -798,6 +804,8 @@ impl PointerState {
self.last_move_time = time;
}
self.started_decidedly_dragging = self.is_decidedly_dragging() && !was_decidedly_dragging;
self
}
@ -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,
@ -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:#?}"));

View File

@ -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,
@ -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
@ -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) {

View File

@ -14,7 +14,7 @@ use crate::{
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
// TODO(emilk): we should be using bit sets instead of so many bools
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Response {
// CONTEXT:
/// Used for optionally showing a tooltip and checking for more interactions.
@ -64,7 +64,11 @@ pub struct Response {
#[doc(hidden)]
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
/// The widgets is being dragged
/// The widget started being dragged this frame.
#[doc(hidden)]
pub drag_started: bool,
/// The widgets is being dragged.
#[doc(hidden)]
pub dragged: bool,
@ -90,48 +94,6 @@ pub struct Response {
pub changed: bool,
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
ctx: _,
layer_id,
id,
rect,
sense,
enabled,
contains_pointer,
hovered,
highlighted,
clicked,
double_clicked,
triple_clicked,
dragged,
drag_released,
is_pointer_button_down_on,
interact_pointer_pos,
changed,
} = self;
f.debug_struct("Response")
.field("layer_id", layer_id)
.field("id", id)
.field("rect", rect)
.field("sense", sense)
.field("enabled", enabled)
.field("contains_pointer", contains_pointer)
.field("hovered", hovered)
.field("highlighted", highlighted)
.field("clicked", clicked)
.field("double_clicked", double_clicked)
.field("triple_clicked", triple_clicked)
.field("dragged", dragged)
.field("drag_released", drag_released)
.field("is_pointer_button_down_on", is_pointer_button_down_on)
.field("interact_pointer_pos", interact_pointer_pos)
.field("changed", changed)
.finish()
}
}
impl Response {
/// Returns true if this widget was clicked this frame by the primary button.
///
@ -295,45 +257,50 @@ impl Response {
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
}
/// Did a drag on this widgets begin this frame?
///
/// This is only true if the widget sense drags.
/// If the widget also senses clicks, this will only become true if the pointer has moved a bit.
///
/// This will only be true for a single frame.
#[inline]
pub fn drag_started(&self) -> bool {
self.drag_started
}
/// Did a drag on this widgets by the button begin this frame?
///
/// This is only true if the widget sense drags.
/// If the widget also senses clicks, this will only become true if the pointer has moved a bit.
///
/// This will only be true for a single frame.
#[inline]
pub fn drag_started_by(&self, button: PointerButton) -> bool {
self.drag_started() && self.ctx.input(|i| i.pointer.button_down(button))
}
/// The widgets is being dragged.
///
/// To find out which button(s), query [`crate::PointerState::button_down`]
/// (`ui.input(|i| i.pointer.button_down(…))`).
/// To find out which button(s), use [`Self::dragged_by`].
///
/// Note that the widget must be sensing drags with [`Sense::drag`].
/// If the widget is only sensitive to drags, this is `true` as soon as the pointer presses down on it.
/// If the widget is also sensitive to drags, this won't be true until the pointer has moved a bit,
/// or the user has pressed down for long enough.
/// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details.
///
/// 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`]).
///
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
#[inline(always)]
pub fn dragged(&self) -> bool {
self.dragged
}
/// The Widget is being decidedly dragged.
///
/// This helper function checks both the output of [`Self::dragged`] and [`crate::PointerState::is_decidedly_dragging`].
#[inline]
pub fn decidedly_dragged(&self) -> bool {
self.dragged() && self.ctx.input(|i| i.pointer.is_decidedly_dragging())
}
#[inline]
pub fn dragged_by(&self, button: PointerButton) -> bool {
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
}
/// Did a drag on this widgets begin this frame?
#[inline]
pub fn drag_started(&self) -> bool {
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
}
/// Did a drag on this widgets by the button begin this frame?
#[inline]
pub fn drag_started_by(&self, button: PointerButton) -> bool {
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
}
/// The widget was being dragged, but now it has been released.
#[inline]
pub fn drag_released(&self) -> bool {
@ -356,6 +323,7 @@ impl Response {
}
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
///
/// `None` if the widget is not being interacted with.
#[inline]
pub fn interact_pointer_pos(&self) -> Option<Pos2> {
@ -363,6 +331,7 @@ impl Response {
}
/// If it is a good idea to show a tooltip, where is pointer?
///
/// None if the pointer is outside the response area.
#[inline]
pub fn hover_pos(&self) -> Option<Pos2> {
@ -374,7 +343,11 @@ impl Response {
}
/// Is the pointer button currently down on this widget?
/// This is true if the pointer is pressing down or dragging a widget
///
/// This is true if the pointer is pressing down or dragging a widget,
/// even when dragging outside the widget.
///
/// This could also be thought of as "is this widget being interacted with?".
#[inline(always)]
pub fn is_pointer_button_down_on(&self) -> bool {
self.is_pointer_button_down_on
@ -793,6 +766,7 @@ impl Response {
self.triple_clicked[3] || other.triple_clicked[3],
self.triple_clicked[4] || other.triple_clicked[4],
],
drag_started: self.drag_started || other.drag_started,
dragged: self.dragged || other.dragged,
drag_released: self.drag_released || other.drag_released,
is_pointer_button_down_on: self.is_pointer_button_down_on

View File

@ -306,10 +306,58 @@ impl super::View for TableTest {
// ----------------------------------------------------------------------------
struct HistoryEntry {
text: String,
repeated: usize,
}
#[derive(Default)]
struct DeduplicatedHistory {
history: std::collections::VecDeque<HistoryEntry>,
}
impl DeduplicatedHistory {
fn add(&mut self, text: String) {
if let Some(entry) = self.history.back_mut() {
if entry.text == text {
entry.repeated += 1;
return;
}
}
self.history.push_back(HistoryEntry { text, repeated: 1 });
if self.history.len() > 100 {
self.history.pop_front();
}
}
fn ui(&self, ui: &mut egui::Ui) {
egui::ScrollArea::vertical()
.auto_shrink(false)
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 4.0;
for HistoryEntry { text, repeated } in self.history.iter().rev() {
ui.horizontal(|ui| {
if text.is_empty() {
ui.weak("(empty)");
} else {
ui.label(text);
}
if 1 < *repeated {
ui.weak(format!(" x{repeated}"));
}
});
}
});
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Default)]
pub struct InputTest {
info: String,
#[cfg_attr(feature = "serde", serde(skip))]
history: [DeduplicatedHistory; 4],
show_hovers: bool,
}
impl super::Demo for InputTest {
@ -319,8 +367,10 @@ impl super::Demo for InputTest {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.default_width(800.0)
.open(open)
.resizable(false)
.resizable(true)
.scroll2(false)
.show(ctx, |ui| {
use super::View as _;
self.ui(ui);
@ -330,52 +380,102 @@ impl super::Demo for InputTest {
impl super::View for InputTest {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.spacing_mut().item_spacing.y = 8.0;
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
let response = ui.add(
egui::Button::new("Click, double-click, triple-click or drag me with any mouse button")
.sense(egui::Sense::click_and_drag()),
);
ui.horizontal(|ui| {
if ui.button("Clear").clicked() {
*self = Default::default();
}
let mut new_info = String::new();
for &button in &[
egui::PointerButton::Primary,
egui::PointerButton::Secondary,
egui::PointerButton::Middle,
egui::PointerButton::Extra1,
egui::PointerButton::Extra2,
] {
use std::fmt::Write as _;
ui.checkbox(&mut self.show_hovers, "Show hover state");
});
if response.clicked_by(button) {
writeln!(new_info, "Clicked by {button:?} button").ok();
}
if response.double_clicked_by(button) {
writeln!(new_info, "Double-clicked by {button:?} button").ok();
}
if response.triple_clicked_by(button) {
writeln!(new_info, "Triple-clicked by {button:?} button").ok();
}
if response.dragged_by(button) {
writeln!(
new_info,
"Dragged by {:?} button, delta: {:?}",
button,
response.drag_delta()
)
.ok();
}
}
if !new_info.is_empty() {
self.info = new_info;
}
ui.label("This tests how egui::Response reports events.\n\
The different buttons are sensitive to different things.\n\
Try interacting with them with any mouse button by clicking, double-clicking, triple-clicking, or dragging them.");
ui.label(&self.info);
ui.columns(4, |columns| {
for (i, (sense_name, sense)) in [
("Sense::hover", egui::Sense::hover()),
("Sense::click", egui::Sense::click()),
("Sense::drag", egui::Sense::drag()),
("Sense::click_and_drag", egui::Sense::click_and_drag()),
]
.into_iter()
.enumerate()
{
columns[i].push_id(i, |ui| {
let response = ui.add(egui::Button::new(sense_name).sense(sense));
let info = response_summary(&response, self.show_hovers);
self.history[i].add(info.trim().to_owned());
self.history[i].ui(ui);
});
}
});
}
}
fn response_summary(response: &egui::Response, show_hovers: bool) -> String {
use std::fmt::Write as _;
let mut new_info = String::new();
if show_hovers {
if response.hovered() {
writeln!(new_info, "hovered").ok();
}
if response.contains_pointer() {
writeln!(new_info, "contains_pointer").ok();
}
if response.is_pointer_button_down_on() {
writeln!(new_info, "pointer_down_on").ok();
}
}
for &button in &[
egui::PointerButton::Primary,
egui::PointerButton::Secondary,
egui::PointerButton::Middle,
egui::PointerButton::Extra1,
egui::PointerButton::Extra2,
] {
let button_suffix = if button == egui::PointerButton::Primary {
// Reduce visual clutter in common case:
String::default()
} else {
format!(" by {button:?} button")
};
// These are in inverse logical/chonological order, because we show them in the ui that way:
if response.triple_clicked_by(button) {
writeln!(new_info, "Triple-clicked{button_suffix}").ok();
}
if response.double_clicked_by(button) {
writeln!(new_info, "Double-clicked{button_suffix}").ok();
}
if response.clicked_by(button) {
writeln!(new_info, "Clicked{button_suffix}").ok();
}
if response.drag_released_by(button) {
writeln!(new_info, "Drag ended{button_suffix}").ok();
}
if response.dragged_by(button) {
writeln!(new_info, "Dragged{button_suffix}").ok();
}
if response.drag_started_by(button) {
writeln!(new_info, "Drag started{button_suffix}").ok();
}
}
new_info
}
// ----------------------------------------------------------------------------
pub struct WindowResizeTest {