1use pass_state::PerWidgetTooltipState;
4
5use crate::{
6 pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id,
7 InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2,
8 Widget, WidgetText,
9};
10
11fn when_was_a_toolip_last_shown_id() -> Id {
14 Id::new("when_was_a_toolip_last_shown")
15}
16
17pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
18 let when_was_a_toolip_last_shown =
19 ctx.data(|d| d.get_temp::<f64>(when_was_a_toolip_last_shown_id()));
20
21 if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
22 let now = ctx.input(|i| i.time);
23 (now - when_was_a_toolip_last_shown) as f32
24 } else {
25 f32::INFINITY
26 }
27}
28
29fn remember_that_tooltip_was_shown(ctx: &Context) {
30 let now = ctx.input(|i| i.time);
31 ctx.data_mut(|data| data.insert_temp::<f64>(when_was_a_toolip_last_shown_id(), now));
32}
33
34pub fn show_tooltip<R>(
54 ctx: &Context,
55 parent_layer: LayerId,
56 widget_id: Id,
57 add_contents: impl FnOnce(&mut Ui) -> R,
58) -> Option<R> {
59 show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
60}
61
62pub fn show_tooltip_at_pointer<R>(
80 ctx: &Context,
81 parent_layer: LayerId,
82 widget_id: Id,
83 add_contents: impl FnOnce(&mut Ui) -> R,
84) -> Option<R> {
85 ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| {
86 let allow_placing_below = true;
87
88 let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
91
92 pointer_rect.min.x = pointer_pos.x;
94
95 if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) {
97 pointer_rect = from_global * pointer_rect;
98 }
99
100 show_tooltip_at_dyn(
101 ctx,
102 parent_layer,
103 widget_id,
104 allow_placing_below,
105 &pointer_rect,
106 Box::new(add_contents),
107 )
108 })
109}
110
111pub fn show_tooltip_for<R>(
115 ctx: &Context,
116 parent_layer: LayerId,
117 widget_id: Id,
118 widget_rect: &Rect,
119 add_contents: impl FnOnce(&mut Ui) -> R,
120) -> R {
121 let is_touch_screen = ctx.input(|i| i.any_touches());
122 let allow_placing_below = !is_touch_screen; show_tooltip_at_dyn(
124 ctx,
125 parent_layer,
126 widget_id,
127 allow_placing_below,
128 widget_rect,
129 Box::new(add_contents),
130 )
131}
132
133pub fn show_tooltip_at<R>(
137 ctx: &Context,
138 parent_layer: LayerId,
139 widget_id: Id,
140 suggested_position: Pos2,
141 add_contents: impl FnOnce(&mut Ui) -> R,
142) -> R {
143 let allow_placing_below = true;
144 let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
145 show_tooltip_at_dyn(
146 ctx,
147 parent_layer,
148 widget_id,
149 allow_placing_below,
150 &rect,
151 Box::new(add_contents),
152 )
153}
154
155fn show_tooltip_at_dyn<'c, R>(
156 ctx: &Context,
157 parent_layer: LayerId,
158 widget_id: Id,
159 allow_placing_below: bool,
160 widget_rect: &Rect,
161 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
162) -> R {
163 let mut widget_rect = *widget_rect;
165 if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) {
166 widget_rect = to_global * widget_rect;
167 }
168
169 remember_that_tooltip_was_shown(ctx);
170
171 let mut state = ctx.pass_state_mut(|fs| {
172 fs.layers
174 .entry(parent_layer)
175 .or_default()
176 .widget_with_tooltip = Some(widget_id);
177
178 fs.tooltips
179 .widget_tooltips
180 .get(&widget_id)
181 .copied()
182 .unwrap_or(PerWidgetTooltipState {
183 bounding_rect: widget_rect,
184 tooltip_count: 0,
185 })
186 });
187
188 let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count);
189 let expected_tooltip_size = AreaState::load(ctx, tooltip_area_id)
190 .and_then(|area| area.size)
191 .unwrap_or(vec2(64.0, 32.0));
192
193 let screen_rect = ctx.screen_rect();
194
195 let (pivot, anchor) = find_tooltip_position(
196 screen_rect,
197 state.bounding_rect,
198 allow_placing_below,
199 expected_tooltip_size,
200 );
201
202 let InnerResponse { inner, response } = Area::new(tooltip_area_id)
203 .kind(UiKind::Popup)
204 .order(Order::Tooltip)
205 .pivot(pivot)
206 .fixed_pos(anchor)
207 .default_width(ctx.style().spacing.tooltip_width)
208 .sense(Sense::hover()) .show(ctx, |ui| {
210 ui.style_mut().interaction.selectable_labels = false;
216
217 Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
218 });
219
220 state.tooltip_count += 1;
221 state.bounding_rect = state.bounding_rect.union(response.rect);
222 ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
223
224 inner
225}
226
227pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
229 let tooltip_count = ctx.pass_state(|fs| {
230 fs.tooltips
231 .widget_tooltips
232 .get(&widget_id)
233 .map_or(0, |state| state.tooltip_count)
234 });
235 tooltip_id(widget_id, tooltip_count)
236}
237
238pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
239 widget_id.with(tooltip_count)
240}
241
242fn find_tooltip_position(
248 screen_rect: Rect,
249 widget_rect: Rect,
250 allow_placing_below: bool,
251 tooltip_size: Vec2,
252) -> (Align2, Pos2) {
253 let spacing = 4.0;
254
255 if allow_placing_below
257 && widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
258 {
259 return (
260 Align2::LEFT_TOP,
261 widget_rect.left_bottom() + spacing * Vec2::DOWN,
262 );
263 }
264
265 if screen_rect.top() + tooltip_size.y + spacing <= widget_rect.top() {
267 return (
268 Align2::LEFT_BOTTOM,
269 widget_rect.left_top() + spacing * Vec2::UP,
270 );
271 }
272
273 if widget_rect.right() + spacing + tooltip_size.x <= screen_rect.right() {
275 return (
276 Align2::LEFT_TOP,
277 widget_rect.right_top() + spacing * Vec2::RIGHT,
278 );
279 }
280
281 if screen_rect.left() + tooltip_size.x + spacing <= widget_rect.left() {
283 return (
284 Align2::RIGHT_TOP,
285 widget_rect.left_top() + spacing * Vec2::LEFT,
286 );
287 }
288
289 (Align2::LEFT_TOP, screen_rect.left_top())
293}
294
295pub fn show_tooltip_text(
311 ctx: &Context,
312 parent_layer: LayerId,
313 widget_id: Id,
314 text: impl Into<WidgetText>,
315) -> Option<()> {
316 show_tooltip(ctx, parent_layer, widget_id, |ui| {
317 crate::widgets::Label::new(text).ui(ui);
318 })
319}
320
321pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
323 let primary_tooltip_area_id = tooltip_id(widget_id, 0);
324 ctx.memory(|mem| {
325 mem.areas()
326 .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
327 })
328}
329
330#[derive(Clone, Copy)]
332pub enum PopupCloseBehavior {
333 CloseOnClick,
337
338 CloseOnClickOutside,
341
342 IgnoreClicks,
345}
346
347pub fn popup_below_widget<R>(
349 ui: &Ui,
350 popup_id: Id,
351 widget_response: &Response,
352 close_behavior: PopupCloseBehavior,
353 add_contents: impl FnOnce(&mut Ui) -> R,
354) -> Option<R> {
355 popup_above_or_below_widget(
356 ui,
357 popup_id,
358 widget_response,
359 AboveOrBelow::Below,
360 close_behavior,
361 add_contents,
362 )
363}
364
365pub fn popup_above_or_below_widget<R>(
392 parent_ui: &Ui,
393 popup_id: Id,
394 widget_response: &Response,
395 above_or_below: AboveOrBelow,
396 close_behavior: PopupCloseBehavior,
397 add_contents: impl FnOnce(&mut Ui) -> R,
398) -> Option<R> {
399 if !parent_ui.memory(|mem| mem.is_popup_open(popup_id)) {
400 return None;
401 }
402
403 let (mut pos, pivot) = match above_or_below {
404 AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
405 AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
406 };
407
408 if let Some(to_global) = parent_ui
409 .ctx()
410 .layer_transform_to_global(parent_ui.layer_id())
411 {
412 pos = to_global * pos;
413 }
414
415 let frame = Frame::popup(parent_ui.style());
416 let frame_margin = frame.total_margin();
417 let inner_width = (widget_response.rect.width() - frame_margin.sum().x).max(0.0);
418
419 parent_ui.ctx().pass_state_mut(|fs| {
420 fs.layers
421 .entry(parent_ui.layer_id())
422 .or_default()
423 .open_popups
424 .insert(popup_id)
425 });
426
427 let response = Area::new(popup_id)
428 .kind(UiKind::Popup)
429 .order(Order::Foreground)
430 .fixed_pos(pos)
431 .default_width(inner_width)
432 .pivot(pivot)
433 .show(parent_ui.ctx(), |ui| {
434 frame
435 .show(ui, |ui| {
436 ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
437 ui.set_min_width(inner_width);
438 add_contents(ui)
439 })
440 .inner
441 })
442 .inner
443 });
444
445 let should_close = match close_behavior {
446 PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(),
447 PopupCloseBehavior::CloseOnClickOutside => {
448 widget_response.clicked_elsewhere() && response.response.clicked_elsewhere()
449 }
450 PopupCloseBehavior::IgnoreClicks => false,
451 };
452
453 if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close {
454 parent_ui.memory_mut(|mem| mem.close_popup());
455 }
456 Some(response.inner)
457}