1use frame_state::PerWidgetTooltipState;
4
5use crate::*;
6
7fn when_was_a_toolip_last_shown_id() -> Id {
10 Id::new("when_was_a_toolip_last_shown")
11}
12
13pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
14 let when_was_a_toolip_last_shown =
15 ctx.data(|d| d.get_temp::<f64>(when_was_a_toolip_last_shown_id()));
16
17 if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
18 let now = ctx.input(|i| i.time);
19 (now - when_was_a_toolip_last_shown) as f32
20 } else {
21 f32::INFINITY
22 }
23}
24
25fn remember_that_tooltip_was_shown(ctx: &Context) {
26 let now = ctx.input(|i| i.time);
27 ctx.data_mut(|data| data.insert_temp::<f64>(when_was_a_toolip_last_shown_id(), now));
28}
29
30pub fn show_tooltip<R>(
50 ctx: &Context,
51 parent_layer: LayerId,
52 widget_id: Id,
53 add_contents: impl FnOnce(&mut Ui) -> R,
54) -> Option<R> {
55 show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
56}
57
58pub fn show_tooltip_at_pointer<R>(
76 ctx: &Context,
77 parent_layer: LayerId,
78 widget_id: Id,
79 add_contents: impl FnOnce(&mut Ui) -> R,
80) -> Option<R> {
81 ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| {
82 let allow_placing_below = true;
83
84 let mut exclusion_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
87
88 exclusion_rect.min.x = pointer_pos.x;
90
91 show_tooltip_at_dyn(
92 ctx,
93 parent_layer,
94 widget_id,
95 allow_placing_below,
96 &exclusion_rect,
97 Box::new(add_contents),
98 )
99 })
100}
101
102pub fn show_tooltip_for<R>(
106 ctx: &Context,
107 parent_layer: LayerId,
108 widget_id: Id,
109 widget_rect: &Rect,
110 add_contents: impl FnOnce(&mut Ui) -> R,
111) -> R {
112 let is_touch_screen = ctx.input(|i| i.any_touches());
113 let allow_placing_below = !is_touch_screen; show_tooltip_at_dyn(
115 ctx,
116 parent_layer,
117 widget_id,
118 allow_placing_below,
119 widget_rect,
120 Box::new(add_contents),
121 )
122}
123
124pub fn show_tooltip_at<R>(
128 ctx: &Context,
129 parent_layer: LayerId,
130 widget_id: Id,
131 suggested_position: Pos2,
132 add_contents: impl FnOnce(&mut Ui) -> R,
133) -> R {
134 let allow_placing_below = true;
135 let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
136 show_tooltip_at_dyn(
137 ctx,
138 parent_layer,
139 widget_id,
140 allow_placing_below,
141 &rect,
142 Box::new(add_contents),
143 )
144}
145
146fn show_tooltip_at_dyn<'c, R>(
147 ctx: &Context,
148 parent_layer: LayerId,
149 widget_id: Id,
150 allow_placing_below: bool,
151 widget_rect: &Rect,
152 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
153) -> R {
154 let mut widget_rect = *widget_rect;
155 if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
156 widget_rect = transform * widget_rect;
157 }
158
159 remember_that_tooltip_was_shown(ctx);
160
161 let mut state = ctx.frame_state_mut(|fs| {
162 fs.layers
164 .entry(parent_layer)
165 .or_default()
166 .widget_with_tooltip = Some(widget_id);
167
168 fs.tooltips
169 .widget_tooltips
170 .get(&widget_id)
171 .copied()
172 .unwrap_or(PerWidgetTooltipState {
173 bounding_rect: widget_rect,
174 tooltip_count: 0,
175 })
176 });
177
178 let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count);
179 let expected_tooltip_size = AreaState::load(ctx, tooltip_area_id)
180 .and_then(|area| area.size)
181 .unwrap_or(vec2(64.0, 32.0));
182
183 let screen_rect = ctx.screen_rect();
184
185 let (pivot, anchor) = find_tooltip_position(
186 screen_rect,
187 state.bounding_rect,
188 allow_placing_below,
189 expected_tooltip_size,
190 );
191
192 let InnerResponse { inner, response } = Area::new(tooltip_area_id)
193 .kind(UiKind::Popup)
194 .order(Order::Tooltip)
195 .pivot(pivot)
196 .fixed_pos(anchor)
197 .default_width(ctx.style().spacing.tooltip_width)
198 .sense(Sense::hover()) .show(ctx, |ui| {
200 ui.style_mut().interaction.selectable_labels = false;
206
207 Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
208 });
209
210 state.tooltip_count += 1;
211 state.bounding_rect = state.bounding_rect.union(response.rect);
212 ctx.frame_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
213
214 inner
215}
216
217pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
219 let tooltip_count = ctx.frame_state(|fs| {
220 fs.tooltips
221 .widget_tooltips
222 .get(&widget_id)
223 .map_or(0, |state| state.tooltip_count)
224 });
225 tooltip_id(widget_id, tooltip_count)
226}
227
228pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
229 widget_id.with(tooltip_count)
230}
231
232fn find_tooltip_position(
238 screen_rect: Rect,
239 widget_rect: Rect,
240 allow_placing_below: bool,
241 tooltip_size: Vec2,
242) -> (Align2, Pos2) {
243 let spacing = 4.0;
244
245 if allow_placing_below
247 && widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
248 {
249 return (
250 Align2::LEFT_TOP,
251 widget_rect.left_bottom() + spacing * Vec2::DOWN,
252 );
253 }
254
255 if screen_rect.top() + tooltip_size.y + spacing <= widget_rect.top() {
257 return (
258 Align2::LEFT_BOTTOM,
259 widget_rect.left_top() + spacing * Vec2::UP,
260 );
261 }
262
263 if widget_rect.right() + spacing + tooltip_size.x <= screen_rect.right() {
265 return (
266 Align2::LEFT_TOP,
267 widget_rect.right_top() + spacing * Vec2::RIGHT,
268 );
269 }
270
271 if screen_rect.left() + tooltip_size.x + spacing <= widget_rect.left() {
273 return (
274 Align2::RIGHT_TOP,
275 widget_rect.left_top() + spacing * Vec2::LEFT,
276 );
277 }
278
279 (Align2::LEFT_TOP, screen_rect.left_top())
283}
284
285pub fn show_tooltip_text(
301 ctx: &Context,
302 parent_layer: LayerId,
303 widget_id: Id,
304 text: impl Into<WidgetText>,
305) -> Option<()> {
306 show_tooltip(ctx, parent_layer, widget_id, |ui| {
307 crate::widgets::Label::new(text).ui(ui);
308 })
309}
310
311pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
313 let primary_tooltip_area_id = tooltip_id(widget_id, 0);
314 ctx.memory(|mem| {
315 mem.areas()
316 .visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
317 })
318}
319
320#[derive(Clone, Copy)]
322pub enum PopupCloseBehavior {
323 CloseOnClick,
327
328 CloseOnClickOutside,
331
332 IgnoreClicks,
335}
336
337pub fn popup_below_widget<R>(
339 ui: &Ui,
340 popup_id: Id,
341 widget_response: &Response,
342 close_behavior: PopupCloseBehavior,
343 add_contents: impl FnOnce(&mut Ui) -> R,
344) -> Option<R> {
345 popup_above_or_below_widget(
346 ui,
347 popup_id,
348 widget_response,
349 AboveOrBelow::Below,
350 close_behavior,
351 add_contents,
352 )
353}
354
355pub fn popup_above_or_below_widget<R>(
382 parent_ui: &Ui,
383 popup_id: Id,
384 widget_response: &Response,
385 above_or_below: AboveOrBelow,
386 close_behavior: PopupCloseBehavior,
387 add_contents: impl FnOnce(&mut Ui) -> R,
388) -> Option<R> {
389 if !parent_ui.memory(|mem| mem.is_popup_open(popup_id)) {
390 return None;
391 }
392
393 let (mut pos, pivot) = match above_or_below {
394 AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
395 AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
396 };
397 if let Some(transform) = parent_ui
398 .ctx()
399 .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied())
400 {
401 pos = transform * pos;
402 }
403
404 let frame = Frame::popup(parent_ui.style());
405 let frame_margin = frame.total_margin();
406 let inner_width = widget_response.rect.width() - frame_margin.sum().x;
407
408 parent_ui.ctx().frame_state_mut(|fs| {
409 fs.layers
410 .entry(parent_ui.layer_id())
411 .or_default()
412 .open_popups
413 .insert(popup_id)
414 });
415
416 let response = Area::new(popup_id)
417 .kind(UiKind::Popup)
418 .order(Order::Foreground)
419 .fixed_pos(pos)
420 .default_width(inner_width)
421 .pivot(pivot)
422 .show(parent_ui.ctx(), |ui| {
423 frame
424 .show(ui, |ui| {
425 ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
426 ui.set_min_width(inner_width);
427 add_contents(ui)
428 })
429 .inner
430 })
431 .inner
432 });
433
434 let should_close = match close_behavior {
435 PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(),
436 PopupCloseBehavior::CloseOnClickOutside => {
437 widget_response.clicked_elsewhere() && response.response.clicked_elsewhere()
438 }
439 PopupCloseBehavior::IgnoreClicks => false,
440 };
441
442 if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close {
443 parent_ui.memory_mut(|mem| mem.close_popup());
444 }
445 Some(response.inner)
446}