Skip to content

Commit

Permalink
Interactive Ui:s: add UiBuilder::sense and Ui::response (#5054)
Browse files Browse the repository at this point in the history
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes #5053 
* [x] I have followed the instructions in the PR template


This fixes #5053 by adding a Sense parameter to UiBuilder, using that in
Context::create_widget, so the Widget is registered with the right Sense
/ focusable. Additionally, I've added a ignore_focus param to
create_widget, so the focus isn't surrendered / reregistered on
Ui::interact_bg.

The example from #5053 now works correctly: 


https://github.com/user-attachments/assets/a8a04b5e-7635-4e05-9ed8-e17d64910a35

<details><summary>Updated example code</summary>
<p>

```rust
            ui.button("I can focus");

            ui.scope_builder(
                UiBuilder::new()
                    .sense(Sense::click())
                    .id_source("focus_test"),
                |ui| {
                    ui.label("I can focus for a single frame");
                    let response = ui.interact_bg();
                    let t = if response.has_focus() {
                        "has focus"
                    } else {
                        "doesn't have focus"
                    };
                    ui.label(t);
                },
            );

            ui.button("I can't focus :(");
```

</p>
</details> 



---

Also, I've added `Ui::interact_scope` to make it easier to read a Ui's
response in advance, without having to know about the internals of how
the Ui Ids get created.

This makes it really easy to created interactive container elements or
custom buttons, without having to use Galleys or
Painter::add(Shape::Noop) to style based on the interaction.

<details><summary>
Example usage to create a simple button
</summary>
<p>


```rust
use eframe::egui;
use eframe::egui::{Frame, InnerResponse, Label, RichText, UiBuilder, Widget};
use eframe::NativeOptions;
use egui::{CentralPanel, Sense, WidgetInfo};

pub fn main() -> eframe::Result {
    eframe::run_simple_native("focus test", NativeOptions::default(), |ctx, _frame| {
        CentralPanel::default().show(ctx, |ui| {
            ui.button("Regular egui Button");
            custom_button(ui, |ui| {
                ui.label("Custom Button");
            });

            if custom_button(ui, |ui| {
                ui.label("You can even have buttons inside buttons:");

                if ui.button("button inside button").clicked() {
                    println!("Button inside button clicked!");
                }
            })
            .response
            .clicked()
            {
                println!("Custom button clicked!");
            }
        });
    })
}

fn custom_button<R>(
    ui: &mut egui::Ui,
    content: impl FnOnce(&mut egui::Ui) -> R,
) -> InnerResponse<R> {
    let auto_id = ui.next_auto_id();
    ui.skip_ahead_auto_ids(1);
    let response = ui.interact_scope(
        Sense::click(),
        UiBuilder::new().id_source(auto_id),
        |ui, response| {
            ui.style_mut().interaction.selectable_labels = false;
            let visuals = response
                .map(|r| ui.style().interact(&r))
                .unwrap_or(&ui.visuals().noninteractive());
            let text_color = visuals.text_color();

            Frame::none()
                .fill(visuals.bg_fill)
                .stroke(visuals.bg_stroke)
                .rounding(visuals.rounding)
                .inner_margin(ui.spacing().button_padding)
                .show(ui, |ui| {
                    ui.visuals_mut().override_text_color = Some(text_color);
                    content(ui)
                })
                .inner
        },
    );

    response
        .response
        .widget_info(|| WidgetInfo::new(egui::WidgetType::Button));

    response
}
```

</p>
</details> 



https://github.com/user-attachments/assets/281bd65f-f616-4621-9764-18fd0d07698b

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
lucasmerlin and emilk authored Sep 19, 2024
1 parent bfafddf commit 1b8737c
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 61 deletions.
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 @@ -1114,8 +1114,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 @@ -1125,12 +1128,12 @@ impl Context {
// but also to know when we have reached the widget we are checking for cover.
viewport.this_pass.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 @@ -1143,7 +1146,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 @@ -1179,7 +1182,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 @@ -875,14 +875,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 @@ -90,6 +90,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 @@ -110,6 +118,7 @@ impl Ui {
invisible,
sizing_pass,
style,
sense,
} = ui_builder;

debug_assert!(
Expand All @@ -122,6 +131,7 @@ impl Ui {
let layout = layout.unwrap_or_default();
let disabled = disabled || invisible;
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,
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 @@ -217,6 +232,7 @@ impl Ui {
invisible,
sizing_pass,
style,
sense,
} = ui_builder;

let mut painter = self.painter.clone();
Expand All @@ -230,6 +246,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 @@ -265,18 +282,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 @@ -972,14 +994,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 @@ -994,10 +1019,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 @@ -1020,7 +1097,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 @@ -2168,7 +2245,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)
}

Expand Down Expand Up @@ -2861,9 +2939,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 @@ -114,4 +115,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

0 comments on commit 1b8737c

Please sign in to comment.