1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 05:40:03 -04:00

Add Popup and Tooltip, unifying the previous behaviours (#5713)

This introduces new `Tooltip` and `Popup` structs that unify and extend
the old popups and tooltips.

`Popup` handles the positioning and optionally stores state on whether
the popup is open (for click based popups like `ComboBox`, menus,
context menus).
`Tooltip` is based on `Popup` and handles state of whether the tooltip
should be shown (which turns out to be quite complex to handles all the
edge cases).

Both `Popup` and `Tooltip` can easily be constructed from a `Response`
and then customized via builder methods.

This also introduces `PositionAlign`, for aligning something outside of
a `Rect` (in contrast to `Align2` for aligning inside a `Rect`). But I
don't like the name, any suggestions? Inspired by [mui's tooltip
positioning](https://mui.com/material-ui/react-tooltip/#positioned-tooltips).

* Part of #4607 
* [x] I have followed the instructions in the PR template

TODOs:
- [x] Automatic tooltip positioning based on available space
- [x] Review / fix / remove all code TODOs 
- [x] ~Update the helper fns on `Response` to be consistent in naming
and parameters (Some use tooltip, some hover_ui, some take &self, some
take self)~ actually, I think the naming and parameter make sense on
second thought
- [x] Make sure all old code is marked deprecated

For discussion during review:
- the following check in `show_tooltip_for` still necessary?:
```rust
     let is_touch_screen = ctx.input(|i| i.any_touches());
     let allow_placing_below = !is_touch_screen; // There is a finger below. TODO: Needed?
```
This commit is contained in:
lucasmerlin
2025-02-18 15:53:07 +01:00
committed by GitHub
parent 66c73b9cbf
commit a8e98d3f9b
22 changed files with 1738 additions and 715 deletions

View File

@@ -1,3 +1,5 @@
use egui::{ComboBox, Popup};
#[derive(Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ContextMenus {}
@@ -32,6 +34,20 @@ impl crate::View for ContextMenus {
}
});
ui.horizontal(|ui| {
let response = ui.button("New menu");
Popup::menu(&response).show(Self::nested_menus);
let response = ui.button("New context menu");
Popup::context_menu(&response).show(Self::nested_menus);
ComboBox::new("Hi", "Hi").show_ui(ui, |ui| {
_ = ui.selectable_label(false, "I have some long text that should be wrapped");
_ = ui.selectable_label(false, "Short");
_ = ui.selectable_label(false, "Medium length");
});
});
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
@@ -51,6 +67,7 @@ impl ContextMenus {
ui.close_menu();
}
let _ = ui.button("Item");
ui.menu_button("Recursive", Self::nested_menus)
});
ui.menu_button("SubMenu", |ui| {
if ui.button("Open…").clicked() {

View File

@@ -78,6 +78,7 @@ impl Default for DemoGroups {
Box::<super::multi_touch::MultiTouch>::default(),
Box::<super::painting::Painting>::default(),
Box::<super::panels::Panels>::default(),
Box::<super::popups::PopupsDemo>::default(),
Box::<super::scene::SceneDemo>::default(),
Box::<super::screenshot::Screenshot>::default(),
Box::<super::scrolling::Scrolling>::default(),

View File

@@ -23,6 +23,7 @@ pub mod paint_bezier;
pub mod painting;
pub mod panels;
pub mod password;
mod popups;
pub mod scene;
pub mod screenshot;
pub mod scrolling;

View File

@@ -0,0 +1,181 @@
use egui::{vec2, Align2, ComboBox, Frame, Id, Popup, PopupCloseBehavior, RectAlign, Tooltip, Ui};
/// Showcase [`Popup`].
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct PopupsDemo {
align4: RectAlign,
gap: f32,
#[cfg_attr(feature = "serde", serde(skip))]
close_behavior: PopupCloseBehavior,
popup_open: bool,
}
impl PopupsDemo {
fn apply_options<'a>(&self, popup: Popup<'a>) -> Popup<'a> {
popup
.align(self.align4)
.gap(self.gap)
.close_behavior(self.close_behavior)
}
}
impl Default for PopupsDemo {
fn default() -> Self {
Self {
align4: RectAlign::default(),
gap: 4.0,
close_behavior: PopupCloseBehavior::CloseOnClick,
popup_open: false,
}
}
}
impl crate::Demo for PopupsDemo {
fn name(&self) -> &'static str {
"\u{2755} Popups"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.resizable(false)
.default_width(250.0)
.constrain(false)
.show(ctx, |ui| {
use crate::View as _;
self.ui(ui);
});
}
}
impl crate::View for PopupsDemo {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.style_mut().spacing.item_spacing.x = 0.0;
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
let aligns = [
(Align2::LEFT_TOP, "Left top"),
(Align2::LEFT_CENTER, "Left center"),
(Align2::LEFT_BOTTOM, "Left bottom"),
(Align2::CENTER_TOP, "Center top"),
(Align2::CENTER_CENTER, "Center center"),
(Align2::CENTER_BOTTOM, "Center bottom"),
(Align2::RIGHT_TOP, "Right top"),
(Align2::RIGHT_CENTER, "Right center"),
(Align2::RIGHT_BOTTOM, "Right bottom"),
];
ui.label(label);
ComboBox::new(label, "")
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
.show_ui(ui, |ui| {
for (align2, name) in &aligns {
ui.selectable_value(align, *align2, *name);
}
});
};
ui.label("Align4(");
align_combobox(ui, "parent: ", &mut self.align4.parent);
ui.label(", ");
align_combobox(ui, "child: ", &mut self.align4.child);
ui.label(") ");
let presets = [
(RectAlign::TOP_START, "Top start"),
(RectAlign::TOP, "Top"),
(RectAlign::TOP_END, "Top end"),
(RectAlign::RIGHT_START, "Right start"),
(RectAlign::RIGHT, "Right Center"),
(RectAlign::RIGHT_END, "Right end"),
(RectAlign::BOTTOM_START, "Bottom start"),
(RectAlign::BOTTOM, "Bottom"),
(RectAlign::BOTTOM_END, "Bottom end"),
(RectAlign::LEFT_START, "Left start"),
(RectAlign::LEFT, "Left"),
(RectAlign::LEFT_END, "Left end"),
];
ui.label(" Presets: ");
ComboBox::new("Preset", "")
.selected_text(
presets
.iter()
.find(|(a, _)| a == &self.align4)
.map_or("Select", |(_, name)| *name),
)
.show_ui(ui, |ui| {
for (align4, name) in &presets {
ui.selectable_value(&mut self.align4, *align4, *name);
}
});
});
ui.horizontal(|ui| {
ui.label("Gap:");
ui.add(egui::DragValue::new(&mut self.gap));
});
ui.horizontal(|ui| {
ui.label("Close behavior:");
ui.selectable_value(
&mut self.close_behavior,
PopupCloseBehavior::CloseOnClick,
"Close on click",
)
.on_hover_text("Closes when the user clicks anywhere (inside or outside)");
ui.selectable_value(
&mut self.close_behavior,
PopupCloseBehavior::CloseOnClickOutside,
"Close on click outside",
)
.on_hover_text("Closes when the user clicks outside the popup");
ui.selectable_value(
&mut self.close_behavior,
PopupCloseBehavior::IgnoreClicks,
"Ignore clicks",
)
.on_hover_text("Close only when the button is clicked again");
});
ui.checkbox(&mut self.popup_open, "Show popup");
let response = Frame::group(ui.style())
.inner_margin(vec2(0.0, 25.0))
.show(ui, |ui| {
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
.inner
})
.inner;
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
.show(|ui| {
_ = ui.button("Menu item 1");
_ = ui.button("Menu item 2");
});
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
.show(|ui| {
_ = ui.button("Context menu item 1");
_ = ui.button("Context menu item 2");
});
if self.popup_open {
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
.show(|ui| {
ui.label("Popup contents");
});
}
let mut tooltip = Tooltip::for_enabled(&response);
tooltip.popup = self.apply_options(tooltip.popup);
tooltip.show(|ui| {
ui.label("Tooltips are popups, too!");
});
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
}
}

View File

@@ -83,6 +83,9 @@ impl Tooltips {
ui.label("You can select this text.");
});
ui.label("This tooltip shows at the mouse cursor.")
.on_hover_text_at_pointer("Move me around!!");
ui.separator(); // ---------------------------------------------------------
let tooltip_ui = |ui: &mut egui::Ui| {