egui/containers/
popup.rs

1//! Show popup windows, tooltips, context menus etc.
2
3use frame_state::PerWidgetTooltipState;
4
5use crate::*;
6
7// ----------------------------------------------------------------------------
8
9fn 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
30// ----------------------------------------------------------------------------
31
32/// Show a tooltip at the current pointer position (if any).
33///
34/// Most of the time it is easier to use [`Response::on_hover_ui`].
35///
36/// See also [`show_tooltip_text`].
37///
38/// Returns `None` if the tooltip could not be placed.
39///
40/// ```
41/// # egui::__run_test_ui(|ui| {
42/// if ui.ui_contains_pointer() {
43///     egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
44///         ui.label("Helpful text");
45///     });
46/// }
47/// # });
48/// ```
49pub 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
58/// Show a tooltip at the current pointer position (if any).
59///
60/// Most of the time it is easier to use [`Response::on_hover_ui`].
61///
62/// See also [`show_tooltip_text`].
63///
64/// Returns `None` if the tooltip could not be placed.
65///
66/// ```
67/// # egui::__run_test_ui(|ui| {
68/// if ui.ui_contains_pointer() {
69///     egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
70///         ui.label("Helpful text");
71///     });
72/// }
73/// # });
74/// ```
75pub 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        // Add a small exclusion zone around the pointer to avoid tooltips
85        // covering what we're hovering over.
86        let mut exclusion_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
87
88        // Keep the left edge of the tooltip in line with the cursor:
89        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
102/// Show a tooltip under the given area.
103///
104/// If the tooltip does not fit under the area, it tries to place it above it instead.
105pub 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; // There is a finger below.
114    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
124/// Show a tooltip at the given position.
125///
126/// Returns `None` if the tooltip could not be placed.
127pub 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        // Remember that this is the widget showing the tooltip:
163        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()) // don't click to bring to front
199        .show(ctx, |ui| {
200            // By default the text in tooltips aren't selectable.
201            // This means that most tooltips aren't interactable,
202            // which also mean they won't stick around so you can click them.
203            // Only tooltips that have actual interactive stuff (buttons, links, …)
204            // will stick around when you try to click them.
205            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
217/// What is the id of the next tooltip for this widget?
218pub 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
232/// Returns `(PIVOT, POS)` to mean: put the `PIVOT` corner of the tooltip at `POS`.
233///
234/// Note: the position might need to be constrained to the screen,
235/// (e.g. moved sideways if shown under the widget)
236/// but the `Area` will take care of that.
237fn 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    // Does it fit below?
246    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    // Does it fit above?
256    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    // Does it fit to the right?
264    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    // Does it fit to the left?
272    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    // It doesn't fit anywhere :(
280
281    // Just show it anyway:
282    (Align2::LEFT_TOP, screen_rect.left_top())
283}
284
285/// Show some text at the current pointer position (if any).
286///
287/// Most of the time it is easier to use [`Response::on_hover_text`].
288///
289/// See also [`show_tooltip`].
290///
291/// Returns `None` if the tooltip could not be placed.
292///
293/// ```
294/// # egui::__run_test_ui(|ui| {
295/// if ui.ui_contains_pointer() {
296///     egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
297/// }
298/// # });
299/// ```
300pub 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
311/// Was this popup visible last frame?
312pub 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/// Determines popup's close behavior
321#[derive(Clone, Copy)]
322pub enum PopupCloseBehavior {
323    /// Popup will be closed on click anywhere, inside or outside the popup.
324    ///
325    /// It is used in [`ComboBox`].
326    CloseOnClick,
327
328    /// Popup will be closed if the click happened somewhere else
329    /// but in the popup's body
330    CloseOnClickOutside,
331
332    /// Clicks will be ignored. Popup might be closed manually by calling [`Memory::close_popup`]
333    /// or by pressing the escape button
334    IgnoreClicks,
335}
336
337/// Helper for [`popup_above_or_below_widget`].
338pub 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
355/// Shows a popup above or below another widget.
356///
357/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
358///
359/// The opened popup will have a minimum width matching its parent.
360///
361/// You must open the popup with [`Memory::open_popup`] or  [`Memory::toggle_popup`].
362///
363/// Returns `None` if the popup is not open.
364///
365/// ```
366/// # egui::__run_test_ui(|ui| {
367/// let response = ui.button("Open popup");
368/// let popup_id = ui.make_persistent_id("my_unique_id");
369/// if response.clicked() {
370///     ui.memory_mut(|mem| mem.toggle_popup(popup_id));
371/// }
372/// let below = egui::AboveOrBelow::Below;
373/// let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside;
374/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
375///     ui.set_min_width(200.0); // if you want to control the size
376///     ui.label("Some more info, or things you can select:");
377///     ui.label("…");
378/// });
379/// # });
380/// ```
381pub 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}