Use bitfield instead of bools in `Response` and `Sense` (#5556)

<!--
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 <https://github.com/emilk/egui/issues/3862>.

Factoring the `bool` members of `Response` into a bitfield, the size of
`Response` is now 96 bytes (down from 104).

I gave `Sense` the same treatment, however this has no effects on
`Response` due to padding. I've decided not to pursue `PointerState`, as
it is quite large (_many_ members that are sized and aligned to
multiples of 8 bytes), so I don't expect any noticeable benefit from
making handful of `bool`s slightly leaner.

In any case, the changes to `Sense` are already quite a bit more
intrusive than those to `Response`.
The previous implementation overloaded the names of the attributes
`click` and `drag` with similarly named methods that _construct_ `Sense`
with the corresponding flag set. Now, that the attributes can no longer
be accessed directly, I had to introduce methods with new names
(`senses_click()`, `senses_drag()` and `is_focusable()`). I don't think
this is the cleanest solution: the old methods are essentially redundant
now that the named constants like `Sense::CLICK` exist. I did however
not want to needlessly break backwards compatibility.
I am happy to revert it (or go further 🙂) if there are concerns.
This commit is contained in:
Pol Welter 2025-01-06 19:29:53 +01:00 committed by GitHub
parent 0fac8eadfc
commit 35860418ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 266 additions and 279 deletions

View File

@ -1260,6 +1260,7 @@ dependencies = [
"accesskit",
"ahash",
"backtrace",
"bitflags 2.6.0",
"document-features",
"emath",
"epaint",

View File

@ -73,6 +73,7 @@ ahash = { version = "0.8.11", default-features = false, features = [
"std",
] }
backtrace = "0.3"
bitflags = "2.6"
bytemuck = "1.7.2"
criterion = { version = "0.5.1", default-features = false }
dify = { version = "0.7", default-features = false }

View File

@ -148,6 +148,7 @@ Light Theme:
* [`ab_glyph`](https://crates.io/crates/ab_glyph)
* [`ahash`](https://crates.io/crates/ahash)
* [`bitflags`](https://crates.io/crates/bitflags)
* [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
* [`parking_lot`](https://crates.io/crates/parking_lot)

View File

@ -84,6 +84,7 @@ emath = { workspace = true, default-features = false }
epaint = { workspace = true, default-features = false }
ahash.workspace = true
bitflags.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true

View File

@ -86,11 +86,7 @@ impl Modal {
response,
} = area.show(ctx, |ui| {
let bg_rect = ui.ctx().screen_rect();
let bg_sense = Sense {
click: true,
drag: true,
focusable: false,
};
let bg_sense = Sense::CLICK | Sense::DRAG;
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
backdrop.set_min_size(bg_rect.size());
ui.painter().rect_filled(bg_rect, 0.0, backdrop_color);
@ -101,14 +97,9 @@ impl Modal {
// We need the extra scope with the sense since frame can't have a sense and since we
// need to prevent the clicks from passing through to the backdrop.
let inner = ui
.scope_builder(
UiBuilder::new().sense(Sense {
click: true,
drag: true,
focusable: false,
}),
|ui| frame.show(ui, content).inner,
)
.scope_builder(UiBuilder::new().sense(Sense::CLICK | Sense::DRAG), |ui| {
frame.show(ui, content).inner
})
.inner;
(inner, backdrop_response)

View File

@ -30,7 +30,7 @@ use crate::{
os::OperatingSystem,
output::FullOutput,
pass_state::PassState,
resize, scroll_area,
resize, response, scroll_area,
util::IdTypeMap,
viewport::ViewportClass,
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
@ -1151,8 +1151,9 @@ impl Context {
/// 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 {
let interested_in_focus =
w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id));
let interested_in_focus = w.enabled
&& w.sense.is_focusable()
&& self.memory(|mem| mem.allows_interaction(w.layer_id));
// Remember this widget
self.write(|ctx| {
@ -1173,7 +1174,7 @@ impl Context {
self.memory_mut(|mem| mem.surrender_focus(w.id));
}
if w.sense.interactive() || w.sense.focusable {
if w.sense.interactive() || w.sense.is_focusable() {
self.check_for_id_clash(w.id, w.rect, "widget");
}
@ -1181,7 +1182,7 @@ impl Context {
let res = self.get_response(w);
#[cfg(feature = "accesskit")]
if allow_focus && w.sense.focusable {
if allow_focus && w.sense.is_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.
@ -1213,11 +1214,13 @@ impl Context {
#[deprecated = "Use Response.contains_pointer or Context::read_response instead"]
pub fn widget_contains_pointer(&self, id: Id) -> bool {
self.read_response(id)
.map_or(false, |response| response.contains_pointer)
.map_or(false, |response| response.contains_pointer())
}
/// Do all interaction for an existing widget, without (re-)registering it.
pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response {
use response::Flags;
let WidgetRect {
id,
layer_id,
@ -1237,61 +1240,72 @@ impl Context {
rect,
interact_rect,
sense,
enabled,
contains_pointer: false,
hovered: false,
highlighted,
clicked: false,
fake_primary_click: false,
long_touched: false,
drag_started: false,
dragged: false,
drag_stopped: false,
is_pointer_button_down_on: false,
flags: Flags::empty(),
interact_pointer_pos: None,
changed: false,
intrinsic_size: None,
};
res.flags.set(Flags::ENABLED, enabled);
res.flags.set(Flags::HIGHLIGHTED, highlighted);
self.write(|ctx| {
let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default();
res.contains_pointer = viewport.interact_widgets.contains_pointer.contains(&id);
res.flags.set(
Flags::CONTAINS_POINTER,
viewport.interact_widgets.contains_pointer.contains(&id),
);
let input = &viewport.input;
let memory = &mut ctx.memory;
if enabled
&& sense.click
&& sense.senses_click()
&& memory.has_focus(id)
&& (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter))
{
// Space/enter works like a primary click for e.g. selected buttons
res.fake_primary_click = true;
res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true);
}
#[cfg(feature = "accesskit")]
if enabled
&& sense.click
&& sense.senses_click()
&& input.has_accesskit_action_request(id, accesskit::Action::Click)
{
res.fake_primary_click = true;
res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true);
}
if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched {
res.long_touched = true;
if enabled && sense.senses_click() && Some(id) == viewport.interact_widgets.long_touched
{
res.flags.set(Flags::LONG_TOUCHED, true);
}
let interaction = memory.interaction();
res.is_pointer_button_down_on = interaction.potential_click_id == Some(id)
|| interaction.potential_drag_id == Some(id);
res.flags.set(
Flags::IS_POINTER_BUTTON_DOWN_ON,
interaction.potential_click_id == Some(id)
|| interaction.potential_drag_id == Some(id),
);
if res.enabled {
res.hovered = viewport.interact_widgets.hovered.contains(&id);
res.dragged = Some(id) == viewport.interact_widgets.dragged;
res.drag_started = Some(id) == viewport.interact_widgets.drag_started;
res.drag_stopped = Some(id) == viewport.interact_widgets.drag_stopped;
if res.enabled() {
res.flags.set(
Flags::HOVERED,
viewport.interact_widgets.hovered.contains(&id),
);
res.flags.set(
Flags::DRAGGED,
Some(id) == viewport.interact_widgets.dragged,
);
res.flags.set(
Flags::DRAG_STARTED,
Some(id) == viewport.interact_widgets.drag_started,
);
res.flags.set(
Flags::DRAG_STOPPED,
Some(id) == viewport.interact_widgets.drag_stopped,
);
}
let clicked = Some(id) == viewport.interact_widgets.clicked;
@ -1304,20 +1318,22 @@ impl Context {
any_press = true;
}
PointerEvent::Released { click, .. } => {
if enabled && sense.click && clicked && click.is_some() {
res.clicked = true;
if enabled && sense.senses_click() && clicked && click.is_some() {
res.flags.set(Flags::CLICKED, true);
}
res.is_pointer_button_down_on = false;
res.dragged = false;
res.flags.set(Flags::IS_POINTER_BUTTON_DOWN_ON, false);
res.flags.set(Flags::DRAGGED, false);
}
}
}
// is_pointer_button_down_on is false when released, but we want interact_pointer_pos
// to still work.
let is_interacted_with =
res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped;
let is_interacted_with = res.is_pointer_button_down_on()
|| res.long_touched()
|| clicked
|| res.drag_stopped();
if is_interacted_with {
res.interact_pointer_pos = input.pointer.interact_pos();
if let (Some(to_global), Some(pos)) = (
@ -1330,10 +1346,10 @@ impl Context {
if input.pointer.any_down() && !is_interacted_with {
// We don't hover widgets while interacting with *other* widgets:
res.hovered = false;
res.flags.set(Flags::HOVERED, false);
}
let pointer_pressed_elsewhere = any_press && !res.hovered;
let pointer_pressed_elsewhere = any_press && !res.hovered();
if pointer_pressed_elsewhere && memory.has_focus(id) {
memory.surrender_focus(id);
}
@ -2152,11 +2168,12 @@ impl Context {
let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING);
for rect in rects {
if rect.sense.interactive() {
let (color, text) = if rect.sense.click && rect.sense.drag {
let (color, text) = if rect.sense.senses_click() && rect.sense.senses_drag()
{
(Color32::from_rgb(0x88, 0, 0x88), "click+drag")
} else if rect.sense.click {
} else if rect.sense.senses_click() {
(Color32::from_rgb(0x88, 0, 0), "click")
} else if rect.sense.drag {
} else if rect.sense.senses_drag() {
(Color32::from_rgb(0, 0, 0x88), "drag")
} else {
// unreachable since we only show interactive
@ -3131,7 +3148,7 @@ impl Context {
// TODO(emilk): `Sense::hover_highlight()`
let response =
ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()));
if response.hovered && is_visible {
if response.hovered() && is_visible {
ui.ctx()
.debug_painter()
.debug_rect(area.rect(), Color32::RED, "");

View File

@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects};
use crate::{ahash, emath, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects};
/// Result of a hit-test against [`WidgetRects`].
///
@ -128,8 +128,8 @@ pub fn hit_test(
// the `enabled` flag everywhere:
for w in &mut close {
if !w.enabled {
w.sense.click = false;
w.sense.drag = false;
w.sense -= Sense::CLICK;
w.sense -= Sense::DRAG;
}
}
@ -158,11 +158,11 @@ pub fn hit_test(
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.drag {
debug_assert!(wr.sense.drag);
debug_assert!(wr.sense.senses_drag());
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.click {
debug_assert!(wr.sense.click);
debug_assert!(wr.sense.senses_click());
restore_widget_rect(wr);
}
}
@ -179,8 +179,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
#![allow(clippy::collapsible_else_if)]
// First find the best direct hits:
let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0);
let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0);
let hit_click = find_closest_within(
close.iter().copied().filter(|w| w.sense.senses_click()),
pos,
0.0,
);
let hit_drag = find_closest_within(
close.iter().copied().filter(|w| w.sense.senses_drag()),
pos,
0.0,
);
match (hit_click, hit_drag) {
(None, None) => {
@ -190,14 +198,14 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
close
.iter()
.copied()
.filter(|w| w.sense.click || w.sense.drag),
.filter(|w| w.sense.senses_click() || w.sense.senses_drag()),
pos,
);
if let Some(closest) = closest {
WidgetHits {
click: closest.sense.click.then_some(closest),
drag: closest.sense.drag.then_some(closest),
click: closest.sense.senses_click().then_some(closest),
drag: closest.sense.senses_drag().then_some(closest),
..Default::default()
}
} else {
@ -218,9 +226,12 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// or a moveable window.
// It could also be something small, like a slider, or panel resize handle.
let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos);
let closest_click = find_closest(
close.iter().copied().filter(|w| w.sense.senses_click()),
pos,
);
if let Some(closest_click) = closest_click {
if closest_click.sense.drag {
if closest_click.sense.senses_drag() {
// We have something close that sense both clicks and drag.
// Should we use it over the direct drag-hit?
if hit_drag
@ -244,7 +255,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
}
}
} else {
// These is a close pure-click widget.
// This is a close pure-click widget.
// However, we should be careful to only return two different widgets
// when it is absolutely not going to confuse the user.
if hit_drag
@ -277,7 +288,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
close
.iter()
.copied()
.filter(|w| w.sense.drag && w.id != hit_drag.id),
.filter(|w| w.sense.senses_drag() && w.id != hit_drag.id),
pos,
);
@ -331,7 +342,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
let click_is_on_top_of_drag = drag_idx < click_idx;
if click_is_on_top_of_drag {
if hit_click.sense.drag {
if hit_click.sense.senses_drag() {
// The top thing senses both clicks and drags.
WidgetHits {
click: Some(hit_click),
@ -349,7 +360,7 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
}
}
} else {
if hit_drag.sense.click {
if hit_drag.sense.senses_click() {
// The top thing senses both clicks and drags.
WidgetHits {
click: Some(hit_drag),
@ -393,7 +404,7 @@ fn find_closest_within(
if dist_sq == closest_dist_sq {
// It's a tie! Pick the thin candidate over the thick one.
// This makes it easier to hit a thin resize-handle, for instance:
if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) {
if should_prioritize_hits_on_back(closest.interact_rect, widget.interact_rect) {
continue;
}
}
@ -409,12 +420,12 @@ fn find_closest_within(
closest
}
/// Should we prioritizie hits on `back` over those on `front`?
/// Should we prioritize hits on `back` over those on `front`?
///
/// `back` should be behind the `front` widget.
///
/// Returns true if `back` is a small hit-target and `front` is not.
fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool {
fn should_prioritize_hits_on_back(back: Rect, front: Rect) -> bool {
if front.contains_rect(back) {
return false; // back widget is fully occluded; no way to hit it
}
@ -484,7 +495,7 @@ mod tests {
assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag"));
assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag"));
// Close hit - should still ignore the drag-background so as not to confuse the userr:
// Close hit - should still ignore the drag-background so as not to confuse the user:
let hits = hit_test_on_close(&widgets, pos2(105.0, 5.0));
assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag"));
assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag"));

View File

@ -192,14 +192,14 @@ pub(crate) fn interact(
// Check if we started dragging something new:
if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) {
if widget.enabled {
let is_dragged = if widget.sense.click && widget.sense.drag {
let is_dragged = if widget.sense.senses_click() && widget.sense.senses_drag() {
// This widget is sensitive to both clicks and drags.
// When the mouse first is pressed, it could be either,
// so we postpone the decision until we know.
input.pointer.is_decidedly_dragging()
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.drag
widget.sense.senses_drag()
};
if is_dragged {
@ -271,7 +271,7 @@ pub(crate) fn interact(
let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect();
for w in &hits.contains_pointer {
let is_interactive = w.sense.click || w.sense.drag;
let is_interactive = w.sense.senses_click() || w.sense.senses_drag();
if is_interactive {
// The only interactive widgets we mark as hovered are the ones
// in `hits.click` and `hits.drag`!

View File

@ -9,14 +9,14 @@ use crate::{
/// The result of adding a widget to a [`Ui`].
///
/// A [`Response`] lets you know whether or not a widget is being hovered, clicked or dragged.
/// A [`Response`] lets you know whether a widget is being hovered, clicked or dragged.
/// It also lets you easily show a tooltip on hover.
///
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
///
/// ⚠️ The `Response` contains a clone of [`Context`], and many methods lock the `Context`.
/// It can therefor be a deadlock to use `Context` from within a context-locking closures,
/// It can therefore be a deadlock to use `Context` from within a context-locking closures,
/// such as [`Context::input`].
#[derive(Clone, Debug)]
pub struct Response {
@ -50,78 +50,12 @@ pub struct Response {
/// (that is handled by the `Painter` directly).
pub sense: Sense,
/// Was the widget enabled?
/// If `false`, there was no interaction attempted (not even hover).
#[doc(hidden)]
pub enabled: bool,
// OUT:
/// The pointer is above this widget with no other blocking it.
#[doc(hidden)]
pub contains_pointer: bool,
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
#[doc(hidden)]
pub hovered: bool,
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
#[doc(hidden)]
pub highlighted: bool,
/// This widget was clicked this frame.
///
/// Which pointer and how many times we don't know,
/// and ask [`crate::InputState`] about at runtime.
///
/// This is only set to true if the widget was clicked
/// by an actual mouse.
#[doc(hidden)]
pub clicked: bool,
/// This widget should act as if clicked due
/// to something else than a click.
///
/// This is set to true if the widget has keyboard focus and
/// the user hit the Space or Enter key.
#[doc(hidden)]
pub fake_primary_click: bool,
/// This widget was long-pressed on a touch screen to simulate a secondary click.
#[doc(hidden)]
pub long_touched: bool,
/// The widget started being dragged this frame.
#[doc(hidden)]
pub drag_started: bool,
/// The widget is being dragged.
#[doc(hidden)]
pub dragged: bool,
/// The widget was being dragged, but now it has been released.
#[doc(hidden)]
pub drag_stopped: bool,
/// Is the pointer button currently down on this widget?
/// This is true if the pointer is pressing down or dragging a widget
#[doc(hidden)]
pub is_pointer_button_down_on: bool,
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
#[doc(hidden)]
pub interact_pointer_pos: Option<Pos2>,
/// Was the underlying data changed?
///
/// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc.
/// Always `false` for something like a [`Button`](crate::Button).
///
/// Note that this can be `true` even if the user did not interact with the widget,
/// for instance if an existing slider value was clamped to the given range.
#[doc(hidden)]
pub changed: bool,
/// The intrinsic / desired size of the widget.
///
/// For a button, this will be the size of the label + the frames padding,
@ -133,6 +67,73 @@ pub struct Response {
/// for improved layouting.
/// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
pub intrinsic_size: Option<Vec2>,
#[doc(hidden)]
pub flags: Flags,
}
/// A bit set for various boolean properties of `Response`.
#[doc(hidden)]
#[derive(Copy, Clone, Debug)]
pub struct Flags(u16);
bitflags::bitflags! {
impl Flags: u16 {
/// Was the widget enabled?
/// If `false`, there was no interaction attempted (not even hover).
const ENABLED = 1<<0;
/// The pointer is above this widget with no other blocking it.
const CONTAINS_POINTER = 1<<1;
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
const HOVERED = 1<<2;
/// The widget is highlighted via a call to [`Response::highlight`] or
/// [`Context::highlight_widget`].
const HIGHLIGHTED = 1<<3;
/// This widget was clicked this frame.
///
/// Which pointer and how many times we don't know,
/// and ask [`crate::InputState`] about at runtime.
///
/// This is only set to true if the widget was clicked
/// by an actual mouse.
const CLICKED = 1<<4;
/// This widget should act as if clicked due
/// to something else than a click.
///
/// This is set to true if the widget has keyboard focus and
/// the user hit the Space or Enter key.
const FAKE_PRIMARY_CLICKED = 1<<5;
/// This widget was long-pressed on a touch screen to simulate a secondary click.
const LONG_TOUCHED = 1<<6;
/// The widget started being dragged this frame.
const DRAG_STARTED = 1<<7;
/// The widget is being dragged.
const DRAGGED = 1<<8;
/// The widget was being dragged, but now it has been released.
const DRAG_STOPPED = 1<<9;
/// Is the pointer button currently down on this widget?
/// This is true if the pointer is pressing down or dragging a widget
const IS_POINTER_BUTTON_DOWN_ON = 1<<10;
/// Was the underlying data changed?
///
/// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc.
/// Always `false` for something like a [`Button`](crate::Button).
///
/// Note that this can be `true` even if the user did not interact with the widget,
/// for instance if an existing slider value was clamped to the given range.
const CHANGED = 1<<11;
}
}
impl Response {
@ -150,7 +151,7 @@ impl Response {
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
#[inline(always)]
pub fn clicked(&self) -> bool {
self.fake_primary_click || self.clicked_by(PointerButton::Primary)
self.flags.contains(Flags::FAKE_PRIMARY_CLICKED) || self.clicked_by(PointerButton::Primary)
}
/// Returns true if this widget was clicked this frame by the given mouse button.
@ -163,7 +164,7 @@ impl Response {
/// Use [`Self::secondary_clicked`] instead to also detect that.
#[inline]
pub fn clicked_by(&self, button: PointerButton) -> bool {
self.clicked && self.ctx.input(|i| i.pointer.button_clicked(button))
self.flags.contains(Flags::CLICKED) && self.ctx.input(|i| i.pointer.button_clicked(button))
}
/// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button).
@ -171,7 +172,7 @@ impl Response {
/// This also returns true if the widget was pressed-and-held on a touch screen.
#[inline]
pub fn secondary_clicked(&self) -> bool {
self.long_touched || self.clicked_by(PointerButton::Secondary)
self.flags.contains(Flags::LONG_TOUCHED) || self.clicked_by(PointerButton::Secondary)
}
/// Was this long-pressed on a touch screen?
@ -179,7 +180,7 @@ impl Response {
/// Usually you want to check [`Self::secondary_clicked`] instead.
#[inline]
pub fn long_touched(&self) -> bool {
self.long_touched
self.flags.contains(Flags::LONG_TOUCHED)
}
/// Returns true if this widget was clicked this frame by the middle mouse button.
@ -203,13 +204,15 @@ impl Response {
/// Returns true if this widget was double-clicked this frame by the given button.
#[inline]
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
self.clicked && self.ctx.input(|i| i.pointer.button_double_clicked(button))
self.flags.contains(Flags::CLICKED)
&& self.ctx.input(|i| i.pointer.button_double_clicked(button))
}
/// Returns true if this widget was triple-clicked this frame by the given button.
#[inline]
pub fn triple_clicked_by(&self, button: PointerButton) -> bool {
self.clicked && self.ctx.input(|i| i.pointer.button_triple_clicked(button))
self.flags.contains(Flags::CLICKED)
&& self.ctx.input(|i| i.pointer.button_triple_clicked(button))
}
/// `true` if there was a click *outside* the rect of this widget.
@ -224,7 +227,7 @@ impl Response {
let pointer = &i.pointer;
if pointer.any_click() {
if self.contains_pointer || self.hovered {
if self.contains_pointer() || self.hovered() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.interact_rect.contains(pos)
@ -242,7 +245,7 @@ impl Response {
/// and the widget should be drawn in a gray disabled look.
#[inline(always)]
pub fn enabled(&self) -> bool {
self.enabled
self.flags.contains(Flags::ENABLED)
}
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
@ -251,7 +254,7 @@ impl Response {
/// `hovered` is always `false` for disabled widgets.
#[inline(always)]
pub fn hovered(&self) -> bool {
self.hovered
self.flags.contains(Flags::HOVERED)
}
/// Returns true if the pointer is contained by the response rect, and no other widget is covering it.
@ -264,14 +267,14 @@ impl Response {
/// [`Self::contains_pointer`] also checks that no other widget is covering this response rectangle.
#[inline(always)]
pub fn contains_pointer(&self) -> bool {
self.contains_pointer
self.flags.contains(Flags::CONTAINS_POINTER)
}
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
#[doc(hidden)]
#[inline(always)]
pub fn highlighted(&self) -> bool {
self.highlighted
self.flags.contains(Flags::HIGHLIGHTED)
}
/// This widget has the keyboard focus (i.e. is receiving key presses).
@ -316,7 +319,7 @@ impl Response {
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
}
/// Did a drag on this widgets begin this frame?
/// Did a drag on this widget begin this frame?
///
/// This is only true if the widget sense drags.
/// If the widget also senses clicks, this will only become true if the pointer has moved a bit.
@ -324,10 +327,10 @@ impl Response {
/// This will only be true for a single frame.
#[inline]
pub fn drag_started(&self) -> bool {
self.drag_started
self.flags.contains(Flags::DRAG_STARTED)
}
/// Did a drag on this widgets by the button begin this frame?
/// Did a drag on this widget by the button begin this frame?
///
/// This is only true if the widget sense drags.
/// If the widget also senses clicks, this will only become true if the pointer has moved a bit.
@ -354,7 +357,7 @@ impl Response {
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
#[inline(always)]
pub fn dragged(&self) -> bool {
self.dragged
self.flags.contains(Flags::DRAGGED)
}
/// See [`Self::dragged`].
@ -366,7 +369,7 @@ impl Response {
/// The widget was being dragged, but now it has been released.
#[inline]
pub fn drag_stopped(&self) -> bool {
self.drag_stopped
self.flags.contains(Flags::DRAG_STOPPED)
}
/// The widget was being dragged by the button, but now it has been released.
@ -378,7 +381,7 @@ impl Response {
#[inline]
#[deprecated = "Renamed 'drag_stopped'"]
pub fn drag_released(&self) -> bool {
self.drag_stopped
self.drag_stopped()
}
/// The widget was being dragged by the button, but now it has been released.
@ -422,7 +425,7 @@ impl Response {
crate::DragAndDrop::set_payload(&self.ctx, payload);
}
if self.hovered() && !self.sense.click {
if self.hovered() && !self.sense.senses_click() {
// Things that can be drag-dropped should use the Grab cursor icon,
// but if the thing is _also_ clickable, that can be annoying.
self.ctx.set_cursor_icon(CursorIcon::Grab);
@ -460,7 +463,7 @@ impl Response {
}
}
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
///
/// `None` if the widget is not being interacted with.
#[inline]
@ -492,7 +495,7 @@ impl Response {
/// This could also be thought of as "is this widget being interacted with?".
#[inline(always)]
pub fn is_pointer_button_down_on(&self) -> bool {
self.is_pointer_button_down_on
self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON)
}
/// Was the underlying data changed?
@ -510,7 +513,7 @@ impl Response {
/// for instance if an existing slider value was clamped to the given range.
#[inline(always)]
pub fn changed(&self) -> bool {
self.changed
self.flags.contains(Flags::CHANGED)
}
/// Report the data shown by this widget changed.
@ -519,10 +522,10 @@ impl Response {
/// e.g. checkboxes, sliders etc.
///
/// This should be called when the *content* changes, but not when the view does.
/// So we call this when the text of a [`crate::TextEdit`], but not when the cursors changes.
/// So we call this when the text of a [`crate::TextEdit`], but not when the cursor changes.
#[inline(always)]
pub fn mark_changed(&mut self) {
self.changed = true;
self.flags.set(Flags::CHANGED, true);
}
/// Show this UI if the widget was hovered (i.e. a tooltip).
@ -547,7 +550,7 @@ impl Response {
/// ```
#[doc(alias = "tooltip")]
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if self.enabled && self.should_show_hover_ui() {
if self.flags.contains(Flags::ENABLED) && self.should_show_hover_ui() {
self.show_tooltip_ui(add_contents);
}
self
@ -555,7 +558,7 @@ impl Response {
/// Show this UI when hovering if the widget is disabled.
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if !self.enabled && self.should_show_hover_ui() {
if !self.enabled() && self.should_show_hover_ui() {
crate::containers::show_tooltip_for(
&self.ctx,
self.layer_id,
@ -569,7 +572,7 @@ impl Response {
/// Like `on_hover_ui`, but show the ui next to cursor.
pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if self.enabled && self.should_show_hover_ui() {
if self.enabled() && self.should_show_hover_ui() {
crate::containers::show_tooltip_at_pointer(
&self.ctx,
self.layer_id,
@ -725,8 +728,8 @@ impl Response {
}
// Fast early-outs:
if self.enabled {
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
if self.enabled() {
if !self.hovered() || !self.ctx.input(|i| i.pointer.has_pointer()) {
return false;
}
} else if !self.ctx.rect_contains_pointer(self.layer_id, self.rect) {
@ -734,7 +737,7 @@ impl Response {
}
// There is a tooltip_delay before showing the first tooltip,
// but once one tooltips is show, moving the mouse cursor to
// but once one tooltip is show, moving the mouse cursor to
// another widget should show the tooltip for that widget right away.
// Let the user quickly move over some dead space to hover the next thing
@ -817,7 +820,7 @@ impl Response {
#[inline]
pub fn highlight(mut self) -> Self {
self.ctx.highlight_widget(self.id);
self.highlighted = true;
self.flags.set(Flags::HIGHLIGHTED, true);
self
}
@ -888,7 +891,7 @@ impl Response {
rect: self.rect,
interact_rect: self.interact_rect,
sense: self.sense | sense,
enabled: self.enabled,
enabled: self.enabled(),
},
true,
)
@ -951,7 +954,7 @@ impl Response {
Some(OutputEvent::TripleClicked(make_info()))
} else if self.gained_focus() {
Some(OutputEvent::FocusGained(make_info()))
} else if self.changed {
} else if self.changed() {
Some(OutputEvent::ValueChanged(make_info()))
} else {
None
@ -983,7 +986,7 @@ impl Response {
#[cfg(feature = "accesskit")]
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::Node) {
if !self.enabled {
if !self.enabled() {
builder.set_disabled();
}
builder.set_bounds(accesskit::Rect {
@ -992,10 +995,10 @@ impl Response {
x1: self.rect.max.x.into(),
y1: self.rect.max.y.into(),
});
if self.sense.focusable {
if self.sense.is_focusable() {
builder.add_action(accesskit::Action::Focus);
}
if self.sense.click {
if self.sense.senses_click() {
builder.add_action(accesskit::Action::Click);
}
}
@ -1125,9 +1128,9 @@ impl Response {
pub fn paint_debug_info(&self) {
self.ctx.debug_painter().debug_rect(
self.rect,
if self.hovered {
if self.hovered() {
crate::Color32::DARK_GREEN
} else if self.enabled {
} else if self.enabled() {
crate::Color32::BLUE
} else {
crate::Color32::RED
@ -1157,20 +1160,8 @@ impl Response {
rect: self.rect.union(other.rect),
interact_rect: self.interact_rect.union(other.interact_rect),
sense: self.sense.union(other.sense),
enabled: self.enabled || other.enabled,
contains_pointer: self.contains_pointer || other.contains_pointer,
hovered: self.hovered || other.hovered,
highlighted: self.highlighted || other.highlighted,
clicked: self.clicked || other.clicked,
fake_primary_click: self.fake_primary_click || other.fake_primary_click,
long_touched: self.long_touched || other.long_touched,
drag_started: self.drag_started || other.drag_started,
dragged: self.dragged || other.dragged,
drag_stopped: self.drag_stopped || other.drag_stopped,
is_pointer_button_down_on: self.is_pointer_button_down_on
|| other.is_pointer_button_down_on,
flags: self.flags | other.flags,
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
changed: self.changed || other.changed,
intrinsic_size: None,
}
}

View File

@ -1,36 +1,37 @@
/// What sort of interaction is a widget sensitive to?
#[derive(Clone, Copy, Eq, PartialEq)]
// #[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Sense {
/// Buttons, sliders, windows, …
pub click: bool,
pub struct Sense(u8);
/// Sliders, windows, scroll bars, scroll areas, …
pub drag: bool,
bitflags::bitflags! {
impl Sense: u8 {
/// This widget wants focus.
///
/// Anything interactive + labels that can be focused
/// for the benefit of screen readers.
pub focusable: bool,
const HOVER = 0;
/// Buttons, sliders, windows, …
const CLICK = 1<<0;
/// Sliders, windows, scroll bars, scroll areas, …
const DRAG = 1<<1;
/// This widget wants focus.
///
/// Anything interactive + labels that can be focused
/// for the benefit of screen readers.
const FOCUSABLE = 1<<2;
}
}
impl std::fmt::Debug for Sense {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
click,
drag,
focusable,
} = self;
write!(f, "Sense {{")?;
if *click {
if self.senses_click() {
write!(f, " click")?;
}
if *drag {
if self.senses_drag() {
write!(f, " drag")?;
}
if *focusable {
if self.is_focusable() {
write!(f, " focusable")?;
}
write!(f, " }}")
@ -42,42 +43,26 @@ impl Sense {
#[doc(alias = "none")]
#[inline]
pub fn hover() -> Self {
Self {
click: false,
drag: false,
focusable: false,
}
Self::empty()
}
/// Senses no clicks or drags, but can be focused with the keyboard.
/// Used for labels that can be focused for the benefit of screen readers.
#[inline]
pub fn focusable_noninteractive() -> Self {
Self {
click: false,
drag: false,
focusable: true,
}
Self::FOCUSABLE
}
/// Sense clicks and hover, but not drags.
#[inline]
pub fn click() -> Self {
Self {
click: true,
drag: false,
focusable: true,
}
Self::CLICK | Self::FOCUSABLE
}
/// Sense drags and hover, but not clicks.
#[inline]
pub fn drag() -> Self {
Self {
click: false,
drag: true,
focusable: true,
}
Self::DRAG | Self::FOCUSABLE
}
/// Sense both clicks, drags and hover (e.g. a slider or window).
@ -90,43 +75,27 @@ impl Sense {
/// See [`crate::PointerState::is_decidedly_dragging`] for details.
#[inline]
pub fn click_and_drag() -> Self {
Self {
click: true,
drag: true,
focusable: true,
}
}
/// The logical "or" of two [`Sense`]s.
#[must_use]
#[inline]
pub fn union(self, other: Self) -> Self {
Self {
click: self.click | other.click,
drag: self.drag | other.drag,
focusable: self.focusable | other.focusable,
}
Self::CLICK | Self::FOCUSABLE | Self::DRAG
}
/// Returns true if we sense either clicks or drags.
#[inline]
pub fn interactive(&self) -> bool {
self.click || self.drag
self.intersects(Self::CLICK | Self::DRAG)
}
}
impl std::ops::BitOr for Sense {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
pub fn senses_click(&self) -> bool {
self.contains(Self::CLICK)
}
}
impl std::ops::BitOrAssign for Sense {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = self.union(rhs);
pub fn senses_drag(&self) -> bool {
self.contains(Self::DRAG)
}
#[inline]
pub fn is_focusable(&self) -> bool {
self.contains(Self::FOCUSABLE)
}
}

View File

@ -484,7 +484,7 @@ impl LabelSelectionState {
) -> Vec<RowVertexIndices> {
let widget_id = response.id;
if response.hovered {
if response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::Text);
}

View File

@ -122,7 +122,7 @@ impl TextCursorState {
secondary: galley.from_ccursor(ccursor_range.secondary),
}));
true
} else if response.sense.drag {
} else if response.sense.senses_drag() {
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
// The start of a drag (or a click).
if ui.input(|i| i.modifiers.shift) {

View File

@ -2079,7 +2079,7 @@ impl Ui {
// only touch `*radians` if we actually changed the degree value
if degrees != radians.to_degrees() {
*radians = degrees.to_radians();
response.changed = true;
response.mark_changed();
}
response
@ -2102,7 +2102,7 @@ impl Ui {
// only touch `*radians` if we actually changed the value
if taus != *radians / TAU {
*radians = taus * TAU;
response.changed = true;
response.mark_changed();
}
response

View File

@ -387,7 +387,7 @@ impl Widget for Button<'_> {
}
if let Some(cursor) = ui.visuals().interact_cursor {
if response.hovered {
if response.hovered() {
ui.ctx().set_cursor_icon(cursor);
}
}

View File

@ -660,7 +660,9 @@ impl<'a> Widget for DragValue<'a> {
response
};
response.changed = get(&mut get_set_value) != old_value;
if get(&mut get_set_value) != old_value {
response.mark_changed();
}
response.widget_info(|| WidgetInfo::drag_value(ui.is_enabled(), value));

View File

@ -146,7 +146,7 @@ impl Label {
} else {
Sense::click()
};
select_sense.focusable = false; // Don't move focus to labels with TAB key.
select_sense -= Sense::FOCUSABLE; // Don't move focus to labels with TAB key.
sense = sense.union(select_sense);
}

View File

@ -946,7 +946,9 @@ impl<'a> Slider<'a> {
self.slider_ui(ui, &response);
let value = self.get_value();
response.changed = value != old_value;
if value != old_value {
response.mark_changed();
}
response.widget_info(|| WidgetInfo::slider(ui.is_enabled(), value, self.text.text()));
#[cfg(feature = "accesskit")]

View File

@ -7,7 +7,7 @@ use crate::{
epaint,
os::OperatingSystem,
output::OutputEvent,
text_selection,
response, text_selection,
text_selection::{
text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange,
},
@ -565,8 +565,8 @@ impl<'t> TextEdit<'t> {
let mut response = ui.interact(outer_rect, id, sense);
response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y));
response.fake_primary_click = false; // Don't sent `OutputEvent::Clicked` when a user presses the space bar
// Don't sent `OutputEvent::Clicked` when a user presses the space bar
response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
let text_clip_rect = rect;
let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
@ -740,14 +740,14 @@ impl<'t> TextEdit<'t> {
let primary_cursor_rect =
cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height);
if response.changed || selection_changed {
if response.changed() || selection_changed {
// Scroll to keep primary cursor in view:
ui.scroll_to_rect(primary_cursor_rect + margin, None);
}
if text.is_mutable() && interactive {
let now = ui.ctx().input(|i| i.time);
if response.changed || selection_changed {
if response.changed() || selection_changed {
state.last_interaction_time = now;
}
@ -794,7 +794,7 @@ impl<'t> TextEdit<'t> {
state.clone().store(ui.ctx(), id);
if response.changed {
if response.changed() {
response.widget_info(|| {
WidgetInfo::text_edit(
ui.is_enabled(),