mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 14:50:03 -04:00
Make non-interactive tooltips not interactable (#8362)
Fixes two bugs around tooltips: - During sizing pass tooltips would render at a different size, at which point they might overlap the interacted widget causing the hover state to change and the tooltip to never be shown - Fix: During sizing pass make the area `interactable: false` so events pass through - When hovering close to the boarder of a widget (in the area where `interact_radius` takes effect), there could be a feedback loop where the tooltip is shown one frame and hidden the next. - Fix: Always make tooltips `interactable: false` when they don't have interactive contents This PR changes the behavior of `Area::interactable` to it's original behavior: Now interactions will pass through the area background _and_ it's containing widgets. It worked this way initially but got changed in #4026 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -454,14 +454,12 @@ impl Area {
|
||||
state.size = None;
|
||||
}
|
||||
state.pivot = pivot;
|
||||
state.interactable = interactable;
|
||||
if let Some(new_pos) = new_pos {
|
||||
state.pivot_pos = Some(new_pos);
|
||||
}
|
||||
state.pivot_pos.get_or_insert_with(|| {
|
||||
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
|
||||
});
|
||||
state.interactable = interactable;
|
||||
|
||||
let size = *state.size.get_or_insert_with(|| {
|
||||
sizing_pass = true;
|
||||
@@ -484,6 +482,10 @@ impl Area {
|
||||
size
|
||||
});
|
||||
|
||||
// We should never be interactable during a sizing pass, since then we are shown at a different
|
||||
// size which might interfere with hover state of the hovered widget causing popup feedback loops.
|
||||
state.interactable = interactable && !sizing_pass;
|
||||
|
||||
// TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in
|
||||
let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ pub struct Popup<'a> {
|
||||
/// Default width passed to the Area
|
||||
width: Option<f32>,
|
||||
sense: Sense,
|
||||
interactable: bool,
|
||||
layout: Layout,
|
||||
frame: Option<Frame>,
|
||||
style: StyleModifier,
|
||||
@@ -202,6 +203,7 @@ impl<'a> Popup<'a> {
|
||||
gap: 0.0,
|
||||
width: None,
|
||||
sense: Sense::click(),
|
||||
interactable: true,
|
||||
layout: Layout::default(),
|
||||
frame: None,
|
||||
style: StyleModifier::default(),
|
||||
@@ -369,6 +371,15 @@ impl<'a> Popup<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// If `false`, the pointer goes straight through the popup and it's widgets to whatever is behind it.
|
||||
///
|
||||
/// Default: `true`.
|
||||
#[inline]
|
||||
pub fn interactable(mut self, interactable: bool) -> Self {
|
||||
self.interactable = interactable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sense of the popup.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
@@ -546,6 +557,7 @@ impl<'a> Popup<'a> {
|
||||
gap,
|
||||
width,
|
||||
sense,
|
||||
interactable,
|
||||
layout,
|
||||
frame,
|
||||
style,
|
||||
@@ -570,6 +582,7 @@ impl<'a> Popup<'a> {
|
||||
.pivot(pivot)
|
||||
.fixed_pos(anchor)
|
||||
.sense(sense)
|
||||
.interactable(interactable)
|
||||
.layout(layout)
|
||||
.sizing_pass(!was_open_last_frame)
|
||||
.info(info.unwrap_or_else(|| {
|
||||
|
||||
@@ -129,7 +129,15 @@ impl Tooltip<'_> {
|
||||
});
|
||||
|
||||
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
|
||||
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
|
||||
|
||||
// Tooltips without interactive contents should not be interactable (hover should pass
|
||||
// through to the widget below).
|
||||
let interactable = Self::had_interactive_widgets(popup.ctx(), tooltip_area_id);
|
||||
|
||||
popup = popup
|
||||
.anchor(state.bounding_rect)
|
||||
.id(tooltip_area_id)
|
||||
.interactable(interactable);
|
||||
|
||||
let response = popup.show(|ui| {
|
||||
// By default, the text in tooltips aren't selectable.
|
||||
@@ -192,6 +200,20 @@ impl Tooltip<'_> {
|
||||
widget_id.with(tooltip_count)
|
||||
}
|
||||
|
||||
/// Did this tooltip contain anything the user can interact with, last pass?
|
||||
///
|
||||
/// Most tooltips are just text. Those should not react to the pointer at all,
|
||||
/// or they would steal the hover from the widget they belong to.
|
||||
fn had_interactive_widgets(ctx: &Context, tooltip_id: Id) -> bool {
|
||||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||
ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.get_layer(tooltip_layer_id)
|
||||
.any(|w| w.enabled && w.sense.interactive())
|
||||
})
|
||||
}
|
||||
|
||||
/// Should we show a tooltip for this response?
|
||||
///
|
||||
/// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that
|
||||
@@ -247,15 +269,9 @@ impl Tooltip<'_> {
|
||||
// Check if we should automatically stay open:
|
||||
|
||||
let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
|
||||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||
|
||||
let tooltip_has_interactive_widget = allow_interactive_tooltip
|
||||
&& response.ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.get_layer(tooltip_layer_id)
|
||||
.any(|w| w.enabled && w.sense.interactive())
|
||||
});
|
||||
&& Self::had_interactive_widgets(&response.ctx, tooltip_id);
|
||||
|
||||
if tooltip_has_interactive_widget {
|
||||
// We keep the tooltip open if hovered,
|
||||
|
||||
@@ -466,7 +466,13 @@ impl ContextImpl {
|
||||
viewport.this_pass.begin_pass();
|
||||
|
||||
{
|
||||
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
|
||||
// Areas that are not interactable are click-through: skip them in the hit-test.
|
||||
let mut layers: Vec<LayerId> = viewport
|
||||
.prev_pass
|
||||
.widgets
|
||||
.layer_ids()
|
||||
.filter(|layer_id| self.memory.areas().is_interactable(*layer_id))
|
||||
.collect();
|
||||
layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b));
|
||||
|
||||
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
|
||||
|
||||
@@ -1194,6 +1194,11 @@ impl Areas {
|
||||
self.areas.get_mut(&id)
|
||||
}
|
||||
|
||||
/// Can the user interact with this layer or it's widgets, or do clicks go straight through it?
|
||||
pub(crate) fn is_interactable(&self, layer_id: LayerId) -> bool {
|
||||
self.get(layer_id.id).is_none_or(|area| area.interactable)
|
||||
}
|
||||
|
||||
/// All layers back-to-front, top is last.
|
||||
pub(crate) fn order(&self) -> &[LayerId] {
|
||||
&self.order
|
||||
|
||||
Reference in New Issue
Block a user