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:
@@ -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() {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
181
crates/egui_demo_lib/src/demo/popups.rs
Normal file
181
crates/egui_demo_lib/src/demo/popups.rs
Normal 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!());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user