egui/containers/
tooltip.rs

1use crate::pass_state::PerWidgetTooltipState;
2use crate::{
3    AreaState, Context, Id, InnerResponse, LayerId, Layout, Order, Popup, PopupAnchor, PopupKind,
4    Response, Sense,
5};
6use emath::Vec2;
7
8pub struct Tooltip<'a> {
9    pub popup: Popup<'a>,
10
11    /// The layer of the parent widget.
12    parent_layer: LayerId,
13
14    /// The id of the widget that owns this tooltip.
15    parent_widget: Id,
16}
17
18impl Tooltip<'_> {
19    /// Show a tooltip that is always open.
20    #[deprecated = "Use `Tooltip::always_open` instead."]
21    pub fn new(
22        parent_widget: Id,
23        ctx: Context,
24        anchor: impl Into<PopupAnchor>,
25        parent_layer: LayerId,
26    ) -> Self {
27        Self {
28            popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
29                .kind(PopupKind::Tooltip)
30                .gap(4.0)
31                .sense(Sense::hover()),
32            parent_layer,
33            parent_widget,
34        }
35    }
36
37    /// Show a tooltip that is always open.
38    pub fn always_open(
39        ctx: Context,
40        parent_layer: LayerId,
41        parent_widget: Id,
42        anchor: impl Into<PopupAnchor>,
43    ) -> Self {
44        let width = ctx.style().spacing.tooltip_width;
45        Self {
46            popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
47                .kind(PopupKind::Tooltip)
48                .gap(4.0)
49                .width(width)
50                .sense(Sense::hover()),
51            parent_layer,
52            parent_widget,
53        }
54    }
55
56    /// Show a tooltip for a widget. Always open (as long as this function is called).
57    pub fn for_widget(response: &Response) -> Self {
58        let popup = Popup::from_response(response)
59            .kind(PopupKind::Tooltip)
60            .gap(4.0)
61            .width(response.ctx.style().spacing.tooltip_width)
62            .sense(Sense::hover());
63        Self {
64            popup,
65            parent_layer: response.layer_id,
66            parent_widget: response.id,
67        }
68    }
69
70    /// Show a tooltip when hovering an enabled widget.
71    pub fn for_enabled(response: &Response) -> Self {
72        let mut tooltip = Self::for_widget(response);
73        tooltip.popup = tooltip
74            .popup
75            .open(response.enabled() && Self::should_show_tooltip(response));
76        tooltip
77    }
78
79    /// Show a tooltip when hovering a disabled widget.
80    pub fn for_disabled(response: &Response) -> Self {
81        let mut tooltip = Self::for_widget(response);
82        tooltip.popup = tooltip
83            .popup
84            .open(!response.enabled() && Self::should_show_tooltip(response));
85        tooltip
86    }
87
88    /// Show the tooltip at the pointer position.
89    #[inline]
90    pub fn at_pointer(mut self) -> Self {
91        self.popup = self.popup.at_pointer();
92        self
93    }
94
95    /// Set the gap between the tooltip and the anchor
96    ///
97    /// Default: 5.0
98    #[inline]
99    pub fn gap(mut self, gap: f32) -> Self {
100        self.popup = self.popup.gap(gap);
101        self
102    }
103
104    /// Set the layout of the tooltip
105    #[inline]
106    pub fn layout(mut self, layout: Layout) -> Self {
107        self.popup = self.popup.layout(layout);
108        self
109    }
110
111    /// Set the width of the tooltip
112    #[inline]
113    pub fn width(mut self, width: f32) -> Self {
114        self.popup = self.popup.width(width);
115        self
116    }
117
118    /// Show the tooltip
119    pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
120        let Self {
121            mut popup,
122            parent_layer,
123            parent_widget,
124        } = self;
125
126        if !popup.is_open() {
127            return None;
128        }
129
130        let rect = popup.get_anchor_rect()?;
131
132        let mut state = popup.ctx().pass_state_mut(|fs| {
133            // Remember that this is the widget showing the tooltip:
134            fs.layers
135                .entry(parent_layer)
136                .or_default()
137                .widget_with_tooltip = Some(parent_widget);
138
139            fs.tooltips
140                .widget_tooltips
141                .get(&parent_widget)
142                .copied()
143                .unwrap_or(PerWidgetTooltipState {
144                    bounding_rect: rect,
145                    tooltip_count: 0,
146                })
147        });
148
149        let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
150        popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
151
152        let response = popup.show(|ui| {
153            // By default, the text in tooltips aren't selectable.
154            // This means that most tooltips aren't interactable,
155            // which also mean they won't stick around so you can click them.
156            // Only tooltips that have actual interactive stuff (buttons, links, …)
157            // will stick around when you try to click them.
158            ui.style_mut().interaction.selectable_labels = false;
159
160            content(ui)
161        });
162
163        // The popup might not be shown on at_pointer if there is no pointer.
164        if let Some(response) = &response {
165            state.tooltip_count += 1;
166            state.bounding_rect |= response.response.rect;
167            response
168                .response
169                .ctx
170                .pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(parent_widget, state));
171            Self::remember_that_tooltip_was_shown(&response.response.ctx);
172        }
173
174        response
175    }
176
177    fn when_was_a_toolip_last_shown_id() -> Id {
178        Id::new("when_was_a_toolip_last_shown")
179    }
180
181    pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
182        let when_was_a_toolip_last_shown =
183            ctx.data(|d| d.get_temp::<f64>(Self::when_was_a_toolip_last_shown_id()));
184
185        if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
186            let now = ctx.input(|i| i.time);
187            (now - when_was_a_toolip_last_shown) as f32
188        } else {
189            f32::INFINITY
190        }
191    }
192
193    fn remember_that_tooltip_was_shown(ctx: &Context) {
194        let now = ctx.input(|i| i.time);
195        ctx.data_mut(|data| data.insert_temp::<f64>(Self::when_was_a_toolip_last_shown_id(), now));
196    }
197
198    /// What is the id of the next tooltip for this widget?
199    pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
200        let tooltip_count = ctx.pass_state(|fs| {
201            fs.tooltips
202                .widget_tooltips
203                .get(&widget_id)
204                .map_or(0, |state| state.tooltip_count)
205        });
206        Self::tooltip_id(widget_id, tooltip_count)
207    }
208
209    pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
210        widget_id.with(tooltip_count)
211    }
212
213    /// Should we show a tooltip for this response?
214    pub fn should_show_tooltip(response: &Response) -> bool {
215        if response.ctx.memory(|mem| mem.everything_is_visible()) {
216            return true;
217        }
218
219        let any_open_popups = response.ctx.prev_pass_state(|fs| {
220            fs.layers
221                .get(&response.layer_id)
222                .is_some_and(|layer| !layer.open_popups.is_empty())
223        });
224        if any_open_popups {
225            // Hide tooltips if the user opens a popup (menu, combo-box, etc.) in the same layer.
226            return false;
227        }
228
229        let style = response.ctx.style();
230
231        let tooltip_delay = style.interaction.tooltip_delay;
232        let tooltip_grace_time = style.interaction.tooltip_grace_time;
233
234        let (
235            time_since_last_scroll,
236            time_since_last_click,
237            time_since_last_pointer_movement,
238            pointer_pos,
239            pointer_dir,
240        ) = response.ctx.input(|i| {
241            (
242                i.time_since_last_scroll(),
243                i.pointer.time_since_last_click(),
244                i.pointer.time_since_last_movement(),
245                i.pointer.hover_pos(),
246                i.pointer.direction(),
247            )
248        });
249
250        if time_since_last_scroll < tooltip_delay {
251            // See https://github.com/emilk/egui/issues/4781
252            // Note that this means we cannot have `ScrollArea`s in a tooltip.
253            response
254                .ctx
255                .request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
256            return false;
257        }
258
259        let is_our_tooltip_open = response.is_tooltip_open();
260
261        if is_our_tooltip_open {
262            // Check if we should automatically stay open:
263
264            let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
265            let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
266
267            let tooltip_has_interactive_widget = response.ctx.viewport(|vp| {
268                vp.prev_pass
269                    .widgets
270                    .get_layer(tooltip_layer_id)
271                    .any(|w| w.enabled && w.sense.interactive())
272            });
273
274            if tooltip_has_interactive_widget {
275                // We keep the tooltip open if hovered,
276                // or if the pointer is on its way to it,
277                // so that the user can interact with the tooltip
278                // (i.e. click links that are in it).
279                if let Some(area) = AreaState::load(&response.ctx, tooltip_id) {
280                    let rect = area.rect();
281
282                    if let Some(pos) = pointer_pos {
283                        if rect.contains(pos) {
284                            return true; // hovering interactive tooltip
285                        }
286                        if pointer_dir != Vec2::ZERO
287                            && rect.intersects_ray(pos, pointer_dir.normalized())
288                        {
289                            return true; // on the way to interactive tooltip
290                        }
291                    }
292                }
293            }
294        }
295
296        let clicked_more_recently_than_moved =
297            time_since_last_click < time_since_last_pointer_movement + 0.1;
298        if clicked_more_recently_than_moved {
299            // It is common to click a widget and then rest the mouse there.
300            // It would be annoying to then see a tooltip for it immediately.
301            // Similarly, clicking should hide the existing tooltip.
302            // Only hovering should lead to a tooltip, not clicking.
303            // The offset is only to allow small movement just right after the click.
304            return false;
305        }
306
307        if is_our_tooltip_open {
308            // Check if we should automatically stay open:
309
310            if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
311                // Handle the case of a big tooltip that covers the widget:
312                return true;
313            }
314        }
315
316        let is_other_tooltip_open = response.ctx.prev_pass_state(|fs| {
317            if let Some(already_open_tooltip) = fs
318                .layers
319                .get(&response.layer_id)
320                .and_then(|layer| layer.widget_with_tooltip)
321            {
322                already_open_tooltip != response.id
323            } else {
324                false
325            }
326        });
327        if is_other_tooltip_open {
328            // We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
329            return false;
330        }
331
332        // Fast early-outs:
333        if response.enabled() {
334            if !response.hovered() || !response.ctx.input(|i| i.pointer.has_pointer()) {
335                return false;
336            }
337        } else if !response
338            .ctx
339            .rect_contains_pointer(response.layer_id, response.rect)
340        {
341            return false;
342        }
343
344        // There is a tooltip_delay before showing the first tooltip,
345        // but once one tooltip is show, moving the mouse cursor to
346        // another widget should show the tooltip for that widget right away.
347
348        // Let the user quickly move over some dead space to hover the next thing
349        let tooltip_was_recently_shown =
350            Self::seconds_since_last_tooltip(&response.ctx) < tooltip_grace_time;
351
352        if !tooltip_was_recently_shown && !is_our_tooltip_open {
353            if style.interaction.show_tooltips_only_when_still {
354                // We only show the tooltip when the mouse pointer is still.
355                if !response
356                    .ctx
357                    .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
358                {
359                    // wait for mouse to stop
360                    response.ctx.request_repaint();
361                    return false;
362                }
363            }
364
365            let time_since_last_interaction = time_since_last_scroll
366                .min(time_since_last_pointer_movement)
367                .min(time_since_last_click);
368            let time_til_tooltip = tooltip_delay - time_since_last_interaction;
369
370            if 0.0 < time_til_tooltip {
371                // Wait until the mouse has been still for a while
372                response.ctx.request_repaint_after_secs(time_til_tooltip);
373                return false;
374            }
375        }
376
377        // We don't want tooltips of things while we are dragging them,
378        // but we do want tooltips while holding down on an item on a touch screen.
379        if response
380            .ctx
381            .input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
382        {
383            return false;
384        }
385
386        // All checks passed: show the tooltip!
387
388        true
389    }
390
391    /// Was this tooltip visible last frame?
392    pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
393        let primary_tooltip_area_id = Self::tooltip_id(widget_id, 0);
394        ctx.memory(|mem| {
395            mem.areas()
396                .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
397        })
398    }
399}