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 #[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 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 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 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 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 #[inline]
90 pub fn at_pointer(mut self) -> Self {
91 self.popup = self.popup.at_pointer();
92 self
93 }
94
95 #[inline]
99 pub fn gap(mut self, gap: f32) -> Self {
100 self.popup = self.popup.gap(gap);
101 self
102 }
103
104 #[inline]
106 pub fn layout(mut self, layout: Layout) -> Self {
107 self.popup = self.popup.layout(layout);
108 self
109 }
110
111 #[inline]
113 pub fn width(mut self, width: f32) -> Self {
114 self.popup = self.popup.width(width);
115 self
116 }
117
118 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 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 ui.style_mut().interaction.selectable_labels = false;
159
160 content(ui)
161 });
162
163 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 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 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 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 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 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 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; }
290 if pointer_dir != Vec2::ZERO
291 && rect.intersects_ray(pos, pointer_dir.normalized())
292 {
293 return true; }
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 return false;
309 }
310
311 if is_our_tooltip_open {
312 if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
315 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 return false;
334 }
335
336 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 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 if !response
360 .ctx
361 .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
362 {
363 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 response.ctx.request_repaint_after_secs(time_til_tooltip);
377 return false;
378 }
379 }
380
381 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 true
393 }
394
395 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}