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

Interactive Ui:s: add UiBuilder::sense and Ui::response #5054

Merged
merged 11 commits into from
Sep 19, 2024
19 changes: 11 additions & 8 deletions crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 11 additions & 8 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
13 changes: 8 additions & 5 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> Response {
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
// Remember this widget
self.write(|ctx| {
let viewport = ctx.viewport();
Expand All @@ -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));
}
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ impl MenuState {

self.open_submenu(sub_id, pos);
} else if open
&& ui.interact_bg(Sense::hover()).contains_pointer()
&& ui.read_response().contains_pointer()
&& !button.hovered()
&& !self.hovering_current_submenu(&pointer)
{
Expand Down
19 changes: 11 additions & 8 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
131 changes: 104 additions & 27 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ pub struct Ui {

/// The [`UiStack`] for this [`Ui`].
stack: Arc<UiStack>,

/// 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 {
Expand All @@ -109,6 +116,7 @@ impl Ui {
invisible,
sizing_pass,
style,
sense,
} = ui_builder;

debug_assert!(
Expand All @@ -122,6 +130,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 {
Expand All @@ -142,18 +151,23 @@ impl Ui {
sizing_pass: false,
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:
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();
Expand Down Expand Up @@ -220,6 +234,7 @@ impl Ui {
invisible,
sizing_pass,
style,
sense,
} = ui_builder;

let mut painter = self.painter.clone();
Expand All @@ -234,6 +249,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,
Expand Down Expand Up @@ -269,18 +285,23 @@ impl Ui {
sizing_pass,
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:
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
}
Expand Down Expand Up @@ -948,14 +969,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.
Expand All @@ -970,13 +994,59 @@ 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 {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
// 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`].
pub fn interact_bg(&self, sense: Sense) -> Response {
/// 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"]
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
pub fn interact_bg(&self) -> Response {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
// 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: self.sense,
enabled: self.enabled,
},
false,
)
}

/// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]?
Expand Down Expand Up @@ -2142,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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this purely an optimization?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think it's not very elegant but I couldn't come up with a better way. I'll add a comment clarifying that it's an optimization.

self.allocate_rect(child_ui.min_rect(), Sense::hover());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if we should replace this with Ui::advance_cursor_after_rect since we don't need this response?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #5130

InnerResponse::new(ret, response)
}

Expand Down Expand Up @@ -2811,6 +2884,10 @@ impl Ui {
#[cfg(debug_assertions)]
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
impl Drop for Ui {
fn drop(&mut self) {
if self.should_interact_bg_on_drop {
#[allow(deprecated)]
self.interact_bg();
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
}
register_rect(self, self.min_rect());
}
}
Expand Down
10 changes: 9 additions & 1 deletion crates/egui/src/ui_builder.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,6 +21,7 @@ pub struct UiBuilder {
pub invisible: bool,
pub sizing_pass: bool,
pub style: Option<Arc<Style>>,
pub sense: Option<Sense>,
}

impl UiBuilder {
Expand Down Expand Up @@ -116,4 +117,11 @@ impl UiBuilder {
self.style = Some(style.into());
self
}

/// Sense of the Ui. Should be the same as the one passed to [`Ui::interact_bg`]
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
self
}
}
4 changes: 2 additions & 2 deletions crates/egui/src/widget_rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading