diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index e287444b295..ab01aa6ab30 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -17,7 +17,7 @@ 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"); /// }); /// } @@ -25,10 +25,11 @@ use crate::*; /// ``` pub fn show_tooltip( ctx: &Context, + parent_layer: LayerId, widget_id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - 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). @@ -42,7 +43,7 @@ pub fn show_tooltip( /// ``` /// # 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"); /// }); /// } @@ -50,11 +51,18 @@ pub fn show_tooltip( /// ``` pub fn show_tooltip_at_pointer( ctx: &Context, + parent_layer: LayerId, widget_id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { 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( /// If the tooltip does not fit under the area, it tries to place it above it instead. pub fn show_tooltip_for( 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, @@ -83,14 +93,16 @@ pub fn show_tooltip_for( /// Returns `None` if the tooltip could not be placed. pub fn show_tooltip_at( 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, @@ -98,21 +110,32 @@ pub fn show_tooltip_at( ) } -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 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, }) }); @@ -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) -> Option<()> { - show_tooltip(ctx, widget_id, |ui| { +pub fn show_tooltip_text( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + text: impl Into, +) -> Option<()> { + show_tooltip(ctx, parent_layer, widget_id, |ui| { crate::widgets::Label::new(text).ui(ui); }) } diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index 4e1aa390017..4f434a60ebc 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -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, + + /// 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, } 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(); } } @@ -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). diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 706af9f01ab..f0f10b9d4c1 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -545,7 +545,13 @@ 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 } @@ -553,7 +559,12 @@ 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() { - 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 } @@ -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. @@ -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()) {