Only show one tooltip per layer at a time (#4763)

Before, you could accidentally get multiple tooltips if a tooltips was
interactive (e.g. had a link in it) and on the way to interact with it
you would hover another widget with a tooltip.

This PR ensures that each `LayerId` only has one tooltip open at a time.
You can still have a tooltip for an item inside of a tooltip.
This commit is contained in:
Emil Ernerfeldt 2024-07-03 11:28:26 +02:00 committed by GitHub
parent c0296fb47b
commit d1be5a1efb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 26 deletions

View File

@ -17,7 +17,7 @@ use crate::*;
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() { /// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), egui::Id::new("my_tooltip"), |ui| { /// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text"); /// ui.label("Helpful text");
/// }); /// });
/// } /// }
@ -25,10 +25,11 @@ use crate::*;
/// ``` /// ```
pub fn show_tooltip<R>( pub fn show_tooltip<R>(
ctx: &Context, ctx: &Context,
parent_layer: LayerId,
widget_id: Id, widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> { ) -> Option<R> {
show_tooltip_at_pointer(ctx, widget_id, add_contents) show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
} }
/// Show a tooltip at the current pointer position (if any). /// Show a tooltip at the current pointer position (if any).
@ -42,7 +43,7 @@ pub fn show_tooltip<R>(
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() { /// if ui.ui_contains_pointer() {
/// egui::show_tooltip_at_pointer(ui.ctx(), egui::Id::new("my_tooltip"), |ui| { /// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text"); /// ui.label("Helpful text");
/// }); /// });
/// } /// }
@ -50,11 +51,18 @@ pub fn show_tooltip<R>(
/// ``` /// ```
pub fn show_tooltip_at_pointer<R>( pub fn show_tooltip_at_pointer<R>(
ctx: &Context, ctx: &Context,
parent_layer: LayerId,
widget_id: Id, widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> { ) -> Option<R> {
ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| { ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| {
show_tooltip_at(ctx, widget_id, pointer_pos + vec2(16.0, 16.0), add_contents) show_tooltip_at(
ctx,
parent_layer,
widget_id,
pointer_pos + vec2(16.0, 16.0),
add_contents,
)
}) })
} }
@ -63,14 +71,16 @@ pub fn show_tooltip_at_pointer<R>(
/// If the tooltip does not fit under the area, it tries to place it above it instead. /// If the tooltip does not fit under the area, it tries to place it above it instead.
pub fn show_tooltip_for<R>( pub fn show_tooltip_for<R>(
ctx: &Context, ctx: &Context,
parent_layer: LayerId,
widget_id: Id, widget_id: Id,
widget_rect: &Rect, widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> R { ) -> R {
let is_touch_screen = ctx.input(|i| i.any_touches()); let is_touch_screen = ctx.input(|i| i.any_touches());
let allow_placing_below = !is_touch_screen; // There is a finger below. let allow_placing_below = !is_touch_screen; // There is a finger below.
show_tooltip_at_avoid_dyn( show_tooltip_at_dyn(
ctx, ctx,
parent_layer,
widget_id, widget_id,
allow_placing_below, allow_placing_below,
widget_rect, widget_rect,
@ -83,14 +93,16 @@ pub fn show_tooltip_for<R>(
/// Returns `None` if the tooltip could not be placed. /// Returns `None` if the tooltip could not be placed.
pub fn show_tooltip_at<R>( pub fn show_tooltip_at<R>(
ctx: &Context, ctx: &Context,
parent_layer: LayerId,
widget_id: Id, widget_id: Id,
suggested_position: Pos2, suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> R { ) -> R {
let allow_placing_below = true; let allow_placing_below = true;
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO); let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
show_tooltip_at_avoid_dyn( show_tooltip_at_dyn(
ctx, ctx,
parent_layer,
widget_id, widget_id,
allow_placing_below, allow_placing_below,
&rect, &rect,
@ -98,21 +110,32 @@ pub fn show_tooltip_at<R>(
) )
} }
fn show_tooltip_at_avoid_dyn<'c, R>( fn show_tooltip_at_dyn<'c, R>(
ctx: &Context, ctx: &Context,
parent_layer: LayerId,
widget_id: Id, widget_id: Id,
allow_placing_below: bool, allow_placing_below: bool,
widget_rect: &Rect, widget_rect: &Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> R { ) -> R {
let mut widget_rect = *widget_rect;
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
widget_rect = transform * widget_rect;
}
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work. // if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
let mut state = ctx.frame_state(|fs| { let mut state = ctx.frame_state_mut(|fs| {
// Remember that this is the widget showing the tooltip:
fs.tooltip_state
.per_layer_tooltip_widget
.insert(parent_layer, widget_id);
fs.tooltip_state fs.tooltip_state
.widget_tooltips .widget_tooltips
.get(&widget_id) .get(&widget_id)
.copied() .copied()
.unwrap_or(PerWidgetTooltipState { .unwrap_or(PerWidgetTooltipState {
bounding_rect: *widget_rect, bounding_rect: widget_rect,
tooltip_count: 0, tooltip_count: 0,
}) })
}); });
@ -235,12 +258,17 @@ fn find_tooltip_position(
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() { /// if ui.ui_contains_pointer() {
/// egui::show_tooltip_text(ui.ctx(), egui::Id::new("my_tooltip"), "Helpful text"); /// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
/// } /// }
/// # }); /// # });
/// ``` /// ```
pub fn show_tooltip_text(ctx: &Context, widget_id: Id, text: impl Into<WidgetText>) -> Option<()> { pub fn show_tooltip_text(
show_tooltip(ctx, widget_id, |ui| { ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
text: impl Into<WidgetText>,
) -> Option<()> {
show_tooltip(ctx, parent_layer, widget_id, |ui| {
crate::widgets::Label::new(text).ui(ui); crate::widgets::Label::new(text).ui(ui);
}) })
} }

View File

@ -1,13 +1,27 @@
use crate::{id::IdSet, *}; use crate::{id::IdSet, *};
/// Reset at the start of each frame.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct TooltipFrameState { pub struct TooltipFrameState {
/// If a tooltip has been shown this frame, where was it?
/// This is used to prevent multiple tooltips to cover each other.
pub widget_tooltips: IdMap<PerWidgetTooltipState>, pub widget_tooltips: IdMap<PerWidgetTooltipState>,
/// For each layer, which widget is showing a tooltip (if any)?
///
/// Only one widget per layer may show a tooltip.
/// But if a tooltip contains a tooltip, you can show a tooltip on top of a tooltip.
pub per_layer_tooltip_widget: ahash::HashMap<LayerId, Id>,
} }
impl TooltipFrameState { impl TooltipFrameState {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.widget_tooltips.clear(); let Self {
widget_tooltips,
per_layer_tooltip_widget,
} = self;
widget_tooltips.clear();
per_layer_tooltip_widget.clear();
} }
} }
@ -51,9 +65,6 @@ pub struct FrameState {
/// How much space is used by panels. /// How much space is used by panels.
pub used_by_panels: Rect, pub used_by_panels: Rect,
/// If a tooltip has been shown this frame, where was it?
/// This is used to prevent multiple tooltips to cover each other.
/// Reset at the start of each frame.
pub tooltip_state: TooltipFrameState, pub tooltip_state: TooltipFrameState,
/// The current scroll area should scroll to this range (horizontal, vertical). /// The current scroll area should scroll to this range (horizontal, vertical).

View File

@ -545,7 +545,13 @@ impl Response {
/// Show this UI when hovering if the widget is disabled. /// Show this UI when hovering if the widget is disabled.
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { 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.id, &self.rect, add_contents); crate::containers::show_tooltip_for(
&self.ctx,
self.layer_id,
self.id,
&self.rect,
add_contents,
);
} }
self self
} }
@ -553,7 +559,12 @@ impl Response {
/// Like `on_hover_ui`, but show the ui next to cursor. /// 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 { 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.id, add_contents); crate::containers::show_tooltip_at_pointer(
&self.ctx,
self.layer_id,
self.id,
add_contents,
);
} }
self self
} }
@ -562,14 +573,13 @@ impl Response {
/// ///
/// This can be used to give attention to a widget during a tutorial. /// This can be used to give attention to a widget during a tutorial.
pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) { pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) {
let mut rect = self.rect; crate::containers::show_tooltip_for(
if let Some(transform) = self &self.ctx,
.ctx self.layer_id,
.memory(|m| m.layer_transforms.get(&self.layer_id).copied()) self.id,
{ &self.rect,
rect = transform * rect; add_contents,
} );
crate::containers::show_tooltip_for(&self.ctx, self.id, &rect, add_contents);
} }
/// Always show this tooltip, even if disabled and the user isn't hovering it. /// Always show this tooltip, even if disabled and the user isn't hovering it.
@ -635,6 +645,22 @@ impl Response {
} }
} }
let is_other_tooltip_open = self.ctx.prev_frame_state(|fs| {
if let Some(already_open_tooltip) = fs
.tooltip_state
.per_layer_tooltip_widget
.get(&self.layer_id)
{
already_open_tooltip != &self.id
} else {
false
}
});
if is_other_tooltip_open {
// We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
return false;
}
// Fast early-outs: // Fast early-outs:
if self.enabled { if self.enabled {
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) { if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {