egui/containers/
tooltip.rs1use 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 parent_layer: LayerId,
13
14 parent_widget: Id,
16}
17
18impl Tooltip<'_> {
19 pub fn always_open(
21 ctx: Context,
22 parent_layer: LayerId,
23 parent_widget: Id,
24 anchor: impl Into<PopupAnchor>,
25 ) -> Self {
26 let width = ctx.global_style().spacing.tooltip_width;
27 Self {
28 popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
29 .kind(PopupKind::Tooltip)
30 .gap(4.0)
31 .width(width)
32 .sense(Sense::hover()),
33 parent_layer,
34 parent_widget,
35 }
36 }
37
38 pub fn for_widget(response: &Response) -> Self {
40 let popup = Popup::from_response(response)
41 .kind(PopupKind::Tooltip)
42 .gap(4.0)
43 .width(response.ctx.global_style().spacing.tooltip_width)
44 .sense(Sense::hover());
45 Self {
46 popup,
47 parent_layer: response.layer_id,
48 parent_widget: response.id,
49 }
50 }
51
52 pub fn for_enabled(response: &Response) -> Self {
54 let mut tooltip = Self::for_widget(response);
55 tooltip.popup = tooltip
56 .popup
57 .open(response.enabled() && Self::should_show_tooltip(response, true));
58 tooltip
59 }
60
61 pub fn for_disabled(response: &Response) -> Self {
63 let mut tooltip = Self::for_widget(response);
64 tooltip.popup = tooltip
65 .popup
66 .open(!response.enabled() && Self::should_show_tooltip(response, true));
67 tooltip
68 }
69
70 #[inline]
72 pub fn at_pointer(mut self) -> Self {
73 self.popup = self.popup.at_pointer();
74 self
75 }
76
77 #[inline]
81 pub fn gap(mut self, gap: f32) -> Self {
82 self.popup = self.popup.gap(gap);
83 self
84 }
85
86 #[inline]
88 pub fn layout(mut self, layout: Layout) -> Self {
89 self.popup = self.popup.layout(layout);
90 self
91 }
92
93 #[inline]
95 pub fn width(mut self, width: f32) -> Self {
96 self.popup = self.popup.width(width);
97 self
98 }
99
100 pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
102 let Self {
103 mut popup,
104 parent_layer,
105 parent_widget,
106 } = self;
107
108 if !popup.is_open() {
109 return None;
110 }
111
112 let rect = popup.get_anchor_rect()?;
113
114 let mut state = popup.ctx().pass_state_mut(|fs| {
115 fs.layers
117 .entry(parent_layer)
118 .or_default()
119 .widget_with_tooltip = Some(parent_widget);
120
121 fs.tooltips
122 .widget_tooltips
123 .get(&parent_widget)
124 .copied()
125 .unwrap_or(PerWidgetTooltipState {
126 bounding_rect: rect,
127 tooltip_count: 0,
128 })
129 });
130
131 let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
132
133 let interactable = Self::had_interactive_widgets(popup.ctx(), tooltip_area_id);
136
137 popup = popup
138 .anchor(state.bounding_rect)
139 .id(tooltip_area_id)
140 .interactable(interactable);
141
142 let response = popup.show(|ui| {
143 ui.style_mut().interaction.selectable_labels = false;
149
150 content(ui)
151 });
152
153 if let Some(response) = &response {
155 state.tooltip_count += 1;
156 state.bounding_rect |= response.response.rect;
157 response
158 .response
159 .ctx
160 .pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(parent_widget, state));
161 Self::remember_that_tooltip_was_shown(&response.response.ctx);
162 }
163
164 response
165 }
166
167 fn when_was_a_toolip_last_shown_id() -> Id {
168 Id::new("when_was_a_toolip_last_shown")
169 }
170
171 pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
172 let when_was_a_toolip_last_shown =
173 ctx.data(|d| d.get_temp::<f64>(Self::when_was_a_toolip_last_shown_id()));
174
175 if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
176 let now = ctx.input(|i| i.time);
177 (now - when_was_a_toolip_last_shown) as f32
178 } else {
179 f32::INFINITY
180 }
181 }
182
183 fn remember_that_tooltip_was_shown(ctx: &Context) {
184 let now = ctx.input(|i| i.time);
185 ctx.data_mut(|data| data.insert_temp::<f64>(Self::when_was_a_toolip_last_shown_id(), now));
186 }
187
188 pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
190 let tooltip_count = ctx.pass_state(|fs| {
191 fs.tooltips
192 .widget_tooltips
193 .get(&widget_id)
194 .map_or(0, |state| state.tooltip_count)
195 });
196 Self::tooltip_id(widget_id, tooltip_count)
197 }
198
199 pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
200 widget_id.with(tooltip_count)
201 }
202
203 fn had_interactive_widgets(ctx: &Context, tooltip_id: Id) -> bool {
208 let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
209 ctx.viewport(|vp| {
210 vp.prev_pass
211 .widgets
212 .get_layer(tooltip_layer_id)
213 .any(|w| w.enabled && w.sense.interactive())
214 })
215 }
216
217 pub fn should_show_tooltip(response: &Response, allow_interactive_tooltip: bool) -> bool {
222 if response.ctx.memory(|mem| mem.everything_is_visible()) {
223 return true;
224 }
225
226 let any_open_popups = response.ctx.prev_pass_state(|fs| {
227 fs.layers
228 .get(&response.layer_id)
229 .is_some_and(|layer| !layer.open_popups.is_empty())
230 });
231 if any_open_popups {
232 return false;
234 }
235
236 let style = response.ctx.global_style();
237
238 let tooltip_delay = style.interaction.tooltip_delay;
239 let tooltip_grace_time = style.interaction.tooltip_grace_time;
240
241 let (
242 time_since_last_scroll,
243 time_since_last_click,
244 time_since_last_pointer_movement,
245 pointer_pos,
246 pointer_dir,
247 ) = response.ctx.input(|i| {
248 (
249 i.time_since_last_scroll(),
250 i.pointer.time_since_last_click(),
251 i.pointer.time_since_last_movement(),
252 i.pointer.hover_pos(),
253 i.pointer.direction(),
254 )
255 });
256
257 if time_since_last_scroll < tooltip_delay {
258 response
261 .ctx
262 .request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
263 return false;
264 }
265
266 let is_our_tooltip_open = response.is_tooltip_open();
267
268 if is_our_tooltip_open {
269 let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
272
273 let tooltip_has_interactive_widget = allow_interactive_tooltip
274 && Self::had_interactive_widgets(&response.ctx, tooltip_id);
275
276 if tooltip_has_interactive_widget {
277 if let Some(area) = AreaState::load(&response.ctx, tooltip_id) {
282 let rect = area.rect();
283
284 if let Some(pos) = pointer_pos {
285 if rect.contains(pos) {
286 return true; }
288 if pointer_dir != Vec2::ZERO
289 && rect.intersects_ray(pos, pointer_dir.normalized())
290 {
291 return true; }
293 }
294 }
295 }
296 }
297
298 let clicked_more_recently_than_moved =
299 time_since_last_click < time_since_last_pointer_movement + 0.1;
300 if clicked_more_recently_than_moved {
301 return false;
307 }
308
309 if is_our_tooltip_open {
310 if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
313 return true;
315 }
316 }
317
318 let is_other_tooltip_open = response.ctx.prev_pass_state(|fs| {
319 if let Some(already_open_tooltip) = fs
320 .layers
321 .get(&response.layer_id)
322 .and_then(|layer| layer.widget_with_tooltip)
323 {
324 already_open_tooltip != response.id
325 } else {
326 false
327 }
328 });
329 if is_other_tooltip_open {
330 return false;
332 }
333
334 if response.enabled() {
336 if !response.hovered() || !response.ctx.input(|i| i.pointer.has_pointer()) {
337 return false;
338 }
339 } else if !response
340 .ctx
341 .rect_contains_pointer(response.layer_id, response.rect)
342 {
343 return false;
344 }
345
346 let tooltip_was_recently_shown =
352 Self::seconds_since_last_tooltip(&response.ctx) < tooltip_grace_time;
353
354 if !tooltip_was_recently_shown && !is_our_tooltip_open {
355 if style.interaction.show_tooltips_only_when_still {
356 if !response
358 .ctx
359 .input(|i| i.pointer.is_still() && !i.is_scrolling())
360 {
361 response.ctx.request_repaint();
363 return false;
364 }
365 }
366
367 let time_since_last_interaction = time_since_last_scroll
368 .min(time_since_last_pointer_movement)
369 .min(time_since_last_click);
370 let time_til_tooltip = tooltip_delay - time_since_last_interaction;
371
372 if 0.0 < time_til_tooltip {
373 response.ctx.request_repaint_after_secs(time_til_tooltip);
375 return false;
376 }
377 }
378
379 if response
382 .ctx
383 .input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
384 {
385 return false;
386 }
387
388 true
391 }
392
393 pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
395 let primary_tooltip_area_id = Self::tooltip_id(widget_id, 0);
396 ctx.memory(|mem| {
397 mem.areas()
398 .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
399 })
400 }
401}