From bd78a5b14e5e6540796f5acac70767702c0e94e5 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 1 Sep 2024 20:09:04 +0200 Subject: [PATCH 01/11] Add Ui::interact_scope --- crates/egui/src/containers/area.rs | 19 ++--- crates/egui/src/containers/window.rs | 19 ++--- crates/egui/src/context.rs | 8 +-- crates/egui/src/response.rs | 19 ++--- crates/egui/src/ui.rs | 104 ++++++++++++++++++++------- crates/egui/src/ui_builder.rs | 9 ++- 6 files changed, 124 insertions(+), 54 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 378b0edc8d9..3a77e2c0457 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, + }, + false, + ); 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..9fc21a67a5f 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, + }, + false, + ); SideResponse { hover: response.hovered(), drag: response.dragged(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 162ff4a739e..d6496d6b81c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1050,7 +1050,7 @@ impl Context { /// /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { + pub(crate) fn create_widget(&self, w: WidgetRect, ignore_focus: bool) -> Response { // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1060,12 +1060,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 w.sense.focusable && !ignore_focus { ctx.memory.interested_in_focus(w.id); } }); - if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { + if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() && !ignore_focus { // Not interested or allowed input: self.memory_mut(|mem| mem.surrender_focus(w.id)); } @@ -1078,7 +1078,7 @@ impl Context { let res = self.get_response(w); #[cfg(feature = "accesskit")] - if w.sense.focusable { + if w.sense.focusable && !ignore_focus { // 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. diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index d6a9a8a0c7f..a52d54a709c 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, + }, + false, + ) } /// 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..a0f1fa36d75 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -109,6 +109,7 @@ impl Ui { invisible, sizing_pass, style, + sense, } = ui_builder; debug_assert!( @@ -146,14 +147,17 @@ impl Ui { // 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: sense.unwrap_or(Sense::hover()), + enabled: ui.enabled, + }, + false, + ); if disabled { ui.disable(); @@ -220,6 +224,7 @@ impl Ui { invisible, sizing_pass, style, + sense, } = ui_builder; let mut painter = self.painter.clone(); @@ -273,14 +278,17 @@ impl Ui { // 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: sense.unwrap_or(Sense::hover()), + enabled: child_ui.enabled, + }, + false, + ); child_ui } @@ -948,14 +956,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, + }, + false, + ) } /// Deprecated: use [`Self::interact`] instead. @@ -975,8 +986,24 @@ impl Ui { /// /// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`]. pub fn interact_bg(&self, sense: Sense) -> Response { + // 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.interact(self.min_rect(), self.id, sense) + 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, + enabled: self.enabled, + }, + true, + ) } /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? @@ -2681,6 +2708,33 @@ impl Ui { (InnerResponse { inner, response }, payload) } + + /// Create a child ui scope and pass in its response. + /// You can use this e.g. to create interactive containers or custom buttons. + /// + /// This basically does three things: + /// 1. Read [`Ui`]s response via [`Context::read_response`]. + /// 2. Create a child ui scope and call the content fn + /// 3. Call [`Ui::interact_bg`] to set the right [`WidgetRect`] + pub fn interact_scope( + &mut self, + sense: Sense, + content: impl FnOnce(&mut Self, Option) -> R, + ) -> InnerResponse { + let id_source = "interact_ui"; + let id = self.id.with(Id::new(id_source)); + let response = self.ctx().read_response(id); + + self.scope_dyn( + UiBuilder::new().sense(sense).id_source(id_source), + Box::new(|ui| { + let inner = content(ui, response); + let response = ui.interact_bg(sense); + InnerResponse::new(inner, response) + }), + ) + .inner + } } /// # Menus diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 34d4acbb4ab..2dee686b9c2 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,10 @@ impl UiBuilder { self.style = Some(style.into()); self } + + /// Sense of the Ui. Should be the same as the one passed to [`Ui::interact_bg`] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = Some(sense); + self + } } From 043f899171eea622607e238294df40dd52260450 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 1 Sep 2024 20:34:34 +0200 Subject: [PATCH 02/11] Remember sense and remove it from interact_bg --- crates/egui/src/menu.rs | 2 +- crates/egui/src/ui.rs | 28 +++++++++++++++++++--------- crates/egui/src/ui_builder.rs | 1 + 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 1683125d4e9..11ce4ed7dde 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.interact_bg().contains_pointer() && !button.hovered() && !self.hovering_current_submenu(&pointer) { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index a0f1fa36d75..2c66280f2db 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -89,6 +89,10 @@ pub struct Ui { /// The [`UiStack`] for this [`Ui`]. stack: Arc, + + /// The sense for the ui background. + /// It will be used for [Ui::interact_bg]. + sense: Sense, } impl Ui { @@ -123,6 +127,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 { @@ -143,6 +148,7 @@ impl Ui { sizing_pass: false, menu_state: None, stack: Arc::new(ui_stack), + sense, }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -153,7 +159,7 @@ impl Ui { layer_id: ui.layer_id(), rect: start_rect, interact_rect: start_rect, - sense: sense.unwrap_or(Sense::hover()), + sense, enabled: ui.enabled, }, false, @@ -239,6 +245,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, @@ -274,6 +281,7 @@ impl Ui { sizing_pass, menu_state: self.menu_state.clone(), stack: Arc::new(ui_stack), + sense, }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -284,7 +292,7 @@ impl Ui { layer_id: child_ui.layer_id(), rect: start_rect, interact_rect: start_rect, - sense: sense.unwrap_or(Sense::hover()), + sense, enabled: child_ui.enabled, }, false, @@ -985,7 +993,8 @@ impl 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 { + /// You can customize the [`Sense`] via [`UiBuilder::sense`]. + pub fn interact_bg(&self) -> Response { // 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? @@ -999,7 +1008,7 @@ impl Ui { layer_id: self.layer_id(), rect: self.min_rect(), interact_rect: self.clip_rect().intersect(self.min_rect()), - sense, + sense: self.sense, enabled: self.enabled, }, true, @@ -2709,8 +2718,8 @@ impl Ui { (InnerResponse { inner, response }, payload) } - /// Create a child ui scope and pass in its response. - /// You can use this e.g. to create interactive containers or custom buttons. + /// Create a child ui scope and pass its response to the closure. + /// You can use this to easily create interactive containers or custom buttons. /// /// This basically does three things: /// 1. Read [`Ui`]s response via [`Context::read_response`]. @@ -2719,17 +2728,18 @@ impl Ui { pub fn interact_scope( &mut self, sense: Sense, + builder: UiBuilder, content: impl FnOnce(&mut Self, Option) -> R, ) -> InnerResponse { - let id_source = "interact_ui"; + let id_source = builder.id_source.unwrap_or(Id::new("interact_scope")); let id = self.id.with(Id::new(id_source)); let response = self.ctx().read_response(id); self.scope_dyn( - UiBuilder::new().sense(sense).id_source(id_source), + builder.sense(sense).id_source(id_source), Box::new(|ui| { let inner = content(ui, response); - let response = ui.interact_bg(sense); + let response = ui.interact_bg(); InnerResponse::new(inner, response) }), ) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 2dee686b9c2..fcb74c27e79 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -119,6 +119,7 @@ impl UiBuilder { } /// Sense of the Ui. Should be the same as the one passed to [`Ui::interact_bg`] + #[inline] pub fn sense(mut self, sense: Sense) -> Self { self.sense = Some(sense); self From 36ee61bab60b19365b00e76cd74453014f88f062 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 2 Sep 2024 11:13:09 +0200 Subject: [PATCH 03/11] Invert ignore_focus to allow_focus --- crates/egui/src/containers/area.rs | 2 +- crates/egui/src/containers/window.rs | 2 +- crates/egui/src/context.rs | 11 +++++++---- crates/egui/src/response.rs | 2 +- crates/egui/src/ui.rs | 8 ++++---- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 3a77e2c0457..db9bd496fa6 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -471,7 +471,7 @@ impl Area { sense, enabled, }, - false, + true, ); if movable && move_response.dragged() { diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 9fc21a67a5f..438d562ede7 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -842,7 +842,7 @@ fn resize_interaction( sense: Sense::drag(), enabled: true, }, - false, + true, ); SideResponse { hover: response.hovered(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index d6496d6b81c..92d3e236b33 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::interact_bg`] (false)). #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, w: WidgetRect, ignore_focus: bool) -> 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 && !ignore_focus { + if allow_focus && w.sense.focusable { ctx.memory.interested_in_focus(w.id); } }); - if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() && !ignore_focus { + 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 && !ignore_focus { + 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. diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index a52d54a709c..8dadc24df2a 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -866,7 +866,7 @@ impl Response { sense: self.sense | sense, enabled: self.enabled, }, - false, + true, ) } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 2c66280f2db..83d10ab1cb6 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -162,7 +162,7 @@ impl Ui { sense, enabled: ui.enabled, }, - false, + true, ); if disabled { @@ -295,7 +295,7 @@ impl Ui { sense, enabled: child_ui.enabled, }, - false, + true, ); child_ui @@ -973,7 +973,7 @@ impl Ui { sense, enabled: self.enabled, }, - false, + true, ) } @@ -1011,7 +1011,7 @@ impl Ui { sense: self.sense, enabled: self.enabled, }, - true, + false, ) } From 556de04858813a66321cae959615d7d037bbfe8a Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 2 Sep 2024 11:21:36 +0200 Subject: [PATCH 04/11] Fixes after rebase --- crates/egui/src/ui.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 83d10ab1cb6..2f671edd041 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2731,12 +2731,12 @@ impl Ui { builder: UiBuilder, content: impl FnOnce(&mut Self, Option) -> R, ) -> InnerResponse { - let id_source = builder.id_source.unwrap_or(Id::new("interact_scope")); - let id = self.id.with(Id::new(id_source)); + let id_salt = builder.id_salt.unwrap_or(Id::new("interact_scope")); + let id = self.id.with(Id::new(id_salt)); let response = self.ctx().read_response(id); self.scope_dyn( - builder.sense(sense).id_source(id_source), + builder.sense(sense).id_salt(id_salt), Box::new(|ui| { let inner = content(ui, response); let response = ui.interact_bg(); From 3556b7295e6dee4b5b8f78306cd0d3fc614c5c82 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 2 Sep 2024 12:15:55 +0200 Subject: [PATCH 05/11] Add Ui::read_response and remove Ui::interact_scope --- crates/egui/src/context.rs | 2 +- crates/egui/src/menu.rs | 2 +- crates/egui/src/ui.rs | 71 ++++++++++++++++++++++---------------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 92d3e236b33..4e44ddc1f10 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1117,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 11ce4ed7dde..7f7575ad253 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().contains_pointer() + && ui.read_response().contains_pointer() && !button.hovered() && !self.hovering_current_submenu(&pointer) { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 2f671edd041..b663571eb24 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -93,6 +93,9 @@ pub struct Ui { /// The sense for the ui background. /// It will be used for [Ui::interact_bg]. sense: Sense, + + /// Whether [`Ui::interact_bg`] should be called when the [`Ui`] is dropped. + should_interact_bg_on_drop: bool, } impl Ui { @@ -149,6 +152,7 @@ impl Ui { menu_state: None, stack: Arc::new(ui_stack), sense, + should_interact_bg_on_drop: true, }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -282,6 +286,7 @@ impl Ui { menu_state: self.menu_state.clone(), stack: Arc::new(ui_stack), sense, + should_interact_bg_on_drop: true, }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -989,11 +994,40 @@ 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 read_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", + ) + } + /// 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`]. /// You can customize the [`Sense`] via [`UiBuilder::sense`]. + // This is marked as deprecated for public use but still makes sense to use internally. + #[deprecated = "Use Ui::read_response instead"] pub fn interact_bg(&self) -> Response { // We remove the id from used_ids to prevent a duplicate id warning from showing // when the ui was created with `UiBuilder::sense`. @@ -2178,7 +2212,10 @@ 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()); + #[allow(deprecated)] + let response = child_ui.interact_bg(); + child_ui.should_interact_bg_on_drop = false; + self.allocate_rect(child_ui.min_rect(), Sense::hover()); InnerResponse::new(ret, response) } @@ -2717,34 +2754,6 @@ impl Ui { (InnerResponse { inner, response }, payload) } - - /// Create a child ui scope and pass its response to the closure. - /// You can use this to easily create interactive containers or custom buttons. - /// - /// This basically does three things: - /// 1. Read [`Ui`]s response via [`Context::read_response`]. - /// 2. Create a child ui scope and call the content fn - /// 3. Call [`Ui::interact_bg`] to set the right [`WidgetRect`] - pub fn interact_scope( - &mut self, - sense: Sense, - builder: UiBuilder, - content: impl FnOnce(&mut Self, Option) -> R, - ) -> InnerResponse { - let id_salt = builder.id_salt.unwrap_or(Id::new("interact_scope")); - let id = self.id.with(Id::new(id_salt)); - let response = self.ctx().read_response(id); - - self.scope_dyn( - builder.sense(sense).id_salt(id_salt), - Box::new(|ui| { - let inner = content(ui, response); - let response = ui.interact_bg(); - InnerResponse::new(inner, response) - }), - ) - .inner - } } /// # Menus @@ -2875,6 +2884,10 @@ impl Ui { #[cfg(debug_assertions)] impl Drop for Ui { fn drop(&mut self) { + if self.should_interact_bg_on_drop { + #[allow(deprecated)] + self.interact_bg(); + } register_rect(self, self.min_rect()); } } From d02906df62c99d9dbd77ebd2e49f0b43842ea6ee Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 2 Sep 2024 12:55:03 +0200 Subject: [PATCH 06/11] Add interactive_container demo --- .../src/demo/demo_app_windows.rs | 1 + .../src/demo/interactive_container.rs | 81 +++++++++++++++++++ crates/egui_demo_lib/src/demo/mod.rs | 1 + 3 files changed, 83 insertions(+) create mode 100644 crates/egui_demo_lib/src/demo/interactive_container.rs 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..6797844f6e9 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(), 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..ee0fc9e9fa8 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/interactive_container.rs @@ -0,0 +1,81 @@ +use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget}; + +/// Showcase [`egui::Ui::read_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}TextEdit" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .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.label("This demo showcases how to use Ui::read_response 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.read_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; From 1b4fcebc2ebd1012ad11857295f511852a0817c9 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 2 Sep 2024 13:04:26 +0200 Subject: [PATCH 07/11] Fix example title --- crates/egui_demo_lib/src/demo/interactive_container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/demo/interactive_container.rs b/crates/egui_demo_lib/src/demo/interactive_container.rs index ee0fc9e9fa8..4de0ff70c76 100644 --- a/crates/egui_demo_lib/src/demo/interactive_container.rs +++ b/crates/egui_demo_lib/src/demo/interactive_container.rs @@ -10,7 +10,7 @@ pub struct InteractiveContainerDemo { impl crate::Demo for InteractiveContainerDemo { fn name(&self) -> &'static str { - "\u{20E3}TextEdit" + "\u{20E3} Interactive Container" } fn show(&mut self, ctx: &egui::Context, open: &mut bool) { From 51266e9172cfd960445eae23a23f318d5481631d Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 2 Sep 2024 13:11:32 +0200 Subject: [PATCH 08/11] Example improvements --- crates/egui_demo_lib/src/demo/demo_app_windows.rs | 3 ++- crates/egui_demo_lib/src/demo/interactive_container.rs | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) 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 6797844f6e9..d55cc6aff3f 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -259,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 index 4de0ff70c76..083d4eb0c1a 100644 --- a/crates/egui_demo_lib/src/demo/interactive_container.rs +++ b/crates/egui_demo_lib/src/demo/interactive_container.rs @@ -17,6 +17,7 @@ impl crate::Demo for InteractiveContainerDemo { egui::Window::new(self.name()) .open(open) .resizable(false) + .default_width(250.0) .show(ctx, |ui| { use crate::View as _; self.ui(ui); @@ -30,7 +31,12 @@ impl crate::View for InteractiveContainerDemo { ui.add(crate::egui_github_link_file!()); }); - ui.label("This demo showcases how to use Ui::read_response to create interactive container widgets that may contain other widgets."); + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This demo showcases how to use "); + ui.code("Ui::read_response"); + ui.label(" to create interactive container widgets that may contain other widgets."); + }); let response = ui .scope_builder( From 22466f92ee84664b3ea844cc630f04b28b82923f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 6 Sep 2024 07:35:21 +0200 Subject: [PATCH 09/11] Update comment --- crates/egui/src/widget_rect.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From 6cf190696ae04d29f222ef794e28db1fc64a40fd Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 10 Sep 2024 18:20:02 +0200 Subject: [PATCH 10/11] Apply suggestions from emil Co-authored-by: Emil Ernerfeldt --- crates/egui/src/menu.rs | 2 +- crates/egui/src/ui.rs | 5 +++-- crates/egui/src/ui_builder.rs | 4 +++- crates/egui_demo_lib/src/demo/interactive_container.rs | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 7f7575ad253..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.read_response().contains_pointer() + && ui.response().contains_pointer() && !button.hovered() && !self.hovering_current_submenu(&pointer) { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index b663571eb24..405aed28dfb 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1002,7 +1002,7 @@ impl Ui { /// /// On the first frame, when the [`Ui`] is created, this will return a [`Response`] with a /// [`Rect`] of [`Rect::NOTHING`]. - pub fn read_response(&self) -> Response { + 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. @@ -1027,7 +1027,7 @@ impl Ui { /// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`]. /// You can customize the [`Sense`] via [`UiBuilder::sense`]. // This is marked as deprecated for public use but still makes sense to use internally. - #[deprecated = "Use Ui::read_response instead"] + #[deprecated = "Use Uibuilder::sense with Ui::response instead"] pub fn interact_bg(&self) -> Response { // We remove the id from used_ids to prevent a duplicate id warning from showing // when the ui was created with `UiBuilder::sense`. @@ -2886,6 +2886,7 @@ impl Drop for Ui { fn drop(&mut self) { if self.should_interact_bg_on_drop { #[allow(deprecated)] + // Register our final `min_rect` self.interact_bg(); } register_rect(self, self.min_rect()); diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index fcb74c27e79..c19af8c336f 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -118,7 +118,9 @@ impl UiBuilder { self } - /// Sense of the Ui. Should be the same as the one passed to [`Ui::interact_bg`] + /// Set if you want sense clicks and/or drags. + /// + /// The response can be read with [`Ui::response`]. #[inline] pub fn sense(mut self, sense: Sense) -> Self { self.sense = Some(sense); diff --git a/crates/egui_demo_lib/src/demo/interactive_container.rs b/crates/egui_demo_lib/src/demo/interactive_container.rs index 083d4eb0c1a..11d8afa48fe 100644 --- a/crates/egui_demo_lib/src/demo/interactive_container.rs +++ b/crates/egui_demo_lib/src/demo/interactive_container.rs @@ -1,6 +1,6 @@ use egui::{Frame, Label, RichText, Sense, UiBuilder, Widget}; -/// Showcase [`egui::Ui::read_response`]. +/// Showcase [`egui::Ui::response`]. #[derive(PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -34,7 +34,7 @@ impl crate::View for InteractiveContainerDemo { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("This demo showcases how to use "); - ui.code("Ui::read_response"); + ui.code("Ui::response"); ui.label(" to create interactive container widgets that may contain other widgets."); }); @@ -44,7 +44,7 @@ impl crate::View for InteractiveContainerDemo { .id_salt("interactive_container") .sense(Sense::click()), |ui| { - let response = ui.read_response(); + let response = ui.response(); let visuals = ui.style().interact(&response); let text_color = visuals.text_color(); From aca4a0aef8be254b434977270afed035035c1870 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 10 Sep 2024 18:49:25 +0200 Subject: [PATCH 11/11] Restore interact_bg behavior, rename Ui::should_interact_bg_on_drop and improve docs --- crates/egui/src/context.rs | 2 +- crates/egui/src/ui.rs | 46 +++++++++++++++++++---------------- crates/egui/src/ui_builder.rs | 7 ++++-- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 4e44ddc1f10..ce690c7bb97 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1051,7 +1051,7 @@ impl Context { /// 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::interact_bg`] (false)). + /// 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, allow_focus: bool) -> Response { // Remember this widget diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 405aed28dfb..6a1e7e69bd6 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -91,11 +91,12 @@ pub struct Ui { stack: Arc, /// The sense for the ui background. - /// It will be used for [Ui::interact_bg]. sense: Sense, - /// Whether [`Ui::interact_bg`] should be called when the [`Ui`] is dropped. - should_interact_bg_on_drop: bool, + /// 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 { @@ -152,7 +153,7 @@ impl Ui { menu_state: None, stack: Arc::new(ui_stack), sense, - should_interact_bg_on_drop: true, + min_rect_already_remembered: false, }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -286,7 +287,7 @@ impl Ui { menu_state: self.menu_state.clone(), stack: Arc::new(ui_stack), sense, - should_interact_bg_on_drop: true, + min_rect_already_remembered: false, }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -1021,14 +1022,10 @@ impl Ui { ) } - /// 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`]. - /// You can customize the [`Sense`] via [`UiBuilder::sense`]. - // This is marked as deprecated for public use but still makes sense to use internally. - #[deprecated = "Use Uibuilder::sense with Ui::response instead"] - pub fn interact_bg(&self) -> Response { + /// 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? @@ -1049,6 +1046,16 @@ impl Ui { ) } + /// 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) + } + /// 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, @@ -1066,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()) } @@ -2212,9 +2219,7 @@ 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); - #[allow(deprecated)] - let response = child_ui.interact_bg(); - child_ui.should_interact_bg_on_drop = false; + let response = child_ui.remember_min_rect(); self.allocate_rect(child_ui.min_rect(), Sense::hover()); InnerResponse::new(ret, response) } @@ -2881,14 +2886,13 @@ impl Ui { } } -#[cfg(debug_assertions)] impl Drop for Ui { fn drop(&mut self) { - if self.should_interact_bg_on_drop { - #[allow(deprecated)] + if !self.min_rect_already_remembered { // Register our final `min_rect` - self.interact_bg(); + 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 c19af8c336f..cfbee61ef2e 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -118,9 +118,12 @@ impl UiBuilder { self } - /// Set if you want sense clicks and/or drags. + /// 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 with [`Ui::response`]. + /// The response can be read early with [`Ui::response`]. #[inline] pub fn sense(mut self, sense: Sense) -> Self { self.sense = Some(sense);