Skip to content

Commit

Permalink
Never mark another widget as hovered when dragging another widget
Browse files Browse the repository at this point in the history
  • Loading branch information
emilk committed Jan 22, 2024
1 parent 2d725d1 commit 261d624
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 104 deletions.
232 changes: 141 additions & 91 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,13 @@ impl ContextImpl {
.native_pixels_per_point
.unwrap_or(1.0);

viewport.layer_rects_prev_frame = std::mem::take(&mut viewport.layer_rects_this_frame);
{
std::mem::swap(
&mut viewport.layer_rects_prev_frame,
&mut viewport.layer_rects_this_frame,
);
viewport.layer_rects_this_frame.clear();
}

let all_viewport_ids: ViewportIdSet = self.all_viewport_ids();

Expand Down Expand Up @@ -865,91 +871,25 @@ impl Context {
.at_most(Vec2::splat(5.0)),
);

// Respect clip rectangle when interacting
// Respect clip rectangle when interacting:
let interact_rect = clip_rect.intersect(interact_rect);
let mut hovered = self.rect_contains_pointer(layer_id, interact_rect);

// This solves the problem of overlapping widgets.
// Whichever widget is added LAST (=on top) gets the input:
if interact_rect.is_positive() && sense.interactive() {
#[cfg(debug_assertions)]
if self.style().debug.show_interactive_widgets {
Self::layer_painter(self, LayerId::debug()).rect(
interact_rect,
0.0,
Color32::YELLOW.additive().linear_multiply(0.005),
Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)),
);
}

#[cfg(debug_assertions)]
let mut show_blocking_widget = None;

self.write(|ctx| {
let viewport = ctx.viewport();

viewport
.layer_rects_this_frame
.entry(layer_id)
.or_default()
.push(WidgetRect {
id,
rect: interact_rect,
sense,
});

if hovered {
let pointer_pos = viewport.input.pointer.interact_pos();
if let Some(pointer_pos) = pointer_pos {
if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) {
for &WidgetRect {
id: prev_id,
rect: prev_rect,
sense: prev_sense,
} in rects.iter().rev()
{
if prev_id == id {
break; // there is no other interactive widget covering us at the pointer position.
}
// We don't want a click-only button to block drag-events to a `ScrollArea`:
let has_conflicting_sense = (prev_sense.click && sense.click)
|| (prev_sense.drag && sense.drag);
if prev_rect.contains(pointer_pos) && has_conflicting_sense {
// Another interactive widget is covering us at the pointer position,
// so we aren't hovered.

#[cfg(debug_assertions)]
if ctx.memory.options.style.debug.show_blocking_widget {
// Store the rects to use them outside the write() call to
// avoid deadlock
show_blocking_widget = Some((interact_rect, prev_rect));
}

hovered = false;
break;
}
}
}
}
}
});
let contains_pointer = self.widget_contains_pointer(layer_id, id, sense, interact_rect);

#[cfg(debug_assertions)]
if let Some((interact_rect, prev_rect)) = show_blocking_widget {
Self::layer_painter(self, LayerId::debug()).debug_rect(
interact_rect,
Color32::GREEN,
"Covered",
);
Self::layer_painter(self, LayerId::debug()).debug_rect(
prev_rect,
Color32::LIGHT_BLUE,
"On top",
);
}
#[cfg(debug_assertions)]
if sense.interactive()
&& interact_rect.is_positive()
&& self.style().debug.show_interactive_widgets
{
Self::layer_painter(self, LayerId::debug()).rect(
interact_rect,
0.0,
Color32::YELLOW.additive().linear_multiply(0.005),
Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)),
);
}

self.interact_with_hovered(layer_id, id, rect, sense, enabled, hovered)
self.interact_with_hovered(layer_id, id, rect, sense, enabled, contains_pointer)
}

