diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 22741b237d5..996f7a7ec5f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -198,100 +198,6 @@ impl ContextImpl { // ---------------------------------------------------------------------------- -/// Used to store each widget's [Id], [Rect] and [Sense] each frame. -/// Used to check for overlaps between widgets when handling events. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WidgetRect { - /// The globally unique widget id. - /// - /// For interactive widgets, this better be globally unique. - /// If not there will be weird bugs, - /// and also big red warning test on the screen in debug builds - /// (see [`Options::warn_on_id_clash`]). - /// - /// You can ensure globally unique ids using [`Ui::push_id`]. - pub id: Id, - - /// 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. - pub interact_rect: Rect, - - /// 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. -/// -/// Actually, only those that are on screen. -#[derive(Default, Clone, PartialEq, Eq)] -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) { - 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. - pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { - if !widget_rect.interact_rect.is_positive() { - return; - } - - let Self { by_layer, by_id } = self; - - let layer_widgets = by_layer.entry(layer_id).or_default(); - - match 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(…)` to add more interaction. - let existing = entry.get_mut(); - 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; - - // 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; - } - } - } - } - } -} - -// ---------------------------------------------------------------------------- - /// State stored per viewport #[derive(Default)] struct ViewportState { @@ -546,12 +452,7 @@ impl ContextImpl { .map(|(i, id)| (*id, i)) .collect(); - let mut layers: Vec = viewport - .widgets_prev_frame - .by_layer - .keys() - .copied() - .collect(); + let mut layers: Vec = viewport.widgets_prev_frame.layer_ids().collect(); layers.sort_by(|a, b| { if a.order == b.order { @@ -1124,23 +1025,19 @@ impl Context { w.sense.drag = false; } - if w.interact_rect.is_positive() { - // Remember this widget - self.write(|ctx| { - let viewport = ctx.viewport(); + // 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(w.layer_id, w); + // 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(w.layer_id, w); - if w.sense.focusable { - ctx.memory.interested_in_focus(w.id); - } - }); - } else { - // Don't remember invisible widgets - } + if w.sense.focusable { + ctx.memory.interested_in_focus(w.id); + } + }); if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { // Not interested or allowed input: @@ -1175,9 +1072,8 @@ impl Context { let viewport = ctx.viewport(); viewport .widgets_this_frame - .by_id - .get(&id) - .or_else(|| viewport.widgets_prev_frame.by_id.get(&id)) + .get(id) + .or_else(|| viewport.widgets_prev_frame.get(id)) .copied() }) .map(|widget_rect| self.get_response(widget_rect)) @@ -1916,13 +1812,16 @@ impl Context { #[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); + let rect = widget.interact_rect; + if rect.is_positive() { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(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()) + self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).cloned()) { paint_widget(&widget, text, color); } @@ -1931,8 +1830,8 @@ impl Context { 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); + for (layer_id, rects) in rects.layers() { + let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); for rect in rects { if rect.sense.interactive() { let (color, text) = if rect.sense.click && rect.sense.drag { diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index fe8b085a354..2b778e9d719 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -56,8 +56,7 @@ pub fn hit_test( let mut close: Vec = layer_order .iter() .filter(|layer| layer.order.allow_interaction()) - .filter_map(|layer_id| widgets.by_layer.get(layer_id)) - .flatten() + .flat_map(|&layer_id| widgets.get_layer(layer_id)) .filter(|&w| { let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index f1a3e2a3247..2f1925c7528 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -107,13 +107,13 @@ pub(crate) fn interact( crate::profile_function!(); if let Some(id) = interaction.potential_click_id { - if !widgets.by_id.contains_key(&id) { + if !widgets.contains(id) { // The widget we were interested in clicking is gone. interaction.potential_click_id = None; } } if let Some(id) = interaction.potential_drag_id { - if !widgets.by_id.contains_key(&id) { + if !widgets.contains(id) { // The widget we were interested in dragging is gone. // This is fine! This could be drag-and-drop, // and the widget being dragged is now "in the air" and thus @@ -145,7 +145,7 @@ pub(crate) fn interact( if click.is_some() { if let Some(widget) = interaction .potential_click_id - .and_then(|id| widgets.by_id.get(&id)) + .and_then(|id| widgets.get(id)) { clicked = Some(widget.id); } @@ -160,10 +160,7 @@ pub(crate) fn interact( 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)) - { + if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.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, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c715b859e6c..9c065811d34 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -403,6 +403,7 @@ pub mod text_selection; mod ui; pub mod util; pub mod viewport; +mod widget_rect; pub mod widget_text; pub mod widgets; @@ -443,7 +444,7 @@ pub mod text { pub use { containers::*, - context::{Context, RepaintCause, RequestRepaintInfo, WidgetRect, WidgetRects}, + context::{Context, RepaintCause, RequestRepaintInfo}, data::{ input::*, output::{ @@ -466,6 +467,7 @@ pub use { text::{Galley, TextFormat}, ui::Ui, viewport::*, + widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, }; diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 6e791fc5772..82349993e38 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -658,7 +658,7 @@ impl Response { id: self.id, rect: self.rect, interact_rect: self.interact_rect, - sense, + sense: self.sense | sense, enabled: self.enabled, }) } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 182c5782f15..b5f861897c7 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -75,7 +75,7 @@ impl Ui { /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { let style = ctx.style(); - Ui { + let ui = Ui { id, next_auto_id_source: id.with("auto").value(), painter: Painter::new(ctx, layer_id, clip_rect), @@ -83,7 +83,20 @@ impl Ui { placer: Placer::new(max_rect, Layout::default()), enabled: true, menu_state: None, - } + }; + + // Register in the widget stack early, to ensure we are behind all widgets we contain: + let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + ui.ctx().create_widget(WidgetRect { + id: ui.id, + layer_id: ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense: Sense::hover(), + enabled: ui.enabled, + }); + + ui } /// Create a new [`Ui`] at a specific region. @@ -101,7 +114,7 @@ impl Ui { crate::egui_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); - Ui { + let child_ui = Ui { id: self.id.with(id_source), next_auto_id_source, painter: self.painter.clone(), @@ -109,7 +122,20 @@ impl Ui { placer: Placer::new(max_rect, layout), enabled: self.enabled, menu_state: self.menu_state.clone(), - } + }; + + // Register in the widget stack early, to ensure we are behind all widgets we contain: + let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + child_ui.ctx().create_widget(WidgetRect { + id: child_ui.id, + layer_id: child_ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense: Sense::hover(), + enabled: child_ui.enabled, + }); + + child_ui } // ------------------------------------------------- @@ -668,6 +694,15 @@ impl Ui { self.interact(rect, id, sense) } + /// Interact with the background of this [`Ui`], + /// i.e. behind all the widgets. + /// + /// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`]. + pub fn interact_bg(&self, sense: Sense) -> Response { + // This will update the WidgetRect that was first created in `Ui::new`. + self.interact(self.min_rect(), self.id, sense) + } + /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? /// /// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance, diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs new file mode 100644 index 00000000000..1ef8c28bd96 --- /dev/null +++ b/crates/egui/src/widget_rect.rs @@ -0,0 +1,125 @@ +use ahash::HashMap; + +use crate::*; + +/// Used to store each widget's [Id], [Rect] and [Sense] each frame. +/// +/// Used to check which widget gets input when a user clicks somewhere. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WidgetRect { + /// The globally unique widget id. + /// + /// For interactive widgets, this better be globally unique. + /// If not there will be weird bugs, + /// and also big red warning test on the screen in debug builds + /// (see [`Options::warn_on_id_clash`]). + /// + /// You can ensure globally unique ids using [`Ui::push_id`]. + pub id: Id, + + /// 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. + pub interact_rect: Rect, + + /// How the widget responds to interaction. + pub sense: Sense, + + /// Is the widget enabled? + pub enabled: bool, +} + +/// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. +/// +/// All [`Ui`]s have a [`WidgetRects`], but whether or not their rects are correct +/// depends on if [`Ui::interact_bg`] was ever called. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct WidgetRects { + /// All widgets, in painting order. + by_layer: HashMap>, + + /// All widgets, by id, and their order in their respective layer + by_id: IdMap<(usize, WidgetRect)>, +} + +impl WidgetRects { + /// All known layers with widgets. + pub fn layer_ids(&self) -> impl ExactSizeIterator + '_ { + self.by_layer.keys().copied() + } + + pub fn layers(&self) -> impl Iterator + '_ { + self.by_layer + .iter() + .map(|(layer_id, rects)| (layer_id, &rects[..])) + } + + #[inline] + pub fn get(&self, id: Id) -> Option<&WidgetRect> { + self.by_id.get(&id).map(|(_, w)| w) + } + + #[inline] + pub fn contains(&self, id: Id) -> bool { + self.by_id.contains_key(&id) + } + + /// All widgets in this layer, sorted back-to-front. + #[inline] + pub fn get_layer(&self, layer_id: LayerId) -> impl Iterator + '_ { + self.by_layer.get(&layer_id).into_iter().flatten() + } + + /// Clear the contents while retaining allocated memory. + pub fn clear(&mut self) { + 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. + pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { + let Self { by_layer, by_id } = self; + + let layer_widgets = by_layer.entry(layer_id).or_default(); + + match by_id.entry(widget_rect.id) { + std::collections::hash_map::Entry::Vacant(entry) => { + // A new widget + let idx_in_layer = layer_widgets.len(); + entry.insert((idx_in_layer, widget_rect)); + layer_widgets.push(widget_rect); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + // This is a known widget, but we might need to update it! + // e.g. calling `response.interact(…)` to add more interaction. + let (idx_in_layer, existing) = entry.get_mut(); + + // Update it: + existing.rect = widget_rect.rect; // last wins + existing.interact_rect = widget_rect.interact_rect; // last wins + existing.sense |= widget_rect.sense; + existing.enabled |= widget_rect.enabled; + + egui_assert!( + existing.layer_id == widget_rect.layer_id, + "Widget changed layer_id during the frame" + ); + + if existing.layer_id == widget_rect.layer_id { + layer_widgets[*idx_in_layer] = *existing; + } + } + } + } +}