Skip to content

Commit

Permalink
Only show one tooltip per layer at a time (#4763)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
emilk authored Jul 3, 2024
1 parent c0296fb commit d1be5a1
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 26 deletions.
52 changes: 40 additions & 12 deletions crates/egui/src/containers/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ use crate::*;
/// ```
/// # egui::__run_test_ui(|ui| {
/// 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");
/// });
/// }
/// # });
/// ```
pub fn show_tooltip<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> 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).
Expand All @@ -42,19 +43,26 @@ pub fn show_tooltip<R>(
/// ```
/// # egui::__run_test_ui(|ui| {
/// 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");
/// });
/// }
/// # });
/// ```
pub fn show_tooltip_at_pointer<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
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,
)
})
}

Expand All @@ -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.
pub fn show_tooltip_for<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> R {
let is_touch_screen = ctx.input(|i| i.any_touches());
let allow_placing_below = !is_touch_screen; // There is a finger below.
show_tooltip_at_avoid_dyn(
show_tooltip_at_dyn(
ctx,
parent_layer,
widget_id,
allow_placing_below,
widget_rect,
Expand All @@ -83,36 +93,49 @@ pub fn show_tooltip_for<R>(
/// Returns `None` if the tooltip could not be placed.
pub fn show_tooltip_at<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> R {
let allow_placing_below = true;
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
show_tooltip_at_avoid_dyn(
show_tooltip_at_dyn(
ctx,
parent_layer,
widget_id,
allow_placing_below,
&rect,
Box::new(add_contents),
)
}

fn show_tooltip_at_avoid_dyn<'c, R>(
fn show_tooltip_at_dyn<'c, R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
allow_placing_below: bool,
widget_rect: &Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> 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.
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
.widget_tooltips
.get(&widget_id)
.copied()
.unwrap_or(PerWidgetTooltipState {
bounding_rect: *widget_rect,
bounding_rect: widget_rect,
tooltip_count: 0,
})
});
Expand Down Expand Up @@ -235,12 +258,17 @@ fn find_tooltip_position(
/// ```
/// # egui::__run_test_ui(|ui| {
/// 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<()> {
show_tooltip(ctx, widget_id, |ui| {
pub fn show_tooltip_text(
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);
})
}
Expand Down
19 changes: 15 additions & 4 deletions crates/egui/src/frame_state.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
use crate::{id::IdSet, *};

/// Reset at the start of each frame.
#[derive(Clone, Debug, Default)]
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>,

/// 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 {
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();
}
}

Expand Down Expand Up @@ -51,9 +65,6 @@ pub struct FrameState {
/// How much space is used by panels.
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,

/// The current scroll area should scroll to this range (horizontal, vertical).
Expand Down
46 changes: 36 additions & 10 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,26 @@ 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() {
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
}

/// 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() {
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
}
Expand All @@ -562,14 +573,13 @@ impl Response {
///
/// 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)) {
let mut rect = self.rect;
if let Some(transform) = self
.ctx
.memory(|m| m.layer_transforms.get(&self.layer_id).copied())
{
rect = transform * rect;
}
crate::containers::show_tooltip_for(&self.ctx, self.id, &rect, add_contents);
crate::containers::show_tooltip_for(
&self.ctx,
self.layer_id,
self.id,
&self.rect,
add_contents,
);
}

/// Always show this tooltip, even if disabled and the user isn't hovering it.
Expand Down Expand Up @@ -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:
if self.enabled {
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
Expand Down

0 comments on commit d1be5a1

Please sign in to comment.