diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 378b0edc8d9..db9bd496fa6 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -462,14 +462,17 @@ impl Area { } }); - let move_response = ctx.create_widget(WidgetRect { - id: interact_id, - layer_id, - rect: state.rect(), - interact_rect: state.rect(), - sense, - enabled, - }); + let move_response = ctx.create_widget( + WidgetRect { + id: interact_id, + layer_id, + rect: state.rect(), + interact_rect: state.rect(), + sense, + enabled, + }, + true, + ); if movable && move_response.dragged() { if let Some(pivot_pos) = &mut state.pivot_pos { diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index c183a4dc731..438d562ede7 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -833,14 +833,17 @@ fn resize_interaction( } let is_dragging = |rect, id| { - let response = ctx.create_widget(WidgetRect { - layer_id, - id, - rect, - interact_rect: rect, - sense: Sense::drag(), - enabled: true, - }); + let response = ctx.create_widget( + WidgetRect { + layer_id, + id, + rect, + interact_rect: rect, + sense: Sense::drag(), + enabled: true, + }, + true, + ); SideResponse { hover: response.hovered(), drag: response.dragged(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 162ff4a739e..ce690c7bb97 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1049,8 +1049,11 @@ impl Context { /// You should use [`Ui::interact`] instead. /// /// If the widget already exists, its state (sense, Rect, etc) will be updated. + /// + /// `allow_focus` should usually be true, unless you call this function multiple times with the + /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { + pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1060,12 +1063,12 @@ impl Context { // but also to know when we have reached the widget we are checking for cover. viewport.this_frame.widgets.insert(w.layer_id, w); - if w.sense.focusable { + if allow_focus && w.sense.focusable { ctx.memory.interested_in_focus(w.id); } }); - if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { + if allow_focus && (!w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction()) { // Not interested or allowed input: self.memory_mut(|mem| mem.surrender_focus(w.id)); } @@ -1078,7 +1081,7 @@ impl Context { let res = self.get_response(w); #[cfg(feature = "accesskit")] - if w.sense.focusable { + if allow_focus && w.sense.focusable { // Make sure anything that can receive focus has an AccessKit node. // TODO(mwcampbell): For nodes that are filled from widget info, // some information is written to the node twice. @@ -1114,7 +1117,7 @@ impl Context { } /// Do all interaction for an existing widget, without (re-)registering it. - fn get_response(&self, widget_rect: WidgetRect) -> Response { + pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response { let WidgetRect { id, layer_id, diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 1683125d4e9..0633f1cbd05 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -706,7 +706,7 @@ impl MenuState { self.open_submenu(sub_id, pos); } else if open - && ui.interact_bg(Sense::hover()).contains_pointer() + && ui.response().contains_pointer() && !button.hovered() && !self.hovering_current_submenu(&pointer) { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index d6a9a8a0c7f..8dadc24df2a 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -857,14 +857,17 @@ impl Response { return self.clone(); } - self.ctx.create_widget(WidgetRect { - layer_id: self.layer_id, - id: self.id, - rect: self.rect, - interact_rect: self.interact_rect, - sense: self.sense | sense, - enabled: self.enabled, - }) + self.ctx.create_widget( + WidgetRect { + layer_id: self.layer_id, + id: self.id, + rect: self.rect, + interact_rect: self.interact_rect, + sense: self.sense | sense, + enabled: self.enabled, + }, + true, + ) } /// Adjust the scroll position until this UI becomes visible. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 1db33052d24..6a1e7e69bd6 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -89,6 +89,14 @@ pub struct Ui { /// The [`UiStack`] for this [`Ui`]. stack: Arc, + + /// The sense for the ui background. + sense: Sense, + + /// Whether [`Ui::remember_min_rect`] should be called when the [`Ui`] is dropped. + /// This is an optimization, so we don't call [`Ui::remember_min_rect`] multiple times at the + /// end of a [`Ui::scope`]. + min_rect_already_remembered: bool, } impl Ui { @@ -109,6 +117,7 @@ impl Ui { invisible, sizing_pass, style, + sense, } = ui_builder; debug_assert!( @@ -122,6 +131,7 @@ impl Ui { let invisible = invisible || sizing_pass; let disabled = disabled || invisible || sizing_pass; let style = style.unwrap_or_else(|| ctx.style()); + let sense = sense.unwrap_or(Sense::hover()); let placer = Placer::new(max_rect, layout); let ui_stack = UiStack { @@ -142,18 +152,23 @@ impl Ui { sizing_pass: false, menu_state: None, stack: Arc::new(ui_stack), + sense, + min_rect_already_remembered: false, }; // 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.ctx().create_widget( + WidgetRect { + id: ui.id, + layer_id: ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense, + enabled: ui.enabled, + }, + true, + ); if disabled { ui.disable(); @@ -220,6 +235,7 @@ impl Ui { invisible, sizing_pass, style, + sense, } = ui_builder; let mut painter = self.painter.clone(); @@ -234,6 +250,7 @@ impl Ui { } let sizing_pass = self.sizing_pass || sizing_pass; let style = style.unwrap_or_else(|| self.style.clone()); + let sense = sense.unwrap_or(Sense::hover()); if self.sizing_pass { // During the sizing pass we want widgets to use up as little space as possible, @@ -269,18 +286,23 @@ impl Ui { sizing_pass, menu_state: self.menu_state.clone(), stack: Arc::new(ui_stack), + sense, + min_rect_already_remembered: false, }; // 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.ctx().create_widget( + WidgetRect { + id: child_ui.id, + layer_id: child_ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense, + enabled: child_ui.enabled, + }, + true, + ); child_ui } @@ -948,14 +970,17 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { - self.ctx().create_widget(WidgetRect { - id, - layer_id: self.layer_id(), - rect, - interact_rect: self.clip_rect().intersect(rect), - sense, - enabled: self.enabled, - }) + self.ctx().create_widget( + WidgetRect { + id, + layer_id: self.layer_id(), + rect, + interact_rect: self.clip_rect().intersect(rect), + sense, + enabled: self.enabled, + }, + true, + ) } /// Deprecated: use [`Self::interact`] instead. @@ -970,10 +995,62 @@ impl Ui { self.interact(rect, id, sense) } + /// Read the [`Ui`]s background [`Response`]. + /// It's [`Sense`] will be based on the [`UiBuilder::sense`] used to create this [`Ui`]. + /// + /// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`] + /// of the last frame. + /// + /// On the first frame, when the [`Ui`] is created, this will return a [`Response`] with a + /// [`Rect`] of [`Rect::NOTHING`]. + pub fn response(&self) -> Response { + // This is the inverse of Context::read_response. We prefer a response + // based on last frame's widget rect since the one from this frame is Rect::NOTHING until + // Ui::interact_bg is called or the Ui is dropped. + self.ctx() + .viewport(|viewport| { + viewport + .prev_frame + .widgets + .get(self.id) + .or_else(|| viewport.this_frame.widgets.get(self.id)) + .copied() + }) + .map(|widget_rect| self.ctx().get_response(widget_rect)) + .expect( + "Since we always call Context::create_widget in Ui::new, this should never be None", + ) + } + + /// Update the [`WidgetRect`] created in [`Ui::new`] or [`Ui::new_child`] with the current + /// [`Ui::min_rect`]. + fn remember_min_rect(&mut self) -> Response { + self.min_rect_already_remembered = true; + // We remove the id from used_ids to prevent a duplicate id warning from showing + // when the ui was created with `UiBuilder::sense`. + // This is a bit hacky, is there a better way? + self.ctx().frame_state_mut(|fs| { + fs.used_ids.remove(&self.id); + }); + // This will update the WidgetRect that was first created in `Ui::new`. + self.ctx().create_widget( + WidgetRect { + id: self.id, + layer_id: self.layer_id(), + rect: self.min_rect(), + interact_rect: self.clip_rect().intersect(self.min_rect()), + sense: self.sense, + enabled: self.enabled, + }, + false, + ) + } + /// 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`]. + #[deprecated = "Use UiBuilder::sense with Ui::response instead"] 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) @@ -996,7 +1073,7 @@ impl Ui { /// /// Note that this tests against the _current_ [`Ui::min_rect`]. /// If you want to test against the final `min_rect`, - /// use [`Self::interact_bg`] instead. + /// use [`Self::response`] instead. pub fn ui_contains_pointer(&self) -> bool { self.rect_contains_pointer(self.min_rect()) } @@ -2142,7 +2219,8 @@ impl Ui { let mut child_ui = self.new_child(ui_builder); self.next_auto_id_salt = next_auto_id_salt; // HACK: we want `scope` to only increment this once, so that `ui.scope` is equivalent to `ui.allocate_space`. let ret = add_contents(&mut child_ui); - let response = self.allocate_rect(child_ui.min_rect(), Sense::hover()); + let response = child_ui.remember_min_rect(); + self.allocate_rect(child_ui.min_rect(), Sense::hover()); InnerResponse::new(ret, response) } @@ -2808,9 +2886,13 @@ impl Ui { } } -#[cfg(debug_assertions)] impl Drop for Ui { fn drop(&mut self) { + if !self.min_rect_already_remembered { + // Register our final `min_rect` + self.remember_min_rect(); + } + #[cfg(debug_assertions)] register_rect(self, self.min_rect()); } } diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 34d4acbb4ab..cfbee61ef2e 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -1,6 +1,6 @@ use std::{hash::Hash, sync::Arc}; -use crate::{Id, Layout, Rect, Style, UiStackInfo}; +use crate::{Id, Layout, Rect, Sense, Style, UiStackInfo}; #[allow(unused_imports)] // Used for doclinks use crate::Ui; @@ -21,6 +21,7 @@ pub struct UiBuilder { pub invisible: bool, pub sizing_pass: bool, pub style: Option>, + pub sense: Option, } impl UiBuilder { @@ -116,4 +117,16 @@ impl UiBuilder { self.style = Some(style.into()); self } + + /// Set if you want sense clicks and/or drags. Default is [`Sense::hover`]. + /// The sense will be registered below the Senses of any widgets contained in this [`Ui`], so + /// if the user clicks a button contained within this [`Ui`], that button will receive the click + /// instead. + /// + /// The response can be read early with [`Ui::response`]. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = Some(sense); + self + } } diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index 7be3897dcea..e69badb8701 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -44,8 +44,8 @@ pub struct WidgetRect { /// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. /// -/// All [`crate::Ui`]s have a [`WidgetRects`], but whether or not their rects are correct -/// depends on if [`crate::Ui::interact_bg`] was ever called. +/// All [`crate::Ui`]s have a [`WidgetRect`]. It is created in [`crate::Ui::new`] with [`Rect::NOTHING`] +/// and updated with the correct [`Rect`] when the [`crate::Ui`] is dropped. #[derive(Default, Clone)] pub struct WidgetRects { /// All widgets, in painting order. diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 576a69e66df..d55cc6aff3f 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -31,6 +31,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), @@ -258,7 +259,8 @@ impl DemoWindows { fn desktop_ui(&mut self, ctx: &Context) { egui::SidePanel::right("egui_demo_panel") .resizable(false) - .default_width(150.0) + .default_width(160.0) + .min_width(160.0) .show(ctx, |ui| { ui.add_space(4.0); ui.vertical_centered(|ui| { diff --git a/crates/egui_demo_lib/src/demo/interactive_container.rs b/crates/egui_demo_lib/src/demo/interactive_container.rs new file mode 100644 index 00000000000..11d8afa48fe --- /dev/null +++ b/crates/egui_demo_lib/src/demo/interactive_container.rs @@ -0,0 +1,87 @@ +use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget}; + +/// Showcase [`egui::Ui::response`]. +#[derive(PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct InteractiveContainerDemo { + count: usize, +} + +impl crate::Demo for InteractiveContainerDemo { + fn name(&self) -> &'static str { + "\u{20E3} Interactive Container" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .default_width(250.0) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for InteractiveContainerDemo { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This demo showcases how to use "); + ui.code("Ui::response"); + ui.label(" to create interactive container widgets that may contain other widgets."); + }); + + let response = ui + .scope_builder( + UiBuilder::new() + .id_salt("interactive_container") + .sense(Sense::click()), + |ui| { + let response = ui.response(); + let visuals = ui.style().interact(&response); + let text_color = visuals.text_color(); + + Frame::canvas(ui.style()) + .fill(visuals.bg_fill.gamma_multiply(0.3)) + .stroke(visuals.bg_stroke) + .inner_margin(ui.spacing().menu_margin) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + + ui.add_space(32.0); + ui.vertical_centered(|ui| { + Label::new( + RichText::new(format!("{}", self.count)) + .color(text_color) + .size(32.0), + ) + .selectable(false) + .ui(ui); + }); + ui.add_space(32.0); + + ui.horizontal(|ui| { + if ui.button("Reset").clicked() { + self.count = 0; + } + if ui.button("+ 100").clicked() { + self.count += 100; + } + }); + }); + }, + ) + .response; + + if response.clicked() { + self.count += 1; + } + } +} diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index e5d7e595931..828bdd896a3 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -15,6 +15,7 @@ pub mod extra_viewport; pub mod font_book; pub mod frame_demo; pub mod highlighting; +pub mod interactive_container; pub mod misc_demo_window; pub mod multi_touch; pub mod paint_bezier;