From 3964a16cec23e0787b768273afca4c357a782e9c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 00:05:53 +0100 Subject: [PATCH 01/34] Better naming `widgets_this_frame` and `widgets_prev_frame` --- crates/egui/src/context.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 67bcb48fab5f..1388e145b7df 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -283,10 +283,10 @@ struct ViewportState { used: bool, /// Written to during the frame. - layer_rects_this_frame: WidgetRects, + widgets_this_frame: WidgetRects, /// Read - layer_rects_prev_frame: WidgetRects, + widgets_prev_frame: WidgetRects, /// State related to repaint scheduling. repaint: ViewportRepaintInfo, @@ -1125,7 +1125,7 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport.layer_rects_this_frame.insert( + viewport.widgets_this_frame.insert( layer_id, WidgetRect { id, @@ -1865,7 +1865,7 @@ impl Context { self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); if self.options(|o| o.debug_paint_interactive_widgets) { - let rects = self.write(|ctx| ctx.viewport().layer_rects_this_frame.clone()); + let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); for (layer_id, rects) in rects.by_layer { let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); for rect in rects { @@ -1965,16 +1965,16 @@ impl ContextImpl { { if self.memory.options.repaint_on_widget_change { crate::profile_function!("compare-widget-rects"); - if viewport.layer_rects_prev_frame != viewport.layer_rects_this_frame { + if viewport.widgets_prev_frame != viewport.widgets_this_frame { repaint_needed = true; // Some widget has moved } } std::mem::swap( - &mut viewport.layer_rects_prev_frame, - &mut viewport.layer_rects_this_frame, + &mut viewport.widgets_prev_frame, + &mut viewport.widgets_this_frame, ); - viewport.layer_rects_this_frame.clear(); + viewport.widgets_this_frame.clear(); } if repaint_needed || viewport.input.wants_repaint() { @@ -2353,7 +2353,7 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport.layer_rects_this_frame.insert( + viewport.widgets_this_frame.insert( layer_id, WidgetRect { id, @@ -2367,7 +2367,7 @@ impl Context { if contains_pointer { let pointer_pos = viewport.input.pointer.interact_pos(); if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) { + if let Some(rects) = viewport.widgets_prev_frame.by_layer.get(&layer_id) { // Iterate backwards, i.e. topmost widgets first. for blocking in rects.iter().rev() { if blocking.id == id { From ce06b03716e97a4d8dcc7bd9c85b1be5e32f7fe6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 00:22:06 +0100 Subject: [PATCH 02/34] Add HitTest --- crates/egui/src/hit_test.rs | 115 ++++++++++++++++++++++++++++++++++++ crates/egui/src/lib.rs | 1 + 2 files changed, 116 insertions(+) create mode 100644 crates/egui/src/hit_test.rs diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs new file mode 100644 index 000000000000..5e5701e1b333 --- /dev/null +++ b/crates/egui/src/hit_test.rs @@ -0,0 +1,115 @@ +use crate::*; + +/// Result of a hit-test agains [`WidgetRects`]. +/// +/// Answers the question "what is under the mouse pointer?". +/// +/// Note that this doesn't care if the mouse button is pressed or not, +/// or if we're currently already dragging something. +/// +/// For that you need the `InteractionState`. +#[derive(Clone, Debug, Default)] +pub struct WidgetHits { + /// All widgets that contains the pointer. + /// + /// i.e. both a Window and the button in it can ontain the pointer. + /// + /// Show tooltips for all of these. + /// Why? So you can do `ui.scope(|ui| …).response.on_hover_text(…)` + /// and get a tooltip for the whole ui, even if individual things + /// in the ui also had a tooltip. + pub contains_pointer: IdMap, + + /// The topmost widget under the pointer, interactive or not. + /// + /// Used for nothing? + pub top: Option, + + /// If the user would start a clicking now, this is what would be clicked. + /// + /// This is the top one under the pointer, or closest one of the top-most. + pub click: Option, + + /// If the user would start a dragging now, this is what would be dragged. + /// + /// This is the top one under the pointer, or closest one of the top-most. + pub drag: Option, +} + +impl WidgetHits { + #[inline] + pub fn is_tooltip_candidate(&self, id: Id) -> bool { + self.contains_pointer.contains_key(&id) + || self.top.map_or(false, |w| w.id == id) + || self.click.map_or(false, |w| w.id == id) + || self.drag.map_or(false, |w| w.id == id) + } +} + +/// Find the top or closest widgets to the given position, +/// None which is closer than `search_radius`. +pub fn hit_test( + widgets: &WidgetRects, + layer_order: &[LayerId], + pos: Pos2, + search_radius: f32, +) -> WidgetHits { + crate::profile_function!(); + + let hit_rect = Rect::from_center_size(pos, Vec2::splat(2.0 * search_radius)); + + // The few widgets close to the given position, sorted back-to-front. + let close: Vec = layer_order + .iter() + .filter_map(|layer_id| widgets.by_layer.get(layer_id)) + .flatten() + .filter(|widget| widget.interact_rect.intersects(hit_rect)) + .filter(|w| w.interact_rect.distance_to_pos(pos) <= search_radius) + .copied() + .collect(); + + // Only those widgets directly under the `pos`. + let hits: Vec = close + .iter() + .filter(|widget| widget.interact_rect.contains(pos)) + .copied() + .collect(); + + let hit = hits.last().copied(); + let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); + let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); + + let closest = find_closest(close.iter().copied(), pos); + let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); + let closest_drag = find_closest(close.iter().copied().filter(|w| w.sense.drag), pos); + + let top = hit.or(closest); + let click = hit_click.or(closest_click); + let drag = hit_drag.or(closest_drag); + + // Which widgets which will have tooltips: + let contains_pointer = hits.into_iter().map(|w| (w.id, w)).collect(); + + WidgetHits { + contains_pointer, + top, + click, + drag, + } +} + +fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { + let mut closest = None; + let mut cloest_dist_sq = f32::INFINITY; + for widget in widgets { + let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); + + // In case of a tie, take the last one = the one on top. + if dist_sq <= cloest_dist_sq { + cloest_dist_sq = dist_sq; + closest = Some(widget); + } + } + + closest +} diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index ad931885ea8c..6233a7b82634 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -352,6 +352,7 @@ mod drag_and_drop; mod frame_state; pub(crate) mod grid; pub mod gui_zoom; +mod hit_test; mod id; mod input_state; pub mod introspection; From 0d1f8690cba0830a40f479ae5ffe37e3604f88d2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 00:24:36 +0100 Subject: [PATCH 03/34] Calculate the WidgetHits each frame --- crates/egui/src/context.rs | 49 ++++++++++++++++++++++++++++++++++++++ crates/egui/src/style.rs | 10 ++++++++ 2 files changed, 59 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1388e145b7df..8bc196d99c72 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -20,6 +20,8 @@ use crate::{ TextureHandle, ViewportCommand, *, }; +use self::hit_test::WidgetHits; + /// Information given to the backend about when it is time to repaint the ui. /// /// This is given in the callback set by [`Context::set_request_repaint_callback`]. @@ -291,8 +293,15 @@ struct ViewportState { /// State related to repaint scheduling. repaint: ViewportRepaintInfo, + // ---------------------- + // Updated at the start of the frame: + // + /// Which widgets are under the pointer? + hits: WidgetHits, + // ---------------------- // The output of a frame: + // graphics: GraphicLayers, // Most of the things in `PlatformOutput` are not actually viewport dependent. output: PlatformOutput, @@ -489,6 +498,46 @@ impl ContextImpl { viewport.frame_state.begin_frame(&viewport.input); + { + let area_order: HashMap = self + .memory + .areas() + .order() + .iter() + .enumerate() + .map(|(i, id)| (*id, i)) + .collect(); + + let mut layers: Vec = viewport + .widgets_prev_frame + .by_layer + .keys() + .copied() + .collect(); + + layers.sort_by(|a, b| { + if a.order == b.order { + // Maybe both are windows, so respect area order: + area_order.get(a).cmp(&area_order.get(b)) + } else { + // comparing e.g. background to tooltips + a.order.cmp(&b.order) + } + }); + + viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { + let interact_radius = self.memory.options.style.interaction.interact_radius; + crate::hit_test::hit_test( + &viewport.widgets_prev_frame, + &layers, + pos, + interact_radius, + ) + } else { + WidgetHits::default() + }; + } + // Ensure we register the background area so panels and background ui can catch clicks: let screen_rect = viewport.input.screen_rect(); self.memory.areas_mut().set_state( diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 645a7d188f29..59ab8be6e0c7 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -709,6 +709,12 @@ impl std::ops::Add for Margin { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Interaction { + /// How close a widget must be to the mouse to have a chance to register as a click or drag. + /// + /// If this is larger than zero, it gets easier to hit widgets, + /// which is important for e.g. touch screens. + pub interact_radius: f32, + /// Mouse must be this close to the side of a window to resize pub resize_grab_radius_side: f32, @@ -1125,6 +1131,7 @@ impl Default for Spacing { impl Default for Interaction { fn default() -> Self { Self { + interact_radius: 3.0, resize_grab_radius_side: 5.0, resize_grab_radius_corner: 10.0, show_tooltips_only_when_still: true, @@ -1592,6 +1599,7 @@ fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) { impl Interaction { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { + interact_radius, resize_grab_radius_side, resize_grab_radius_corner, show_tooltips_only_when_still, @@ -1599,6 +1607,8 @@ impl Interaction { selectable_labels, multi_widget_text_select, } = self; + ui.add(Slider::new(interact_radius, 0.0..=20.0).text("interact_radius")) + .on_hover_text("Interact witgh ghe closest widget within this radius."); ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side")); ui.add( Slider::new(resize_grab_radius_corner, 0.0..=20.0).text("resize_grab_radius_corner"), From 5ed87a11500fd7bc628e3ec38418cded08e23f02 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 01:13:19 +0100 Subject: [PATCH 04/34] WidgetRects: add a by_id map --- crates/egui/src/context.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 8bc196d99c72..3975b1edbf68 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -226,14 +226,21 @@ pub struct WidgetRect { pub struct WidgetRects { /// All widgets, in painting order. pub by_layer: HashMap>, + + /// All widgets + pub by_id: IdMap, } impl WidgetRects { /// Clear the contents while retaining allocated memory. pub fn clear(&mut self) { - for rects in self.by_layer.values_mut() { + let Self { by_layer, by_id } = self; + + for rects in by_layer.values_mut() { rects.clear(); } + + by_id.clear(); } /// Insert the given widget rect in the given layer. @@ -254,6 +261,18 @@ impl WidgetRects { } layer_widgets.push(widget_rect); + + match self.by_id.entry(widget_rect.id) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(widget_rect); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + // e.g. calling `response.interact(…)` right after interacting. + let existing = entry.get_mut(); + existing.sense |= widget_rect.sense; + existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect); + } + } } } From 78e1c939ed760a1b8e2a1cc889f1a754edec5c94 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 01:13:41 +0100 Subject: [PATCH 05/34] Rename "Interaction" to "InteractionState" --- crates/egui/src/introspection.rs | 2 +- crates/egui/src/memory.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 43b3a88b81fa..3fcd717296cb 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -189,7 +189,7 @@ impl Widget for &mut epaint::TessellationOptions { } } -impl Widget for &memory::Interaction { +impl Widget for &memory::InteractionState { fn ui(self, ui: &mut Ui) -> Response { ui.vertical(|ui| { ui.label(format!("click_id: {:?}", self.click_id)); diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 8b969dc4bb52..c97fb70e3021 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -90,7 +90,7 @@ pub struct Memory { areas: ViewportIdMap, #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) interactions: ViewportIdMap, + pub(crate) interactions: ViewportIdMap, #[cfg_attr(feature = "persistence", serde(skip))] window_interactions: ViewportIdMap, @@ -290,6 +290,9 @@ impl Options { // ---------------------------------------------------------------------------- +/// The state of the interaction in egui, +/// i.e. what is being dragged. +/// /// Say there is a button in a scroll area. /// If the user clicks the button, the button should click. /// If the user drags the button we should scroll the scroll area. @@ -298,7 +301,7 @@ impl Options { /// If the user releases the button without moving the mouse we register it as a click on `click_id`. /// If the cursor moves too much we clear the `click_id` and start passing move events to `drag_id`. #[derive(Clone, Debug, Default)] -pub(crate) struct Interaction { +pub(crate) struct InteractionState { /// A widget interested in clicks that has a mouse press on it. pub click_id: Option, @@ -373,7 +376,7 @@ impl FocusWidget { } } -impl Interaction { +impl InteractionState { /// Are we currently clicking or dragging an egui widget? pub fn is_using_pointer(&self) -> bool { self.click_id.is_some() || self.drag_id.is_some() @@ -835,13 +838,13 @@ impl Memory { } } - pub(crate) fn interaction(&self) -> &Interaction { + pub(crate) fn interaction(&self) -> &InteractionState { self.interactions .get(&self.viewport_id) .expect("Failed to get interaction") } - pub(crate) fn interaction_mut(&mut self) -> &mut Interaction { + pub(crate) fn interaction_mut(&mut self) -> &mut InteractionState { self.interactions.entry(self.viewport_id).or_default() } } From 7f6adcaadbe9e597d0cea8c2df199f9abfcc3333 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 01:13:50 +0100 Subject: [PATCH 06/34] Add new interaction code --- crates/egui/src/interaction.rs | 136 +++++++++++++++++++++++++++++++++ crates/egui/src/lib.rs | 1 + 2 files changed, 137 insertions(+) create mode 100644 crates/egui/src/interaction.rs diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs new file mode 100644 index 000000000000..901ae2a0ae8e --- /dev/null +++ b/crates/egui/src/interaction.rs @@ -0,0 +1,136 @@ +//! How mouse and touch interzcts with widgets. + +use crate::*; + +use self::{hit_test::WidgetHits, input_state::PointerEvent, memory::InteractionState}; + +/// Calculated at the start of each frame +/// based on: +/// * Widget rects from precious frame +/// * Mouse/touch input +/// * Current [`InteractionState`]. +#[derive(Clone, Default)] +pub struct InteractionSnapshot { + /// The widget that got clicked this frame. + pub clicked: Option, + + /// Drag started on this widget this frame. + /// + /// This will also be found in `dragged` this frame. + pub drag_started: Option, + + /// This widget is being dragged this frame. + /// + /// Set the same frame a drag starts, + /// but unset the frame a drag ends. + pub dragged: Option, + + /// This widget was let go this frame, + /// after having been dragged. + /// + /// The widget will not be found in [`Self::dragged`] this frame. + pub drag_ended: Option, + + pub contains_pointer: IdMap, + pub hovered: IdMap, +} + +pub(crate) fn interact( + prev_snapshot: &InteractionSnapshot, + widgets: &WidgetRects, + hits: &WidgetHits, + input: &InputState, + interaction: &mut InteractionState, +) -> InteractionSnapshot { + if let Some(id) = interaction.click_id { + if !widgets.by_id.contains_key(&id) { + // The widget we were interested in clicking is gone. + interaction.click_id = None; + } + } + if let Some(id) = interaction.drag_id { + if !widgets.by_id.contains_key(&id) { + // The widget we were interested in dragging is gone. + interaction.drag_id = None; + } + } + + let mut clicked = None; + + // Note: in the current code a press-release in the same frame is NOT considered a drag. + for pointer_event in &input.pointer.pointer_events { + match pointer_event { + PointerEvent::Moved(_) => {} + + PointerEvent::Pressed { .. } => { + // Maybe new click? + if interaction.click_id.is_none() { + interaction.click_id = hits.click.map(|w| w.id); + } + + // Maybe new drag? + if interaction.drag_id.is_none() { + interaction.drag_id = hits.drag.map(|w| w.id); + } + } + + PointerEvent::Released { click, button: _ } => { + if click.is_some() { + if let Some(widget) = interaction.click_id.and_then(|id| widgets.by_id.get(&id)) + { + clicked = Some(*widget); + } + } + + interaction.drag_id = None; + interaction.click_id = None; + } + } + } + + // Check if we're dragging something: + let mut dragged = None; + if let Some(widget) = interaction.drag_id.and_then(|id| widgets.by_id.get(&id)) { + let is_dragged = if widget.sense.click && widget.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. + input.pointer.is_decidedly_dragging() + } else { + // This widget is just sensitive to drags, so we can mark it as dragged right away: + widget.sense.drag + }; + + if is_dragged { + dragged = Some(*widget); + } + } + + let drag_changed = dragged != prev_snapshot.dragged; + let drag_ended = drag_changed.then_some(prev_snapshot.dragged).flatten(); + let drag_started = drag_changed.then_some(dragged).flatten(); + + let contains_pointer: IdMap = hits + .contains_pointer + .values() + .chain(&hits.top) + .chain(&hits.click) + .chain(&hits.drag) + .map(|w| (w.id, *w)) + .collect(); + + let hovered = if clicked.is_some() || dragged.is_some() { + clicked.iter().chain(&dragged).map(|w| (w.id, *w)).collect() + } else { + contains_pointer.clone() + }; + + InteractionSnapshot { + clicked, + drag_started, + dragged, + drag_ended, + contains_pointer, + hovered, + } +} diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 6233a7b82634..33ed9013fb97 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -355,6 +355,7 @@ pub mod gui_zoom; mod hit_test; mod id; mod input_state; +mod interaction; pub mod introspection; pub mod layers; mod layout; From bad236488c9c665a4d31ba742583f2f137c5d4b9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 09:24:35 +0100 Subject: [PATCH 07/34] Run new interaction code each frame --- crates/egui/src/context.rs | 15 ++++++++++++++- crates/egui/src/hit_test.rs | 10 ---------- crates/egui/src/interaction.rs | 1 + 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3975b1edbf68..a168b8052127 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -20,7 +20,7 @@ use crate::{ TextureHandle, ViewportCommand, *, }; -use self::hit_test::WidgetHits; +use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; /// Information given to the backend about when it is time to repaint the ui. /// @@ -318,6 +318,11 @@ struct ViewportState { /// Which widgets are under the pointer? hits: WidgetHits, + /// What widgets are being interacted with this frame? + /// + /// Based on the widgets from last frame, and input in this frame. + interact_widgets: InteractionSnapshot, + // ---------------------- // The output of a frame: // @@ -555,6 +560,14 @@ impl ContextImpl { } else { WidgetHits::default() }; + + viewport.interact_widgets = crate::interaction::interact( + &viewport.interact_widgets, + &viewport.widgets_prev_frame, + &viewport.hits, + &viewport.input, + self.memory.interaction_mut(), + ); } // Ensure we register the background area so panels and background ui can catch clicks: diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 5e5701e1b333..1095d28cdccf 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -36,16 +36,6 @@ pub struct WidgetHits { pub drag: Option, } -impl WidgetHits { - #[inline] - pub fn is_tooltip_candidate(&self, id: Id) -> bool { - self.contains_pointer.contains_key(&id) - || self.top.map_or(false, |w| w.id == id) - || self.click.map_or(false, |w| w.id == id) - || self.drag.map_or(false, |w| w.id == id) - } -} - /// Find the top or closest widgets to the given position, /// None which is closer than `search_radius`. pub fn hit_test( diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 901ae2a0ae8e..409e3d6ecda1 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -120,6 +120,7 @@ pub(crate) fn interact( .collect(); let hovered = if clicked.is_some() || dragged.is_some() { + // If currently clicking or dragging, nother else is hovered. clicked.iter().chain(&dragged).map(|w| (w.id, *w)).collect() } else { contains_pointer.clone() From d6d5023acba0b79d644789e62f5287bbd13ed15c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 10:15:03 +0100 Subject: [PATCH 08/34] Add LayerId to WidgetRect --- crates/egui/src/context.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index a168b8052127..4c179faee909 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -200,11 +200,6 @@ impl ContextImpl { /// Used to check for overlaps between widgets when handling events. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WidgetRect { - /// Where the widget is. - /// - /// This is after clipping with the parent ui clip rect. - pub interact_rect: Rect, - /// The globally unique widget id. /// /// For interactive widgets, this better be globally unique. @@ -215,6 +210,14 @@ pub struct WidgetRect { /// You can ensure globally unique ids using [`Ui::push_id`]. pub id: Id, + /// What layer the widget is on. + pub layer_id: LayerId, + + /// Where the widget is. + /// + /// This is after clipping with the parent ui clip rect. + pub interact_rect: Rect, + /// How the widget responds to interaction. pub sense: Sense, } @@ -1210,6 +1213,7 @@ impl Context { layer_id, WidgetRect { id, + layer_id, interact_rect, sense, }, @@ -2438,6 +2442,7 @@ impl Context { layer_id, WidgetRect { id, + layer_id, interact_rect, sense, }, From f3d2e17aeab202a9a78eda217aa7b2a3ddeb57b7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 15:49:01 +0100 Subject: [PATCH 09/34] Switch to the new interaction code --- crates/egui/src/containers/area.rs | 3 +- crates/egui/src/containers/window.rs | 370 +++++++++++++-------------- crates/egui/src/context.rs | 242 +++++++----------- crates/egui/src/hit_test.rs | 40 +-- crates/egui/src/interaction.rs | 18 +- crates/egui/src/introspection.rs | 13 +- crates/egui/src/memory.rs | 46 +--- crates/egui/src/sense.rs | 7 + crates/egui/src/style.rs | 20 +- crates/egui/src/ui.rs | 1 - 10 files changed, 335 insertions(+), 425 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 8925e06efa79..689180637253 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -313,7 +313,7 @@ impl Area { let mut move_response = { let interact_id = layer_id.id.with("move"); let sense = if movable { - Sense::click_and_drag() + Sense::drag() } else if interactable { Sense::click() // allow clicks to bring to front } else { @@ -322,7 +322,6 @@ impl Area { let move_response = ctx.interact( Rect::EVERYTHING, - ctx.style().spacing.item_spacing, layer_id, interact_id, state.rect(), diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index f63952641c33..fb12524d083f 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -411,8 +411,7 @@ impl<'open> Window<'open> { let is_collapsed = with_title_bar && !collapsing.is_open(); let possible = PossibleInteractions::new(&area, &resize, is_collapsed); - let area = area.movable(false); // We move it manually, or the area will move the window when we want to resize it - let resize = resize.resizable(false); // We move it manually + let resize = resize.resizable(false); // We resize it manually let mut resize = resize.id(resize_id); let on_top = Some(area_layer_id) == ctx.top_layer_id(); @@ -429,34 +428,29 @@ impl<'open> Window<'open> { (0.0, 0.0) }; - // First interact (move etc) to avoid frame delay: - let last_frame_outer_rect = area.state().rect(); - let interaction = if possible.movable || possible.resizable() { - window_interaction( - ctx, - possible, - area_layer_id, - area_id.with("frame_resize"), - last_frame_outer_rect, - ) - .and_then(|window_interaction| { + // First check for resize to avoid frame delay: + let resize_interaction = if possible.movable || possible.resizable() { + let last_frame_outer_rect = area.state().rect(); + let resize_interaction = + resize_hover(ctx, possible, area_layer_id, last_frame_outer_rect); + if let Some(resize_interaction) = resize_interaction { let margins = window_frame.outer_margin.sum() + window_frame.inner_margin.sum() + vec2(0.0, title_bar_height); interact( - window_interaction, + resize_interaction, ctx, margins, area_layer_id, &mut area, resize_id, - ) - }) + ); + } + resize_interaction } else { None }; - let hover_interaction = resize_hover(ctx, possible, area_layer_id, last_frame_outer_rect); let mut area_content_ui = area.content_ui(ctx); @@ -550,22 +544,8 @@ impl<'open> Window<'open> { collapsing.store(ctx); - if let Some(interaction) = interaction { - paint_frame_interaction( - &area_content_ui, - outer_rect, - interaction, - ctx.style().visuals.widgets.active, - ); - } else if let Some(hover_interaction) = hover_interaction { - if ctx.input(|i| i.pointer.has_pointer()) { - paint_frame_interaction( - &area_content_ui, - outer_rect, - hover_interaction, - ctx.style().visuals.widgets.hovered, - ); - } + if let Some(resize_interaction) = resize_interaction { + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); } content_inner }; @@ -635,44 +615,76 @@ impl PossibleInteractions { } } -/// Either a move or resize +/// Resizing the window edges. #[derive(Clone, Copy, Debug)] -pub(crate) struct WindowInteraction { - pub(crate) area_layer_id: LayerId, - pub(crate) start_rect: Rect, - pub(crate) left: bool, - pub(crate) right: bool, - pub(crate) top: bool, - pub(crate) bottom: bool, +struct ResizeInteraction { + start_rect: Rect, + left: SideResponse, + right: SideResponse, + top: SideResponse, + bottom: SideResponse, +} + +/// A minitature version of `Response`, for each side of the window. +#[derive(Clone, Copy, Debug, Default)] +struct SideResponse { + hover: bool, + drag: bool, } -impl WindowInteraction { +impl SideResponse { + pub fn any(&self) -> bool { + self.hover || self.drag + } +} + +impl std::ops::BitOrAssign for SideResponse { + fn bitor_assign(&mut self, rhs: Self) { + *self = Self { + hover: self.hover || rhs.hover, + drag: self.drag || rhs.drag, + }; + } +} + +impl ResizeInteraction { pub fn set_cursor(&self, ctx: &Context) { - if (self.left && self.top) || (self.right && self.bottom) { + let left = self.left.any(); + let right = self.right.any(); + let top = self.top.any(); + let bottom = self.bottom.any(); + + if (left && top) || (right && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNwSe); - } else if (self.right && self.top) || (self.left && self.bottom) { + } else if (right && top) || (left && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNeSw); - } else if self.left || self.right { + } else if left || right { ctx.set_cursor_icon(CursorIcon::ResizeHorizontal); - } else if self.bottom || self.top { + } else if bottom || top { ctx.set_cursor_icon(CursorIcon::ResizeVertical); } } - pub fn is_resize(&self) -> bool { - self.left || self.right || self.top || self.bottom + pub fn any_hovered(&self) -> bool { + self.left.hover || self.right.hover || self.top.hover || self.bottom.hover + } + + pub fn any_dragged(&self) -> bool { + self.left.drag || self.right.drag || self.top.drag || self.bottom.drag } } fn interact( - window_interaction: WindowInteraction, + resize_interaction: ResizeInteraction, ctx: &Context, margins: Vec2, area_layer_id: LayerId, area: &mut area::Prepared, resize_id: Id, -) -> Option { - let new_rect = move_and_resize_window(ctx, &window_interaction)?; +) { + let Some(new_rect) = move_and_resize_window(ctx, &resize_interaction) else { + return; + }; let mut new_rect = ctx.round_rect_to_pixels(new_rect); if area.constrain() { @@ -682,7 +694,7 @@ fn interact( // TODO(emilk): add this to a Window state instead as a command "move here next frame" area.state_mut().set_left_top_pos(new_rect.left_top()); - if window_interaction.is_resize() { + if resize_interaction.any_dragged() { if let Some(mut state) = resize::State::load(ctx, resize_id) { state.requested_size = Some(new_rect.size() - margins); state.store(ctx, resize_id); @@ -690,191 +702,169 @@ fn interact( } ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id)); - Some(window_interaction) } -fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option { - window_interaction.set_cursor(ctx); - - // Only move/resize windows with primary mouse button: - if !ctx.input(|i| i.pointer.primary_down()) { +fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option { + if !interaction.any_dragged() { return None; } let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?; - let mut rect = window_interaction.start_rect; // prevent drift + let mut rect = interaction.start_rect; // prevent drift - if window_interaction.is_resize() { - if window_interaction.left { - rect.min.x = ctx.round_to_pixel(pointer_pos.x); - } else if window_interaction.right { - rect.max.x = ctx.round_to_pixel(pointer_pos.x); - } + if interaction.left.drag { + rect.min.x = ctx.round_to_pixel(pointer_pos.x); + } else if interaction.right.drag { + rect.max.x = ctx.round_to_pixel(pointer_pos.x); + } - if window_interaction.top { - rect.min.y = ctx.round_to_pixel(pointer_pos.y); - } else if window_interaction.bottom { - rect.max.y = ctx.round_to_pixel(pointer_pos.y); - } - } else { - // Movement. - - // We do window interaction first (to avoid frame delay), - // but we want anything interactive in the window (e.g. slider) to steal - // the drag from us. It is therefor important not to move the window the first frame, - // but instead let other widgets to the steal. HACK. - if !ctx.input(|i| i.pointer.any_pressed()) { - let press_origin = ctx.input(|i| i.pointer.press_origin())?; - let delta = pointer_pos - press_origin; - rect = rect.translate(delta); - } + if interaction.top.drag { + rect.min.y = ctx.round_to_pixel(pointer_pos.y); + } else if interaction.bottom.drag { + rect.max.y = ctx.round_to_pixel(pointer_pos.y); } Some(rect) } -/// Returns `Some` if there is a move or resize -fn window_interaction( +fn resize_hover( ctx: &Context, possible: PossibleInteractions, - area_layer_id: LayerId, - id: Id, + layer_id: LayerId, rect: Rect, -) -> Option { - if ctx.memory(|mem| mem.dragging_something_else(id)) { - return None; - } - - let mut window_interaction = ctx.memory(|mem| mem.window_interaction()); - - if window_interaction.is_none() { - if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { - hover_window_interaction.set_cursor(ctx); - if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { - ctx.memory_mut(|mem| { - mem.interaction_mut().drag_id = Some(id); - mem.interaction_mut().drag_is_window = true; - window_interaction = Some(hover_window_interaction); - mem.set_window_interaction(window_interaction); - }); - } +) -> Option { + let is_dragging = |rect, id| { + let clip_rect = Rect::EVERYTHING; + let response = ctx.interact(clip_rect, layer_id, id, rect, Sense::drag(), true); + SideResponse { + hover: response.hovered(), + drag: response.dragged(), } - } + }; - if let Some(window_interaction) = window_interaction { - let is_active = ctx.memory_mut(|mem| mem.interaction().drag_id == Some(id)); + let id = Id::new(layer_id).with("edge_drag"); - if is_active && window_interaction.area_layer_id == area_layer_id { - return Some(window_interaction); - } - } + let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; + let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - None -} + let corner_rect = + |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius)); -fn resize_hover( - ctx: &Context, - possible: PossibleInteractions, - area_layer_id: LayerId, - rect: Rect, -) -> Option { - let pointer = ctx.input(|i| i.pointer.interact_pos())?; + // What are we dragging/hovering? + let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4]; + + // ---------------------------------------- + // Check sides first, so that corners are on top, covering the sides (i.e. corners have priority) - if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) { - return None; // already dragging (something) + if possible.resize_right { + let response = is_dragging( + Rect::from_min_max(rect.right_top(), rect.right_bottom()).expand(side_grab_radius), + id.with("right"), + ); + right |= response; + } + if possible.resize_left { + let response = is_dragging( + Rect::from_min_max(rect.left_top(), rect.left_bottom()).expand(side_grab_radius), + id.with("left"), + ); + left |= response; + } + if possible.resize_bottom { + let response = is_dragging( + Rect::from_min_max(rect.left_bottom(), rect.right_bottom()).expand(side_grab_radius), + id.with("bottom"), + ); + bottom |= response; + } + if possible.resize_top { + let response = is_dragging( + Rect::from_min_max(rect.left_top(), rect.right_top()).expand(side_grab_radius), + id.with("top"), + ); + top |= response; } - if let Some(top_layer_id) = ctx.layer_id_at(pointer) { - if top_layer_id != area_layer_id && top_layer_id.order != Order::Background { - return None; // Another window is on top here - } + // ---------------------------------------- + // Now check corners: + + if possible.resize_right && possible.resize_bottom { + let response = is_dragging(corner_rect(rect.right_bottom()), id.with("right_bottom")); + right |= response; + bottom |= response; } - if ctx.memory(|mem| mem.interaction().drag_interest) { - // Another widget will become active if we drag here - return None; + if possible.resize_right && possible.resize_top { + let response = is_dragging(corner_rect(rect.right_top()), id.with("right_top")); + right |= response; + top |= response; } - let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; - let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - if !rect.expand(side_grab_radius).contains(pointer) { - return None; + if possible.resize_left && possible.resize_bottom { + let response = is_dragging(corner_rect(rect.left_bottom()), id.with("left_bottom")); + left |= response; + bottom |= response; } - let mut left = possible.resize_left && (rect.left() - pointer.x).abs() <= side_grab_radius; - let mut right = possible.resize_right && (rect.right() - pointer.x).abs() <= side_grab_radius; - let mut top = possible.resize_top && (rect.top() - pointer.y).abs() <= side_grab_radius; - let mut bottom = - possible.resize_bottom && (rect.bottom() - pointer.y).abs() <= side_grab_radius; - - if possible.resize_right - && possible.resize_bottom - && rect.right_bottom().distance(pointer) < corner_grab_radius - { - right = true; - bottom = true; - } - if possible.resize_right - && possible.resize_top - && rect.right_top().distance(pointer) < corner_grab_radius - { - right = true; - top = true; - } - if possible.resize_left - && possible.resize_top - && rect.left_top().distance(pointer) < corner_grab_radius - { - left = true; - top = true; - } - if possible.resize_left - && possible.resize_bottom - && rect.left_bottom().distance(pointer) < corner_grab_radius - { - left = true; - bottom = true; - } - - let any_resize = left || right || top || bottom; - - if !any_resize && !possible.movable { - return None; + if possible.resize_left && possible.resize_top { + let response = is_dragging(corner_rect(rect.left_top()), id.with("left_top")); + left |= response; + top |= response; } + let any_resize = left.any() || right.any() || top.any() || bottom.any(); + if any_resize || possible.movable { - Some(WindowInteraction { - area_layer_id, + let interaction = ResizeInteraction { start_rect: rect, left, right, top, bottom, - }) + }; + interaction.set_cursor(ctx); + Some(interaction) } else { - None + None // TODO: return a default ResizeInteraction instead } } /// Fill in parts of the window frame when we resize by dragging that part -fn paint_frame_interaction( - ui: &Ui, - rect: Rect, - interaction: WindowInteraction, - visuals: style::WidgetVisuals, -) { +fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) { use epaint::tessellator::path::add_circle_quadrant; + let visuals = if interaction.any_dragged() { + ui.style().visuals.widgets.active + } else if interaction.any_hovered() { + ui.style().visuals.widgets.hovered + } else { + return; + }; + + let [left, right, top, bottom]: [bool; 4]; + + if interaction.any_dragged() { + left = interaction.left.drag; + right = interaction.right.drag; + top = interaction.top.drag; + bottom = interaction.bottom.drag; + } else { + left = interaction.left.hover; + right = interaction.right.hover; + top = interaction.top.hover; + bottom = interaction.bottom.hover; + } + let rounding = ui.visuals().window_rounding; let Rect { min, max } = rect; let mut points = Vec::new(); - if interaction.right && !interaction.bottom && !interaction.top { + if right && !bottom && !top { points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); } - if interaction.right && interaction.bottom { + if right && bottom { points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); add_circle_quadrant( @@ -884,11 +874,11 @@ fn paint_frame_interaction( 0.0, ); } - if interaction.bottom { + if bottom { points.push(pos2(max.x - rounding.se, max.y)); points.push(pos2(min.x + rounding.sw, max.y)); } - if interaction.left && interaction.bottom { + if left && bottom { add_circle_quadrant( &mut points, pos2(min.x + rounding.sw, max.y - rounding.sw), @@ -896,11 +886,11 @@ fn paint_frame_interaction( 1.0, ); } - if interaction.left { + if left { points.push(pos2(min.x, max.y - rounding.sw)); points.push(pos2(min.x, min.y + rounding.nw)); } - if interaction.left && interaction.top { + if left && top { add_circle_quadrant( &mut points, pos2(min.x + rounding.nw, min.y + rounding.nw), @@ -908,11 +898,11 @@ fn paint_frame_interaction( 2.0, ); } - if interaction.top { + if top { points.push(pos2(min.x + rounding.nw, min.y)); points.push(pos2(max.x - rounding.ne, min.y)); } - if interaction.right && interaction.top { + if right && top { add_circle_quadrant( &mut points, pos2(max.x - rounding.ne, min.y + rounding.ne), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 4c179faee909..170bac1010a8 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1102,24 +1102,14 @@ impl Context { pub(crate) fn interact( &self, clip_rect: Rect, - item_spacing: Vec2, layer_id: LayerId, id: Id, rect: Rect, sense: Sense, enabled: bool, ) -> Response { - let gap = 0.1; // Just to make sure we don't accidentally hover two things at once (a small eps should be sufficient). - - // Make it easier to click things: - let interact_rect = rect.expand2( - (0.5 * item_spacing - Vec2::splat(gap)) - .at_least(Vec2::splat(0.0)) - .at_most(Vec2::splat(5.0)), - ); - // Respect clip rectangle when interacting: - let interact_rect = clip_rect.intersect(interact_rect); + let interact_rect = clip_rect.intersect(rect); let contains_pointer = self.widget_contains_pointer(layer_id, id, sense, interact_rect); @@ -1240,93 +1230,41 @@ impl Context { res.clicked[PointerButton::Primary as usize] = true; } - if sense.click || sense.drag { - let interaction = memory.interaction_mut(); - - 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 - } + let interaction = memory.interaction_mut(); - for pointer_event in &input.pointer.pointer_events { - match pointer_event { - PointerEvent::Moved(_) => {} - - PointerEvent::Pressed { .. } => { - 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); - res.is_pointer_button_down_on = true; - } - - // HACK: windows have low priority on dragging. - // This is so that if you drag a slider in a window, - // the slider will steal the drag away from the window. - // This is needed because we do window interaction first (to prevent frame delay), - // and then do content layout. - if sense.drag - && (interaction.drag_id.is_none() || interaction.drag_is_window) - { - // potential start of a drag - interaction.drag_id = Some(id); - interaction.drag_is_window = false; - memory.set_window_interaction(None); // HACK: stop moving windows (if any) - - 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 } => { - 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 = res.hovered && res.is_pointer_button_down_on; - res.clicked[*button as usize] = clicked; - res.double_clicked[*button as usize] = - clicked && click.is_double(); - res.triple_clicked[*button as usize] = - clicked && click.is_triple(); - } - } + res.is_pointer_button_down_on = + interaction.click_id == Some(id) || interaction.drag_id == Some(id); - res.is_pointer_button_down_on = false; - } + res.dragged = Some(id) == viewport.interact_widgets.dragged.map(|w| w.id); + res.drag_started = Some(id) == viewport.interact_widgets.drag_started.map(|w| w.id); + res.drag_released = Some(id) == viewport.interact_widgets.drag_ended.map(|w| w.id); + + let clicked = viewport.interact_widgets.clicked.iter().any(|w| w.id == id); + + if sense.click && clicked { + // We were clicked - what kind of click? + for pointer_event in &input.pointer.pointer_events { + if let PointerEvent::Released { + click: Some(click), + button, + } = pointer_event + { + res.clicked[*button as usize] = true; + res.double_clicked[*button as usize] = click.is_double(); + res.triple_clicked[*button as usize] = click.is_triple(); } } } + for pointer_event in &input.pointer.pointer_events { + if let PointerEvent::Released { .. } = pointer_event { + res.is_pointer_button_down_on = false; + res.dragged = false; + } + } + // is_pointer_button_down_on is false when released, but we want interact_pointer_pos // to still work. - let clicked = res.clicked.iter().any(|c| *c); let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); @@ -1970,6 +1908,62 @@ impl Context { } } + #[cfg(debug_assertions)] + { + let paint = |widget: &WidgetRect, text: &str, color: Color32| { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(widget.interact_rect, color, text); + }; + + if self.style().debug.show_widget_hits { + let hits = self.write(|ctx| ctx.viewport().hits.clone()); + let WidgetHits { + contains_pointer, + top, + click, + drag, + } = hits; + + for widget in &contains_pointer { + paint(widget, "contains_pointer", Color32::BLUE); + } + for widget in &top { + paint(widget, "top", Color32::WHITE); + } + for widget in &click { + paint(widget, "click", Color32::RED); + } + for widget in &drag { + paint(widget, "drag", Color32::GREEN); + } + } + + if self.style().debug.show_interaction_widgets { + let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); + let InteractionSnapshot { + clicked, + drag_started: _, + dragged, + drag_ended: _, + contains_pointer, + hovered, + } = interact_widgets; + + for widget in contains_pointer.values() { + paint(widget, "contains_pointer", Color32::BLUE); + } + for widget in hovered.values() { + paint(widget, "hovered", Color32::WHITE); + } + for widget in &clicked { + paint(widget, "clicked", Color32::RED); + } + for widget in &dragged { + paint(widget, "dragged", Color32::GREEN); + } + } + } + self.write(|ctx| ctx.end_frame()) } } @@ -2428,10 +2422,6 @@ impl Context { return false; // don't even remember this widget } - let contains_pointer = self.rect_contains_pointer(layer_id, interact_rect); - - let mut blocking_widget = None; - self.write(|ctx| { let viewport = ctx.viewport(); @@ -2448,67 +2438,9 @@ impl Context { }, ); - // Check if any other widget is covering us. - // Whichever widget is added LAST (=on top) gets the input. - if contains_pointer { - let pointer_pos = viewport.input.pointer.interact_pos(); - if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = viewport.widgets_prev_frame.by_layer.get(&layer_id) { - // Iterate backwards, i.e. topmost widgets first. - for blocking in rects.iter().rev() { - if blocking.id == id { - // We've checked all widgets there were added after this one last frame, - // which means there are no widgets covering us. - break; - } - if !blocking.interact_rect.contains(pointer_pos) { - continue; - } - if sense.interactive() && !blocking.sense.interactive() { - // Only interactive widgets can block other interactive widgets. - continue; - } - - // The `prev` widget is covering us - do we care? - // We don't want a click-only button to block drag-events to a `ScrollArea`: - - let sense_only_drags = sense.drag && !sense.click; - if sense_only_drags && !blocking.sense.drag { - continue; - } - let sense_only_clicks = sense.click && !sense.drag; - if sense_only_clicks && !blocking.sense.click { - continue; - } - - if blocking.sense.interactive() { - // Another widget is covering us at the pointer position - blocking_widget = Some(blocking.interact_rect); - break; - } - } - } - } - } - }); - - #[cfg(debug_assertions)] - if let Some(blocking_rect) = blocking_widget { - if sense.interactive() && self.memory(|m| m.options.style.debug.show_blocking_widget) { - Self::layer_painter(self, LayerId::debug()).debug_rect( - interact_rect, - Color32::GREEN, - "Covered", - ); - Self::layer_painter(self, LayerId::debug()).debug_rect( - blocking_rect, - Color32::LIGHT_BLUE, - "On top", - ); - } - } - - contains_pointer && blocking_widget.is_none() + // Based on output from previous frame and input in this frame + viewport.interact_widgets.contains_pointer.contains_key(&id) + }) } // --------------------------------------------------------------------- diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 1095d28cdccf..13ae3a0f7a7d 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -10,19 +10,16 @@ use crate::*; /// For that you need the `InteractionState`. #[derive(Clone, Debug, Default)] pub struct WidgetHits { - /// All widgets that contains the pointer. + /// All widgets that contains the pointer, back-to-front. /// /// i.e. both a Window and the button in it can ontain the pointer. /// - /// Show tooltips for all of these. - /// Why? So you can do `ui.scope(|ui| …).response.on_hover_text(…)` - /// and get a tooltip for the whole ui, even if individual things - /// in the ui also had a tooltip. - pub contains_pointer: IdMap, + /// Some of these may be widgets in a layer below the top-most layer. + pub contains_pointer: Vec, /// The topmost widget under the pointer, interactive or not. /// - /// Used for nothing? + /// Used for nothing right now. pub top: Option, /// If the user would start a clicking now, this is what would be clicked. @@ -37,7 +34,7 @@ pub struct WidgetHits { } /// Find the top or closest widgets to the given position, -/// None which is closer than `search_radius`. +/// none which is closer than `search_radius`. pub fn hit_test( widgets: &WidgetRects, layer_order: &[LayerId], @@ -48,24 +45,34 @@ pub fn hit_test( let hit_rect = Rect::from_center_size(pos, Vec2::splat(2.0 * search_radius)); - // The few widgets close to the given position, sorted back-to-front. - let close: Vec = layer_order + let search_radius_sq = search_radius * search_radius; + + // First pass: find the few widgets close to the given position, sorted back-to-front. + let mut close: Vec = layer_order .iter() .filter_map(|layer_id| widgets.by_layer.get(layer_id)) .flatten() .filter(|widget| widget.interact_rect.intersects(hit_rect)) - .filter(|w| w.interact_rect.distance_to_pos(pos) <= search_radius) + .filter(|w| w.interact_rect.distance_sq_to_pos(pos) <= search_radius_sq) .copied() .collect(); // Only those widgets directly under the `pos`. - let hits: Vec = close + let mut hits: Vec = close .iter() .filter(|widget| widget.interact_rect.contains(pos)) .copied() .collect(); - let hit = hits.last().copied(); + let top_hit = hits.last().copied(); + let top_layer = top_hit.map(|w| w.layer_id); + + if let Some(top_layer) = top_layer { + // Ignore everything in a layer below the top-most layer: + close.retain(|w| w.layer_id == top_layer); + hits.retain(|w| w.layer_id == top_layer); + } + let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); @@ -73,15 +80,12 @@ pub fn hit_test( let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); let closest_drag = find_closest(close.iter().copied().filter(|w| w.sense.drag), pos); - let top = hit.or(closest); + let top = top_hit.or(closest); let click = hit_click.or(closest_click); let drag = hit_drag.or(closest_drag); - // Which widgets which will have tooltips: - let contains_pointer = hits.into_iter().map(|w| (w.id, w)).collect(); - WidgetHits { - contains_pointer, + contains_pointer: hits, top, click, drag, diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 409e3d6ecda1..fa2adbd0c672 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -112,7 +112,7 @@ pub(crate) fn interact( let contains_pointer: IdMap = hits .contains_pointer - .values() + .iter() .chain(&hits.top) .chain(&hits.click) .chain(&hits.drag) @@ -122,8 +122,22 @@ pub(crate) fn interact( let hovered = if clicked.is_some() || dragged.is_some() { // If currently clicking or dragging, nother else is hovered. clicked.iter().chain(&dragged).map(|w| (w.id, *w)).collect() + } else if hits.click.is_some() || hits.drag.is_some() { + // We are hovering over an interactive widget or two. Just highlight these two. + hits.click + .iter() + .chain(&hits.drag) + .map(|w| (w.id, *w)) + .collect() } else { - contains_pointer.clone() + // Whatever is topmost is what we are hovering. + // TODO: consider handle hovering over multiple top-most widgets? + // TODO: allow hovering close widgets? + hits.contains_pointer + .last() + .map(|w| (w.id, *w)) + .into_iter() + .collect() }; InteractionSnapshot { diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 3fcd717296cb..760575ff6141 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -191,12 +191,15 @@ impl Widget for &mut epaint::TessellationOptions { impl Widget for &memory::InteractionState { fn ui(self, ui: &mut Ui) -> Response { + let memory::InteractionState { + click_id, + drag_id, + focus: _, + } = self; + ui.vertical(|ui| { - ui.label(format!("click_id: {:?}", self.click_id)); - ui.label(format!("drag_id: {:?}", self.drag_id)); - ui.label(format!("drag_is_window: {:?}", self.drag_is_window)); - ui.label(format!("click_interest: {:?}", self.click_interest)); - ui.label(format!("drag_interest: {:?}", self.drag_interest)); + ui.label(format!("click_id: {click_id:?}")); + ui.label(format!("drag_id: {drag_id:?}")); }) .response } diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index c97fb70e3021..53f5e9f471b8 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,10 +1,8 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs use crate::{ - area, vec2, - window::{self, WindowInteraction}, - EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2, ViewportId, - ViewportIdMap, ViewportIdSet, + area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2, + ViewportId, ViewportIdMap, ViewportIdSet, }; // ---------------------------------------------------------------------------- @@ -91,9 +89,6 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) interactions: ViewportIdMap, - - #[cfg_attr(feature = "persistence", serde(skip))] - window_interactions: ViewportIdMap, } impl Default for Memory { @@ -105,7 +100,6 @@ impl Default for Memory { new_font_definitions: Default::default(), interactions: Default::default(), viewport_id: Default::default(), - window_interactions: Default::default(), areas: Default::default(), popup: Default::default(), everything_is_visible: Default::default(), @@ -314,21 +308,6 @@ pub(crate) struct InteractionState { pub drag_id: Option, pub focus: Focus, - - /// HACK: windows have low priority on dragging. - /// This is so that if you drag a slider in a window, - /// the slider will steal the drag away from the window. - /// This is needed because we do window interaction first (to prevent frame delay), - /// and then do content layout. - pub drag_is_window: bool, - - /// Any interest in catching clicks this frame? - /// Cleared to false at start of each frame. - pub click_interest: bool, - - /// Any interest in catching clicks this frame? - /// Cleared to false at start of each frame. - pub drag_interest: bool, } /// Keeps tracks of what widget has keyboard focus @@ -387,9 +366,6 @@ impl InteractionState { prev_input: &crate::input_state::InputState, new_input: &crate::data::input::RawInput, ) { - self.click_interest = false; - self.drag_interest = false; - if !prev_input.pointer.could_any_button_be_click() { self.click_id = None; } @@ -636,8 +612,6 @@ impl Memory { // Cleanup self.interactions.retain(|id, _| viewports.contains(id)); self.areas.retain(|id, _| viewports.contains(id)); - self.window_interactions - .retain(|id, _| viewports.contains(id)); self.viewport_id = new_input.viewport_id; self.interactions @@ -645,10 +619,6 @@ impl Memory { .or_default() .begin_frame(prev_input, new_input); self.areas.entry(self.viewport_id).or_default(); - - if !prev_input.pointer.any_down() { - self.window_interactions.remove(&self.viewport_id); - } } pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { @@ -826,18 +796,6 @@ impl Memory { self.areas().get(id.into()).map(|state| state.rect()) } - pub(crate) fn window_interaction(&self) -> Option { - self.window_interactions.get(&self.viewport_id).copied() - } - - pub(crate) fn set_window_interaction(&mut self, wi: Option) { - if let Some(wi) = wi { - self.window_interactions.insert(self.viewport_id, wi); - } else { - self.window_interactions.remove(&self.viewport_id); - } - } - pub(crate) fn interaction(&self) -> &InteractionState { self.interactions .get(&self.viewport_id) diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index 53baa3ec3c0d..ca216896aee9 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -59,6 +59,13 @@ impl Sense { } /// Sense both clicks, drags and hover (e.g. a slider or window). + /// + /// Note that this will introduce a latency when dragging, + /// because when the user starts a press egui can't know if this is the start + /// of a click or a drag, and it won't know until the cursor has + /// either moved a certain distance, or the user has released the mouse button. + /// + /// See [`crate::PointerState::is_decidedly_dragging`] for details. #[inline] pub fn click_and_drag() -> Self { Self { diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 59ab8be6e0c7..004895a97ed6 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1047,8 +1047,11 @@ pub struct DebugOptions { /// Show an overlay on all interactive widgets. pub show_interactive_widgets: bool, - /// Show what widget blocks the interaction of another widget. - pub show_blocking_widget: bool, + /// Show interesting widgets under the mouse cursor. + pub show_widget_hits: bool, + + /// Show which widgets are being interacted with. + pub show_interaction_widgets: bool, } #[cfg(debug_assertions)] @@ -1063,7 +1066,8 @@ impl Default for DebugOptions { show_expand_height: false, show_resize: false, show_interactive_widgets: false, - show_blocking_widget: false, + show_widget_hits: false, + show_interaction_widgets: false, } } } @@ -1876,7 +1880,8 @@ impl DebugOptions { show_expand_height, show_resize, show_interactive_widgets, - show_blocking_widget, + show_widget_hits, + show_interaction_widgets, } = self; { @@ -1904,10 +1909,9 @@ impl DebugOptions { "Show an overlay on all interactive widgets", ); - ui.checkbox( - show_blocking_widget, - "Show which widget blocks the interaction of another widget", - ); + ui.checkbox(show_widget_hits, "Show widgets under mouse pointer"); + + ui.checkbox(show_interaction_widgets, "Show interaction widgets"); ui.vertical_centered(|ui| reset_button(ui, self)); } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 087d2dd8fbe3..8979d48ffbca 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -648,7 +648,6 @@ impl Ui { pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { self.ctx().interact( self.clip_rect(), - self.spacing().item_spacing, self.layer_id(), id, rect, From ea18192cfaa6a55f46e4522336b6fa02f6b5f009 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 16:09:34 +0100 Subject: [PATCH 10/34] Simplify window resizing --- crates/egui/src/containers/window.rs | 88 +++++++++++++--------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index fb12524d083f..7382d334cf0d 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -429,28 +429,22 @@ impl<'open> Window<'open> { }; // First check for resize to avoid frame delay: - let resize_interaction = if possible.movable || possible.resizable() { - let last_frame_outer_rect = area.state().rect(); - let resize_interaction = - resize_hover(ctx, possible, area_layer_id, last_frame_outer_rect); - if let Some(resize_interaction) = resize_interaction { - let margins = window_frame.outer_margin.sum() - + window_frame.inner_margin.sum() - + vec2(0.0, title_bar_height); - - interact( - resize_interaction, - ctx, - margins, - area_layer_id, - &mut area, - resize_id, - ); - } - resize_interaction - } else { - None - }; + let last_frame_outer_rect = area.state().rect(); + let resize_interaction = + resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect); + + let margins = window_frame.outer_margin.sum() + + window_frame.inner_margin.sum() + + vec2(0.0, title_bar_height); + + resize_response( + resize_interaction, + ctx, + margins, + area_layer_id, + &mut area, + resize_id, + ); let mut area_content_ui = area.content_ui(ctx); @@ -544,9 +538,8 @@ impl<'open> Window<'open> { collapsing.store(ctx); - if let Some(resize_interaction) = resize_interaction { - paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); - } + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + content_inner }; @@ -586,10 +579,10 @@ fn paint_resize_corner( // ---------------------------------------------------------------------------- +/// Which sides can be resized? #[derive(Clone, Copy, Debug)] struct PossibleInteractions { - movable: bool, - // Which sides can we drag to resize? + // Which sides can we drag to resize or move? resize_left: bool, resize_right: bool, resize_top: bool, @@ -602,7 +595,6 @@ impl PossibleInteractions { let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed; let pivot = area.get_pivot(); Self { - movable, resize_left: resizable && (movable || pivot.x() != Align::LEFT), resize_right: resizable && (movable || pivot.x() != Align::RIGHT), resize_top: resizable && (movable || pivot.y() != Align::TOP), @@ -674,7 +666,7 @@ impl ResizeInteraction { } } -fn interact( +fn resize_response( resize_interaction: ResizeInteraction, ctx: &Context, margins: Vec2, @@ -727,12 +719,22 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt Some(rect) } -fn resize_hover( +fn resize_interaction( ctx: &Context, possible: PossibleInteractions, layer_id: LayerId, rect: Rect, -) -> Option { +) -> ResizeInteraction { + if !possible.resizable() { + return ResizeInteraction { + start_rect: rect, + left: Default::default(), + right: Default::default(), + top: Default::default(), + bottom: Default::default(), + }; + } + let is_dragging = |rect, id| { let clip_rect = Rect::EVERYTHING; let response = ctx.interact(clip_rect, layer_id, id, rect, Sense::drag(), true); @@ -812,21 +814,15 @@ fn resize_hover( top |= response; } - let any_resize = left.any() || right.any() || top.any() || bottom.any(); - - if any_resize || possible.movable { - let interaction = ResizeInteraction { - start_rect: rect, - left, - right, - top, - bottom, - }; - interaction.set_cursor(ctx); - Some(interaction) - } else { - None // TODO: return a default ResizeInteraction instead - } + let interaction = ResizeInteraction { + start_rect: rect, + left, + right, + top, + bottom, + }; + interaction.set_cursor(ctx); + interaction } /// Fill in parts of the window frame when we resize by dragging that part From 8121333cc79acc68fca034c2dfaa2c4b5294e068 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 11 Feb 2024 22:53:09 +0100 Subject: [PATCH 11/34] Bug fixes --- Cargo.lock | 74 +++++++++++++++++++++++++++++----- crates/egui/src/context.rs | 72 +++++++++++++++++++++------------ crates/egui/src/hit_test.rs | 55 +++++++++++++++++++------ crates/egui/src/interaction.rs | 6 ++- crates/egui/src/response.rs | 2 +- crates/egui/src/style.rs | 14 ++++--- crates/egui/src/ui.rs | 2 +- 7 files changed, 168 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92948c4589fc..beb2eb8d8530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,9 +199,9 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arboard" -version = "3.3.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1faa3c733d9a3dd6fbaf85da5d162a2e03b2e0033a90dceb0e2a90fdd1e5380a" +checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" dependencies = [ "clipboard-win", "log", @@ -210,7 +210,8 @@ dependencies = [ "objc_id", "parking_lot", "thiserror", - "x11rb", + "winapi", + "x11rb 0.12.0", ] [[package]] @@ -788,11 +789,13 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clipboard-win" -version = "5.1.0" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec832972fefb8cf9313b45a0d1945e29c9c251f1d4c6eafc5fe2124c02d2e81" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ "error-code", + "str-buf", + "winapi", ] [[package]] @@ -1507,9 +1510,13 @@ dependencies = [ [[package]] name = "error-code" -version = "3.0.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] [[package]] name = "event-listener" @@ -1724,6 +1731,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -3517,6 +3534,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strict-num" version = "0.1.1" @@ -4359,6 +4382,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4594,7 +4626,7 @@ dependencies = [ "web-time", "windows-sys 0.48.0", "x11-dl", - "x11rb", + "x11rb 0.13.0", "xkbcommon-dl", ] @@ -4618,6 +4650,19 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname 0.3.0", + "nix", + "winapi", + "winapi-wsapoll", + "x11rb-protocol 0.12.0", +] + [[package]] name = "x11rb" version = "0.13.0" @@ -4625,12 +4670,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ "as-raw-xcb-connection", - "gethostname", + "gethostname 0.4.3", "libc", "libloading 0.8.0", "once_cell", "rustix 0.38.21", - "x11rb-protocol", + "x11rb-protocol 0.13.0", +] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix", ] [[package]] diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 170bac1010a8..49ae9aca0b47 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -254,17 +254,6 @@ impl WidgetRects { let layer_widgets = self.by_layer.entry(layer_id).or_default(); - if let Some(last) = layer_widgets.last_mut() { - if last.id == widget_rect.id { - // e.g. calling `response.interact(…)` right after interacting. - last.sense |= widget_rect.sense; - last.interact_rect = last.interact_rect.union(widget_rect.interact_rect); - return; - } - } - - layer_widgets.push(widget_rect); - match self.by_id.entry(widget_rect.id) { std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(widget_rect); @@ -276,6 +265,17 @@ impl WidgetRects { existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect); } } + + if let Some(last) = layer_widgets.last_mut() { + if last.id == widget_rect.id { + // e.g. calling `response.interact(…)` right after interacting. + last.sense |= widget_rect.sense; + last.interact_rect = last.interact_rect.union(widget_rect.interact_rect); + return; + } + } + + layer_widgets.push(widget_rect); } } @@ -1105,9 +1105,14 @@ impl Context { layer_id: LayerId, id: Id, rect: Rect, - sense: Sense, + mut sense: Sense, enabled: bool, ) -> Response { + if !enabled { + sense.click = false; + sense.drag = false; + } + // Respect clip rectangle when interacting: let interact_rect = clip_rect.intersect(rect); @@ -1126,7 +1131,7 @@ impl Context { ); } - self.interact_with_hovered( + self.interact_with_existing( layer_id, id, rect, @@ -1139,16 +1144,21 @@ impl Context { /// You specify if a thing is hovered, and the function gives a [`Response`]. #[allow(clippy::too_many_arguments)] - pub(crate) fn interact_with_hovered( + pub(crate) fn interact_with_existing( &self, layer_id: LayerId, id: Id, rect: Rect, interact_rect: Rect, - sense: Sense, + mut sense: Sense, enabled: bool, contains_pointer: bool, ) -> Response { + if !enabled { + sense.click = false; + sense.drag = false; + } + // This is the start - we'll fill in the fields below: let mut res = Response { ctx: self.clone(), @@ -1159,7 +1169,7 @@ impl Context { sense, enabled, contains_pointer, - hovered: contains_pointer && enabled, + hovered: false, highlighted: self.frame_state(|fs| fs.highlight_this_frame.contains(&id)), clicked: Default::default(), double_clicked: Default::default(), @@ -1235,11 +1245,14 @@ impl Context { res.is_pointer_button_down_on = interaction.click_id == Some(id) || interaction.drag_id == Some(id); - res.dragged = Some(id) == viewport.interact_widgets.dragged.map(|w| w.id); - res.drag_started = Some(id) == viewport.interact_widgets.drag_started.map(|w| w.id); - res.drag_released = Some(id) == viewport.interact_widgets.drag_ended.map(|w| w.id); + if enabled { + res.hovered = viewport.interact_widgets.hovered.contains_key(&id); + res.dragged = Some(id) == viewport.interact_widgets.dragged.map(|w| w.id); + res.drag_started = Some(id) == viewport.interact_widgets.drag_started.map(|w| w.id); + res.drag_released = Some(id) == viewport.interact_widgets.drag_ended.map(|w| w.id); + } - let clicked = viewport.interact_widgets.clicked.iter().any(|w| w.id == id); + let clicked = Some(id) == viewport.interact_widgets.clicked.map(|w| w.id); if sense.click && clicked { // We were clicked - what kind of click? @@ -1922,10 +1935,13 @@ impl Context { top, click, drag, + closest_interactive: _, } = hits; - for widget in &contains_pointer { - paint(widget, "contains_pointer", Color32::BLUE); + if false { + for widget in &contains_pointer { + paint(widget, "contains_pointer", Color32::BLUE); + } } for widget in &top { paint(widget, "top", Color32::WHITE); @@ -1949,11 +1965,15 @@ impl Context { hovered, } = interact_widgets; - for widget in contains_pointer.values() { - paint(widget, "contains_pointer", Color32::BLUE); + if false { + for widget in contains_pointer.values() { + paint(widget, "contains_pointer", Color32::BLUE); + } } - for widget in hovered.values() { - paint(widget, "hovered", Color32::WHITE); + if true { + for widget in hovered.values() { + paint(widget, "hovered", Color32::WHITE); + } } for widget in &clicked { paint(widget, "clicked", Color32::RED); diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 13ae3a0f7a7d..3afeee163ac9 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -1,18 +1,18 @@ use crate::*; -/// Result of a hit-test agains [`WidgetRects`]. +/// Result of a hit-test against [`WidgetRects`]. /// /// Answers the question "what is under the mouse pointer?". /// /// Note that this doesn't care if the mouse button is pressed or not, /// or if we're currently already dragging something. /// -/// For that you need the `InteractionState`. +/// For that you need the [`crate::InteractionState`]. #[derive(Clone, Debug, Default)] pub struct WidgetHits { /// All widgets that contains the pointer, back-to-front. /// - /// i.e. both a Window and the button in it can ontain the pointer. + /// i.e. both a Window and the button in it can contain the pointer. /// /// Some of these may be widgets in a layer below the top-most layer. pub contains_pointer: Vec, @@ -31,6 +31,11 @@ pub struct WidgetHits { /// /// This is the top one under the pointer, or closest one of the top-most. pub drag: Option, + + /// The closest interactive widget under the pointer. + /// + /// This is either the same as [`Self::click`] or [`Self::drag`], or both. + pub closest_interactive: Option, } /// Find the top or closest widgets to the given position, @@ -43,16 +48,14 @@ pub fn hit_test( ) -> WidgetHits { crate::profile_function!(); - let hit_rect = Rect::from_center_size(pos, Vec2::splat(2.0 * search_radius)); - let search_radius_sq = search_radius * search_radius; // First pass: find the few widgets close to the given position, sorted back-to-front. let mut close: Vec = layer_order .iter() + .filter(|layer| layer.order.allow_interaction()) .filter_map(|layer_id| widgets.by_layer.get(layer_id)) .flatten() - .filter(|widget| widget.interact_rect.intersects(hit_rect)) .filter(|w| w.interact_rect.distance_sq_to_pos(pos) <= search_radius_sq) .copied() .collect(); @@ -68,7 +71,7 @@ pub fn hit_test( let top_layer = top_hit.map(|w| w.layer_id); if let Some(top_layer) = top_layer { - // Ignore everything in a layer below the top-most layer: + // Ignore all layers not in the same layer as the top hit. close.retain(|w| w.layer_id == top_layer); hits.retain(|w| w.layer_id == top_layer); } @@ -81,26 +84,54 @@ pub fn hit_test( let closest_drag = find_closest(close.iter().copied().filter(|w| w.sense.drag), pos); let top = top_hit.or(closest); - let click = hit_click.or(closest_click); - let drag = hit_drag.or(closest_drag); + let mut click = hit_click.or(closest_click); + let mut drag = hit_drag.or(closest_drag); + + if let (Some(click), Some(drag)) = (&mut click, &mut drag) { + // If one of the widgets is interested in both click and drags, let it win. + // Otherwise we end up in weird situations where both widgets respond to hover, + // but one of the widgets only responds to _one_ of the events. + + if click.sense.click && click.sense.drag { + *drag = *click; + } else if drag.sense.click && drag.sense.drag { + *click = *drag; + } + } + + let closest_interactive = match (click, drag) { + (Some(click), Some(drag)) => { + if click.interact_rect.distance_sq_to_pos(pos) + < drag.interact_rect.distance_sq_to_pos(pos) + { + Some(click) + } else { + Some(drag) + } + } + (Some(click), None) => Some(click), + (None, Some(drag)) => Some(drag), + (None, None) => None, + }; WidgetHits { contains_pointer: hits, top, click, drag, + closest_interactive, } } fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { let mut closest = None; - let mut cloest_dist_sq = f32::INFINITY; + let mut closest_dist_sq = f32::INFINITY; for widget in widgets { let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); // In case of a tie, take the last one = the one on top. - if dist_sq <= cloest_dist_sq { - cloest_dist_sq = dist_sq; + if dist_sq <= closest_dist_sq { + closest_dist_sq = dist_sq; closest = Some(widget); } } diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index fa2adbd0c672..271468e32ea1 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -42,6 +42,8 @@ pub(crate) fn interact( input: &InputState, interaction: &mut InteractionState, ) -> InteractionSnapshot { + crate::profile_function!(); + if let Some(id) = interaction.click_id { if !widgets.by_id.contains_key(&id) { // The widget we were interested in clicking is gone. @@ -120,10 +122,10 @@ pub(crate) fn interact( .collect(); let hovered = if clicked.is_some() || dragged.is_some() { - // If currently clicking or dragging, nother else is hovered. + // If currently clicking or dragging, nothing else is hovered. clicked.iter().chain(&dragged).map(|w| (w.id, *w)).collect() } else if hits.click.is_some() || hits.drag.is_some() { - // We are hovering over an interactive widget or two. Just highlight these two. + // We are hovering over an interactive widget or two. hits.click .iter() .chain(&hits.drag) diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 2cef13e93f6b..747a8c880918 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -617,7 +617,7 @@ impl Response { return self.clone(); } - self.ctx.interact_with_hovered( + self.ctx.interact_with_existing( self.layer_id, self.id, self.rect, diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 004895a97ed6..2a57db491bbb 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -715,10 +715,10 @@ pub struct Interaction { /// which is important for e.g. touch screens. pub interact_radius: f32, - /// Mouse must be this close to the side of a window to resize + /// Radius of the interactive area of the side of a window during drag-to-resize. pub resize_grab_radius_side: f32, - /// Mouse must be this close to the corner of a window to resize + /// Radius of the interactive area of the corner of a window during drag-to-resize. pub resize_grab_radius_corner: f32, /// If `false`, tooltips will show up anytime you hover anything, even is mouse is still moving @@ -1135,9 +1135,9 @@ impl Default for Spacing { impl Default for Interaction { fn default() -> Self { Self { - interact_radius: 3.0, resize_grab_radius_side: 5.0, resize_grab_radius_corner: 10.0, + interact_radius: 8.0, show_tooltips_only_when_still: true, tooltip_delay: 0.3, selectable_labels: true, @@ -1612,7 +1612,7 @@ impl Interaction { multi_widget_text_select, } = self; ui.add(Slider::new(interact_radius, 0.0..=20.0).text("interact_radius")) - .on_hover_text("Interact witgh ghe closest widget within this radius."); + .on_hover_text("Interact with the closest widget within this radius."); ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side")); ui.add( Slider::new(resize_grab_radius_corner, 0.0..=20.0).text("resize_grab_radius_corner"), @@ -1621,7 +1621,11 @@ impl Interaction { show_tooltips_only_when_still, "Only show tooltips if mouse is still", ); - ui.add(Slider::new(tooltip_delay, 0.0..=1.0).text("tooltip_delay")); + ui.add( + Slider::new(tooltip_delay, 0.0..=1.0) + .suffix(" s") + .text("tooltip_delay"), + ); ui.horizontal(|ui| { ui.checkbox(selectable_labels, "Selectable text in labels"); diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 8979d48ffbca..2228800ad184 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -670,7 +670,7 @@ impl Ui { sense: Sense, ) -> Response { let interact_rect = rect.intersect(self.clip_rect()); - self.ctx().interact_with_hovered( + self.ctx().interact_with_existing( self.layer_id(), id, rect, From 21cac17a2748e43443ed197e7117367412c1466f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 15:58:18 +0100 Subject: [PATCH 12/34] Refactor and simplify interaction code --- crates/egui/src/containers/area.rs | 5 +- crates/egui/src/containers/window.rs | 3 +- crates/egui/src/context.rs | 250 ++++++++++++--------------- crates/egui/src/memory.rs | 9 +- crates/egui/src/response.rs | 2 +- crates/egui/src/ui.rs | 8 +- 6 files changed, 125 insertions(+), 152 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 689180637253..99c96f396317 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -320,13 +320,14 @@ impl Area { Sense::hover() }; - let move_response = ctx.interact( - Rect::EVERYTHING, + let move_response = ctx.create_widget( layer_id, interact_id, state.rect(), + state.rect(), sense, enabled, + true, ); if movable && move_response.dragged() { diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 7382d334cf0d..e8842aef22b5 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -736,8 +736,7 @@ fn resize_interaction( } let is_dragging = |rect, id| { - let clip_rect = Rect::EVERYTHING; - let response = ctx.interact(clip_rect, layer_id, id, rect, Sense::drag(), true); + let response = ctx.create_widget(layer_id, id, rect, rect, Sense::drag(), true, true); SideResponse { hover: response.hovered(), drag: response.dragged(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 49ae9aca0b47..82f112fb2fc6 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -213,6 +213,9 @@ pub struct WidgetRect { /// What layer the widget is on. pub layer_id: LayerId, + /// The full widget rectangle. + pub rect: Rect, + /// Where the widget is. /// /// This is after clipping with the parent ui clip rect. @@ -220,6 +223,9 @@ pub struct WidgetRect { /// How the widget responds to interaction. pub sense: Sense, + + /// Is the widget enabled? + pub enabled: bool, } /// Stores the positions of all widgets generated during a single egui update/frame. @@ -256,26 +262,27 @@ impl WidgetRects { match self.by_id.entry(widget_rect.id) { std::collections::hash_map::Entry::Vacant(entry) => { + // A new widget entry.insert(widget_rect); + layer_widgets.push(widget_rect); } std::collections::hash_map::Entry::Occupied(mut entry) => { - // e.g. calling `response.interact(…)` right after interacting. + // e.g. calling `response.interact(…)` to add more interaction. let existing = entry.get_mut(); - existing.sense |= widget_rect.sense; + existing.rect = existing.rect.union(widget_rect.rect); existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect); - } - } + existing.sense |= widget_rect.sense; + existing.enabled |= widget_rect.enabled; - if let Some(last) = layer_widgets.last_mut() { - if last.id == widget_rect.id { - // e.g. calling `response.interact(…)` right after interacting. - last.sense |= widget_rect.sense; - last.interact_rect = last.interact_rect.union(widget_rect.interact_rect); - return; + // Find the existing widget in this layer and update it: + for previous in layer_widgets.iter_mut().rev() { + if previous.id == widget_rect.id { + *previous = *existing; + break; + } + } } } - - layer_widgets.push(widget_rect); } } @@ -703,7 +710,7 @@ impl ContextImpl { } /// The current active viewport - fn viewport(&mut self) -> &mut ViewportState { + pub(crate) fn viewport(&mut self) -> &mut ViewportState { self.viewports.entry(self.viewport_id()).or_default() } @@ -1097,69 +1104,110 @@ impl Context { // --------------------------------------------------------------------- - /// Use `ui.interact` instead + /// Create a widget and check for interaction. + /// + /// If this is not called, the widget doesn't exist. + /// + /// You should use [`Ui::interact`] instead. #[allow(clippy::too_many_arguments)] - pub(crate) fn interact( + pub(crate) fn create_widget( &self, - clip_rect: Rect, layer_id: LayerId, id: Id, rect: Rect, + interact_rect: Rect, mut sense: Sense, enabled: bool, + may_contain_pointer: bool, ) -> Response { if !enabled { sense.click = false; sense.drag = false; } - // Respect clip rectangle when interacting: - let interact_rect = clip_rect.intersect(rect); + let widget_rect = WidgetRect { + id, + layer_id, + rect, + interact_rect, + sense, + enabled, + }; - let contains_pointer = self.widget_contains_pointer(layer_id, id, sense, interact_rect); + if widget_rect.interact_rect.is_positive() { + // Remember this widget + self.write(|ctx| { + let viewport = ctx.viewport(); + + // We add all widgets here, even non-interactive ones, + // because we need this list not only for checking for blocking widgets, + // but also to know when we have reached the widget we are checking for cover. + viewport + .widgets_this_frame + .insert(widget_rect.layer_id, widget_rect); + + // Based on output from previous frame and input in this frame + viewport + .interact_widgets + .contains_pointer + .contains_key(&widget_rect.id) + }); + } else { + // Don't remember invisible widgets + } - #[cfg(debug_assertions)] - if sense.interactive() - && interact_rect.is_positive() - && self.style().debug.show_interactive_widgets - { - Self::layer_painter(self, LayerId::debug()).rect( - interact_rect, - 0.0, - Color32::YELLOW.additive().linear_multiply(0.005), - Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)), - ); + if !enabled || !sense.focusable || !layer_id.allow_interaction() { + // Not interested or allowed input: + self.memory_mut(|mem| mem.surrender_focus(id)); } - self.interact_with_existing( - layer_id, + if sense.interactive() || sense.focusable { + self.check_for_id_clash(id, rect, "widget"); + } + + let mut res = self.get_response(widget_rect); + res.contains_pointer &= may_contain_pointer; + + #[cfg(feature = "accesskit")] + if sense.focusable { + // 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| res.fill_accesskit_node_common(builder)); + } + + res + } + + /// Read the response of some widget, either before or after it was created/interacted. + /// + /// If the widget was not visible the previous frame (or this frame), this will return `None`. + pub fn read_response(&self, id: Id) -> Option { + self.write(|ctx| { + let viewport = ctx.viewport(); + viewport + .widgets_this_frame + .by_id + .get(&id) + .or_else(|| viewport.widgets_prev_frame.by_id.get(&id)) + .copied() + }) + .map(|widget_rect| self.get_response(widget_rect)) + } + + /// Do all interaction for an existing widget, without (re-)registering it. + fn get_response(&self, widget_rect: WidgetRect) -> Response { + let WidgetRect { id, + layer_id, rect, interact_rect, sense, enabled, - contains_pointer, - ) - } + } = widget_rect; - /// You specify if a thing is hovered, and the function gives a [`Response`]. - #[allow(clippy::too_many_arguments)] - pub(crate) fn interact_with_existing( - &self, - layer_id: LayerId, - id: Id, - rect: Rect, - interact_rect: Rect, - mut sense: Sense, - enabled: bool, - contains_pointer: bool, - ) -> Response { - if !enabled { - sense.click = false; - sense.drag = false; - } + let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id)); - // This is the start - we'll fill in the fields below: let mut res = Response { ctx: self.clone(), layer_id, @@ -1168,9 +1216,9 @@ impl Context { interact_rect, sense, enabled, - contains_pointer, + contains_pointer: false, hovered: false, - highlighted: self.frame_state(|fs| fs.highlight_this_frame.contains(&id)), + highlighted, clicked: Default::default(), double_clicked: Default::default(), triple_clicked: Default::default(), @@ -1179,45 +1227,15 @@ impl Context { drag_released: false, is_pointer_button_down_on: false, interact_pointer_pos: None, - changed: false, // must be set by the widget itself + changed: false, }; - if !enabled || !sense.focusable || !layer_id.allow_interaction() { - // Not interested or allowed input: - self.memory_mut(|mem| mem.surrender_focus(id)); - } - - if sense.interactive() || sense.focusable { - self.check_for_id_clash(id, rect, "widget"); - } - - #[cfg(feature = "accesskit")] - if sense.focusable { - // 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| res.fill_accesskit_node_common(builder)); - } - let clicked_elsewhere = res.clicked_elsewhere(); + self.write(|ctx| { let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default(); - // We need to remember this widget. - // `widget_contains_pointer` also does this, but in case of e.g. `Response::interact`, - // that won't be called. - // We add all widgets here, even non-interactive ones, - // because we need this list not only for checking for blocking widgets, - // but also to know when we have reached the widget we are checking for cover. - viewport.widgets_this_frame.insert( - layer_id, - WidgetRect { - id, - layer_id, - interact_rect, - sense, - }, - ); + res.contains_pointer = viewport.interact_widgets.contains_pointer.contains_key(&id); let input = &viewport.input; let memory = &mut ctx.memory; @@ -1227,7 +1245,7 @@ impl Context { } if sense.click - && memory.has_focus(res.id) + && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { // Space/enter works like a primary click for e.g. selected buttons @@ -1235,8 +1253,7 @@ impl Context { } #[cfg(feature = "accesskit")] - if sense.click && input.has_accesskit_action_request(res.id, accesskit::Action::Default) - { + if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) { res.clicked[PointerButton::Primary as usize] = true; } @@ -1245,7 +1262,7 @@ impl Context { res.is_pointer_button_down_on = interaction.click_id == Some(id) || interaction.drag_id == Some(id); - if enabled { + if res.enabled { res.hovered = viewport.interact_widgets.hovered.contains_key(&id); res.dragged = Some(id) == viewport.interact_widgets.dragged.map(|w| w.id); res.drag_started = Some(id) == viewport.interact_widgets.drag_started.map(|w| w.id); @@ -1288,11 +1305,11 @@ impl Context { res.hovered = false; } - if memory.has_focus(res.id) && clicked_elsewhere { + if clicked_elsewhere && memory.has_focus(id) { memory.surrender_focus(id); } - if res.dragged() && !memory.has_focus(res.id) { + if res.dragged() && !memory.has_focus(id) { // e.g.: remove focus from a widget when you drag something else memory.stop_text_input(); } @@ -1900,7 +1917,7 @@ impl Context { self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); - if self.options(|o| o.debug_paint_interactive_widgets) { + if self.style().debug.show_interactive_widgets { let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); for (layer_id, rects) in rects.by_layer { let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); @@ -1913,7 +1930,8 @@ impl Context { } else if rect.sense.drag { (Color32::from_rgb(0, 0, 0x88), "drag") } else { - (Color32::from_rgb(0, 0, 0x88), "hover") + continue; + // (Color32::from_rgb(0, 0, 0x88), "hover") }; painter.debug_rect(rect.interact_rect, color, text); } @@ -2421,48 +2439,6 @@ impl Context { true } - /// Does the given widget contain the mouse pointer? - /// - /// Will return false if some other area is covering the given layer. - /// - /// If another widget is covering us and is listening for the same input (click and/or drag), - /// this will return false. - /// - /// The given rectangle is assumed to have been clipped by its parent clip rect. - /// - /// See also [`Response::contains_pointer`]. - pub fn widget_contains_pointer( - &self, - layer_id: LayerId, - id: Id, - sense: Sense, - interact_rect: Rect, - ) -> bool { - if !interact_rect.is_positive() { - return false; // don't even remember this widget - } - - self.write(|ctx| { - let viewport = ctx.viewport(); - - // We add all widgets here, even non-interactive ones, - // because we need this list not only for checking for blocking widgets, - // but also to know when we have reached the widget we are checking for cover. - viewport.widgets_this_frame.insert( - layer_id, - WidgetRect { - id, - layer_id, - interact_rect, - sense, - }, - ); - - // Based on output from previous frame and input in this frame - viewport.interact_widgets.contains_pointer.contains_key(&id) - }) - } - // --------------------------------------------------------------------- /// Whether or not to debug widget layout on hover. diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 53f5e9f471b8..ffb8661ff990 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -148,6 +148,8 @@ impl FocusDirection { // ---------------------------------------------------------------------------- /// Some global options that you can read and write. +/// +/// See also [`crate::style::DebugOptions`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -208,9 +210,6 @@ pub struct Options { /// /// By default this is `true` in debug builds. pub warn_on_id_clash: bool, - - /// If true, paint all interactive widgets in the order they were added to each layer. - pub debug_paint_interactive_widgets: bool, } impl Default for Options { @@ -224,7 +223,6 @@ impl Default for Options { screen_reader: false, preload_font_glyphs: true, warn_on_id_clash: cfg!(debug_assertions), - debug_paint_interactive_widgets: false, } } } @@ -241,7 +239,6 @@ impl Options { screen_reader: _, // needs to come from the integration preload_font_glyphs: _, warn_on_id_clash, - debug_paint_interactive_widgets, } = self; use crate::Widget as _; @@ -260,8 +257,6 @@ impl Options { ); ui.checkbox(warn_on_id_clash, "Warn if two widgets have the same Id"); - - ui.checkbox(debug_paint_interactive_widgets, "Debug interactive widgets"); }); use crate::containers::*; diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 747a8c880918..77b6489b7a99 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -617,7 +617,7 @@ impl Response { return self.clone(); } - self.ctx.interact_with_existing( + self.ctx.create_widget( self.layer_id, self.id, self.rect, diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 2228800ad184..195de49ea531 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -646,13 +646,15 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { - self.ctx().interact( - self.clip_rect(), + let interact_rect = self.clip_rect().intersect(rect); + self.ctx().create_widget( self.layer_id(), id, rect, + interact_rect, sense, self.enabled, + true, ) } @@ -670,7 +672,7 @@ impl Ui { sense: Sense, ) -> Response { let interact_rect = rect.intersect(self.clip_rect()); - self.ctx().interact_with_existing( + self.ctx().create_widget( self.layer_id(), id, rect, From 0db7a490ca253d6cd9924bce2c5efce598a88bea Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 17:11:18 +0100 Subject: [PATCH 13/34] Better debug-painting --- crates/egui/src/context.rs | 75 ++++++++++++++++++-------------------- crates/egui/src/style.rs | 7 ---- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 82f112fb2fc6..1dcbbb0eeb96 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1917,7 +1917,13 @@ impl Context { self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); + let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(widget.interact_rect, color, text); + }; + if self.style().debug.show_interactive_widgets { + // Show all interactive widgets: let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); for (layer_id, rects) in rects.by_layer { let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); @@ -1937,42 +1943,9 @@ impl Context { } } } - } - - #[cfg(debug_assertions)] - { - let paint = |widget: &WidgetRect, text: &str, color: Color32| { - let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); - painter.debug_rect(widget.interact_rect, color, text); - }; - - if self.style().debug.show_widget_hits { - let hits = self.write(|ctx| ctx.viewport().hits.clone()); - let WidgetHits { - contains_pointer, - top, - click, - drag, - closest_interactive: _, - } = hits; - - if false { - for widget in &contains_pointer { - paint(widget, "contains_pointer", Color32::BLUE); - } - } - for widget in &top { - paint(widget, "top", Color32::WHITE); - } - for widget in &click { - paint(widget, "click", Color32::RED); - } - for widget in &drag { - paint(widget, "drag", Color32::GREEN); - } - } - if self.style().debug.show_interaction_widgets { + // Show the ones actually interacted with: + { let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); let InteractionSnapshot { clicked, @@ -1985,19 +1958,43 @@ impl Context { if false { for widget in contains_pointer.values() { - paint(widget, "contains_pointer", Color32::BLUE); + paint_widget(widget, "contains_pointer", Color32::BLUE); } } if true { for widget in hovered.values() { - paint(widget, "hovered", Color32::WHITE); + paint_widget(widget, "hovered", Color32::WHITE); } } for widget in &clicked { - paint(widget, "clicked", Color32::RED); + paint_widget(widget, "clicked", Color32::RED); } for widget in &dragged { - paint(widget, "dragged", Color32::GREEN); + paint_widget(widget, "dragged", Color32::GREEN); + } + } + } + + #[cfg(debug_assertions)] + { + if self.style().debug.show_widget_hits { + let hits = self.write(|ctx| ctx.viewport().hits.clone()); + let WidgetHits { + contains_pointer, + click, + drag, + } = hits; + + if false { + for widget in &contains_pointer { + paint_widget(widget, "contains_pointer", Color32::BLUE); + } + } + for widget in &click { + paint_widget(widget, "click", Color32::RED); + } + for widget in &drag { + paint_widget(widget, "drag", Color32::GREEN); } } } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 2a57db491bbb..335c32402bbd 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1049,9 +1049,6 @@ pub struct DebugOptions { /// Show interesting widgets under the mouse cursor. pub show_widget_hits: bool, - - /// Show which widgets are being interacted with. - pub show_interaction_widgets: bool, } #[cfg(debug_assertions)] @@ -1067,7 +1064,6 @@ impl Default for DebugOptions { show_resize: false, show_interactive_widgets: false, show_widget_hits: false, - show_interaction_widgets: false, } } } @@ -1885,7 +1881,6 @@ impl DebugOptions { show_resize, show_interactive_widgets, show_widget_hits, - show_interaction_widgets, } = self; { @@ -1915,8 +1910,6 @@ impl DebugOptions { ui.checkbox(show_widget_hits, "Show widgets under mouse pointer"); - ui.checkbox(show_interaction_widgets, "Show interaction widgets"); - ui.vertical_centered(|ui| reset_button(ui, self)); } } From c5b1e0e619cb70c5eff741cf35d8a445ddac0024 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 17:11:29 +0100 Subject: [PATCH 14/34] Better hit test logic --- crates/egui/src/hit_test.rs | 273 +++++++++++++++++++++++++++------ crates/egui/src/interaction.rs | 1 - 2 files changed, 229 insertions(+), 45 deletions(-) diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 3afeee163ac9..50dd0f932d1f 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -17,11 +17,6 @@ pub struct WidgetHits { /// Some of these may be widgets in a layer below the top-most layer. pub contains_pointer: Vec, - /// The topmost widget under the pointer, interactive or not. - /// - /// Used for nothing right now. - pub top: Option, - /// If the user would start a clicking now, this is what would be clicked. /// /// This is the top one under the pointer, or closest one of the top-most. @@ -31,11 +26,6 @@ pub struct WidgetHits { /// /// This is the top one under the pointer, or closest one of the top-most. pub drag: Option, - - /// The closest interactive widget under the pointer. - /// - /// This is either the same as [`Self::click`] or [`Self::drag`], or both. - pub closest_interactive: Option, } /// Find the top or closest widgets to the given position, @@ -51,7 +41,7 @@ pub fn hit_test( let search_radius_sq = search_radius * search_radius; // First pass: find the few widgets close to the given position, sorted back-to-front. - let mut close: Vec = layer_order + let close: Vec = layer_order .iter() .filter(|layer| layer.order.allow_interaction()) .filter_map(|layer_id| widgets.by_layer.get(layer_id)) @@ -60,6 +50,21 @@ pub fn hit_test( .copied() .collect(); + let hits = hit_test_on_close(close, pos); + + if let Some(drag) = hits.drag { + debug_assert!(drag.sense.drag); + } + if let Some(click) = hits.click { + debug_assert!(click.sense.click); + } + + hits +} + +fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { + #![allow(clippy::collapsible_else_if)] + // Only those widgets directly under the `pos`. let mut hits: Vec = close .iter() @@ -79,47 +84,130 @@ pub fn hit_test( let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); - let closest = find_closest(close.iter().copied(), pos); - let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); - let closest_drag = find_closest(close.iter().copied().filter(|w| w.sense.drag), pos); + match (hit_click, hit_drag) { + (None, None) => { + // No direct hit on anything. Find the closest interactive widget. - let top = top_hit.or(closest); - let mut click = hit_click.or(closest_click); - let mut drag = hit_drag.or(closest_drag); + let closest = find_closest( + close + .iter() + .copied() + .filter(|w| w.sense.click || w.sense.drag), + pos, + ); - if let (Some(click), Some(drag)) = (&mut click, &mut drag) { - // If one of the widgets is interested in both click and drags, let it win. - // Otherwise we end up in weird situations where both widgets respond to hover, - // but one of the widgets only responds to _one_ of the events. + if let Some(closest) = closest { + WidgetHits { + contains_pointer: hits, + click: closest.sense.click.then_some(closest), + drag: closest.sense.drag.then_some(closest), + } + } else { + WidgetHits { + contains_pointer: hits, + click: None, + drag: None, + } + } + } + + (None, Some(hit_drag)) => { + // We have a perfect hit on a drag, but not on click. + + // We have a direct hit on something that implements drag. + // This could be a big background thing, like a `ScrollArea` background, + // or a moveable window. + // It could also be something small, like a slider, or panel resize handle. - if click.sense.click && click.sense.drag { - *drag = *click; - } else if drag.sense.click && drag.sense.drag { - *click = *drag; + let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); + if let Some(closest_click) = closest_click { + if closest_click.sense.drag { + // We have something close that sense both clicks and drag. + // Should we use it over the direct drag-hit? + if hit_drag.rect.contains_rect(closest_click.interact_rect) { + // This is a smaller thing on a big background - help the user hit it, + // and ignore the big drag background. + WidgetHits { + contains_pointer: hits, + click: Some(closest_click), + drag: Some(closest_click), + } + } else { + // The drag wiudth is separate from the click wiudth, + // so return only the drag widget + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } else { + // These is a close pure-click widget. + WidgetHits { + contains_pointer: hits, + click: Some(closest_click), + drag: Some(hit_drag), + } + } + } else { + // No close drags + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } } - } + (Some(hit_click), None) => { + // We have a perfect hit on a click-widget, but not on a drag-widget. - let closest_interactive = match (click, drag) { - (Some(click), Some(drag)) => { - if click.interact_rect.distance_sq_to_pos(pos) - < drag.interact_rect.distance_sq_to_pos(pos) - { - Some(click) + WidgetHits { + contains_pointer: hits, + click: Some(hit_click), + drag: None, // TODO: we should maybe look for close drag widgets? + } + } + + (Some(hit_click), Some(hit_drag)) => { + // We have a perfect hit on both click and drag. Which is the topmost? + let click_idx = hits.iter().position(|w| *w == hit_click).unwrap(); + let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap(); + + let click_is_on_top_of_drag = drag_idx < click_idx; + if click_is_on_top_of_drag { + if hit_click.sense.drag { + // The top thing senses both clicks and drags. + WidgetHits { + contains_pointer: hits, + click: Some(hit_click), + drag: Some(hit_click), + } + } else { + // They are interested in different things. + WidgetHits { + contains_pointer: hits, + click: Some(hit_click), + drag: Some(hit_drag), + } + } } else { - Some(drag) + if hit_drag.sense.click { + // The top thing senses both clicks and drags. + WidgetHits { + contains_pointer: hits, + click: Some(hit_drag), + drag: Some(hit_drag), + } + } else { + // The top things senses only drags + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } } } - (Some(click), None) => Some(click), - (None, Some(drag)) => Some(drag), - (None, None) => None, - }; - - WidgetHits { - contains_pointer: hits, - top, - click, - drag, - closest_interactive, } } @@ -138,3 +226,100 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< closest } + +#[cfg(test)] +mod tests { + use super::*; + + fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect { + WidgetRect { + id, + layer_id: LayerId::background(), + rect, + interact_rect: rect, + sense, + enabled: true, + } + } + + #[test] + fn buttons_on_window() { + let widgets = vec![ + wr( + Id::new("bg-area"), + Sense::drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 100.0)), + ), + wr( + Id::new("click"), + Sense::click(), + Rect::from_min_size(pos2(10.0, 10.0), vec2(10.0, 10.0)), + ), + wr( + Id::new("click-and-drag"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(100.0, 10.0), vec2(10.0, 10.0)), + ), + ]; + + // Perfect hit: + let hits = hit_test_on_close(widgets.clone(), pos2(15.0, 15.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-area")); + + // Close hit: + let hits = hit_test_on_close(widgets.clone(), pos2(5.0, 5.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-area")); + + // Perfect hit: + let hits = hit_test_on_close(widgets.clone(), pos2(105.0, 15.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); + assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); + + // Close hit - should still ignore the drag-background so as not to confuse the userr: + let hits = hit_test_on_close(widgets.clone(), pos2(105.0, 5.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); + assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); + } + + #[test] + fn thin_resize_handle_next_to_label() { + let widgets = vec![ + wr( + Id::new("bg-area"), + Sense::drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 100.0)), + ), + wr( + Id::new("bg-left-label"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(50.0, 100.0)), + ), + wr( + Id::new("thin-drag-handle"), + Sense::drag(), + Rect::from_min_size(pos2(40.0, 0.0), vec2(20.0, 100.0)), + ), + ]; + + for (i, w) in widgets.iter().enumerate() { + eprintln!("Widget {i}: {:?}", w.id); + } + + // In the middle of the bg-left-label: + let hits = hit_test_on_close(widgets.clone(), pos2(25.0, 50.0)); + assert_eq!(hits.click.unwrap().id, Id::new("bg-left-label")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-left-label")); + + // Only on the thin-drag-handle: + let hits = hit_test_on_close(widgets.clone(), pos2(55.0, 50.0)); + assert_eq!(hits.click, None); + assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + + // On both the click-and-drag and thin handle, but the thin handle is on top and should win: + let hits = hit_test_on_close(widgets.clone(), pos2(45.0, 50.0)); + assert_eq!(hits.click, None); + assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + } +} diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 271468e32ea1..10b08015dc65 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -115,7 +115,6 @@ pub(crate) fn interact( let contains_pointer: IdMap = hits .contains_pointer .iter() - .chain(&hits.top) .chain(&hits.click) .chain(&hits.drag) .map(|w| (w.id, *w)) From 3508d4b3067a0dca48238710d9f56e375863bc71 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 17:23:05 +0100 Subject: [PATCH 15/34] Implement panel resizing --- crates/egui/src/containers/panel.rs | 119 +++++++++++++--------------- crates/egui/src/style.rs | 2 +- 2 files changed, 58 insertions(+), 63 deletions(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 2ae769e7dc0a..11e69b8289cb 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -238,40 +238,22 @@ impl SidePanel { ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel"); } + let resize_id = id.with("__resize"); let mut resize_hover = false; let mut is_resizing = false; if resizable { - let resize_id = id.with("__resize"); - if let Some(pointer) = ui.ctx().pointer_latest_pos() { - let we_are_on_top = ui - .ctx() - .layer_id_at(pointer) - .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); - - let resize_x = side.opposite().side_x(panel_rect); - let mouse_over_resize_line = we_are_on_top - && panel_rect.y_range().contains(pointer.y) - && (resize_x - pointer.x).abs() - <= ui.style().interaction.resize_grab_radius_side; - - if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) - && mouse_over_resize_line - { - ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); - } - is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); - if is_resizing { - let width = (pointer.x - side.side_x(panel_rect)).abs(); - let width = clamp_to_range(width, width_range).at_most(available_rect.width()); - side.set_rect_width(&mut panel_rect, width); - } + // First we read the resize interaction results, to avoid frame latency in the resize: + if let Some(resize_response) = ui.ctx().read_response(resize_id) { + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); - let dragging_something_else = - ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); - resize_hover = mouse_over_resize_line && !dragging_something_else; - - if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + if is_resizing { + if let Some(pointer) = resize_response.interact_pointer_pos() { + let width = (pointer.x - side.side_x(panel_rect)).abs(); + let width = + clamp_to_range(width, width_range).at_most(available_rect.width()); + side.set_rect_width(&mut panel_rect, width); + } } } } @@ -301,6 +283,22 @@ impl SidePanel { } ui.expand_to_include_rect(rect); + if resizable { + // Now we do the actual resize interaction, on top of all the contents. + // Otherwise its input could be eaten by the contents, e.g. a + // `ScrollArea` on either side of the panel boundary. + let resize_x = side.opposite().side_x(panel_rect); + let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range()) + .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0)); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); + } + + if resize_hover || is_resizing { + ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + } + PanelState { rect }.store(ui.ctx(), id); { @@ -698,42 +696,22 @@ impl TopBottomPanel { .check_for_id_clash(id, panel_rect, "TopBottomPanel"); } + let resize_id = id.with("__resize"); let mut resize_hover = false; let mut is_resizing = false; if resizable { - let resize_id = id.with("__resize"); - let latest_pos = ui.input(|i| i.pointer.latest_pos()); - if let Some(pointer) = latest_pos { - let we_are_on_top = ui - .ctx() - .layer_id_at(pointer) - .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); - - let resize_y = side.opposite().side_y(panel_rect); - let mouse_over_resize_line = we_are_on_top - && panel_rect.x_range().contains(pointer.x) - && (resize_y - pointer.y).abs() - <= ui.style().interaction.resize_grab_radius_side; - - if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) - && mouse_over_resize_line - { - ui.memory_mut(|mem| mem.set_dragged_id(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 = - clamp_to_range(height, height_range).at_most(available_rect.height()); - side.set_rect_height(&mut panel_rect, height); - } + // First we read the resize interaction results, to avoid frame latency in the resize: + if let Some(resize_response) = ui.ctx().read_response(resize_id) { + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); - let dragging_something_else = - ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); - resize_hover = mouse_over_resize_line && !dragging_something_else; - - if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + if is_resizing { + if let Some(pointer) = resize_response.interact_pointer_pos() { + let height = (pointer.y - side.side_y(panel_rect)).abs(); + let height = + clamp_to_range(height, height_range).at_most(available_rect.height()); + side.set_rect_height(&mut panel_rect, height); + } } } } @@ -763,6 +741,23 @@ impl TopBottomPanel { } ui.expand_to_include_rect(rect); + if resizable { + // Now we do the actual resize interaction, on top of all the contents. + // Otherwise its input could be eaten by the contents, e.g. a + // `ScrollArea` on either side of the panel boundary. + + let resize_y = side.opposite().side_y(panel_rect); + let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y) + .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side)); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); + } + + if resize_hover || is_resizing { + ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + } + PanelState { rect }.store(ui.ctx(), id); { diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 335c32402bbd..e42d1996ca15 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1133,7 +1133,7 @@ impl Default for Interaction { Self { resize_grab_radius_side: 5.0, resize_grab_radius_corner: 10.0, - interact_radius: 8.0, + interact_radius: 5.0, show_tooltips_only_when_still: true, tooltip_delay: 0.3, selectable_labels: true, From 4cec71f38c4642620e3968b45a16aaedb5960b01 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 17:36:54 +0100 Subject: [PATCH 16/34] Even better hit test --- crates/egui/src/hit_test.rs | 44 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 50dd0f932d1f..849db9f5a3aa 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -143,10 +143,24 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { } } else { // These is a close pure-click widget. - WidgetHits { - contains_pointer: hits, - click: Some(closest_click), - drag: Some(hit_drag), + // However, we should be careful to only return two different widgets + // when it is absolutely not going to confuse the user. + if hit_drag.rect.contains_rect(closest_click.interact_rect) { + // The drag widget is a big background thing (scroll area), + // so returning a separate click widget should not be confusing + WidgetHits { + contains_pointer: hits, + click: Some(closest_click), + drag: Some(hit_drag), + } + } else { + // The two widgets are just two normal small widgets close to each other. + // Highlighting both would be very confusing. + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } } } } else { @@ -294,12 +308,17 @@ mod tests { wr( Id::new("bg-left-label"), Sense::click_and_drag(), - Rect::from_min_size(pos2(0.0, 0.0), vec2(50.0, 100.0)), + Rect::from_min_size(pos2(0.0, 0.0), vec2(40.0, 100.0)), ), wr( Id::new("thin-drag-handle"), Sense::drag(), - Rect::from_min_size(pos2(40.0, 0.0), vec2(20.0, 100.0)), + Rect::from_min_size(pos2(30.0, 0.0), vec2(70.0, 100.0)), + ), + wr( + Id::new("fg-right-label"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(60.0, 0.0), vec2(50.0, 100.0)), ), ]; @@ -312,14 +331,19 @@ mod tests { assert_eq!(hits.click.unwrap().id, Id::new("bg-left-label")); assert_eq!(hits.drag.unwrap().id, Id::new("bg-left-label")); - // Only on the thin-drag-handle: - let hits = hit_test_on_close(widgets.clone(), pos2(55.0, 50.0)); + // On both the left click-and-drag and thin handle, but the thin handle is on top and should win: + let hits = hit_test_on_close(widgets.clone(), pos2(35.0, 50.0)); assert_eq!(hits.click, None); assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); - // On both the click-and-drag and thin handle, but the thin handle is on top and should win: - let hits = hit_test_on_close(widgets.clone(), pos2(45.0, 50.0)); + // Only on the thin-drag-handle: + let hits = hit_test_on_close(widgets.clone(), pos2(50.0, 50.0)); assert_eq!(hits.click, None); assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + + // On both the thin handle and right label. The label is on top and should win + let hits = hit_test_on_close(widgets.clone(), pos2(65.0, 50.0)); + assert_eq!(hits.click.unwrap().id, Id::new("fg-right-label")); + assert_eq!(hits.drag.unwrap().id, Id::new("fg-right-label")); } } From db22bc2544ebb9f6db115961ed1d9f10fe125351 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 17:54:24 +0100 Subject: [PATCH 17/34] Fix Resize widget --- crates/egui/src/containers/resize.rs | 42 +++++++++++++++++----------- crates/egui/src/hit_test.rs | 18 +++++++----- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 9670ecb415e7..1acc86bff747 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -188,8 +188,8 @@ impl Resize { struct Prepared { id: Id, + corner_id: Option, state: State, - corner_response: Option, content_ui: Ui, } @@ -226,22 +226,16 @@ impl Resize { let mut user_requested_size = state.requested_size.take(); - let corner_response = if self.resizable { - // Resize-corner: - let corner_size = Vec2::splat(ui.visuals().resize_corner_size); - let corner_rect = - Rect::from_min_size(position + state.desired_size - corner_size, corner_size); - let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag()); + let corner_id = self.resizable.then(|| id.with("__resize_corner")); - if let Some(pointer_pos) = corner_response.interact_pointer_pos() { - user_requested_size = - Some(pointer_pos - position + 0.5 * corner_response.rect.size()); + if let Some(corner_id) = corner_id { + if let Some(corner_response) = ui.ctx().read_response(corner_id) { + if let Some(pointer_pos) = corner_response.interact_pointer_pos() { + user_requested_size = + Some(pointer_pos - position + 0.5 * corner_response.rect.size()); + } } - - Some(corner_response) - } else { - None - }; + } if let Some(user_requested_size) = user_requested_size { state.desired_size = user_requested_size; @@ -279,8 +273,8 @@ impl Resize { Prepared { id, + corner_id, state, - corner_response, content_ui, } } @@ -295,8 +289,8 @@ impl Resize { fn end(self, ui: &mut Ui, prepared: Prepared) { let Prepared { id, + corner_id, mut state, - corner_response, content_ui, } = prepared; @@ -320,6 +314,20 @@ impl Resize { // ------------------------------ + let corner_response = if let Some(corner_id) = corner_id { + // We do the corner interaction last to place it on top of the content: + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); + let corner_rect = Rect::from_min_size( + content_ui.min_rect().left_top() + size - corner_size, + corner_size, + ); + Some(ui.interact(corner_rect, corner_id, Sense::drag())) + } else { + None + }; + + // ------------------------------ + if self.with_stroke && corner_response.is_some() { let rect = Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size); let rect = rect.expand(2.0); // breathing room for content diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 849db9f5a3aa..13ec79a4dc7b 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -72,13 +72,15 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { .copied() .collect(); - let top_hit = hits.last().copied(); - let top_layer = top_hit.map(|w| w.layer_id); - - if let Some(top_layer) = top_layer { - // Ignore all layers not in the same layer as the top hit. - close.retain(|w| w.layer_id == top_layer); - hits.retain(|w| w.layer_id == top_layer); + { + let top_hit = hits.last().copied(); + let top_layer = top_hit.map(|w| w.layer_id); + + if let Some(top_layer) = top_layer { + // Ignore all layers not in the same layer as the top hit. + close.retain(|w| w.layer_id == top_layer); + hits.retain(|w| w.layer_id == top_layer); + } } let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); @@ -103,6 +105,7 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { drag: closest.sense.drag.then_some(closest), } } else { + // Found nothing WidgetHits { contains_pointer: hits, click: None, @@ -172,6 +175,7 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { } } } + (Some(hit_click), None) => { // We have a perfect hit on a click-widget, but not on a drag-widget. From 23451062a8f4271a248ecac7c019705f526d99de Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 20:15:37 +0100 Subject: [PATCH 18/34] Use WidgetRect more --- crates/egui/src/containers/area.rs | 14 ++++---- crates/egui/src/containers/window.rs | 12 ++++++- crates/egui/src/context.rs | 48 ++++++++-------------------- crates/egui/src/response.rs | 16 ++++++---- crates/egui/src/ui.rs | 30 +++++++++-------- 5 files changed, 58 insertions(+), 62 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 99c96f396317..6a025e298a5c 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -321,12 +321,14 @@ impl Area { }; let move_response = ctx.create_widget( - layer_id, - interact_id, - state.rect(), - state.rect(), - sense, - enabled, + WidgetRect { + id: interact_id, + layer_id, + rect: state.rect(), + interact_rect: state.rect(), + sense, + enabled, + }, true, ); diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index e8842aef22b5..bba8a2645757 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -736,7 +736,17 @@ fn resize_interaction( } let is_dragging = |rect, id| { - let response = ctx.create_widget(layer_id, id, rect, rect, Sense::drag(), true, true); + let response = ctx.create_widget( + WidgetRect { + layer_id, + id, + rect, + interact_rect: rect, + sense: Sense::drag(), + enabled: true, + }, + true, + ); SideResponse { hover: response.hovered(), drag: response.dragged(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1dcbbb0eeb96..b6f4373c5c09 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1110,31 +1110,13 @@ impl Context { /// /// You should use [`Ui::interact`] instead. #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget( - &self, - layer_id: LayerId, - id: Id, - rect: Rect, - interact_rect: Rect, - mut sense: Sense, - enabled: bool, - may_contain_pointer: bool, - ) -> Response { - if !enabled { - sense.click = false; - sense.drag = false; + pub(crate) fn create_widget(&self, mut w: WidgetRect, may_contain_pointer: bool) -> Response { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; } - let widget_rect = WidgetRect { - id, - layer_id, - rect, - interact_rect, - sense, - enabled, - }; - - if widget_rect.interact_rect.is_positive() { + if w.interact_rect.is_positive() { // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1142,38 +1124,36 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport - .widgets_this_frame - .insert(widget_rect.layer_id, widget_rect); + viewport.widgets_this_frame.insert(w.layer_id, w); // Based on output from previous frame and input in this frame viewport .interact_widgets .contains_pointer - .contains_key(&widget_rect.id) + .contains_key(&w.id) }); } else { // Don't remember invisible widgets } - if !enabled || !sense.focusable || !layer_id.allow_interaction() { + if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { // Not interested or allowed input: - self.memory_mut(|mem| mem.surrender_focus(id)); + self.memory_mut(|mem| mem.surrender_focus(w.id)); } - if sense.interactive() || sense.focusable { - self.check_for_id_clash(id, rect, "widget"); + if w.sense.interactive() || w.sense.focusable { + self.check_for_id_clash(w.id, w.rect, "widget"); } - let mut res = self.get_response(widget_rect); + let mut res = self.get_response(w); res.contains_pointer &= may_contain_pointer; #[cfg(feature = "accesskit")] - if sense.focusable { + if w.sense.focusable { // 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| res.fill_accesskit_node_common(builder)); + self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder)); } res diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 77b6489b7a99..06b0ade6ce80 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -2,7 +2,7 @@ use std::{any::Any, sync::Arc}; use crate::{ emath::{Align, Pos2, Rect, Vec2}, - menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText, + menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetRect, WidgetText, NUM_POINTER_BUTTONS, }; @@ -618,12 +618,14 @@ impl Response { } self.ctx.create_widget( - self.layer_id, - self.id, - self.rect, - self.interact_rect, - sense, - self.enabled, + WidgetRect { + layer_id: self.layer_id, + id: self.id, + rect: self.rect, + interact_rect: self.interact_rect, + sense, + enabled: self.enabled, + }, self.contains_pointer, ) } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 195de49ea531..f232681639b0 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -646,14 +646,15 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { - let interact_rect = self.clip_rect().intersect(rect); self.ctx().create_widget( - self.layer_id(), - id, - rect, - interact_rect, - sense, - self.enabled, + WidgetRect { + id, + layer_id: self.layer_id(), + rect, + interact_rect: self.clip_rect().intersect(rect), + sense, + enabled: self.enabled, + }, true, ) } @@ -671,14 +672,15 @@ impl Ui { id: Id, sense: Sense, ) -> Response { - let interact_rect = rect.intersect(self.clip_rect()); self.ctx().create_widget( - self.layer_id(), - id, - rect, - interact_rect, - sense, - self.enabled, + WidgetRect { + id, + layer_id: self.layer_id(), + rect, + interact_rect: self.clip_rect().intersect(rect), + sense, + enabled: self.enabled, + }, contains_pointer, ) } From ae84d6c0bbceda479328381bfe890889a8de5541 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 20:39:14 +0100 Subject: [PATCH 19/34] Improve hit test --- crates/egui/src/hit_test.rs | 38 ++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 13ec79a4dc7b..b8975b923773 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -127,7 +127,10 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { if closest_click.sense.drag { // We have something close that sense both clicks and drag. // Should we use it over the direct drag-hit? - if hit_drag.rect.contains_rect(closest_click.interact_rect) { + if hit_drag + .interact_rect + .contains_rect(closest_click.interact_rect) + { // This is a smaller thing on a big background - help the user hit it, // and ignore the big drag background. WidgetHits { @@ -148,7 +151,10 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { // These is a close pure-click widget. // However, we should be careful to only return two different widgets // when it is absolutely not going to confuse the user. - if hit_drag.rect.contains_rect(closest_click.interact_rect) { + if hit_drag + .interact_rect + .contains_rect(closest_click.interact_rect) + { // The drag widget is a big background thing (scroll area), // so returning a separate click widget should not be confusing WidgetHits { @@ -167,7 +173,33 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { } } } else { - // No close drags + // No close clicks. + // Maybe there is a close drag widget, that is a smaller + // widget floating on top of a big background? + // If so, it would be nice to help the user click that. + let closest_drag = find_closest( + close + .iter() + .copied() + .filter(|w| w.sense.drag && w.id != hit_drag.id), + pos, + ); + + if let Some(closest_drag) = closest_drag { + if hit_drag + .interact_rect + .contains_rect(closest_drag.interact_rect) + { + // `hit_drag` is a big background thing and `closest_drag` is something small on top of it. + // Be helpful and return the small things: + return WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(closest_drag), + }; + } + } + WidgetHits { contains_pointer: hits, click: None, From e2f99ad739c3f02e74eaa10cee59ffd35bdb4bce Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 20:39:25 +0100 Subject: [PATCH 20/34] Add debug ui for interaction --- crates/egui/src/context.rs | 7 ++++ crates/egui/src/interaction.rs | 59 ++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index b6f4373c5c09..fe26b1a93993 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2598,6 +2598,13 @@ impl Context { crate::text_selection::LabelSelectionState::load(ui.ctx()) )); }); + + CollapsingHeader::new("Interaction") + .default_open(false) + .show(ui, |ui| { + let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); + interact_widgets.ui(ui); + }); } /// Show stats about the allocated textures. diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 10b08015dc65..04987d07bf89 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -31,8 +31,53 @@ pub struct InteractionSnapshot { /// The widget will not be found in [`Self::dragged`] this frame. pub drag_ended: Option, - pub contains_pointer: IdMap, pub hovered: IdMap, + pub contains_pointer: IdMap, +} + +impl InteractionSnapshot { + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + clicked, + drag_started, + dragged, + drag_ended, + hovered, + contains_pointer, + } = self; + + fn wr_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator) { + for widget in widgets { + ui.label(widget.id.short_debug_format()); + } + } + + crate::Grid::new("interaction").show(ui, |ui| { + ui.label("clicked"); + wr_ui(ui, clicked); + ui.end_row(); + + ui.label("drag_started"); + wr_ui(ui, drag_started); + ui.end_row(); + + ui.label("dragged"); + wr_ui(ui, dragged); + ui.end_row(); + + ui.label("drag_ended"); + wr_ui(ui, drag_ended); + ui.end_row(); + + ui.label("hovered"); + wr_ui(ui, hovered.values()); + ui.end_row(); + + ui.label("contains_pointer"); + wr_ui(ui, contains_pointer.values()); + ui.end_row(); + }); + } } pub(crate) fn interact( @@ -53,7 +98,9 @@ pub(crate) fn interact( if let Some(id) = interaction.drag_id { if !widgets.by_id.contains_key(&id) { // The widget we were interested in dragging is gone. - interaction.drag_id = None; + // This is fine! This could be drag-and-drop, + // and the widget being dragged is now "in the air" and thus + // not registered in the new frame. } } @@ -112,6 +159,14 @@ pub(crate) fn interact( let drag_ended = drag_changed.then_some(prev_snapshot.dragged).flatten(); let drag_started = drag_changed.then_some(dragged).flatten(); + // if let Some(drag_started) = drag_started { + // eprintln!( + // "Started dragging {} {:?}", + // drag_started.id.short_debug_format(), + // drag_started.rect + // ); + // } + let contains_pointer: IdMap = hits .contains_pointer .iter() From 7b31547120d4bcf9f1487a4d28eb8987add9f46e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 20:39:37 +0100 Subject: [PATCH 21/34] Fix drag-and-drop --- crates/egui/src/ui.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index f232681639b0..a6c6c5d2c17b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2166,6 +2166,8 @@ impl Ui { let is_being_dragged = self.memory(|mem| mem.is_being_dragged(id)); if is_being_dragged { + crate::DragAndDrop::set_payload(self.ctx(), payload); + // Paint the body to a new layer: let layer_id = LayerId::new(Order::Tooltip, id); let InnerResponse { inner, response } = self.with_layer_id(layer_id, add_contents); @@ -2187,9 +2189,9 @@ impl Ui { let InnerResponse { inner, response } = self.scope(add_contents); // Check for drags: - let dnd_response = self.interact(response.rect, id, Sense::drag()); - - dnd_response.dnd_set_drag_payload(payload); + let dnd_response = self + .interact(response.rect, id, Sense::drag()) + .on_hover_cursor(CursorIcon::Grab); InnerResponse::new(inner, dnd_response | response) } From 9eb811efc153d5438aedc2f9deac54ddae45e58f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 15 Feb 2024 21:14:29 +0100 Subject: [PATCH 22/34] Some compilation fixes and improvements --- crates/egui/src/context.rs | 86 +++++++++++++++++-------------------- crates/egui/src/hit_test.rs | 2 - 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index fe26b1a93993..1291f2a31046 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1126,11 +1126,9 @@ impl Context { // but also to know when we have reached the widget we are checking for cover. viewport.widgets_this_frame.insert(w.layer_id, w); - // Based on output from previous frame and input in this frame - viewport - .interact_widgets - .contains_pointer - .contains_key(&w.id) + if w.sense.focusable { + ctx.memory.interested_in_focus(w.id); + } }); } else { // Don't remember invisible widgets @@ -1159,7 +1157,9 @@ impl Context { res } - /// Read the response of some widget, either before or after it was created/interacted. + /// Read the response of some widget, which may be called _before_ creating the widget (!). + /// + /// This is because widget interaction happens at the start of the frame, using the previous frame's widgets. /// /// If the widget was not visible the previous frame (or this frame), this will return `None`. pub fn read_response(&self, id: Id) -> Option { @@ -1220,10 +1220,6 @@ impl Context { let input = &viewport.input; let memory = &mut ctx.memory; - if sense.focusable { - memory.interested_in_focus(id); - } - if sense.click && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) @@ -1251,23 +1247,16 @@ impl Context { let clicked = Some(id) == viewport.interact_widgets.clicked.map(|w| w.id); - if sense.click && clicked { - // We were clicked - what kind of click? - for pointer_event in &input.pointer.pointer_events { - if let PointerEvent::Released { - click: Some(click), - button, - } = pointer_event - { - res.clicked[*button as usize] = true; - res.double_clicked[*button as usize] = click.is_double(); - res.triple_clicked[*button as usize] = click.is_triple(); + for pointer_event in &input.pointer.pointer_events { + if let PointerEvent::Released { click, button } = pointer_event { + if sense.click && clicked { + if let Some(click) = click { + res.clicked[*button as usize] = true; + res.double_clicked[*button as usize] = click.is_double(); + res.triple_clicked[*button as usize] = click.is_triple(); + } } - } - } - for pointer_event in &input.pointer.pointer_events { - if let PointerEvent::Released { .. } = pointer_event { res.is_pointer_button_down_on = false; res.dragged = false; } @@ -1897,6 +1886,14 @@ impl Context { self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); + #[cfg(debug_assertions)] + self.debug_painting(); + + self.write(|ctx| ctx.end_frame()) + } + + #[cfg(debug_assertions)] + fn debug_painting(&self) { let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); painter.debug_rect(widget.interact_rect, color, text); @@ -1955,31 +1952,26 @@ impl Context { } } - #[cfg(debug_assertions)] - { - if self.style().debug.show_widget_hits { - let hits = self.write(|ctx| ctx.viewport().hits.clone()); - let WidgetHits { - contains_pointer, - click, - drag, - } = hits; - - if false { - for widget in &contains_pointer { - paint_widget(widget, "contains_pointer", Color32::BLUE); - } - } - for widget in &click { - paint_widget(widget, "click", Color32::RED); - } - for widget in &drag { - paint_widget(widget, "drag", Color32::GREEN); + if self.style().debug.show_widget_hits { + let hits = self.write(|ctx| ctx.viewport().hits.clone()); + let WidgetHits { + contains_pointer, + click, + drag, + } = hits; + + if false { + for widget in &contains_pointer { + paint_widget(widget, "contains_pointer", Color32::BLUE); } } + for widget in &click { + paint_widget(widget, "click", Color32::RED); + } + for widget in &drag { + paint_widget(widget, "drag", Color32::GREEN); + } } - - self.write(|ctx| ctx.end_frame()) } } diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index b8975b923773..a798d634f6a7 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -6,8 +6,6 @@ use crate::*; /// /// Note that this doesn't care if the mouse button is pressed or not, /// or if we're currently already dragging something. -/// -/// For that you need the [`crate::InteractionState`]. #[derive(Clone, Debug, Default)] pub struct WidgetHits { /// All widgets that contains the pointer, back-to-front. From 6ca76919d27118e86e70e633374abed2bcfcee53 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 16 Feb 2024 09:11:55 +0100 Subject: [PATCH 23/34] Better docs for `Response` --- crates/egui/src/response.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 06b0ade6ce80..6b1535206b5f 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -595,25 +595,34 @@ impl Response { self } - /// Check for more interactions (e.g. sense clicks on a [`Response`] returned from a label). + /// Sense more interactions (e.g. sense clicks on a [`Response`] returned from a label). + /// + /// The interaction will occur on the same plane as the original widget, + /// i.e. if the response was from a widget behind button, the interaction will also be behind that button. + /// egui gives priority to the _last_ added widget (the one on top gets clicked first). /// /// Note that this call will not add any hover-effects to the widget, so when possible /// it is better to give the widget a [`Sense`] instead, e.g. using [`crate::Label::sense`]. /// + /// Using this method on a `Response` that is the result of calling `union` on multiple `Response`s + /// is undefined behavior. + /// /// ``` /// # egui::__run_test_ui(|ui| { - /// let response = ui.label("hello"); - /// assert!(!response.clicked()); // labels don't sense clicks by default - /// let response = response.interact(egui::Sense::click()); - /// if response.clicked() { /* … */ } + /// let horiz_response = ui.horizontal(|ui| { + /// ui.label("hello"); + /// }).response; + /// assert!(!horiz_response.clicked()); // ui's don't sense clicks by default + /// let horiz_response = horiz_response.interact(egui::Sense::click()); + /// if horiz_response.clicked() { + /// // The background behind the label was clicked + /// } /// # }); /// ``` #[must_use] pub fn interact(&self, sense: Sense) -> Self { - // Test if we must sense something new compared to what we have already sensed. If not, then - // we can return early. This may avoid unnecessarily "masking" some widgets with unneeded - // interactions. if (self.sense | sense) == self.sense { + // Early-out: we already sense everything we need to sense. return self.clone(); } @@ -807,6 +816,8 @@ impl Response { /// For instance `a.union(b).hovered` means "was either a or b hovered?". /// /// The resulting [`Self::id`] will come from the first (`self`) argument. + /// + /// You may not call [`Self::interact`] on the resulting `Response`. pub fn union(&self, other: Self) -> Self { assert!(self.ctx == other.ctx); crate::egui_assert!( @@ -864,6 +875,8 @@ impl Response { } } +/// See [`Response::union`]. +/// /// To summarize the response from many widgets you can use this pattern: /// /// ``` @@ -882,6 +895,8 @@ impl std::ops::BitOr for Response { } } +/// See [`Response::union`]. +/// /// To summarize the response from many widgets you can use this pattern: /// /// ``` From 44544145a22971b9ae718a497aeb75c182df9076 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 16 Feb 2024 14:48:09 +0100 Subject: [PATCH 24/34] Persist dragged id even if widget is gone --- crates/egui/src/context.rs | 71 +++++++++--- crates/egui/src/interaction.rs | 112 ++++++++++--------- crates/egui/src/introspection.rs | 8 +- crates/egui/src/memory.rs | 27 +++-- crates/egui/src/ui.rs | 2 +- crates/egui/src/widgets/drag_value.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 2 +- examples/test_viewports/src/main.rs | 8 +- 8 files changed, 142 insertions(+), 90 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1291f2a31046..9d894d85af87 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1215,7 +1215,7 @@ impl Context { self.write(|ctx| { let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default(); - res.contains_pointer = viewport.interact_widgets.contains_pointer.contains_key(&id); + res.contains_pointer = viewport.interact_widgets.contains_pointer.contains(&id); let input = &viewport.input; let memory = &mut ctx.memory; @@ -1235,17 +1235,17 @@ impl Context { let interaction = memory.interaction_mut(); - res.is_pointer_button_down_on = - interaction.click_id == Some(id) || interaction.drag_id == Some(id); + res.is_pointer_button_down_on = interaction.potential_click_id == Some(id) + || interaction.potential_drag_id == Some(id); if res.enabled { - res.hovered = viewport.interact_widgets.hovered.contains_key(&id); - res.dragged = Some(id) == viewport.interact_widgets.dragged.map(|w| w.id); - res.drag_started = Some(id) == viewport.interact_widgets.drag_started.map(|w| w.id); - res.drag_released = Some(id) == viewport.interact_widgets.drag_ended.map(|w| w.id); + res.hovered = viewport.interact_widgets.hovered.contains(&id); + res.dragged = Some(id) == viewport.interact_widgets.dragged; + res.drag_started = Some(id) == viewport.interact_widgets.drag_started; + res.drag_released = Some(id) == viewport.interact_widgets.drag_ended; } - let clicked = Some(id) == viewport.interact_widgets.clicked.map(|w| w.id); + let clicked = Some(id) == viewport.interact_widgets.clicked; for pointer_event in &input.pointer.pointer_events { if let PointerEvent::Released { click, button } = pointer_event { @@ -1899,6 +1899,14 @@ impl Context { painter.debug_rect(widget.interact_rect, color, text); }; + let paint_widget_id = |id: Id, text: &str, color: Color32| { + if let Some(widget) = + self.write(|ctx| ctx.viewport().widgets_this_frame.by_id.get(&id).cloned()) + { + paint_widget(&widget, text, color); + } + }; + if self.style().debug.show_interactive_widgets { // Show all interactive widgets: let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); @@ -1934,20 +1942,20 @@ impl Context { } = interact_widgets; if false { - for widget in contains_pointer.values() { - paint_widget(widget, "contains_pointer", Color32::BLUE); + for widget in contains_pointer { + paint_widget_id(widget, "contains_pointer", Color32::BLUE); } } if true { - for widget in hovered.values() { - paint_widget(widget, "hovered", Color32::WHITE); + for widget in hovered { + paint_widget_id(widget, "hovered", Color32::WHITE); } } - for widget in &clicked { - paint_widget(widget, "clicked", Color32::RED); + for &widget in &clicked { + paint_widget_id(widget, "clicked", Color32::RED); } - for widget in &dragged { - paint_widget(widget, "dragged", Color32::GREEN); + for &widget in &dragged { + paint_widget_id(widget, "dragged", Color32::GREEN); } } } @@ -3284,6 +3292,37 @@ impl Context { } } +/// ## Interaction +impl Context { + /// Read you what widgets are currently being interacted with. + pub fn interaction_snapshot(&self, reader: impl FnOnce(&InteractionSnapshot) -> R) -> R { + self.write(|w| reader(&w.viewport().interact_widgets)) + } + + /// The widget currently being dragged, if any. + /// + /// For widgets that sense both clicks and drags, this will + /// not be set until the mouse cursor has moved a certain distance. + /// + /// NOTE: if the widget was released this frame, this will be `None`. + /// Use [`Self::drag_ended_id`] instead. + pub fn dragged_id(&self) -> Option { + self.interaction_snapshot(|i| i.dragged) + } + + /// This widget just started being dragged this frame. + /// + /// The same widget should also be found in [`Self::dragged_id`]. + pub fn drag_started_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_started) + } + + /// This widget was being dragged, but was released this frame + pub fn drag_ended_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_ended) + } +} + #[test] fn context_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 04987d07bf89..0b871fb45e99 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -2,7 +2,7 @@ use crate::*; -use self::{hit_test::WidgetHits, input_state::PointerEvent, memory::InteractionState}; +use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState}; /// Calculated at the start of each frame /// based on: @@ -12,27 +12,31 @@ use self::{hit_test::WidgetHits, input_state::PointerEvent, memory::InteractionS #[derive(Clone, Default)] pub struct InteractionSnapshot { /// The widget that got clicked this frame. - pub clicked: Option, + pub clicked: Option, /// Drag started on this widget this frame. /// /// This will also be found in `dragged` this frame. - pub drag_started: Option, + pub drag_started: Option, /// This widget is being dragged this frame. /// /// Set the same frame a drag starts, /// but unset the frame a drag ends. - pub dragged: Option, + /// + /// NOTE: this may not have a corresponding [`WidgetRect`], + /// if this for instance is a drag-and-drop widget which + /// isn't painted whilest being dragged + pub dragged: Option, /// This widget was let go this frame, /// after having been dragged. /// /// The widget will not be found in [`Self::dragged`] this frame. - pub drag_ended: Option, + pub drag_ended: Option, - pub hovered: IdMap, - pub contains_pointer: IdMap, + pub hovered: IdSet, + pub contains_pointer: IdSet, } impl InteractionSnapshot { @@ -46,35 +50,35 @@ impl InteractionSnapshot { contains_pointer, } = self; - fn wr_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator) { - for widget in widgets { - ui.label(widget.id.short_debug_format()); + fn id_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator) { + for id in widgets { + ui.label(id.short_debug_format()); } } crate::Grid::new("interaction").show(ui, |ui| { ui.label("clicked"); - wr_ui(ui, clicked); + id_ui(ui, clicked); ui.end_row(); ui.label("drag_started"); - wr_ui(ui, drag_started); + id_ui(ui, drag_started); ui.end_row(); ui.label("dragged"); - wr_ui(ui, dragged); + id_ui(ui, dragged); ui.end_row(); ui.label("drag_ended"); - wr_ui(ui, drag_ended); + id_ui(ui, drag_ended); ui.end_row(); ui.label("hovered"); - wr_ui(ui, hovered.values()); + id_ui(ui, hovered); ui.end_row(); ui.label("contains_pointer"); - wr_ui(ui, contains_pointer.values()); + id_ui(ui, contains_pointer); ui.end_row(); }); } @@ -89,13 +93,13 @@ pub(crate) fn interact( ) -> InteractionSnapshot { crate::profile_function!(); - if let Some(id) = interaction.click_id { + if let Some(id) = interaction.potential_click_id { if !widgets.by_id.contains_key(&id) { // The widget we were interested in clicking is gone. - interaction.click_id = None; + interaction.potential_click_id = None; } } - if let Some(id) = interaction.drag_id { + if let Some(id) = interaction.potential_drag_id { if !widgets.by_id.contains_key(&id) { // The widget we were interested in dragging is gone. // This is fine! This could be drag-and-drop, @@ -105,6 +109,7 @@ pub(crate) fn interact( } let mut clicked = None; + let mut dragged = prev_snapshot.dragged; // Note: in the current code a press-release in the same frame is NOT considered a drag. for pointer_event in &input.pointer.pointer_events { @@ -113,48 +118,57 @@ pub(crate) fn interact( PointerEvent::Pressed { .. } => { // Maybe new click? - if interaction.click_id.is_none() { - interaction.click_id = hits.click.map(|w| w.id); + if interaction.potential_click_id.is_none() { + interaction.potential_click_id = hits.click.map(|w| w.id); } // Maybe new drag? - if interaction.drag_id.is_none() { - interaction.drag_id = hits.drag.map(|w| w.id); + if interaction.potential_drag_id.is_none() { + interaction.potential_drag_id = hits.drag.map(|w| w.id); } } PointerEvent::Released { click, button: _ } => { if click.is_some() { - if let Some(widget) = interaction.click_id.and_then(|id| widgets.by_id.get(&id)) + if let Some(widget) = interaction + .potential_click_id + .and_then(|id| widgets.by_id.get(&id)) { - clicked = Some(*widget); + clicked = Some(widget.id); } } - interaction.drag_id = None; - interaction.click_id = None; + interaction.potential_drag_id = None; + interaction.potential_click_id = None; + dragged = None; } } } - // Check if we're dragging something: - let mut dragged = None; - if let Some(widget) = interaction.drag_id.and_then(|id| widgets.by_id.get(&id)) { - let is_dragged = if widget.sense.click && widget.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. - input.pointer.is_decidedly_dragging() - } else { - // This widget is just sensitive to drags, so we can mark it as dragged right away: - widget.sense.drag - }; - - if is_dragged { - dragged = Some(*widget); + if dragged.is_none() { + // Check if we started dragging something new: + if let Some(widget) = interaction + .potential_drag_id + .and_then(|id| widgets.by_id.get(&id)) + { + let is_dragged = if widget.sense.click && widget.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. + input.pointer.is_decidedly_dragging() + } else { + // This widget is just sensitive to drags, so we can mark it as dragged right away: + widget.sense.drag + }; + + if is_dragged { + dragged = Some(widget.id); + } } } + // ------------------------------------------------------------------------ + let drag_changed = dragged != prev_snapshot.dragged; let drag_ended = drag_changed.then_some(prev_snapshot.dragged).flatten(); let drag_started = drag_changed.then_some(dragged).flatten(); @@ -167,31 +181,27 @@ pub(crate) fn interact( // ); // } - let contains_pointer: IdMap = hits + let contains_pointer: IdSet = hits .contains_pointer .iter() .chain(&hits.click) .chain(&hits.drag) - .map(|w| (w.id, *w)) + .map(|w| w.id) .collect(); let hovered = if clicked.is_some() || dragged.is_some() { // If currently clicking or dragging, nothing else is hovered. - clicked.iter().chain(&dragged).map(|w| (w.id, *w)).collect() + clicked.iter().chain(&dragged).copied().collect() } else if hits.click.is_some() || hits.drag.is_some() { // We are hovering over an interactive widget or two. - hits.click - .iter() - .chain(&hits.drag) - .map(|w| (w.id, *w)) - .collect() + hits.click.iter().chain(&hits.drag).map(|w| w.id).collect() } else { // Whatever is topmost is what we are hovering. // TODO: consider handle hovering over multiple top-most widgets? // TODO: allow hovering close widgets? hits.contains_pointer .last() - .map(|w| (w.id, *w)) + .map(|w| w.id) .into_iter() .collect() }; diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 760575ff6141..240ff6974a58 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -192,14 +192,14 @@ impl Widget for &mut epaint::TessellationOptions { impl Widget for &memory::InteractionState { fn ui(self, ui: &mut Ui) -> Response { let memory::InteractionState { - click_id, - drag_id, + potential_click_id, + potential_drag_id, focus: _, } = self; ui.vertical(|ui| { - ui.label(format!("click_id: {click_id:?}")); - ui.label(format!("drag_id: {drag_id:?}")); + ui.label(format!("potential_click_id: {potential_click_id:?}")); + ui.label(format!("potential_drag_id: {potential_drag_id:?}")); }) .response } diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index ffb8661ff990..70c739a9f609 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -292,7 +292,7 @@ impl Options { #[derive(Clone, Debug, Default)] pub(crate) struct InteractionState { /// A widget interested in clicks that has a mouse press on it. - pub click_id: Option, + pub potential_click_id: Option, /// A widget interested in drags that has a mouse press on it. /// @@ -300,7 +300,7 @@ pub(crate) struct InteractionState { /// 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 potential_drag_id: Option, pub focus: Focus, } @@ -353,7 +353,7 @@ impl FocusWidget { impl InteractionState { /// Are we currently clicking or dragging an egui widget? pub fn is_using_pointer(&self) -> bool { - self.click_id.is_some() || self.drag_id.is_some() + self.potential_click_id.is_some() || self.potential_drag_id.is_some() } fn begin_frame( @@ -362,13 +362,13 @@ impl InteractionState { new_input: &crate::data::input::RawInput, ) { if !prev_input.pointer.could_any_button_be_click() { - self.click_id = None; + self.potential_click_id = None; } if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() { // pointer button was not down last frame - self.click_id = None; - self.drag_id = None; + self.potential_click_id = None; + self.potential_drag_id = None; } self.focus.begin_frame(new_input); @@ -730,9 +730,10 @@ impl Memory { } /// Is any widget being dragged? + #[deprecated = "Use `Context::dragged_id` instead"] #[inline(always)] pub fn is_anything_being_dragged(&self) -> bool { - self.interaction().drag_id.is_some() + self.interaction().potential_drag_id.is_some() } /// Is this specific widget being dragged? @@ -741,9 +742,10 @@ impl Memory { /// /// 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. + #[deprecated = "Use `Context::dragged_id` instead"] #[inline(always)] pub fn is_being_dragged(&self, id: Id) -> bool { - self.interaction().drag_id == Some(id) + self.interaction().potential_drag_id == Some(id) } /// Get the id of the widget being dragged, if any. @@ -752,21 +754,22 @@ impl Memory { /// 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). + #[deprecated = "Use `Context::dragged_id` instead"] #[inline(always)] pub fn dragged_id(&self) -> Option { - self.interaction().drag_id + self.interaction().potential_drag_id } /// Set which widget is being dragged. #[inline(always)] pub fn set_dragged_id(&mut self, id: Id) { - self.interaction_mut().drag_id = Some(id); + self.interaction_mut().potential_drag_id = Some(id); } /// Stop dragging any widget. #[inline(always)] pub fn stop_dragging(&mut self) { - self.interaction_mut().drag_id = None; + self.interaction_mut().potential_drag_id = None; } /// Is something else being dragged? @@ -774,7 +777,7 @@ impl Memory { /// 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; + let drag_id = self.interaction().potential_drag_id; drag_id.is_some() && drag_id != Some(not_this) } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index a6c6c5d2c17b..12824fa4dabc 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2163,7 +2163,7 @@ impl Ui { where Payload: Any + Send + Sync, { - let is_being_dragged = self.memory(|mem| mem.is_being_dragged(id)); + let is_being_dragged = self.ctx().dragged_id() == Some(id); if is_being_dragged { crate::DragAndDrop::set_payload(self.ctx(), payload); diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 09d3b9f3d7d1..5a4ad1212402 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -374,7 +374,7 @@ impl<'a> Widget for DragValue<'a> { let shift = ui.input(|i| i.modifiers.shift_only()); // The widget has the same ID whether it's in edit or button mode. let id = ui.next_auto_id(); - let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id)); + let is_slow_speed = shift && ui.ctx().dragged_id() == Some(id); // The following ensures that when a `DragValue` receives focus, // it is immediately rendered in edit mode, rather than being rendered diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 452d60c4cb77..302c194093a0 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -551,7 +551,7 @@ impl<'t> TextEdit<'t> { paint_cursor(&painter, ui.visuals(), cursor_rect); } - let is_being_dragged = ui.ctx().memory(|m| m.is_being_dragged(response.id)); + let is_being_dragged = ui.ctx().dragged_id() == Some(response.id); let did_interact = state.cursor.pointer_interaction( ui, &response, diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index e31516e40118..f9c29ed7dff6 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -386,7 +386,7 @@ fn drag_and_drop_test(ui: &mut egui::Ui) { for (id, value) in data.read().cols(container_id, col) { drag_source(ui, id, |ui| { ui.add(egui::Label::new(value).sense(egui::Sense::click())); - if ui.memory(|mem| mem.is_being_dragged(id)) { + if ui.ctx().dragged_id() == Some(id) { is_dragged = Some(id); } }); @@ -408,7 +408,7 @@ fn drag_source( id: egui::Id, body: impl FnOnce(&mut egui::Ui) -> R, ) -> InnerResponse { - let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id)); + let is_being_dragged = ui.ctx().dragged_id() == Some(id); if !is_being_dragged { let res = ui.scope(body); @@ -435,12 +435,12 @@ fn drag_source( } } -// This is taken from crates/egui_demo_lib/src/debo/drag_and_drop.rs +// TODO: Update to be more like `crates/egui_demo_lib/src/debo/drag_and_drop.rs` fn drop_target( ui: &mut egui::Ui, body: impl FnOnce(&mut egui::Ui) -> R, ) -> egui::InnerResponse { - let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged()); + let is_being_dragged = ui.ctx().dragged_id().is_some(); let margin = egui::Vec2::splat(ui.visuals().clip_rect_margin); // 3.0 From ec91006f58d8a98d5500cfc772d31287eb5e2cc6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 16 Feb 2024 14:57:44 +0100 Subject: [PATCH 25/34] Deprecate drag-interaction on `Memory` --- crates/egui/src/context.rs | 33 +++++++++++++++++++++++++++++++++ crates/egui/src/memory.rs | 3 +++ 2 files changed, 36 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9d894d85af87..df7f29b1658e 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3321,6 +3321,39 @@ impl Context { pub fn drag_ended_id(&self) -> Option { self.interaction_snapshot(|i| i.drag_ended) } + + /// Set which widget is being dragged. + pub fn set_dragged_id(&self, id: Id) { + self.write(|ctx| { + let vp = ctx.viewport(); + let i = &mut vp.interact_widgets; + if i.dragged != Some(id) { + i.drag_ended = i.dragged.or(i.drag_ended); + i.dragged = Some(id); + i.drag_started = Some(id); + } + }); + } + + /// Stop dragging any widget. + pub fn stop_dragging(&self) { + self.write(|ctx| { + let vp = ctx.viewport(); + let i = &mut vp.interact_widgets; + if i.dragged.is_some() { + i.drag_ended = i.dragged; + i.dragged = 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 dragged = self.dragged_id(); + dragged.is_some() && dragged != Some(not_this) + } } #[test] diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 70c739a9f609..6642011f7ba5 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -762,12 +762,14 @@ impl Memory { /// Set which widget is being dragged. #[inline(always)] + #[deprecated = "Use `Context::set_dragged_id` instead"] pub fn set_dragged_id(&mut self, id: Id) { self.interaction_mut().potential_drag_id = Some(id); } /// Stop dragging any widget. #[inline(always)] + #[deprecated = "Use `Context::stop_dragging` instead"] pub fn stop_dragging(&mut self) { self.interaction_mut().potential_drag_id = None; } @@ -776,6 +778,7 @@ impl Memory { /// /// Returns true if we are dragging something, but not the given widget. #[inline(always)] + #[deprecated = "Use `Context::dragging_something_else` instead"] pub fn dragging_something_else(&self, not_this: Id) -> bool { let drag_id = self.interaction().potential_drag_id; drag_id.is_some() && drag_id != Some(not_this) From f636e2e56131ad0328d6d8e6d20d30d077ebd073 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 16 Feb 2024 15:07:15 +0100 Subject: [PATCH 26/34] Fix for drag stuff --- crates/egui/src/context.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index df7f29b1658e..75f7ee6e202d 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3332,6 +3332,8 @@ impl Context { i.dragged = Some(id); i.drag_started = Some(id); } + + ctx.memory.interaction_mut().potential_drag_id = Some(id); }); } @@ -3344,6 +3346,8 @@ impl Context { i.drag_ended = i.dragged; i.dragged = None; } + + ctx.memory.interaction_mut().potential_drag_id = None; }); } /// Is something else being dragged? From a132f0889b111f6b9a37edfc1cdeace83e52d104 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 09:44:28 +0100 Subject: [PATCH 27/34] Consistency: call it `drag_stopped` for symmetry with `drag_started` --- crates/egui/src/context.rs | 19 ++++++++++--------- crates/egui/src/interaction.rs | 12 ++++++------ crates/egui/src/response.rs | 21 +++++++++++++++++---- crates/egui_demo_lib/src/demo/tests.rs | 4 ++-- crates/egui_plot/src/lib.rs | 2 +- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 75f7ee6e202d..3753cbb20cb9 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1204,7 +1204,7 @@ impl Context { triple_clicked: Default::default(), drag_started: false, dragged: false, - drag_released: false, + drag_stopped: false, is_pointer_button_down_on: false, interact_pointer_pos: None, changed: false, @@ -1242,7 +1242,7 @@ impl Context { res.hovered = viewport.interact_widgets.hovered.contains(&id); res.dragged = Some(id) == viewport.interact_widgets.dragged; res.drag_started = Some(id) == viewport.interact_widgets.drag_started; - res.drag_released = Some(id) == viewport.interact_widgets.drag_ended; + res.drag_stopped = Some(id) == viewport.interact_widgets.drag_stopped; } let clicked = Some(id) == viewport.interact_widgets.clicked; @@ -1264,7 +1264,7 @@ impl Context { // is_pointer_button_down_on is false when released, but we want interact_pointer_pos // to still work. - let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released; + let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_stopped; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); } @@ -1936,7 +1936,7 @@ impl Context { clicked, drag_started: _, dragged, - drag_ended: _, + drag_stopped: _, contains_pointer, hovered, } = interact_widgets; @@ -3305,7 +3305,8 @@ impl Context { /// not be set until the mouse cursor has moved a certain distance. /// /// NOTE: if the widget was released this frame, this will be `None`. - /// Use [`Self::drag_ended_id`] instead. + /// Use [`Self::drag_stopped + /// _id`] instead. pub fn dragged_id(&self) -> Option { self.interaction_snapshot(|i| i.dragged) } @@ -3318,8 +3319,8 @@ impl Context { } /// This widget was being dragged, but was released this frame - pub fn drag_ended_id(&self) -> Option { - self.interaction_snapshot(|i| i.drag_ended) + pub fn drag_stopped_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_stopped) } /// Set which widget is being dragged. @@ -3328,7 +3329,7 @@ impl Context { let vp = ctx.viewport(); let i = &mut vp.interact_widgets; if i.dragged != Some(id) { - i.drag_ended = i.dragged.or(i.drag_ended); + i.drag_stopped = i.dragged.or(i.drag_stopped); i.dragged = Some(id); i.drag_started = Some(id); } @@ -3343,7 +3344,7 @@ impl Context { let vp = ctx.viewport(); let i = &mut vp.interact_widgets; if i.dragged.is_some() { - i.drag_ended = i.dragged; + i.drag_stopped = i.dragged; i.dragged = None; } diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 0b871fb45e99..2f87dbc271e3 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -33,7 +33,7 @@ pub struct InteractionSnapshot { /// after having been dragged. /// /// The widget will not be found in [`Self::dragged`] this frame. - pub drag_ended: Option, + pub drag_stopped: Option, pub hovered: IdSet, pub contains_pointer: IdSet, @@ -45,7 +45,7 @@ impl InteractionSnapshot { clicked, drag_started, dragged, - drag_ended, + drag_stopped, hovered, contains_pointer, } = self; @@ -69,8 +69,8 @@ impl InteractionSnapshot { id_ui(ui, dragged); ui.end_row(); - ui.label("drag_ended"); - id_ui(ui, drag_ended); + ui.label("drag_stopped"); + id_ui(ui, drag_stopped); ui.end_row(); ui.label("hovered"); @@ -170,7 +170,7 @@ pub(crate) fn interact( // ------------------------------------------------------------------------ let drag_changed = dragged != prev_snapshot.dragged; - let drag_ended = drag_changed.then_some(prev_snapshot.dragged).flatten(); + let drag_stopped = drag_changed.then_some(prev_snapshot.dragged).flatten(); let drag_started = drag_changed.then_some(dragged).flatten(); // if let Some(drag_started) = drag_started { @@ -210,7 +210,7 @@ pub(crate) fn interact( clicked, drag_started, dragged, - drag_ended, + drag_stopped, contains_pointer, hovered, } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 6b1535206b5f..0447f1dfd5c4 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -85,7 +85,7 @@ pub struct Response { /// The widget was being dragged, but now it has been released. #[doc(hidden)] - pub drag_released: bool, + pub drag_stopped: bool, /// Is the pointer button currently down on this widget? /// This is true if the pointer is pressing down or dragging a widget @@ -317,13 +317,26 @@ impl Response { /// The widget was being dragged, but now it has been released. #[inline] + pub fn drag_stopped(&self) -> bool { + self.drag_stopped + } + + /// The widget was being dragged by the button, but now it has been released. + pub fn drag_stopped_by(&self, button: PointerButton) -> bool { + self.drag_stopped() && self.ctx.input(|i| i.pointer.button_released(button)) + } + + /// The widget was being dragged, but now it has been released. + #[inline] + #[deprecated = "Renamed 'dragged_stopped'"] pub fn drag_released(&self) -> bool { - self.drag_released + self.drag_stopped } /// The widget was being dragged by the button, but now it has been released. + #[deprecated = "Renamed 'dragged_stopped_by'"] pub fn drag_released_by(&self, button: PointerButton) -> bool { - self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button)) + self.drag_stopped_by(button) } /// If dragged, how many points were we dragged and in what direction? @@ -858,7 +871,7 @@ impl Response { ], drag_started: self.drag_started || other.drag_started, dragged: self.dragged || other.dragged, - drag_released: self.drag_released || other.drag_released, + drag_stopped: self.drag_stopped || other.drag_stopped, is_pointer_button_down_on: self.is_pointer_button_down_on || other.is_pointer_button_down_on, interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 4b0368cc50b3..2dd6ea64a864 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -475,8 +475,8 @@ fn response_summary(response: &egui::Response, show_hovers: bool) -> String { writeln!(new_info, "Clicked{button_suffix}").ok(); } - if response.drag_released_by(button) { - writeln!(new_info, "Drag ended{button_suffix}").ok(); + if response.drag_stopped_by(button) { + writeln!(new_info, "Drag stopped{button_suffix}").ok(); } if response.dragged_by(button) { writeln!(new_info, "Dragged{button_suffix}").ok(); diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 0796edc292c7..692399e8bd3e 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1052,7 +1052,7 @@ impl Plot { )); } // when the click is release perform the zoom - if response.drag_released() { + if response.drag_stopped() { let box_start_pos = mem.transform.value_from_position(box_start_pos); let box_end_pos = mem.transform.value_from_position(box_end_pos); let new_bounds = PlotBounds { From dc12d837c73d42a9708d8819deeb048a3f31dc4c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 09:53:36 +0100 Subject: [PATCH 28/34] Deprecate `ui.interact_with_hovered` --- crates/egui/src/containers/area.rs | 19 ++++++------- crates/egui/src/containers/window.rs | 19 ++++++------- crates/egui/src/context.rs | 5 ++-- crates/egui/src/response.rs | 19 ++++++------- crates/egui/src/ui.rs | 41 ++++++++-------------------- 5 files changed, 38 insertions(+), 65 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 6a025e298a5c..d438ac0602c3 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -320,17 +320,14 @@ impl Area { Sense::hover() }; - let move_response = ctx.create_widget( - WidgetRect { - id: interact_id, - layer_id, - rect: state.rect(), - interact_rect: state.rect(), - sense, - enabled, - }, - true, - ); + let move_response = ctx.create_widget(WidgetRect { + id: interact_id, + layer_id, + rect: state.rect(), + interact_rect: state.rect(), + sense, + enabled, + }); if movable && move_response.dragged() { state.pivot_pos += ctx.input(|i| i.pointer.delta()); diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index bba8a2645757..8cf5475ebbde 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -736,17 +736,14 @@ fn resize_interaction( } let is_dragging = |rect, id| { - let response = ctx.create_widget( - WidgetRect { - layer_id, - id, - rect, - interact_rect: rect, - sense: Sense::drag(), - enabled: true, - }, - true, - ); + let response = ctx.create_widget(WidgetRect { + layer_id, + id, + rect, + interact_rect: rect, + sense: Sense::drag(), + enabled: true, + }); SideResponse { hover: response.hovered(), drag: response.dragged(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3753cbb20cb9..ffa320da543a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1110,7 +1110,7 @@ impl Context { /// /// You should use [`Ui::interact`] instead. #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, mut w: WidgetRect, may_contain_pointer: bool) -> Response { + pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { if !w.enabled { w.sense.click = false; w.sense.drag = false; @@ -1143,8 +1143,7 @@ impl Context { self.check_for_id_clash(w.id, w.rect, "widget"); } - let mut res = self.get_response(w); - res.contains_pointer &= may_contain_pointer; + let res = self.get_response(w); #[cfg(feature = "accesskit")] if w.sense.focusable { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 0447f1dfd5c4..06efa54545b3 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -639,17 +639,14 @@ impl Response { return self.clone(); } - self.ctx.create_widget( - WidgetRect { - layer_id: self.layer_id, - id: self.id, - rect: self.rect, - interact_rect: self.interact_rect, - sense, - enabled: self.enabled, - }, - self.contains_pointer, - ) + self.ctx.create_widget(WidgetRect { + layer_id: self.layer_id, + id: self.id, + rect: self.rect, + interact_rect: self.interact_rect, + sense, + enabled: self.enabled, + }) } /// Adjust the scroll position until this UI becomes visible. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 12824fa4dabc..b528dded1d75 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -646,43 +646,26 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { - self.ctx().create_widget( - WidgetRect { - id, - layer_id: self.layer_id(), - rect, - interact_rect: self.clip_rect().intersect(rect), - sense, - enabled: self.enabled, - }, - true, - ) + self.ctx().create_widget(WidgetRect { + id, + layer_id: self.layer_id(), + rect, + interact_rect: self.clip_rect().intersect(rect), + sense, + enabled: self.enabled, + }) } - /// Check for clicks, and drags on a specific region that is hovered. - /// This can be used once you have checked that some shape you are painting has been hovered, - /// and want to check for clicks and drags on hovered items this frame. - /// - /// The given [`Rect`] should approximately be where the thing is, - /// as will be the rectangle for the returned [`Response::rect`]. + /// Deprecated: use [`Self::interact`] instead. + #[deprecated = "The contains_pointer argument is ignored. Use `ui.interact` instead."] pub fn interact_with_hovered( &self, rect: Rect, - contains_pointer: bool, + _contains_pointer: bool, id: Id, sense: Sense, ) -> Response { - self.ctx().create_widget( - WidgetRect { - id, - layer_id: self.layer_id(), - rect, - interact_rect: self.clip_rect().intersect(rect), - sense, - enabled: self.enabled, - }, - contains_pointer, - ) + self.interact(rect, id, sense) } /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? From 05f6505ba7e126f601fd32372e0d119a3f00bfd8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 10:12:04 +0100 Subject: [PATCH 29/34] Document widget interaction --- crates/egui/src/context.rs | 1 + crates/egui/src/interaction.rs | 2 +- crates/egui/src/lib.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index ffa320da543a..5b0cb7d07bf9 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3350,6 +3350,7 @@ impl Context { ctx.memory.interaction_mut().potential_drag_id = None; }); } + /// Is something else being dragged? /// /// Returns true if we are dragging something, but not the given widget. diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 2f87dbc271e3..2b9e68a56211 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -26,7 +26,7 @@ pub struct InteractionSnapshot { /// /// NOTE: this may not have a corresponding [`WidgetRect`], /// if this for instance is a drag-and-drop widget which - /// isn't painted whilest being dragged + /// isn't painted whilst being dragged pub dragged: Option, /// This widget was let go this frame, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 33ed9013fb97..c715b859e6c1 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -267,6 +267,37 @@ //! } //! ``` //! +//! +//! ## Widget interaction +//! Each widget has a [`Sense`], which defines whether or not the widget +//! is sensitive to clickicking and/or drags. +//! +//! For instance, a [`Button`] only has a [`Sense::click`] (by default). +//! This means if you drag a button it will not respond with [`Response::dragged`]. +//! Instead, the drag will continue through the button to the first +//! widget behind it that is sensitive to dragging, which for instance could be +//! a [`ScrollArea`]. This lets you scroll by dragging a scroll area (important +//! on touch screens), just as long as you don't drag on a widget that is sensitive +//! to drags (e.g. a [`Slider`]). +//! +//! When widgets overlap it is the last added one +//! that is considered to be on top and which will get input priority. +//! +//! The widget interaction logic is run at the _start_ of each frame, +//! based on the output from the previous frame. +//! This means that when a new widget shows up you cannot click it in the same +//! frame (i.e. in the same fraction of a second), but unless the user +//! is spider-man, they wouldn't be fast enough to do so anyways. +//! +//! By running the interaction code early, egui can actually +//! tell you if a widget is being interacted with _before_ you add it, +//! as long as you know its [`Id`] before-hand (e.g. using [`Ui::next_auto_id`]), +//! by calling [`Context::read_response`]. +//! This can be useful in some circumstances in order to style a widget, +//! or to respond to interactions before adding the widget +//! (perhaps on top of other widgets). +//! +//! //! ## Auto-sizing panels and windows //! In egui, all panels and windows auto-shrink to fit the content. //! If the window or panel is also resizable, this can lead to a weird behavior From 8d7cf52dbad1bad95ffceff2bf649ccd3ad29a89 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 10:17:04 +0100 Subject: [PATCH 30/34] Silence a clippy thing --- crates/egui/src/context.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 5b0cb7d07bf9..c13d5ced7722 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1143,6 +1143,7 @@ impl Context { self.check_for_id_clash(w.id, w.rect, "widget"); } + #[allow(clippy::let_and_return)] let res = self.get_response(w); #[cfg(feature = "accesskit")] From 6e875df9b16c377f359011c979cfdb6832c23a51 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 10:18:06 +0100 Subject: [PATCH 31/34] Revert changes to Cargo.lock --- Cargo.lock | 74 ++++++++---------------------------------------------- 1 file changed, 10 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beb2eb8d8530..92948c4589fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,9 +199,9 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arboard" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" +checksum = "1faa3c733d9a3dd6fbaf85da5d162a2e03b2e0033a90dceb0e2a90fdd1e5380a" dependencies = [ "clipboard-win", "log", @@ -210,8 +210,7 @@ dependencies = [ "objc_id", "parking_lot", "thiserror", - "winapi", - "x11rb 0.12.0", + "x11rb", ] [[package]] @@ -789,13 +788,11 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clipboard-win" -version = "4.5.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +checksum = "3ec832972fefb8cf9313b45a0d1945e29c9c251f1d4c6eafc5fe2124c02d2e81" dependencies = [ "error-code", - "str-buf", - "winapi", ] [[package]] @@ -1510,13 +1507,9 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] +checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" [[package]] name = "event-listener" @@ -1731,16 +1724,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "gethostname" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "gethostname" version = "0.4.3" @@ -3534,12 +3517,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "strict-num" version = "0.1.1" @@ -4382,15 +4359,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4626,7 +4594,7 @@ dependencies = [ "web-time", "windows-sys 0.48.0", "x11-dl", - "x11rb 0.13.0", + "x11rb", "xkbcommon-dl", ] @@ -4650,19 +4618,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "x11rb" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" -dependencies = [ - "gethostname 0.3.0", - "nix", - "winapi", - "winapi-wsapoll", - "x11rb-protocol 0.12.0", -] - [[package]] name = "x11rb" version = "0.13.0" @@ -4670,21 +4625,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ "as-raw-xcb-connection", - "gethostname 0.4.3", + "gethostname", "libc", "libloading 0.8.0", "once_cell", "rustix 0.38.21", - "x11rb-protocol 0.13.0", -] - -[[package]] -name = "x11rb-protocol" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" -dependencies = [ - "nix", + "x11rb-protocol", ] [[package]] From 709ed34da9c3160c5f36f9cbc93d00f0d603dfb7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 10:22:24 +0100 Subject: [PATCH 32/34] Add `Context::is_being_dragged` --- crates/egui/src/context.rs | 13 +++++++++++-- crates/egui/src/memory.rs | 2 +- crates/egui/src/ui.rs | 2 +- crates/egui/src/widgets/drag_value.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 2 +- examples/test_viewports/src/main.rs | 4 ++-- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index c13d5ced7722..167c398a9d2a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3305,12 +3305,21 @@ impl Context { /// not be set until the mouse cursor has moved a certain distance. /// /// NOTE: if the widget was released this frame, this will be `None`. - /// Use [`Self::drag_stopped - /// _id`] instead. + /// Use [`Self::drag_stopped_id`] instead. pub fn dragged_id(&self) -> Option { self.interaction_snapshot(|i| i.dragged) } + /// Is this specific widget being dragged? + /// + /// A widget that sense both clicks and drags is only marked as "dragged" + /// when the mouse has moved a bit + /// + /// See also: [`crate::Response::dragged`]. + pub fn is_being_dragged(&self, id: Id) -> bool { + self.dragged_id() == Some(id) + } + /// This widget just started being dragged this frame. /// /// The same widget should also be found in [`Self::dragged_id`]. diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 6642011f7ba5..db8904e10d2a 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -742,7 +742,7 @@ impl Memory { /// /// 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. - #[deprecated = "Use `Context::dragged_id` instead"] + #[deprecated = "Use `Context::is_being_dragged` instead"] #[inline(always)] pub fn is_being_dragged(&self, id: Id) -> bool { self.interaction().potential_drag_id == Some(id) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index b528dded1d75..4747c41efaf4 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2146,7 +2146,7 @@ impl Ui { where Payload: Any + Send + Sync, { - let is_being_dragged = self.ctx().dragged_id() == Some(id); + let is_being_dragged = self.ctx().is_being_dragged(id); if is_being_dragged { crate::DragAndDrop::set_payload(self.ctx(), payload); diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 5a4ad1212402..f0875ba30553 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -374,7 +374,7 @@ impl<'a> Widget for DragValue<'a> { let shift = ui.input(|i| i.modifiers.shift_only()); // The widget has the same ID whether it's in edit or button mode. let id = ui.next_auto_id(); - let is_slow_speed = shift && ui.ctx().dragged_id() == Some(id); + let is_slow_speed = shift && ui.ctx().is_being_dragged(id); // The following ensures that when a `DragValue` receives focus, // it is immediately rendered in edit mode, rather than being rendered diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 302c194093a0..22a5623f077c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -551,7 +551,7 @@ impl<'t> TextEdit<'t> { paint_cursor(&painter, ui.visuals(), cursor_rect); } - let is_being_dragged = ui.ctx().dragged_id() == Some(response.id); + let is_being_dragged = ui.ctx().is_being_dragged(response.id); let did_interact = state.cursor.pointer_interaction( ui, &response, diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index f9c29ed7dff6..0ee31e2480de 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -386,7 +386,7 @@ fn drag_and_drop_test(ui: &mut egui::Ui) { for (id, value) in data.read().cols(container_id, col) { drag_source(ui, id, |ui| { ui.add(egui::Label::new(value).sense(egui::Sense::click())); - if ui.ctx().dragged_id() == Some(id) { + if ui.ctx().is_being_dragged(id) { is_dragged = Some(id); } }); @@ -408,7 +408,7 @@ fn drag_source( id: egui::Id, body: impl FnOnce(&mut egui::Ui) -> R, ) -> InnerResponse { - let is_being_dragged = ui.ctx().dragged_id() == Some(id); + let is_being_dragged = ui.ctx().is_being_dragged(id); if !is_being_dragged { let res = ui.scope(body); From bea5fc29a0dd8f16a1383c869d4f94347353b9ff Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 11:00:36 +0100 Subject: [PATCH 33/34] Improve docstrings and comments --- crates/egui/src/containers/resize.rs | 1 + crates/egui/src/context.rs | 13 +++++++++++-- crates/egui/src/hit_test.rs | 20 +++++++++++++++++--- crates/egui/src/interaction.rs | 13 +++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 1acc86bff747..22286368b7c6 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -231,6 +231,7 @@ impl Resize { if let Some(corner_id) = corner_id { if let Some(corner_response) = ui.ctx().read_response(corner_id) { if let Some(pointer_pos) = corner_response.interact_pointer_pos() { + // Respond to the interaction early to avoid frame delay. user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size()); } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 167c398a9d2a..6d7f9cc13c3f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -258,9 +258,11 @@ impl WidgetRects { return; } - let layer_widgets = self.by_layer.entry(layer_id).or_default(); + let Self { by_layer, by_id } = self; + + let layer_widgets = by_layer.entry(layer_id).or_default(); - match self.by_id.entry(widget_rect.id) { + match by_id.entry(widget_rect.id) { std::collections::hash_map::Entry::Vacant(entry) => { // A new widget entry.insert(widget_rect); @@ -1175,6 +1177,13 @@ impl Context { .map(|widget_rect| self.get_response(widget_rect)) } + /// Returns `true` if the widget with the given `Id` contains the pointer. + #[deprecated = "Use Response.contains_pointer or Context::read_response instead"] + pub fn widget_contains_pointer(&self, id: Id) -> bool { + self.read_response(id) + .map_or(false, |response| response.contains_pointer) + } + /// Do all interaction for an existing widget, without (re-)registering it. fn get_response(&self, widget_rect: WidgetRect) -> Response { let WidgetRect { diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index a798d634f6a7..b2af8ffbf405 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -208,11 +208,21 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { (Some(hit_click), None) => { // We have a perfect hit on a click-widget, but not on a drag-widget. + // + // Note that we don't look for a close drag widget in this case, + // because I can't think of a case where that would be helpful. + // This is in contrast with the opposite case, + // where when hovering directly over a drag-widget (like a big ScrollArea), + // we look for close click-widgets (e.g. buttons). + // This is because big background drag-widgets (ScrollArea, Window) are common, + // but bit clickable things aren't. + // Even if they were, I think it would be confusing for a user if clicking + // a drag-only widget would click something _behind_ it. WidgetHits { contains_pointer: hits, click: Some(hit_click), - drag: None, // TODO: we should maybe look for close drag widgets? + drag: None, } } @@ -231,7 +241,9 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { drag: Some(hit_click), } } else { - // They are interested in different things. + // They are interested in different things, + // and click is on top. Report both hits, + // e.g. the top Button and the ScrollArea behind it. WidgetHits { contains_pointer: hits, click: Some(hit_click), @@ -247,7 +259,9 @@ fn hit_test_on_close(mut close: Vec, pos: Pos2) -> WidgetHits { drag: Some(hit_drag), } } else { - // The top things senses only drags + // The top things senses only drags, + // so we ignore the click-widget, because it would be confusing + // if clicking a drag-widget would actually click something else below it. WidgetHits { contains_pointer: hits, click: None, diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 2b9e68a56211..f1a3e2a32475 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -35,7 +35,20 @@ pub struct InteractionSnapshot { /// The widget will not be found in [`Self::dragged`] this frame. pub drag_stopped: Option, + /// A small set of widgets (usually 0-1) that the pointer is hovering over. + /// + /// Show these widgets as highlighted, if they are interactive. + /// + /// While dragging or clicking something, nothing else is hovered. + /// + /// Use [`Self::contains_pointer`] to find a drop-zone for drag-and-drop. pub hovered: IdSet, + + /// All widgets that contain the pointer this frame, + /// regardless if the user is currently clicking or dragging. + /// + /// This is usually a larger set than [`Self::hovered`], + /// and can be used for e.g. drag-and-drop zones. pub contains_pointer: IdSet, } From 472612a658e1587d62c54c47fb666ef65aa4ad6d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 15:18:47 +0100 Subject: [PATCH 34/34] Small tweaks --- crates/egui/src/context.rs | 4 +++- crates/egui_extras/src/layout.rs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 4a7ead5dc158..22741b237d5c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1115,6 +1115,8 @@ impl Context { /// If this is not called, the widget doesn't exist. /// /// You should use [`Ui::interact`] instead. + /// + /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { if !w.enabled { @@ -1246,7 +1248,7 @@ impl Context { res.clicked[PointerButton::Primary as usize] = true; } - let interaction = memory.interaction_mut(); + let interaction = memory.interaction(); res.is_pointer_button_down_on = interaction.potential_click_id == Some(id) || interaction.potential_drag_id == Some(id); diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 9bce5ec4f35f..0876d9db07dc 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -203,6 +203,7 @@ impl<'l> StripLayout<'l> { } add_cell_contents(&mut child_ui); + child_ui.min_rect() }