Support interacting with the background of a `Ui` (#4074)
Add `Ui::interact_bg` which interacts with the ui _behind_ any of its children.
This commit is contained in:
parent
a33ae64785
commit
9096abdeec
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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::*,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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…
Reference in New Issue