Browse Source

Support interacting with the background of a `Ui` (#4074)

Add `Ui::interact_bg` which interacts with the ui _behind_ any of its
children.
pull/4077/head
Emil Ernerfeldt 9 months ago
committed by GitHub
parent
commit
9096abdeec
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 145
      crates/egui/src/context.rs
  2. 3
      crates/egui/src/hit_test.rs
  3. 11
      crates/egui/src/interaction.rs
  4. 4
      crates/egui/src/lib.rs
  5. 2
      crates/egui/src/response.rs
  6. 43
      crates/egui/src/ui.rs
  7. 125
      crates/egui/src/widget_rect.rs

145
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<LayerId, Vec<WidgetRect>>,
/// All widgets
pub by_id: IdMap<WidgetRect>,
}
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<LayerId> = viewport
.widgets_prev_frame
.by_layer
.keys()
.copied()
.collect();
let mut layers: Vec<LayerId> = 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 {

3
crates/egui/src/hit_test.rs

@ -56,8 +56,7 @@ pub fn hit_test(
let mut close: Vec<WidgetRect> = 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);

11
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,

4
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::*,
};

2
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,
})
}

43
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,

125
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<LayerId, Vec<WidgetRect>>,
/// 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<Item = LayerId> + '_ {
self.by_layer.keys().copied()
}
pub fn layers(&self) -> impl Iterator<Item = (&LayerId, &[WidgetRect])> + '_ {
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<Item = &WidgetRect> + '_ {
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;
}
}
}
}
}
Loading…
Cancel
Save