diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 734ea5fb..2ae769e7 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -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 = diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 4ea40637..ac77c425 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -726,12 +726,8 @@ fn window_interaction( id: Id, rect: Rect, ) -> Option { - { - 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()); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 6e73b22e..89f44fb1 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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 diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 8b82efbc..1a31c9dd 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -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:#?}")); diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 213d9c8b..d1881c93 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -238,6 +238,11 @@ pub(crate) struct Interaction { pub click_id: Option, /// 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, 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 { 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) { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 108a24d9..178c28a6 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -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 { @@ -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 { @@ -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 diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 2a0285a4..3ff3e034 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -306,10 +306,58 @@ impl super::View for TableTest { // ---------------------------------------------------------------------------- +struct HistoryEntry { + text: String, + repeated: usize, +} + +#[derive(Default)] +struct DeduplicatedHistory { + history: std::collections::VecDeque, +} + +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 {