Skip to content

Commit

Permalink
Support interacting with the background of a Ui (#4074)
Browse files Browse the repository at this point in the history
Add `Ui::interact_bg` which interacts with the ui _behind_ any of its
children.
  • Loading branch information
emilk authored Feb 20, 2024
1 parent a33ae64 commit 9096abd
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 138 deletions.
145 changes: 22 additions & 123 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions crates/egui/src/hit_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 4 additions & 7 deletions crates/egui/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -443,7 +444,7 @@ pub mod text {

pub use {
containers::*,
context::{Context, RepaintCause, RequestRepaintInfo, WidgetRect, WidgetRects},
context::{Context, RepaintCause, RequestRepaintInfo},
data::{
input::*,
output::{
Expand All @@ -466,6 +467,7 @@ pub use {
text::{Galley, TextFormat},
ui::Ui,
viewport::*,
widget_rect::{WidgetRect, WidgetRects},
widget_text::{RichText, WidgetText},
widgets::*,
};
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
Expand Down
43 changes: 39 additions & 4 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,28 @@ 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),
style,
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.
Expand All @@ -101,15 +114,28 @@ 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(),
style: self.style.clone(),
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
}

// -------------------------------------------------
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 9096abd

Please sign in to comment.