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, true));
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, true));
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    ///
215    /// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that
216    /// contains interactive widgets
217    pub fn should_show_tooltip(response: &Response, allow_interactive_tooltip: bool) -> bool {
218        if response.ctx.memory(|mem| mem.everything_is_visible()) {
219            return true;
220        }
221
222        let any_open_popups = response.ctx.prev_pass_state(|fs| {
223            fs.layers
224                .get(&response.layer_id)
225                .is_some_and(|layer| !layer.open_popups.is_empty())
226        });
227        if any_open_popups {
228            // Hide tooltips if the user opens a popup (menu, combo-box, etc.) in the same layer.
229            return false;
230        }
231
232        let style = response.ctx.style();
233
234        let tooltip_delay = style.interaction.tooltip_delay;
235        let tooltip_grace_time = style.interaction.tooltip_grace_time;
236
237        let (
238            time_since_last_scroll,
239            time_since_last_click,
240            time_since_last_pointer_movement,
241            pointer_pos,
242            pointer_dir,
243        ) = response.ctx.input(|i| {
244            (
245                i.time_since_last_scroll(),
246                i.pointer.time_since_last_click(),
247                i.pointer.time_since_last_movement(),
248                i.pointer.hover_pos(),
249                i.pointer.direction(),
250            )
251        });
252
253        if time_since_last_scroll < tooltip_delay {
254            // See https://github.com/emilk/egui/issues/4781
255            // Note that this means we cannot have `ScrollArea`s in a tooltip.
256            response
257                .ctx
258                .request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
259            return false;
260        }
261
262        let is_our_tooltip_open = response.is_tooltip_open();
263
264        if is_our_tooltip_open {
265            // Check if we should automatically stay open:
266
267            let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
268            let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
269
270            let tooltip_has_interactive_widget = allow_interactive_tooltip
271                && response.ctx.viewport(|vp| {
272                    vp.prev_pass
273                        .widgets
274                        .get_layer(tooltip_layer_id)
275                        .any(|w| w.enabled && w.sense.interactive())
276                });
277
278            if tooltip_has_interactive_widget {
279                // We keep the tooltip open if hovered,
280                // or if the pointer is on its way to it,
281                // so that the user can interact with the tooltip
282                // (i.e. click links that are in it).
283                if let Some(area) = AreaState::load(&response.ctx, tooltip_id) {
284                    let rect = area.rect();
285
286                    if let Some(pos) = pointer_pos {
287                        if rect.contains(pos) {
288                            return true; // hovering interactive tooltip
289                        }
290                        if pointer_dir != Vec2::ZERO
291                            && rect.intersects_ray(pos, pointer_dir.normalized())
292                        {
293                            return true; // on the way to interactive tooltip
294                        }
295                    }
296                }
297            }
298        }
299
300        let clicked_more_recently_than_moved =
301            time_since_last_click < time_since_last_pointer_movement + 0.1;
302        if clicked_more_recently_than_moved {
303            // It is common to click a widget and then rest the mouse there.
304            // It would be annoying to then see a tooltip for it immediately.
305            // Similarly, clicking should hide the existing tooltip.
306            // Only hovering should lead to a tooltip, not clicking.
307            // The offset is only to allow small movement just right after the click.
308            return false;
309        }
310
311        if is_our_tooltip_open {
312            // Check if we should automatically stay open:
313
314            if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
315                // Handle the case of a big tooltip that covers the widget:
316                return true;
317            }
318        }
319
320        let is_other_tooltip_open = response.ctx.prev_pass_state(|fs| {
321            if let Some(already_open_tooltip) = fs
322                .layers
323                .get(&response.layer_id)
324                .and_then(|layer| layer.widget_with_tooltip)
325            {
326                already_open_tooltip != response.id
327            } else {
328                false
329            }
330        });
331        if is_other_tooltip_open {
332            // We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
333            return false;
334        }
335
336        // Fast early-outs:
337        if response.enabled() {
338            if !response.hovered() || !response.ctx.input(|i| i.pointer.has_pointer()) {
339                return false;
340            }
341        } else if !response
342            .ctx
343            .rect_contains_pointer(response.layer_id, response.rect)
344        {
345            return false;
346        }
347
348        // There is a tooltip_delay before showing the first tooltip,
349        // but once one tooltip is show, moving the mouse cursor to
350        // another widget should show the tooltip for that widget right away.
351
352        // Let the user quickly move over some dead space to hover the next thing
353        let tooltip_was_recently_shown =
354            Self::seconds_since_last_tooltip(&response.ctx) < tooltip_grace_time;
355
356        if !tooltip_was_recently_shown && !is_our_tooltip_open {
357            if style.interaction.show_tooltips_only_when_still {
358                // We only show the tooltip when the mouse pointer is still.
359                if !response
360                    .ctx
361                    .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
362                {
363                    // wait for mouse to stop
364                    response.ctx.request_repaint();
365                    return false;
366                }
367            }
368
369            let time_since_last_interaction = time_since_last_scroll
370                .min(time_since_last_pointer_movement)
371                .min(time_since_last_click);
372            let time_til_tooltip = tooltip_delay - time_since_last_interaction;
373
374            if 0.0 < time_til_tooltip {
375                // Wait until the mouse has been still for a while
376                response.ctx.request_repaint_after_secs(time_til_tooltip);
377                return false;
378            }
379        }
380
381        // We don't want tooltips of things while we are dragging them,
382        // but we do want tooltips while holding down on an item on a touch screen.
383        if response
384            .ctx
385            .input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
386        {
387            return false;
388        }
389
390        // All checks passed: show the tooltip!
391
392        true
393    }
394
395    /// Was this tooltip visible last frame?
396    pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
397        let primary_tooltip_area_id = Self::tooltip_id(widget_id, 0);
398        ctx.memory(|mem| {
399            mem.areas()
400                .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
401        })
402    }
403}