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::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();
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.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
136 changes: 109 additions & 27 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ pub struct Ui {

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

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

debug_assert!(
Expand All @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -220,6 +235,7 @@ impl Ui {
invisible,
sizing_pass,
style,
sense,
} = ui_builder;

let mut painter = self.painter.clone();
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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())
}
Expand Down Expand Up @@ -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());
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 @@ -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());
}
}
Expand Down
15 changes: 14 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,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
}
}
Loading
Loading