/// You specify if a thing is hovered, and the function gives a [`Response`].
Expand All @@ -960,9 +900,9 @@ impl Context {
rect: Rect,
sense: Sense,
enabled: bool,
hovered: bool,
contains_pointer: bool,
) -> Response {
let hovered = hovered && enabled; // can't even hover disabled widgets
let hovered = contains_pointer && enabled; // can't even hover disabled widgets

let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id));

Expand All @@ -973,6 +913,7 @@ impl Context {
rect,
sense,
enabled,
contains_pointer,
hovered,
highlighted,
clicked: Default::default(),
Expand Down Expand Up @@ -1092,7 +1033,8 @@ impl Context {
}

if input.pointer.any_down() {
response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets
// We don't hover widgets while interacting with *other* widgets:
response.hovered &= response.is_pointer_button_down_on;
}

if memory.has_focus(response.id) && clicked_elsewhere {
Expand Down Expand Up @@ -2028,15 +1970,123 @@ impl Context {
self.memory(|mem| mem.areas().top_layer_id(Order::Middle))
}

pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
rect.is_positive() && {
let pointer_pos = self.input(|i| i.pointer.interact_pos());
if let Some(pointer_pos) = pointer_pos {
rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
/// Does the given rectangle contain the mouse pointer?
///
/// Will return false if some other area is covering the given layer.
///
/// See also [`Response::contains_pointer`].
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
if !rect.is_positive() {
return false;
}

let pointer_pos = self.input(|i| i.pointer.interact_pos());
let Some(pointer_pos) = pointer_pos else {
return false;
};

if !rect.contains(pointer_pos) {
return false;
}

if self.layer_id_at(pointer_pos) != Some(layer_id) {
return false;
}

true
}

/// Does the given widget contain the mouse pointer?
///
/// Will return false if some other area is covering the given layer.
///
/// If another widget is covering us and is listening for the same input (click and/or drag),
/// this will return false.
///
/// See also [`Response::contains_pointer`].
pub fn widget_contains_pointer(
&self,
layer_id: LayerId,
id: Id,
sense: Sense,
rect: Rect,
) -> bool {
let contains_pointer = self.rect_contains_pointer(layer_id, rect);

let mut blocking_widget = None;

self.write(|ctx| {
let viewport = ctx.viewport();

// We add all widgets here, even non-interactive ones,
// because we need this list not only for checking for blocking widgets,
// but also to know when we have reach the widget we are checking for covere.

Check warning on line 2023 in crates/egui/src/context.rs

View workflow job for this annotation

GitHub Actions / typos

"covere" should be "cover".
viewport
.layer_rects_this_frame
.entry(layer_id)
.or_default()
.push(WidgetRect { id, rect, sense });

// Check if any other widget is covering us.
// Whichever widget is added LAST (=on top) gets the input.
if contains_pointer {
let pointer_pos = viewport.input.pointer.interact_pos();
if let Some(pointer_pos) = pointer_pos {
if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) {
for blocking in rects.iter().rev() {
if blocking.id == id {
// There are no earlier widgets before this one,
// which means there are no widgets covering us.
break;
}
if !blocking.rect.contains(pointer_pos) {
continue;
}
if sense.interactive() && !blocking.sense.interactive() {
// Only interactiv widgets can block other interactive widgets.

Check warning on line 2046 in crates/egui/src/context.rs

View workflow job for this annotation

GitHub Actions / typos

"interactiv" should be "interactive".
continue;
}

// The `prev` widget is covering us - do we care?
// We don't want a click-only button to block drag-events to a `ScrollArea`:

let sense_only_drags = sense.drag && !sense.click;
if sense_only_drags && !blocking.sense.drag {
continue;
}
let sense_only_clicks = sense.click && !sense.drag;
if sense_only_clicks && !blocking.sense.click {
continue;
}

if blocking.sense.interactive() {
// Another widget is covering us at the pointer position
blocking_widget = Some(blocking.rect);
break;
}
}
}
}
}
});

#[cfg(debug_assertions)]
if let Some(blocking_rect) = blocking_widget {
if sense.interactive() && self.memory(|m| m.options.style.debug.show_blocking_widget) {
Self::layer_painter(self, LayerId::debug()).debug_rect(
rect,
Color32::GREEN,
"Covered",
);
Self::layer_painter(self, LayerId::debug()).debug_rect(
blocking_rect,
Color32::LIGHT_BLUE,
"On top",
);
}
}

contains_pointer && blocking_widget.is_none()
}

// ---------------------------------------------------------------------
Expand Down
25 changes: 16 additions & 9 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub struct Response {
pub enabled: bool,

// OUT:
/// The pointer is hovering above this widget.
#[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,
Expand Down Expand Up @@ -95,6 +99,7 @@ impl std::fmt::Debug for Response {
rect,
sense,
enabled,
contains_pointer,
hovered,
highlighted,
clicked,
Expand All @@ -112,6 +117,7 @@ impl std::fmt::Debug for Response {
.field("rect", rect)
.field("sense", sense)
.field("enabled", enabled)
.field("contains_pointer", contains_pointer)
.field("hovered", hovered)
.field("highlighted", highlighted)
.field("clicked", clicked)
Expand Down Expand Up @@ -212,12 +218,8 @@ impl Response {

/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
///
/// This will be `false` whenever some other widget is being dragged.
///
/// Note that this is slightly different from [`Self::contains_pointer`].
/// For one, the hover rectangle is slightly larger, by half of the current item spacing
/// (to make it easier to click things). But `hovered` also checks that no other area
/// is covering this response rectangle.
/// In contrast to [`Self::contains_pointer`], this will be `false` whenever some other widget is being dragged.
/// `hovered` is always `false` for disabled widgets.
#[inline(always)]
pub fn hovered(&self) -> bool {
self.hovered
Expand All @@ -227,15 +229,19 @@ impl Response {
///
/// In contrast to [`Self::hovered`], this can be `true` even if some other widget is being dragged.
/// This means it is useful for styling things like drag-and-drop targets.
/// `contains_pointer` can also be `true` for disabled widgets.
///
/// In contrast to [`Self::hovered`], this is true even when dragging some other widget
/// onto this one.
/// This is slightly different from [`Ui::rect_contains_pointer`] and [`Context::rect_contains_pointer`],
/// The rectangle used here is slightly larger, by half of the current item spacing.
/// [`Self::contains_pointer`] also checks that no other widget is covering this response rectangle.
#[inline(always)]
pub fn contains_pointer(&self) -> bool {
self.ctx.rect_contains_pointer(self.layer_id, self.rect)
self.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
}
Expand Down Expand Up @@ -739,6 +745,7 @@ impl Response {
rect: self.rect.union(other.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: [
Expand Down
17 changes: 13 additions & 4 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,23 +640,32 @@ impl Ui {
/// Check for clicks, and drags on a specific region that is hovered.
/// This can be used once you have checked that some shape you are painting has been hovered,
/// and want to check for clicks and drags on hovered items this frame.
///
/// The given [`Rect`] should approximately be where the thing is,
/// as it is just where warnings will be painted if there is an [`Id`] clash.
/// as will be the rectangle for the returned [`Response::rect`].
pub fn interact_with_hovered(
&self,
rect: Rect,
hovered: bool,
contains_pointer: bool,
id: Id,
sense: Sense,
) -> Response {
self.ctx()
.interact_with_hovered(self.layer_id(), id, rect, sense, self.enabled, hovered)
self.ctx().interact_with_hovered(
self.layer_id(),
id,
rect,
sense,
self.enabled,
contains_pointer,
)
}

/// 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,
/// if this [`Ui`] is behind some other window, this will always return `false`.
///
/// However, this will NOT check if any other _widget_ in the same layer is covering this widget. For that, use [`Response::contains_pointer`] instead.
pub fn rect_contains_pointer(&self, rect: Rect) -> bool {
self.ctx()
.rect_contains_pointer(self.layer_id(), self.clip_rect().intersect(rect))
Expand Down

0 comments on commit 261d624

Please sign in to comment.