Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support interacting with the background of a Ui #4074

Merged
merged 7 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading