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));
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));
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) -> 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 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 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 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 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; }
286 if pointer_dir != Vec2::ZERO
287 && rect.intersects_ray(pos, pointer_dir.normalized())
288 {
289 return true; }
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 return false;
305 }
306
307 if is_our_tooltip_open {
308 if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
311 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 return false;
330 }
331
332 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 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 if !response
356 .ctx
357 .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
358 {
359 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 response.ctx.request_repaint_after_secs(time_til_tooltip);
373 return false;
374 }
375 }
376
377 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 true
389 }
390
391 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}