egui/containers/
popup.rs

1//! Show popup windows, tooltips, context menus etc.
2
3use 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
11// ----------------------------------------------------------------------------
12
13fn 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
34// ----------------------------------------------------------------------------
35
36/// Show a tooltip at the current pointer position (if any).
37///
38/// Most of the time it is easier to use [`Response::on_hover_ui`].
39///
40/// See also [`show_tooltip_text`].
41///
42/// Returns `None` if the tooltip could not be placed.
43///
44/// ```
45/// # egui::__run_test_ui(|ui| {
46/// if ui.ui_contains_pointer() {
47///     egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
48///         ui.label("Helpful text");
49///     });
50/// }
51/// # });
52/// ```
53pub 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
62/// Show a tooltip at the current pointer position (if any).
63///
64/// Most of the time it is easier to use [`Response::on_hover_ui`].
65///
66/// See also [`show_tooltip_text`].
67///
68/// Returns `None` if the tooltip could not be placed.
69///
70/// ```
71/// # egui::__run_test_ui(|ui| {
72/// if ui.ui_contains_pointer() {
73///     egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
74///         ui.label("Helpful text");
75///     });
76/// }
77/// # });
78/// ```
79pub 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        // Add a small exclusion zone around the pointer to avoid tooltips
89        // covering what we're hovering over.
90        let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
91
92        // Keep the left edge of the tooltip in line with the cursor:
93        pointer_rect.min.x = pointer_pos.x;
94
95        // Transform global coords to layer coords:
96        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
111/// Show a tooltip under the given area.
112///
113/// If the tooltip does not fit under the area, it tries to place it above it instead.
114pub 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; // There is a finger below.
123    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
133/// Show a tooltip at the given position.
134///
135/// Returns `None` if the tooltip could not be placed.
136pub 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    // Transform layer coords to global coords:
164    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        // Remember that this is the widget showing the tooltip:
173        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()) // don't click to bring to front
209        .show(ctx, |ui| {
210            // By default the text in tooltips aren't selectable.
211            // This means that most tooltips aren't interactable,
212            // which also mean they won't stick around so you can click them.
213            // Only tooltips that have actual interactive stuff (buttons, links, …)
214            // will stick around when you try to click them.
215            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
227/// What is the id of the next tooltip for this widget?
228pub 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
242/// Returns `(PIVOT, POS)` to mean: put the `PIVOT` corner of the tooltip at `POS`.
243///
244/// Note: the position might need to be constrained to the screen,
245/// (e.g. moved sideways if shown under the widget)
246/// but the `Area` will take care of that.
247fn 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    // Does it fit below?
256    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    // Does it fit above?
266    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    // Does it fit to the right?
274    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    // Does it fit to the left?
282    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    // It doesn't fit anywhere :(
290
291    // Just show it anyway:
292    (Align2::LEFT_TOP, screen_rect.left_top())
293}
294
295/// Show some text at the current pointer position (if any).
296///
297/// Most of the time it is easier to use [`Response::on_hover_text`].
298///
299/// See also [`show_tooltip`].
300///
301/// Returns `None` if the tooltip could not be placed.
302///
303/// ```
304/// # egui::__run_test_ui(|ui| {
305/// if ui.ui_contains_pointer() {
306///     egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
307/// }
308/// # });
309/// ```
310pub 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
321/// Was this popup visible last frame?
322pub 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/// Determines popup's close behavior
331#[derive(Clone, Copy)]
332pub enum PopupCloseBehavior {
333    /// Popup will be closed on click anywhere, inside or outside the popup.
334    ///
335    /// It is used in [`crate::ComboBox`].
336    CloseOnClick,
337
338    /// Popup will be closed if the click happened somewhere else
339    /// but in the popup's body
340    CloseOnClickOutside,
341
342    /// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_popup`]
343    /// or by pressing the escape button
344    IgnoreClicks,
345}
346
347/// Helper for [`popup_above_or_below_widget`].
348pub 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
365/// Shows a popup above or below another widget.
366///
367/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
368///
369/// The opened popup will have a minimum width matching its parent.
370///
371/// You must open the popup with [`crate::Memory::open_popup`] or  [`crate::Memory::toggle_popup`].
372///
373/// Returns `None` if the popup is not open.
374///
375/// ```
376/// # egui::__run_test_ui(|ui| {
377/// let response = ui.button("Open popup");
378/// let popup_id = ui.make_persistent_id("my_unique_id");
379/// if response.clicked() {
380///     ui.memory_mut(|mem| mem.toggle_popup(popup_id));
381/// }
382/// let below = egui::AboveOrBelow::Below;
383/// let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside;
384/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
385///     ui.set_min_width(200.0); // if you want to control the size
386///     ui.label("Some more info, or things you can select:");
387///     ui.label("…");
388/// });
389/// # });
390/// ```
391pub 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}