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:
parent
66c73b9cbf
commit
a8e98d3f9b
|
|
@ -5,8 +5,8 @@
|
||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
|
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
|
||||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// State of an [`Area`] that is persisted between frames.
|
/// State of an [`Area`] that is persisted between frames.
|
||||||
|
|
@ -120,6 +120,7 @@ pub struct Area {
|
||||||
anchor: Option<(Align2, Vec2)>,
|
anchor: Option<(Align2, Vec2)>,
|
||||||
new_pos: Option<Pos2>,
|
new_pos: Option<Pos2>,
|
||||||
fade_in: bool,
|
fade_in: bool,
|
||||||
|
layout: Layout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetWithState for Area {
|
impl WidgetWithState for Area {
|
||||||
|
|
@ -145,6 +146,7 @@ impl Area {
|
||||||
pivot: Align2::LEFT_TOP,
|
pivot: Align2::LEFT_TOP,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
fade_in: true,
|
fade_in: true,
|
||||||
|
layout: Layout::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,6 +341,13 @@ impl Area {
|
||||||
self.fade_in = fade_in;
|
self.fade_in = fade_in;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the layout for the child Ui.
|
||||||
|
#[inline]
|
||||||
|
pub fn layout(mut self, layout: Layout) -> Self {
|
||||||
|
self.layout = layout;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Prepared {
|
pub(crate) struct Prepared {
|
||||||
|
|
@ -358,6 +367,7 @@ pub(crate) struct Prepared {
|
||||||
sizing_pass: bool,
|
sizing_pass: bool,
|
||||||
|
|
||||||
fade_in: bool,
|
fade_in: bool,
|
||||||
|
layout: Layout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Area {
|
impl Area {
|
||||||
|
|
@ -390,6 +400,7 @@ impl Area {
|
||||||
constrain,
|
constrain,
|
||||||
constrain_rect,
|
constrain_rect,
|
||||||
fade_in,
|
fade_in,
|
||||||
|
layout,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
|
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
|
||||||
|
|
@ -516,6 +527,7 @@ impl Area {
|
||||||
constrain_rect,
|
constrain_rect,
|
||||||
sizing_pass,
|
sizing_pass,
|
||||||
fade_in,
|
fade_in,
|
||||||
|
layout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -543,7 +555,8 @@ impl Prepared {
|
||||||
let mut ui_builder = UiBuilder::new()
|
let mut ui_builder = UiBuilder::new()
|
||||||
.ui_stack_info(UiStackInfo::new(self.kind))
|
.ui_stack_info(UiStackInfo::new(self.kind))
|
||||||
.layer_id(self.layer_id)
|
.layer_id(self.layer_id)
|
||||||
.max_rect(max_rect);
|
.max_rect(max_rect)
|
||||||
|
.layout(self.layout);
|
||||||
|
|
||||||
if !self.enabled {
|
if !self.enabled {
|
||||||
ui_builder = ui_builder.disabled();
|
ui_builder = ui_builder.disabled();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use epaint::Shape;
|
use epaint::Shape;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter,
|
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter, Popup,
|
||||||
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
|
||||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||||
};
|
};
|
||||||
|
|
@ -9,15 +9,8 @@ use crate::{
|
||||||
#[allow(unused_imports)] // Documentation
|
#[allow(unused_imports)] // Documentation
|
||||||
use crate::style::Spacing;
|
use crate::style::Spacing;
|
||||||
|
|
||||||
/// Indicate whether a popup will be shown above or below the box.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub enum AboveOrBelow {
|
|
||||||
Above,
|
|
||||||
Below,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A function that paints the [`ComboBox`] icon
|
/// A function that paints the [`ComboBox`] icon
|
||||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
|
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
|
||||||
|
|
||||||
/// A drop-down selection menu with a descriptive label.
|
/// A drop-down selection menu with a descriptive label.
|
||||||
///
|
///
|
||||||
|
|
@ -135,7 +128,6 @@ impl ComboBox {
|
||||||
/// rect: egui::Rect,
|
/// rect: egui::Rect,
|
||||||
/// visuals: &egui::style::WidgetVisuals,
|
/// visuals: &egui::style::WidgetVisuals,
|
||||||
/// _is_open: bool,
|
/// _is_open: bool,
|
||||||
/// _above_or_below: egui::AboveOrBelow,
|
|
||||||
/// ) {
|
/// ) {
|
||||||
/// let rect = egui::Rect::from_center_size(
|
/// let rect = egui::Rect::from_center_size(
|
||||||
/// rect.center(),
|
/// rect.center(),
|
||||||
|
|
@ -154,10 +146,8 @@ impl ComboBox {
|
||||||
/// .show_ui(ui, |_ui| {});
|
/// .show_ui(ui, |_ui| {});
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn icon(
|
#[inline]
|
||||||
mut self,
|
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
||||||
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.icon = Some(Box::new(icon_fn));
|
self.icon = Some(Box::new(icon_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -322,22 +312,6 @@ fn combo_box_dyn<'c, R>(
|
||||||
|
|
||||||
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
||||||
|
|
||||||
let popup_height = ui.memory(|m| {
|
|
||||||
m.areas()
|
|
||||||
.get(popup_id)
|
|
||||||
.and_then(|state| state.size)
|
|
||||||
.map_or(100.0, |size| size.y)
|
|
||||||
});
|
|
||||||
|
|
||||||
let above_or_below =
|
|
||||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
|
||||||
< ui.ctx().screen_rect().bottom()
|
|
||||||
{
|
|
||||||
AboveOrBelow::Below
|
|
||||||
} else {
|
|
||||||
AboveOrBelow::Above
|
|
||||||
};
|
|
||||||
|
|
||||||
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
||||||
|
|
||||||
let close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick);
|
let close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick);
|
||||||
|
|
@ -385,15 +359,9 @@ fn combo_box_dyn<'c, R>(
|
||||||
icon_rect.expand(visuals.expansion),
|
icon_rect.expand(visuals.expansion),
|
||||||
visuals,
|
visuals,
|
||||||
is_popup_open,
|
is_popup_open,
|
||||||
above_or_below,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
paint_default_icon(
|
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||||
ui.painter(),
|
|
||||||
icon_rect.expand(visuals.expansion),
|
|
||||||
visuals,
|
|
||||||
above_or_below,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
||||||
|
|
@ -402,19 +370,15 @@ fn combo_box_dyn<'c, R>(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if button_response.clicked() {
|
|
||||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let height = height.unwrap_or_else(|| ui.spacing().combo_height);
|
let height = height.unwrap_or_else(|| ui.spacing().combo_height);
|
||||||
|
|
||||||
let inner = crate::popup::popup_above_or_below_widget(
|
let inner = Popup::menu(&button_response)
|
||||||
ui,
|
.id(popup_id)
|
||||||
popup_id,
|
.width(button_response.rect.width())
|
||||||
&button_response,
|
.close_behavior(close_behavior)
|
||||||
above_or_below,
|
.show(|ui| {
|
||||||
close_behavior,
|
ui.set_min_width(ui.available_width());
|
||||||
|ui| {
|
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.max_height(height)
|
.max_height(height)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
|
@ -427,8 +391,8 @@ fn combo_box_dyn<'c, R>(
|
||||||
menu_contents(ui)
|
menu_contents(ui)
|
||||||
})
|
})
|
||||||
.inner
|
.inner
|
||||||
},
|
})
|
||||||
);
|
.map(|r| r.inner);
|
||||||
|
|
||||||
InnerResponse {
|
InnerResponse {
|
||||||
inner,
|
inner,
|
||||||
|
|
@ -484,33 +448,19 @@ fn button_frame(
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_default_icon(
|
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||||
painter: &Painter,
|
|
||||||
rect: Rect,
|
|
||||||
visuals: &WidgetVisuals,
|
|
||||||
above_or_below: AboveOrBelow,
|
|
||||||
) {
|
|
||||||
let rect = Rect::from_center_size(
|
let rect = Rect::from_center_size(
|
||||||
rect.center(),
|
rect.center(),
|
||||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||||
);
|
);
|
||||||
|
|
||||||
match above_or_below {
|
// Downward pointing triangle
|
||||||
AboveOrBelow::Above => {
|
// Previously, we would show an up arrow when we expected the popup to open upwards
|
||||||
// Upward pointing triangle
|
// (due to lack of space below the button), but this could look weird in edge cases, so this
|
||||||
painter.add(Shape::convex_polygon(
|
// feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245)
|
||||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
painter.add(Shape::convex_polygon(
|
||||||
visuals.fg_stroke.color,
|
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||||
Stroke::NONE,
|
visuals.fg_stroke.color,
|
||||||
));
|
Stroke::NONE,
|
||||||
}
|
));
|
||||||
AboveOrBelow::Below => {
|
|
||||||
// Downward pointing triangle
|
|
||||||
painter.add(Shape::convex_polygon(
|
|
||||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
|
||||||
visuals.fg_stroke.color,
|
|
||||||
Stroke::NONE,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ pub mod collapsing_header;
|
||||||
mod combo_box;
|
mod combo_box;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
|
pub mod old_popup;
|
||||||
pub mod panel;
|
pub mod panel;
|
||||||
pub mod popup;
|
mod popup;
|
||||||
pub(crate) mod resize;
|
pub(crate) mod resize;
|
||||||
mod scene;
|
mod scene;
|
||||||
pub mod scroll_area;
|
pub mod scroll_area;
|
||||||
mod sides;
|
mod sides;
|
||||||
|
mod tooltip;
|
||||||
pub(crate) mod window;
|
pub(crate) mod window;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
|
|
@ -21,11 +23,13 @@ pub use {
|
||||||
combo_box::*,
|
combo_box::*,
|
||||||
frame::Frame,
|
frame::Frame,
|
||||||
modal::{Modal, ModalResponse},
|
modal::{Modal, ModalResponse},
|
||||||
|
old_popup::*,
|
||||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||||
popup::*,
|
popup::*,
|
||||||
resize::Resize,
|
resize::Resize,
|
||||||
scene::Scene,
|
scene::Scene,
|
||||||
scroll_area::ScrollArea,
|
scroll_area::ScrollArea,
|
||||||
sides::Sides,
|
sides::Sides,
|
||||||
|
tooltip::*,
|
||||||
window::Window,
|
window::Window,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
//! Old and deprecated API for popups. Use [`Popup`] instead.
|
||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
|
use crate::containers::tooltip::Tooltip;
|
||||||
|
use crate::{
|
||||||
|
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
|
||||||
|
Response, Ui, Widget, WidgetText,
|
||||||
|
};
|
||||||
|
use emath::RectAlign;
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Show a tooltip at the current pointer position (if any).
|
||||||
|
///
|
||||||
|
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
||||||
|
///
|
||||||
|
/// See also [`show_tooltip_text`].
|
||||||
|
///
|
||||||
|
/// Returns `None` if the tooltip could not be placed.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// # #[allow(deprecated)]
|
||||||
|
/// if ui.ui_contains_pointer() {
|
||||||
|
/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
|
||||||
|
/// ui.label("Helpful text");
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||||
|
pub fn show_tooltip<R>(
|
||||||
|
ctx: &Context,
|
||||||
|
parent_layer: LayerId,
|
||||||
|
widget_id: Id,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a tooltip at the current pointer position (if any).
|
||||||
|
///
|
||||||
|
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
||||||
|
///
|
||||||
|
/// See also [`show_tooltip_text`].
|
||||||
|
///
|
||||||
|
/// Returns `None` if the tooltip could not be placed.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// if ui.ui_contains_pointer() {
|
||||||
|
/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
|
||||||
|
/// ui.label("Helpful text");
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||||
|
pub fn show_tooltip_at_pointer<R>(
|
||||||
|
ctx: &Context,
|
||||||
|
parent_layer: LayerId,
|
||||||
|
widget_id: Id,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
Tooltip::new(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer)
|
||||||
|
.gap(12.0)
|
||||||
|
.show(add_contents)
|
||||||
|
.map(|response| response.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a tooltip under the given area.
|
||||||
|
///
|
||||||
|
/// If the tooltip does not fit under the area, it tries to place it above it instead.
|
||||||
|
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||||
|
pub fn show_tooltip_for<R>(
|
||||||
|
ctx: &Context,
|
||||||
|
parent_layer: LayerId,
|
||||||
|
widget_id: Id,
|
||||||
|
widget_rect: &Rect,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
Tooltip::new(widget_id, ctx.clone(), *widget_rect, parent_layer)
|
||||||
|
.show(add_contents)
|
||||||
|
.map(|response| response.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a tooltip at the given position.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the tooltip could not be placed.
|
||||||
|
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||||
|
pub fn show_tooltip_at<R>(
|
||||||
|
ctx: &Context,
|
||||||
|
parent_layer: LayerId,
|
||||||
|
widget_id: Id,
|
||||||
|
suggested_position: Pos2,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
Tooltip::new(widget_id, ctx.clone(), suggested_position, parent_layer)
|
||||||
|
.show(add_contents)
|
||||||
|
.map(|response| response.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show some text at the current pointer position (if any).
|
||||||
|
///
|
||||||
|
/// Most of the time it is easier to use [`Response::on_hover_text`].
|
||||||
|
///
|
||||||
|
/// See also [`show_tooltip`].
|
||||||
|
///
|
||||||
|
/// Returns `None` if the tooltip could not be placed.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// if ui.ui_contains_pointer() {
|
||||||
|
/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
|
||||||
|
/// }
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[deprecated = "Use `egui::Tooltip` instead"]
|
||||||
|
pub fn show_tooltip_text(
|
||||||
|
ctx: &Context,
|
||||||
|
parent_layer: LayerId,
|
||||||
|
widget_id: Id,
|
||||||
|
text: impl Into<WidgetText>,
|
||||||
|
) -> Option<()> {
|
||||||
|
show_tooltip(ctx, parent_layer, widget_id, |ui| {
|
||||||
|
crate::widgets::Label::new(text).ui(ui);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Was this tooltip visible last frame?
|
||||||
|
#[deprecated = "Use `Tooltip::was_tooltip_open_last_frame` instead"]
|
||||||
|
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
|
||||||
|
Tooltip::was_tooltip_open_last_frame(ctx, widget_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicate whether a popup will be shown above or below the box.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum AboveOrBelow {
|
||||||
|
Above,
|
||||||
|
Below,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for [`popup_above_or_below_widget`].
|
||||||
|
#[deprecated = "Use `egui::Popup` instead"]
|
||||||
|
pub fn popup_below_widget<R>(
|
||||||
|
ui: &Ui,
|
||||||
|
popup_id: Id,
|
||||||
|
widget_response: &Response,
|
||||||
|
close_behavior: PopupCloseBehavior,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
popup_above_or_below_widget(
|
||||||
|
ui,
|
||||||
|
popup_id,
|
||||||
|
widget_response,
|
||||||
|
AboveOrBelow::Below,
|
||||||
|
close_behavior,
|
||||||
|
add_contents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a popup above or below another widget.
|
||||||
|
///
|
||||||
|
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
||||||
|
///
|
||||||
|
/// The opened popup will have a minimum width matching its parent.
|
||||||
|
///
|
||||||
|
/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`].
|
||||||
|
///
|
||||||
|
/// Returns `None` if the popup is not open.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// let response = ui.button("Open popup");
|
||||||
|
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
||||||
|
/// if response.clicked() {
|
||||||
|
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||||
|
/// }
|
||||||
|
/// let below = egui::AboveOrBelow::Below;
|
||||||
|
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
|
||||||
|
/// # #[allow(deprecated)]
|
||||||
|
/// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
|
||||||
|
/// ui.set_min_width(200.0); // if you want to control the size
|
||||||
|
/// ui.label("Some more info, or things you can select:");
|
||||||
|
/// ui.label("…");
|
||||||
|
/// });
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[deprecated = "Use `egui::Popup` instead"]
|
||||||
|
pub fn popup_above_or_below_widget<R>(
|
||||||
|
_parent_ui: &Ui,
|
||||||
|
popup_id: Id,
|
||||||
|
widget_response: &Response,
|
||||||
|
above_or_below: AboveOrBelow,
|
||||||
|
close_behavior: PopupCloseBehavior,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
let response = Popup::from_response(widget_response)
|
||||||
|
.layout(Layout::top_down_justified(Align::LEFT))
|
||||||
|
.open_memory(None, close_behavior)
|
||||||
|
.id(popup_id)
|
||||||
|
.align(match above_or_below {
|
||||||
|
AboveOrBelow::Above => RectAlign::TOP_START,
|
||||||
|
AboveOrBelow::Below => RectAlign::BOTTOM_START,
|
||||||
|
})
|
||||||
|
.width(widget_response.rect.width())
|
||||||
|
.show(|ui| {
|
||||||
|
ui.set_min_width(ui.available_width());
|
||||||
|
add_contents(ui)
|
||||||
|
})?;
|
||||||
|
Some(response.inner)
|
||||||
|
}
|
||||||
|
|
@ -1,334 +1,75 @@
|
||||||
//! Show popup windows, tooltips, context menus etc.
|
|
||||||
|
|
||||||
use pass_state::PerWidgetTooltipState;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id,
|
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
|
||||||
InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2,
|
Sense, Ui, UiKind,
|
||||||
Widget, WidgetText,
|
|
||||||
};
|
};
|
||||||
|
use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
|
||||||
|
use std::iter::once;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
/// What should we anchor the popup to?
|
||||||
|
/// The final position for the popup will be calculated based on [`RectAlign`]
|
||||||
|
/// and can be customized with [`Popup::align`] and [`Popup::align_alternatives`].
|
||||||
|
/// [`PopupAnchor`] is the parent rect of [`RectAlign`].
|
||||||
|
///
|
||||||
|
/// For [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`] and [`PopupAnchor::Position`],
|
||||||
|
/// the rect will be derived via [`Rect::from_pos`] (so a zero-sized rect at the given position).
|
||||||
|
///
|
||||||
|
/// The rect should be in global coordinates. `PopupAnchor::from(&response)` will automatically
|
||||||
|
/// do this conversion.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PopupAnchor {
|
||||||
|
/// Show the popup relative to some parent [`Rect`].
|
||||||
|
ParentRect(Rect),
|
||||||
|
|
||||||
fn when_was_a_toolip_last_shown_id() -> Id {
|
/// Show the popup relative to the mouse pointer.
|
||||||
Id::new("when_was_a_toolip_last_shown")
|
Pointer,
|
||||||
|
|
||||||
|
/// Remember the mouse position and show the popup relative to that (like a context menu).
|
||||||
|
PointerFixed,
|
||||||
|
|
||||||
|
/// Show the popup relative to a specific position.
|
||||||
|
Position(Pos2),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
|
impl From<Rect> for PopupAnchor {
|
||||||
let when_was_a_toolip_last_shown =
|
fn from(rect: Rect) -> Self {
|
||||||
ctx.data(|d| d.get_temp::<f64>(when_was_a_toolip_last_shown_id()));
|
Self::ParentRect(rect)
|
||||||
|
|
||||||
if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
|
|
||||||
let now = ctx.input(|i| i.time);
|
|
||||||
(now - when_was_a_toolip_last_shown) as f32
|
|
||||||
} else {
|
|
||||||
f32::INFINITY
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remember_that_tooltip_was_shown(ctx: &Context) {
|
impl From<Pos2> for PopupAnchor {
|
||||||
let now = ctx.input(|i| i.time);
|
fn from(pos: Pos2) -> Self {
|
||||||
ctx.data_mut(|data| data.insert_temp::<f64>(when_was_a_toolip_last_shown_id(), now));
|
Self::Position(pos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
impl From<&Response> for PopupAnchor {
|
||||||
|
fn from(response: &Response) -> Self {
|
||||||
/// Show a tooltip at the current pointer position (if any).
|
let mut widget_rect = response.rect;
|
||||||
///
|
if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
|
||||||
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
widget_rect = to_global * widget_rect;
|
||||||
///
|
|
||||||
/// See also [`show_tooltip_text`].
|
|
||||||
///
|
|
||||||
/// Returns `None` if the tooltip could not be placed.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// if ui.ui_contains_pointer() {
|
|
||||||
/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
|
|
||||||
/// ui.label("Helpful text");
|
|
||||||
/// });
|
|
||||||
/// }
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
pub fn show_tooltip<R>(
|
|
||||||
ctx: &Context,
|
|
||||||
parent_layer: LayerId,
|
|
||||||
widget_id: Id,
|
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
||||||
) -> Option<R> {
|
|
||||||
show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a tooltip at the current pointer position (if any).
|
|
||||||
///
|
|
||||||
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
|
||||||
///
|
|
||||||
/// See also [`show_tooltip_text`].
|
|
||||||
///
|
|
||||||
/// Returns `None` if the tooltip could not be placed.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// if ui.ui_contains_pointer() {
|
|
||||||
/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
|
|
||||||
/// ui.label("Helpful text");
|
|
||||||
/// });
|
|
||||||
/// }
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
pub fn show_tooltip_at_pointer<R>(
|
|
||||||
ctx: &Context,
|
|
||||||
parent_layer: LayerId,
|
|
||||||
widget_id: Id,
|
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
||||||
) -> Option<R> {
|
|
||||||
ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| {
|
|
||||||
let allow_placing_below = true;
|
|
||||||
|
|
||||||
// Add a small exclusion zone around the pointer to avoid tooltips
|
|
||||||
// covering what we're hovering over.
|
|
||||||
let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
|
|
||||||
|
|
||||||
// Keep the left edge of the tooltip in line with the cursor:
|
|
||||||
pointer_rect.min.x = pointer_pos.x;
|
|
||||||
|
|
||||||
// Transform global coords to layer coords:
|
|
||||||
if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) {
|
|
||||||
pointer_rect = from_global * pointer_rect;
|
|
||||||
}
|
}
|
||||||
|
Self::ParentRect(widget_rect)
|
||||||
show_tooltip_at_dyn(
|
|
||||||
ctx,
|
|
||||||
parent_layer,
|
|
||||||
widget_id,
|
|
||||||
allow_placing_below,
|
|
||||||
&pointer_rect,
|
|
||||||
Box::new(add_contents),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a tooltip under the given area.
|
|
||||||
///
|
|
||||||
/// If the tooltip does not fit under the area, it tries to place it above it instead.
|
|
||||||
pub fn show_tooltip_for<R>(
|
|
||||||
ctx: &Context,
|
|
||||||
parent_layer: LayerId,
|
|
||||||
widget_id: Id,
|
|
||||||
widget_rect: &Rect,
|
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
||||||
) -> R {
|
|
||||||
let is_touch_screen = ctx.input(|i| i.any_touches());
|
|
||||||
let allow_placing_below = !is_touch_screen; // There is a finger below.
|
|
||||||
show_tooltip_at_dyn(
|
|
||||||
ctx,
|
|
||||||
parent_layer,
|
|
||||||
widget_id,
|
|
||||||
allow_placing_below,
|
|
||||||
widget_rect,
|
|
||||||
Box::new(add_contents),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a tooltip at the given position.
|
|
||||||
///
|
|
||||||
/// Returns `None` if the tooltip could not be placed.
|
|
||||||
pub fn show_tooltip_at<R>(
|
|
||||||
ctx: &Context,
|
|
||||||
parent_layer: LayerId,
|
|
||||||
widget_id: Id,
|
|
||||||
suggested_position: Pos2,
|
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
||||||
) -> R {
|
|
||||||
let allow_placing_below = true;
|
|
||||||
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
|
|
||||||
show_tooltip_at_dyn(
|
|
||||||
ctx,
|
|
||||||
parent_layer,
|
|
||||||
widget_id,
|
|
||||||
allow_placing_below,
|
|
||||||
&rect,
|
|
||||||
Box::new(add_contents),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_tooltip_at_dyn<'c, R>(
|
|
||||||
ctx: &Context,
|
|
||||||
parent_layer: LayerId,
|
|
||||||
widget_id: Id,
|
|
||||||
allow_placing_below: bool,
|
|
||||||
widget_rect: &Rect,
|
|
||||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
|
||||||
) -> R {
|
|
||||||
// Transform layer coords to global coords:
|
|
||||||
let mut widget_rect = *widget_rect;
|
|
||||||
if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) {
|
|
||||||
widget_rect = to_global * widget_rect;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remember_that_tooltip_was_shown(ctx);
|
|
||||||
|
|
||||||
let mut state = ctx.pass_state_mut(|fs| {
|
|
||||||
// Remember that this is the widget showing the tooltip:
|
|
||||||
fs.layers
|
|
||||||
.entry(parent_layer)
|
|
||||||
.or_default()
|
|
||||||
.widget_with_tooltip = Some(widget_id);
|
|
||||||
|
|
||||||
fs.tooltips
|
|
||||||
.widget_tooltips
|
|
||||||
.get(&widget_id)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(PerWidgetTooltipState {
|
|
||||||
bounding_rect: widget_rect,
|
|
||||||
tooltip_count: 0,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count);
|
|
||||||
let expected_tooltip_size = AreaState::load(ctx, tooltip_area_id)
|
|
||||||
.and_then(|area| area.size)
|
|
||||||
.unwrap_or(vec2(64.0, 32.0));
|
|
||||||
|
|
||||||
let screen_rect = ctx.screen_rect();
|
|
||||||
|
|
||||||
let (pivot, anchor) = find_tooltip_position(
|
|
||||||
screen_rect,
|
|
||||||
state.bounding_rect,
|
|
||||||
allow_placing_below,
|
|
||||||
expected_tooltip_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
let InnerResponse { inner, response } = Area::new(tooltip_area_id)
|
|
||||||
.kind(UiKind::Popup)
|
|
||||||
.order(Order::Tooltip)
|
|
||||||
.pivot(pivot)
|
|
||||||
.fixed_pos(anchor)
|
|
||||||
.default_width(ctx.style().spacing.tooltip_width)
|
|
||||||
.sense(Sense::hover()) // don't click to bring to front
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
// By default the text in tooltips aren't selectable.
|
|
||||||
// This means that most tooltips aren't interactable,
|
|
||||||
// which also mean they won't stick around so you can click them.
|
|
||||||
// Only tooltips that have actual interactive stuff (buttons, links, …)
|
|
||||||
// will stick around when you try to click them.
|
|
||||||
ui.style_mut().interaction.selectable_labels = false;
|
|
||||||
|
|
||||||
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
|
|
||||||
});
|
|
||||||
|
|
||||||
state.tooltip_count += 1;
|
|
||||||
state.bounding_rect = state.bounding_rect.union(response.rect);
|
|
||||||
ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
|
|
||||||
|
|
||||||
inner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What is the id of the next tooltip for this widget?
|
impl PopupAnchor {
|
||||||
pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
|
/// Get the rect the popup should be shown relative to.
|
||||||
let tooltip_count = ctx.pass_state(|fs| {
|
/// Returns `Rect::from_pos` for [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`]
|
||||||
fs.tooltips
|
/// and [`PopupAnchor::Position`] (so the rect will be zero-sized).
|
||||||
.widget_tooltips
|
pub fn rect(self, popup_id: Id, ctx: &Context) -> Option<Rect> {
|
||||||
.get(&widget_id)
|
match self {
|
||||||
.map_or(0, |state| state.tooltip_count)
|
Self::ParentRect(rect) => Some(rect),
|
||||||
});
|
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
|
||||||
tooltip_id(widget_id, tooltip_count)
|
Self::PointerFixed => ctx
|
||||||
}
|
.memory(|mem| mem.popup_position(popup_id))
|
||||||
|
.map(Rect::from_pos),
|
||||||
pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
|
Self::Position(pos) => Some(Rect::from_pos(pos)),
|
||||||
widget_id.with(tooltip_count)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `(PIVOT, POS)` to mean: put the `PIVOT` corner of the tooltip at `POS`.
|
|
||||||
///
|
|
||||||
/// Note: the position might need to be constrained to the screen,
|
|
||||||
/// (e.g. moved sideways if shown under the widget)
|
|
||||||
/// but the `Area` will take care of that.
|
|
||||||
fn find_tooltip_position(
|
|
||||||
screen_rect: Rect,
|
|
||||||
widget_rect: Rect,
|
|
||||||
allow_placing_below: bool,
|
|
||||||
tooltip_size: Vec2,
|
|
||||||
) -> (Align2, Pos2) {
|
|
||||||
let spacing = 4.0;
|
|
||||||
|
|
||||||
// Does it fit below?
|
|
||||||
if allow_placing_below
|
|
||||||
&& widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
Align2::LEFT_TOP,
|
|
||||||
widget_rect.left_bottom() + spacing * Vec2::DOWN,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does it fit above?
|
|
||||||
if screen_rect.top() + tooltip_size.y + spacing <= widget_rect.top() {
|
|
||||||
return (
|
|
||||||
Align2::LEFT_BOTTOM,
|
|
||||||
widget_rect.left_top() + spacing * Vec2::UP,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Does it fit to the right?
|
|
||||||
if widget_rect.right() + spacing + tooltip_size.x <= screen_rect.right() {
|
|
||||||
return (
|
|
||||||
Align2::LEFT_TOP,
|
|
||||||
widget_rect.right_top() + spacing * Vec2::RIGHT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Does it fit to the left?
|
|
||||||
if screen_rect.left() + tooltip_size.x + spacing <= widget_rect.left() {
|
|
||||||
return (
|
|
||||||
Align2::RIGHT_TOP,
|
|
||||||
widget_rect.left_top() + spacing * Vec2::LEFT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// It doesn't fit anywhere :(
|
|
||||||
|
|
||||||
// Just show it anyway:
|
|
||||||
(Align2::LEFT_TOP, screen_rect.left_top())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show some text at the current pointer position (if any).
|
|
||||||
///
|
|
||||||
/// Most of the time it is easier to use [`Response::on_hover_text`].
|
|
||||||
///
|
|
||||||
/// See also [`show_tooltip`].
|
|
||||||
///
|
|
||||||
/// Returns `None` if the tooltip could not be placed.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// if ui.ui_contains_pointer() {
|
|
||||||
/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
|
|
||||||
/// }
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
pub fn show_tooltip_text(
|
|
||||||
ctx: &Context,
|
|
||||||
parent_layer: LayerId,
|
|
||||||
widget_id: Id,
|
|
||||||
text: impl Into<WidgetText>,
|
|
||||||
) -> Option<()> {
|
|
||||||
show_tooltip(ctx, parent_layer, widget_id, |ui| {
|
|
||||||
crate::widgets::Label::new(text).ui(ui);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Was this popup visible last frame?
|
|
||||||
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
|
|
||||||
let primary_tooltip_area_id = tooltip_id(widget_id, 0);
|
|
||||||
ctx.memory(|mem| {
|
|
||||||
mem.areas()
|
|
||||||
.visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines popup's close behavior
|
/// Determines popup's close behavior
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PopupCloseBehavior {
|
pub enum PopupCloseBehavior {
|
||||||
/// Popup will be closed on click anywhere, inside or outside the popup.
|
/// Popup will be closed on click anywhere, inside or outside the popup.
|
||||||
///
|
///
|
||||||
|
|
@ -344,114 +85,480 @@ pub enum PopupCloseBehavior {
|
||||||
IgnoreClicks,
|
IgnoreClicks,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper for [`popup_above_or_below_widget`].
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub fn popup_below_widget<R>(
|
pub enum SetOpenCommand {
|
||||||
ui: &Ui,
|
/// Set the open state to the given value
|
||||||
popup_id: Id,
|
Bool(bool),
|
||||||
widget_response: &Response,
|
|
||||||
close_behavior: PopupCloseBehavior,
|
/// Toggle the open state
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
Toggle,
|
||||||
) -> Option<R> {
|
|
||||||
popup_above_or_below_widget(
|
|
||||||
ui,
|
|
||||||
popup_id,
|
|
||||||
widget_response,
|
|
||||||
AboveOrBelow::Below,
|
|
||||||
close_behavior,
|
|
||||||
add_contents,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a popup above or below another widget.
|
impl From<bool> for SetOpenCommand {
|
||||||
///
|
fn from(b: bool) -> Self {
|
||||||
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
Self::Bool(b)
|
||||||
///
|
|
||||||
/// The opened popup will have a minimum width matching its parent.
|
|
||||||
///
|
|
||||||
/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`].
|
|
||||||
///
|
|
||||||
/// Returns `None` if the popup is not open.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// let response = ui.button("Open popup");
|
|
||||||
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
|
||||||
/// if response.clicked() {
|
|
||||||
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
|
||||||
/// }
|
|
||||||
/// let below = egui::AboveOrBelow::Below;
|
|
||||||
/// let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside;
|
|
||||||
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
|
|
||||||
/// ui.set_min_width(200.0); // if you want to control the size
|
|
||||||
/// ui.label("Some more info, or things you can select:");
|
|
||||||
/// ui.label("…");
|
|
||||||
/// });
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
pub fn popup_above_or_below_widget<R>(
|
|
||||||
parent_ui: &Ui,
|
|
||||||
popup_id: Id,
|
|
||||||
widget_response: &Response,
|
|
||||||
above_or_below: AboveOrBelow,
|
|
||||||
close_behavior: PopupCloseBehavior,
|
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
|
||||||
) -> Option<R> {
|
|
||||||
if !parent_ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (mut pos, pivot) = match above_or_below {
|
/// How do we determine if the popup should be open or closed
|
||||||
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
|
enum OpenKind<'a> {
|
||||||
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
|
/// Always open
|
||||||
};
|
Open,
|
||||||
|
|
||||||
if let Some(to_global) = parent_ui
|
/// Always closed
|
||||||
.ctx()
|
Closed,
|
||||||
.layer_transform_to_global(parent_ui.layer_id())
|
|
||||||
{
|
|
||||||
pos = to_global * pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
let frame = Frame::popup(parent_ui.style());
|
/// Open if the bool is true
|
||||||
let frame_margin = frame.total_margin();
|
Bool(&'a mut bool, PopupCloseBehavior),
|
||||||
let inner_width = (widget_response.rect.width() - frame_margin.sum().x).max(0.0);
|
|
||||||
|
|
||||||
parent_ui.ctx().pass_state_mut(|fs| {
|
/// Store the open state via [`crate::Memory`]
|
||||||
fs.layers
|
Memory {
|
||||||
.entry(parent_ui.layer_id())
|
set: Option<SetOpenCommand>,
|
||||||
.or_default()
|
close_behavior: PopupCloseBehavior,
|
||||||
.open_popups
|
},
|
||||||
.insert(popup_id)
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let response = Area::new(popup_id)
|
impl<'a> OpenKind<'a> {
|
||||||
.kind(UiKind::Popup)
|
/// Returns `true` if the popup should be open
|
||||||
.order(Order::Foreground)
|
fn is_open(&self, id: Id, ctx: &Context) -> bool {
|
||||||
.fixed_pos(pos)
|
match self {
|
||||||
.default_width(inner_width)
|
OpenKind::Open => true,
|
||||||
.pivot(pivot)
|
OpenKind::Closed => false,
|
||||||
.show(parent_ui.ctx(), |ui| {
|
OpenKind::Bool(open, _) => **open,
|
||||||
frame
|
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
|
|
||||||
ui.set_min_width(inner_width);
|
|
||||||
add_contents(ui)
|
|
||||||
})
|
|
||||||
.inner
|
|
||||||
})
|
|
||||||
.inner
|
|
||||||
});
|
|
||||||
|
|
||||||
let should_close = match close_behavior {
|
|
||||||
PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(),
|
|
||||||
PopupCloseBehavior::CloseOnClickOutside => {
|
|
||||||
widget_response.clicked_elsewhere() && response.response.clicked_elsewhere()
|
|
||||||
}
|
}
|
||||||
PopupCloseBehavior::IgnoreClicks => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close {
|
|
||||||
parent_ui.memory_mut(|mem| mem.close_popup());
|
|
||||||
}
|
}
|
||||||
Some(response.inner)
|
}
|
||||||
|
|
||||||
|
/// Is the popup a popup, tooltip or menu?
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PopupKind {
|
||||||
|
Popup,
|
||||||
|
Tooltip,
|
||||||
|
Menu,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Popup<'a> {
|
||||||
|
id: Id,
|
||||||
|
ctx: Context,
|
||||||
|
anchor: PopupAnchor,
|
||||||
|
rect_align: RectAlign,
|
||||||
|
alternative_aligns: Option<&'a [RectAlign]>,
|
||||||
|
layer_id: LayerId,
|
||||||
|
open_kind: OpenKind<'a>,
|
||||||
|
kind: PopupKind,
|
||||||
|
|
||||||
|
/// Gap between the anchor and the popup
|
||||||
|
gap: f32,
|
||||||
|
|
||||||
|
/// Used later depending on close behavior
|
||||||
|
widget_clicked_elsewhere: bool,
|
||||||
|
|
||||||
|
/// Default width passed to the Area
|
||||||
|
width: Option<f32>,
|
||||||
|
sense: Sense,
|
||||||
|
layout: Layout,
|
||||||
|
frame: Option<Frame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Popup<'a> {
|
||||||
|
/// Create a new popup
|
||||||
|
pub fn new(id: Id, ctx: Context, anchor: impl Into<PopupAnchor>, layer_id: LayerId) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
ctx,
|
||||||
|
anchor: anchor.into(),
|
||||||
|
open_kind: OpenKind::Open,
|
||||||
|
kind: PopupKind::Popup,
|
||||||
|
layer_id,
|
||||||
|
rect_align: RectAlign::BOTTOM_START,
|
||||||
|
alternative_aligns: None,
|
||||||
|
gap: 0.0,
|
||||||
|
widget_clicked_elsewhere: false,
|
||||||
|
width: None,
|
||||||
|
sense: Sense::click(),
|
||||||
|
layout: Layout::default(),
|
||||||
|
frame: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
|
||||||
|
#[inline]
|
||||||
|
pub fn kind(mut self, kind: PopupKind) -> Self {
|
||||||
|
self.kind = kind;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the [`RectAlign`] of the popup relative to the [`PopupAnchor`].
|
||||||
|
/// This is the default position, and will be used if it fits.
|
||||||
|
/// See [`Self::align_alternatives`] for more on this.
|
||||||
|
#[inline]
|
||||||
|
pub fn align(mut self, position_align: RectAlign) -> Self {
|
||||||
|
self.rect_align = position_align;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set alternative positions to try if the default one doesn't fit. Set to an empty slice to
|
||||||
|
/// always use the position you set with [`Self::align`].
|
||||||
|
/// By default, this will try [`RectAlign::symmetries`] and then [`RectAlign::MENU_ALIGNS`].
|
||||||
|
#[inline]
|
||||||
|
pub fn align_alternatives(mut self, alternatives: &'a [RectAlign]) -> Self {
|
||||||
|
self.alternative_aligns = Some(alternatives);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a popup relative to some widget.
|
||||||
|
/// The popup will be always open.
|
||||||
|
///
|
||||||
|
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
|
||||||
|
pub fn from_response(response: &Response) -> Self {
|
||||||
|
let mut popup = Self::new(
|
||||||
|
response.id.with("popup"),
|
||||||
|
response.ctx.clone(),
|
||||||
|
response,
|
||||||
|
response.layer_id,
|
||||||
|
);
|
||||||
|
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
|
||||||
|
popup
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a popup when the widget was clicked.
|
||||||
|
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||||
|
pub fn menu(response: &Response) -> Self {
|
||||||
|
Self::from_response(response)
|
||||||
|
.open_memory(
|
||||||
|
if response.clicked() {
|
||||||
|
Some(SetOpenCommand::Toggle)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
PopupCloseBehavior::CloseOnClick,
|
||||||
|
)
|
||||||
|
.layout(Layout::top_down_justified(Align::Min))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a context menu when the widget was secondary clicked.
|
||||||
|
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
|
||||||
|
/// In contrast to [`Self::menu`], this will open at the pointer position.
|
||||||
|
pub fn context_menu(response: &Response) -> Self {
|
||||||
|
Self::from_response(response)
|
||||||
|
.open_memory(
|
||||||
|
response
|
||||||
|
.secondary_clicked()
|
||||||
|
.then_some(SetOpenCommand::Bool(true)),
|
||||||
|
PopupCloseBehavior::CloseOnClick,
|
||||||
|
)
|
||||||
|
.layout(Layout::top_down_justified(Align::Min))
|
||||||
|
.at_pointer_fixed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force the popup to be open or closed.
|
||||||
|
#[inline]
|
||||||
|
pub fn open(mut self, open: bool) -> Self {
|
||||||
|
self.open_kind = if open {
|
||||||
|
OpenKind::Open
|
||||||
|
} else {
|
||||||
|
OpenKind::Closed
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store the open state via [`crate::Memory`].
|
||||||
|
/// You can set the state via the first [`SetOpenCommand`] param.
|
||||||
|
#[inline]
|
||||||
|
pub fn open_memory(
|
||||||
|
mut self,
|
||||||
|
set_state: impl Into<Option<SetOpenCommand>>,
|
||||||
|
close_behavior: PopupCloseBehavior,
|
||||||
|
) -> Self {
|
||||||
|
self.open_kind = OpenKind::Memory {
|
||||||
|
set: set_state.into(),
|
||||||
|
close_behavior,
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store the open state via a mutable bool.
|
||||||
|
#[inline]
|
||||||
|
pub fn open_bool(mut self, open: &'a mut bool, close_behavior: PopupCloseBehavior) -> Self {
|
||||||
|
self.open_kind = OpenKind::Bool(open, close_behavior);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the close behavior of the popup.
|
||||||
|
///
|
||||||
|
/// This will do nothing if [`Popup::open`] was called.
|
||||||
|
#[inline]
|
||||||
|
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
|
||||||
|
match &mut self.open_kind {
|
||||||
|
OpenKind::Memory {
|
||||||
|
close_behavior: behavior,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| OpenKind::Bool(_, behavior) => {
|
||||||
|
*behavior = close_behavior;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the popup relative to the pointer.
|
||||||
|
#[inline]
|
||||||
|
pub fn at_pointer(mut self) -> Self {
|
||||||
|
self.anchor = PopupAnchor::Pointer;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remember the pointer position at the time of opening the popup, and show the popup
|
||||||
|
/// relative to that.
|
||||||
|
#[inline]
|
||||||
|
pub fn at_pointer_fixed(mut self) -> Self {
|
||||||
|
self.anchor = PopupAnchor::PointerFixed;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the popup relative to a specific position.
|
||||||
|
#[inline]
|
||||||
|
pub fn at_position(mut self, position: Pos2) -> Self {
|
||||||
|
self.anchor = PopupAnchor::Position(position);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the popup relative to the given [`PopupAnchor`].
|
||||||
|
#[inline]
|
||||||
|
pub fn anchor(mut self, anchor: impl Into<PopupAnchor>) -> Self {
|
||||||
|
self.anchor = anchor.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the gap between the anchor and the popup.
|
||||||
|
#[inline]
|
||||||
|
pub fn gap(mut self, gap: f32) -> Self {
|
||||||
|
self.gap = gap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the sense of the popup.
|
||||||
|
#[inline]
|
||||||
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
|
self.sense = sense;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the layout of the popup.
|
||||||
|
#[inline]
|
||||||
|
pub fn layout(mut self, layout: Layout) -> Self {
|
||||||
|
self.layout = layout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The width that will be passed to [`Area::default_width`].
|
||||||
|
#[inline]
|
||||||
|
pub fn width(mut self, width: f32) -> Self {
|
||||||
|
self.width = Some(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the id of the Area.
|
||||||
|
#[inline]
|
||||||
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
|
self.id = id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [`Context`]
|
||||||
|
pub fn ctx(&self) -> &Context {
|
||||||
|
&self.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the [`PopupAnchor`] of the popup.
|
||||||
|
pub fn get_anchor(&self) -> PopupAnchor {
|
||||||
|
self.anchor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the anchor rect of the popup.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the anchor is [`PopupAnchor::Pointer`] and there is no pointer.
|
||||||
|
pub fn get_anchor_rect(&self) -> Option<Rect> {
|
||||||
|
self.anchor.rect(self.id, &self.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the expected rect the popup will be shown in.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the popup wasn't shown before or anchor is `PopupAnchor::Pointer` and
|
||||||
|
/// there is no pointer.
|
||||||
|
pub fn get_popup_rect(&self) -> Option<Rect> {
|
||||||
|
let size = self.get_expected_size();
|
||||||
|
if let Some(size) = size {
|
||||||
|
self.get_anchor_rect()
|
||||||
|
.map(|anchor| self.get_best_align().align_rect(&anchor, size, self.gap))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the id of the popup.
|
||||||
|
pub fn get_id(&self) -> Id {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the popup open?
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
match &self.open_kind {
|
||||||
|
OpenKind::Open => true,
|
||||||
|
OpenKind::Closed => false,
|
||||||
|
OpenKind::Bool(open, _) => **open,
|
||||||
|
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_expected_size(&self) -> Option<Vec2> {
|
||||||
|
AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the best alignment for the popup, based on the last size and screen rect.
|
||||||
|
pub fn get_best_align(&self) -> RectAlign {
|
||||||
|
let expected_popup_size = self
|
||||||
|
.get_expected_size()
|
||||||
|
.unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0));
|
||||||
|
|
||||||
|
let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
|
||||||
|
return self.rect_align;
|
||||||
|
};
|
||||||
|
|
||||||
|
RectAlign::find_best_align(
|
||||||
|
#[allow(clippy::iter_on_empty_collections)]
|
||||||
|
once(self.rect_align).chain(
|
||||||
|
self.alternative_aligns
|
||||||
|
// Need the empty slice so the iters have the same type so we can unwrap_or
|
||||||
|
.map(|a| a.iter().copied().chain([].iter().copied()))
|
||||||
|
.unwrap_or(
|
||||||
|
self.rect_align
|
||||||
|
.symmetries()
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.chain(RectAlign::MENU_ALIGNS.iter().copied()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
self.ctx.screen_rect(),
|
||||||
|
anchor_rect,
|
||||||
|
self.gap,
|
||||||
|
expected_popup_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the popup.
|
||||||
|
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
|
||||||
|
/// no pointer.
|
||||||
|
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
|
||||||
|
let best_align = self.get_best_align();
|
||||||
|
|
||||||
|
let Popup {
|
||||||
|
id,
|
||||||
|
ctx,
|
||||||
|
anchor,
|
||||||
|
open_kind,
|
||||||
|
kind,
|
||||||
|
layer_id,
|
||||||
|
rect_align: _,
|
||||||
|
alternative_aligns: _,
|
||||||
|
gap,
|
||||||
|
widget_clicked_elsewhere,
|
||||||
|
width,
|
||||||
|
sense,
|
||||||
|
layout,
|
||||||
|
frame,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let hover_pos = ctx.pointer_hover_pos();
|
||||||
|
if let OpenKind::Memory { set, .. } = open_kind {
|
||||||
|
ctx.memory_mut(|mem| match set {
|
||||||
|
Some(SetOpenCommand::Bool(open)) => {
|
||||||
|
if open {
|
||||||
|
match self.anchor {
|
||||||
|
PopupAnchor::PointerFixed => {
|
||||||
|
mem.open_popup_at(id, hover_pos);
|
||||||
|
}
|
||||||
|
_ => mem.open_popup(id),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mem.close_popup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(SetOpenCommand::Toggle) => {
|
||||||
|
mem.toggle_popup(id);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !open_kind.is_open(id, &ctx) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ui_kind, order) = match kind {
|
||||||
|
PopupKind::Popup => (UiKind::Popup, Order::Foreground),
|
||||||
|
PopupKind::Tooltip => (UiKind::Tooltip, Order::Tooltip),
|
||||||
|
PopupKind::Menu => (UiKind::Menu, Order::Foreground),
|
||||||
|
};
|
||||||
|
|
||||||
|
if kind == PopupKind::Popup {
|
||||||
|
ctx.pass_state_mut(|fs| {
|
||||||
|
fs.layers
|
||||||
|
.entry(layer_id)
|
||||||
|
.or_default()
|
||||||
|
.open_popups
|
||||||
|
.insert(id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let anchor_rect = anchor.rect(id, &ctx)?;
|
||||||
|
|
||||||
|
let (pivot, anchor) = best_align.pivot_pos(&anchor_rect, gap);
|
||||||
|
|
||||||
|
let mut area = Area::new(id)
|
||||||
|
.order(order)
|
||||||
|
.kind(ui_kind)
|
||||||
|
.pivot(pivot)
|
||||||
|
.fixed_pos(anchor)
|
||||||
|
.sense(sense)
|
||||||
|
.layout(layout);
|
||||||
|
|
||||||
|
if let Some(width) = width {
|
||||||
|
area = area.default_width(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style()));
|
||||||
|
|
||||||
|
let response = area.show(&ctx, |ui| frame.show(ui, content).inner);
|
||||||
|
|
||||||
|
let should_close = |close_behavior| {
|
||||||
|
let should_close = match close_behavior {
|
||||||
|
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
|
||||||
|
PopupCloseBehavior::CloseOnClickOutside => {
|
||||||
|
widget_clicked_elsewhere && response.response.clicked_elsewhere()
|
||||||
|
}
|
||||||
|
PopupCloseBehavior::IgnoreClicks => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
should_close || ctx.input(|i| i.key_pressed(Key::Escape))
|
||||||
|
};
|
||||||
|
|
||||||
|
match open_kind {
|
||||||
|
OpenKind::Open | OpenKind::Closed => {}
|
||||||
|
OpenKind::Bool(open, close_behavior) => {
|
||||||
|
if should_close(close_behavior) {
|
||||||
|
*open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenKind::Memory { close_behavior, .. } => {
|
||||||
|
if should_close(close_behavior) {
|
||||||
|
ctx.memory_mut(|mem| mem.close_popup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
use crate::pass_state::PerWidgetTooltipState;
|
||||||
|
use crate::{
|
||||||
|
AreaState, Context, Id, InnerResponse, LayerId, Layout, Order, Popup, PopupAnchor, PopupKind,
|
||||||
|
Response, Sense,
|
||||||
|
};
|
||||||
|
use emath::Vec2;
|
||||||
|
|
||||||
|
pub struct Tooltip<'a> {
|
||||||
|
pub popup: Popup<'a>,
|
||||||
|
layer_id: LayerId,
|
||||||
|
widget_id: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Tooltip<'a> {
|
||||||
|
/// Show a tooltip that is always open
|
||||||
|
pub fn new(
|
||||||
|
widget_id: Id,
|
||||||
|
ctx: Context,
|
||||||
|
anchor: impl Into<PopupAnchor>,
|
||||||
|
layer_id: LayerId,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
// TODO(lucasmerlin): Set width somehow (we're missing context here)
|
||||||
|
popup: Popup::new(widget_id, ctx, anchor.into(), layer_id)
|
||||||
|
.kind(PopupKind::Tooltip)
|
||||||
|
.gap(4.0)
|
||||||
|
.sense(Sense::hover()),
|
||||||
|
layer_id,
|
||||||
|
widget_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a tooltip for a widget. Always open (as long as this function is called).
|
||||||
|
pub fn for_widget(response: &Response) -> Self {
|
||||||
|
let popup = Popup::from_response(response)
|
||||||
|
.kind(PopupKind::Tooltip)
|
||||||
|
.gap(4.0)
|
||||||
|
.width(response.ctx.style().spacing.tooltip_width)
|
||||||
|
.sense(Sense::hover());
|
||||||
|
Self {
|
||||||
|
popup,
|
||||||
|
layer_id: response.layer_id,
|
||||||
|
widget_id: response.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a tooltip when hovering an enabled widget.
|
||||||
|
pub fn for_enabled(response: &Response) -> Self {
|
||||||
|
let mut tooltip = Self::for_widget(response);
|
||||||
|
tooltip.popup = tooltip
|
||||||
|
.popup
|
||||||
|
.open(response.enabled() && Self::should_show_tooltip(response));
|
||||||
|
tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a tooltip when hovering a disabled widget.
|
||||||
|
pub fn for_disabled(response: &Response) -> Self {
|
||||||
|
let mut tooltip = Self::for_widget(response);
|
||||||
|
tooltip.popup = tooltip
|
||||||
|
.popup
|
||||||
|
.open(!response.enabled() && Self::should_show_tooltip(response));
|
||||||
|
tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the tooltip at the pointer position.
|
||||||
|
#[inline]
|
||||||
|
pub fn at_pointer(mut self) -> Self {
|
||||||
|
self.popup = self.popup.at_pointer();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the gap between the tooltip and the anchor
|
||||||
|
///
|
||||||
|
/// Default: 5.0
|
||||||
|
#[inline]
|
||||||
|
pub fn gap(mut self, gap: f32) -> Self {
|
||||||
|
self.popup = self.popup.gap(gap);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the layout of the tooltip
|
||||||
|
#[inline]
|
||||||
|
pub fn layout(mut self, layout: Layout) -> Self {
|
||||||
|
self.popup = self.popup.layout(layout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the width of the tooltip
|
||||||
|
#[inline]
|
||||||
|
pub fn width(mut self, width: f32) -> Self {
|
||||||
|
self.popup = self.popup.width(width);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the tooltip
|
||||||
|
pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
|
||||||
|
let Self {
|
||||||
|
mut popup,
|
||||||
|
layer_id: parent_layer,
|
||||||
|
widget_id,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
if !popup.is_open() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rect = popup.get_anchor_rect()?;
|
||||||
|
|
||||||
|
let mut state = popup.ctx().pass_state_mut(|fs| {
|
||||||
|
// Remember that this is the widget showing the tooltip:
|
||||||
|
fs.layers
|
||||||
|
.entry(parent_layer)
|
||||||
|
.or_default()
|
||||||
|
.widget_with_tooltip = Some(widget_id);
|
||||||
|
|
||||||
|
fs.tooltips
|
||||||
|
.widget_tooltips
|
||||||
|
.get(&widget_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(PerWidgetTooltipState {
|
||||||
|
bounding_rect: rect,
|
||||||
|
tooltip_count: 0,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count);
|
||||||
|
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
|
||||||
|
|
||||||
|
let response = popup.show(|ui| {
|
||||||
|
// By default, the text in tooltips aren't selectable.
|
||||||
|
// This means that most tooltips aren't interactable,
|
||||||
|
// which also mean they won't stick around so you can click them.
|
||||||
|
// Only tooltips that have actual interactive stuff (buttons, links, …)
|
||||||
|
// will stick around when you try to click them.
|
||||||
|
ui.style_mut().interaction.selectable_labels = false;
|
||||||
|
|
||||||
|
content(ui)
|
||||||
|
});
|
||||||
|
|
||||||
|
// The popup might not be shown on at_pointer if there is no pointer.
|
||||||
|
if let Some(response) = &response {
|
||||||
|
state.tooltip_count += 1;
|
||||||
|
state.bounding_rect = state.bounding_rect.union(response.response.rect);
|
||||||
|
response
|
||||||
|
.response
|
||||||
|
.ctx
|
||||||
|
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
|
||||||
|
Self::remember_that_tooltip_was_shown(&response.response.ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn when_was_a_toolip_last_shown_id() -> Id {
|
||||||
|
Id::new("when_was_a_toolip_last_shown")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 {
|
||||||
|
let when_was_a_toolip_last_shown =
|
||||||
|
ctx.data(|d| d.get_temp::<f64>(Self::when_was_a_toolip_last_shown_id()));
|
||||||
|
|
||||||
|
if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown {
|
||||||
|
let now = ctx.input(|i| i.time);
|
||||||
|
(now - when_was_a_toolip_last_shown) as f32
|
||||||
|
} else {
|
||||||
|
f32::INFINITY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remember_that_tooltip_was_shown(ctx: &Context) {
|
||||||
|
let now = ctx.input(|i| i.time);
|
||||||
|
ctx.data_mut(|data| data.insert_temp::<f64>(Self::when_was_a_toolip_last_shown_id(), now));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What is the id of the next tooltip for this widget?
|
||||||
|
pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id {
|
||||||
|
let tooltip_count = ctx.pass_state(|fs| {
|
||||||
|
fs.tooltips
|
||||||
|
.widget_tooltips
|
||||||
|
.get(&widget_id)
|
||||||
|
.map_or(0, |state| state.tooltip_count)
|
||||||
|
});
|
||||||
|
Self::tooltip_id(widget_id, tooltip_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
|
||||||
|
widget_id.with(tooltip_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should we show a tooltip for this response?
|
||||||
|
pub fn should_show_tooltip(response: &Response) -> bool {
|
||||||
|
if response.ctx.memory(|mem| mem.everything_is_visible()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let any_open_popups = response.ctx.prev_pass_state(|fs| {
|
||||||
|
fs.layers
|
||||||
|
.get(&response.layer_id)
|
||||||
|
.is_some_and(|layer| !layer.open_popups.is_empty())
|
||||||
|
});
|
||||||
|
if any_open_popups {
|
||||||
|
// Hide tooltips if the user opens a popup (menu, combo-box, etc.) in the same layer.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = response.ctx.style();
|
||||||
|
|
||||||
|
let tooltip_delay = style.interaction.tooltip_delay;
|
||||||
|
let tooltip_grace_time = style.interaction.tooltip_grace_time;
|
||||||
|
|
||||||
|
let (
|
||||||
|
time_since_last_scroll,
|
||||||
|
time_since_last_click,
|
||||||
|
time_since_last_pointer_movement,
|
||||||
|
pointer_pos,
|
||||||
|
pointer_dir,
|
||||||
|
) = response.ctx.input(|i| {
|
||||||
|
(
|
||||||
|
i.time_since_last_scroll(),
|
||||||
|
i.pointer.time_since_last_click(),
|
||||||
|
i.pointer.time_since_last_movement(),
|
||||||
|
i.pointer.hover_pos(),
|
||||||
|
i.pointer.direction(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if time_since_last_scroll < tooltip_delay {
|
||||||
|
// See https://github.com/emilk/egui/issues/4781
|
||||||
|
// Note that this means we cannot have `ScrollArea`s in a tooltip.
|
||||||
|
response
|
||||||
|
.ctx
|
||||||
|
.request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_our_tooltip_open = response.is_tooltip_open();
|
||||||
|
|
||||||
|
if is_our_tooltip_open {
|
||||||
|
// Check if we should automatically stay open:
|
||||||
|
|
||||||
|
let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
|
||||||
|
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||||
|
|
||||||
|
let tooltip_has_interactive_widget = response.ctx.viewport(|vp| {
|
||||||
|
vp.prev_pass
|
||||||
|
.widgets
|
||||||
|
.get_layer(tooltip_layer_id)
|
||||||
|
.any(|w| w.enabled && w.sense.interactive())
|
||||||
|
});
|
||||||
|
|
||||||
|
if tooltip_has_interactive_widget {
|
||||||
|
// We keep the tooltip open if hovered,
|
||||||
|
// or if the pointer is on its way to it,
|
||||||
|
// so that the user can interact with the tooltip
|
||||||
|
// (i.e. click links that are in it).
|
||||||
|
if let Some(area) = AreaState::load(&response.ctx, tooltip_id) {
|
||||||
|
let rect = area.rect();
|
||||||
|
|
||||||
|
if let Some(pos) = pointer_pos {
|
||||||
|
if rect.contains(pos) {
|
||||||
|
return true; // hovering interactive tooltip
|
||||||
|
}
|
||||||
|
if pointer_dir != Vec2::ZERO
|
||||||
|
&& rect.intersects_ray(pos, pointer_dir.normalized())
|
||||||
|
{
|
||||||
|
return true; // on the way to interactive tooltip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let clicked_more_recently_than_moved =
|
||||||
|
time_since_last_click < time_since_last_pointer_movement + 0.1;
|
||||||
|
if clicked_more_recently_than_moved {
|
||||||
|
// It is common to click a widget and then rest the mouse there.
|
||||||
|
// It would be annoying to then see a tooltip for it immediately.
|
||||||
|
// Similarly, clicking should hide the existing tooltip.
|
||||||
|
// Only hovering should lead to a tooltip, not clicking.
|
||||||
|
// The offset is only to allow small movement just right after the click.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_our_tooltip_open {
|
||||||
|
// Check if we should automatically stay open:
|
||||||
|
|
||||||
|
if pointer_pos.is_some_and(|pointer_pos| response.rect.contains(pointer_pos)) {
|
||||||
|
// Handle the case of a big tooltip that covers the widget:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_other_tooltip_open = response.ctx.prev_pass_state(|fs| {
|
||||||
|
if let Some(already_open_tooltip) = fs
|
||||||
|
.layers
|
||||||
|
.get(&response.layer_id)
|
||||||
|
.and_then(|layer| layer.widget_with_tooltip)
|
||||||
|
{
|
||||||
|
already_open_tooltip != response.id
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if is_other_tooltip_open {
|
||||||
|
// We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast early-outs:
|
||||||
|
if response.enabled() {
|
||||||
|
if !response.hovered() || !response.ctx.input(|i| i.pointer.has_pointer()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if !response
|
||||||
|
.ctx
|
||||||
|
.rect_contains_pointer(response.layer_id, response.rect)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a tooltip_delay before showing the first tooltip,
|
||||||
|
// but once one tooltip is show, moving the mouse cursor to
|
||||||
|
// another widget should show the tooltip for that widget right away.
|
||||||
|
|
||||||
|
// Let the user quickly move over some dead space to hover the next thing
|
||||||
|
let tooltip_was_recently_shown =
|
||||||
|
Self::seconds_since_last_tooltip(&response.ctx) < tooltip_grace_time;
|
||||||
|
|
||||||
|
if !tooltip_was_recently_shown && !is_our_tooltip_open {
|
||||||
|
if style.interaction.show_tooltips_only_when_still {
|
||||||
|
// We only show the tooltip when the mouse pointer is still.
|
||||||
|
if !response
|
||||||
|
.ctx
|
||||||
|
.input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
|
||||||
|
{
|
||||||
|
// wait for mouse to stop
|
||||||
|
response.ctx.request_repaint();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_since_last_interaction = time_since_last_scroll
|
||||||
|
.min(time_since_last_pointer_movement)
|
||||||
|
.min(time_since_last_click);
|
||||||
|
let time_til_tooltip = tooltip_delay - time_since_last_interaction;
|
||||||
|
|
||||||
|
if 0.0 < time_til_tooltip {
|
||||||
|
// Wait until the mouse has been still for a while
|
||||||
|
response.ctx.request_repaint_after_secs(time_til_tooltip);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want tooltips of things while we are dragging them,
|
||||||
|
// but we do want tooltips while holding down on an item on a touch screen.
|
||||||
|
if response
|
||||||
|
.ctx
|
||||||
|
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All checks passed: show the tooltip!
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Was this tooltip visible last frame?
|
||||||
|
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
|
||||||
|
let primary_tooltip_area_id = Self::tooltip_id(widget_id, 0);
|
||||||
|
ctx.memory(|mem| {
|
||||||
|
mem.areas()
|
||||||
|
.visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -458,7 +458,8 @@ pub use epaint::emath;
|
||||||
pub use ecolor::hex_color;
|
pub use ecolor::hex_color;
|
||||||
pub use ecolor::{Color32, Rgba};
|
pub use ecolor::{Color32, Rgba};
|
||||||
pub use emath::{
|
pub use emath::{
|
||||||
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2, Vec2b,
|
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign,
|
||||||
|
Vec2, Vec2b,
|
||||||
};
|
};
|
||||||
pub use epaint::{
|
pub use epaint::{
|
||||||
mutex,
|
mutex,
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,12 @@ pub struct Memory {
|
||||||
|
|
||||||
/// Which popup-window is open (if any)?
|
/// Which popup-window is open (if any)?
|
||||||
/// Could be a combo box, color picker, menu, etc.
|
/// Could be a combo box, color picker, menu, etc.
|
||||||
|
/// Optionally stores the position of the popup (usually this would be the position where
|
||||||
|
/// the user clicked).
|
||||||
|
/// If position is [`None`], the popup position will be calculated based on some configuration
|
||||||
|
/// (e.g. relative to some other widget).
|
||||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||||
popup: Option<Id>,
|
popup: Option<(Id, Option<Pos2>)>,
|
||||||
|
|
||||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||||
everything_is_visible: bool,
|
everything_is_visible: bool,
|
||||||
|
|
@ -1070,7 +1074,7 @@ impl Memory {
|
||||||
impl Memory {
|
impl Memory {
|
||||||
/// Is the given popup open?
|
/// Is the given popup open?
|
||||||
pub fn is_popup_open(&self, popup_id: Id) -> bool {
|
pub fn is_popup_open(&self, popup_id: Id) -> bool {
|
||||||
self.popup == Some(popup_id) || self.everything_is_visible()
|
self.popup.is_some_and(|(id, _)| id == popup_id) || self.everything_is_visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is any popup open?
|
/// Is any popup open?
|
||||||
|
|
@ -1080,7 +1084,18 @@ impl Memory {
|
||||||
|
|
||||||
/// Open the given popup and close all others.
|
/// Open the given popup and close all others.
|
||||||
pub fn open_popup(&mut self, popup_id: Id) {
|
pub fn open_popup(&mut self, popup_id: Id) {
|
||||||
self.popup = Some(popup_id);
|
self.popup = Some((popup_id, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the popup and remember its position.
|
||||||
|
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
|
||||||
|
self.popup = Some((popup_id, pos.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position for this popup.
|
||||||
|
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
|
||||||
|
self.popup
|
||||||
|
.and_then(|(popup_id, pos)| if popup_id == id { pos } else { None })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the open popup, if any.
|
/// Close the open popup, if any.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emath::{Align, Pos2, Rect, Vec2},
|
emath::{Align, Pos2, Rect, Vec2},
|
||||||
menu, pass_state, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui,
|
menu, pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense,
|
||||||
WidgetRect, WidgetText,
|
Tooltip, Ui, WidgetRect, WidgetText,
|
||||||
};
|
};
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -550,36 +550,22 @@ impl Response {
|
||||||
/// ```
|
/// ```
|
||||||
#[doc(alias = "tooltip")]
|
#[doc(alias = "tooltip")]
|
||||||
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||||
if self.flags.contains(Flags::ENABLED) && self.should_show_hover_ui() {
|
Tooltip::for_enabled(&self).show(add_contents);
|
||||||
self.show_tooltip_ui(add_contents);
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show this UI when hovering if the widget is disabled.
|
/// Show this UI when hovering if the widget is disabled.
|
||||||
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||||
if !self.enabled() && self.should_show_hover_ui() {
|
Tooltip::for_disabled(&self).show(add_contents);
|
||||||
crate::containers::show_tooltip_for(
|
|
||||||
&self.ctx,
|
|
||||||
self.layer_id,
|
|
||||||
self.id,
|
|
||||||
&self.rect,
|
|
||||||
add_contents,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like `on_hover_ui`, but show the ui next to cursor.
|
/// Like `on_hover_ui`, but show the ui next to cursor.
|
||||||
pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||||
if self.enabled() && self.should_show_hover_ui() {
|
Tooltip::for_enabled(&self)
|
||||||
crate::containers::show_tooltip_at_pointer(
|
.at_pointer()
|
||||||
&self.ctx,
|
.gap(12.0)
|
||||||
self.layer_id,
|
.show(add_contents);
|
||||||
self.id,
|
|
||||||
add_contents,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,13 +573,9 @@ impl Response {
|
||||||
///
|
///
|
||||||
/// This can be used to give attention to a widget during a tutorial.
|
/// This can be used to give attention to a widget during a tutorial.
|
||||||
pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) {
|
pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) {
|
||||||
crate::containers::show_tooltip_for(
|
Popup::from_response(self)
|
||||||
&self.ctx,
|
.kind(PopupKind::Tooltip)
|
||||||
self.layer_id,
|
.show(add_contents);
|
||||||
self.id,
|
|
||||||
&self.rect,
|
|
||||||
add_contents,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always show this tooltip, even if disabled and the user isn't hovering it.
|
/// Always show this tooltip, even if disabled and the user isn't hovering it.
|
||||||
|
|
@ -607,180 +589,7 @@ impl Response {
|
||||||
|
|
||||||
/// Was the tooltip open last frame?
|
/// Was the tooltip open last frame?
|
||||||
pub fn is_tooltip_open(&self) -> bool {
|
pub fn is_tooltip_open(&self) -> bool {
|
||||||
crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id)
|
Tooltip::was_tooltip_open_last_frame(&self.ctx, self.id)
|
||||||
}
|
|
||||||
|
|
||||||
fn should_show_hover_ui(&self) -> bool {
|
|
||||||
if self.ctx.memory(|mem| mem.everything_is_visible()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let any_open_popups = self.ctx.prev_pass_state(|fs| {
|
|
||||||
fs.layers
|
|
||||||
.get(&self.layer_id)
|
|
||||||
.is_some_and(|layer| !layer.open_popups.is_empty())
|
|
||||||
});
|
|
||||||
if any_open_popups {
|
|
||||||
// Hide tooltips if the user opens a popup (menu, combo-box, etc) in the same layer.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = self.ctx.style();
|
|
||||||
|
|
||||||
let tooltip_delay = style.interaction.tooltip_delay;
|
|
||||||
let tooltip_grace_time = style.interaction.tooltip_grace_time;
|
|
||||||
|
|
||||||
let (
|
|
||||||
time_since_last_scroll,
|
|
||||||
time_since_last_click,
|
|
||||||
time_since_last_pointer_movement,
|
|
||||||
pointer_pos,
|
|
||||||
pointer_dir,
|
|
||||||
) = self.ctx.input(|i| {
|
|
||||||
(
|
|
||||||
i.time_since_last_scroll(),
|
|
||||||
i.pointer.time_since_last_click(),
|
|
||||||
i.pointer.time_since_last_movement(),
|
|
||||||
i.pointer.hover_pos(),
|
|
||||||
i.pointer.direction(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if time_since_last_scroll < tooltip_delay {
|
|
||||||
// See https://github.com/emilk/egui/issues/4781
|
|
||||||
// Note that this means we cannot have `ScrollArea`s in a tooltip.
|
|
||||||
self.ctx
|
|
||||||
.request_repaint_after_secs(tooltip_delay - time_since_last_scroll);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_our_tooltip_open = self.is_tooltip_open();
|
|
||||||
|
|
||||||
if is_our_tooltip_open {
|
|
||||||
// Check if we should automatically stay open:
|
|
||||||
|
|
||||||
let tooltip_id = crate::next_tooltip_id(&self.ctx, self.id);
|
|
||||||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
|
||||||
|
|
||||||
let tooltip_has_interactive_widget = self.ctx.viewport(|vp| {
|
|
||||||
vp.prev_pass
|
|
||||||
.widgets
|
|
||||||
.get_layer(tooltip_layer_id)
|
|
||||||
.any(|w| w.enabled && w.sense.interactive())
|
|
||||||
});
|
|
||||||
|
|
||||||
if tooltip_has_interactive_widget {
|
|
||||||
// We keep the tooltip open if hovered,
|
|
||||||
// or if the pointer is on its way to it,
|
|
||||||
// so that the user can interact with the tooltip
|
|
||||||
// (i.e. click links that are in it).
|
|
||||||
if let Some(area) = AreaState::load(&self.ctx, tooltip_id) {
|
|
||||||
let rect = area.rect();
|
|
||||||
|
|
||||||
if let Some(pos) = pointer_pos {
|
|
||||||
if rect.contains(pos) {
|
|
||||||
return true; // hovering interactive tooltip
|
|
||||||
}
|
|
||||||
if pointer_dir != Vec2::ZERO
|
|
||||||
&& rect.intersects_ray(pos, pointer_dir.normalized())
|
|
||||||
{
|
|
||||||
return true; // on the way to interactive tooltip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let clicked_more_recently_than_moved =
|
|
||||||
time_since_last_click < time_since_last_pointer_movement + 0.1;
|
|
||||||
if clicked_more_recently_than_moved {
|
|
||||||
// It is common to click a widget and then rest the mouse there.
|
|
||||||
// It would be annoying to then see a tooltip for it immediately.
|
|
||||||
// Similarly, clicking should hide the existing tooltip.
|
|
||||||
// Only hovering should lead to a tooltip, not clicking.
|
|
||||||
// The offset is only to allow small movement just right after the click.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_our_tooltip_open {
|
|
||||||
// Check if we should automatically stay open:
|
|
||||||
|
|
||||||
if pointer_pos.is_some_and(|pointer_pos| self.rect.contains(pointer_pos)) {
|
|
||||||
// Handle the case of a big tooltip that covers the widget:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_other_tooltip_open = self.ctx.prev_pass_state(|fs| {
|
|
||||||
if let Some(already_open_tooltip) = fs
|
|
||||||
.layers
|
|
||||||
.get(&self.layer_id)
|
|
||||||
.and_then(|layer| layer.widget_with_tooltip)
|
|
||||||
{
|
|
||||||
already_open_tooltip != self.id
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if is_other_tooltip_open {
|
|
||||||
// We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast early-outs:
|
|
||||||
if self.enabled() {
|
|
||||||
if !self.hovered() || !self.ctx.input(|i| i.pointer.has_pointer()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if !self.ctx.rect_contains_pointer(self.layer_id, self.rect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is a tooltip_delay before showing the first tooltip,
|
|
||||||
// but once one tooltip is show, moving the mouse cursor to
|
|
||||||
// another widget should show the tooltip for that widget right away.
|
|
||||||
|
|
||||||
// Let the user quickly move over some dead space to hover the next thing
|
|
||||||
let tooltip_was_recently_shown =
|
|
||||||
crate::popup::seconds_since_last_tooltip(&self.ctx) < tooltip_grace_time;
|
|
||||||
|
|
||||||
if !tooltip_was_recently_shown && !is_our_tooltip_open {
|
|
||||||
if style.interaction.show_tooltips_only_when_still {
|
|
||||||
// We only show the tooltip when the mouse pointer is still.
|
|
||||||
if !self
|
|
||||||
.ctx
|
|
||||||
.input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
|
|
||||||
{
|
|
||||||
// wait for mouse to stop
|
|
||||||
self.ctx.request_repaint();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let time_since_last_interaction = time_since_last_scroll
|
|
||||||
.min(time_since_last_pointer_movement)
|
|
||||||
.min(time_since_last_click);
|
|
||||||
let time_til_tooltip = tooltip_delay - time_since_last_interaction;
|
|
||||||
|
|
||||||
if 0.0 < time_til_tooltip {
|
|
||||||
// Wait until the mouse has been still for a while
|
|
||||||
self.ctx.request_repaint_after_secs(time_til_tooltip);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't want tooltips of things while we are dragging them,
|
|
||||||
// but we do want tooltips while holding down on an item on a touch screen.
|
|
||||||
if self
|
|
||||||
.ctx
|
|
||||||
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All checks passed: show the tooltip!
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like `on_hover_text`, but show the text next to cursor.
|
/// Like `on_hover_text`, but show the text next to cursor.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use egui::{ComboBox, Popup};
|
||||||
|
|
||||||
#[derive(Clone, Default, PartialEq, Eq)]
|
#[derive(Clone, Default, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct ContextMenus {}
|
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.vertical_centered(|ui| {
|
||||||
ui.add(crate::egui_github_link_file!());
|
ui.add(crate::egui_github_link_file!());
|
||||||
});
|
});
|
||||||
|
|
@ -51,6 +67,7 @@ impl ContextMenus {
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
let _ = ui.button("Item");
|
let _ = ui.button("Item");
|
||||||
|
ui.menu_button("Recursive", Self::nested_menus)
|
||||||
});
|
});
|
||||||
ui.menu_button("SubMenu", |ui| {
|
ui.menu_button("SubMenu", |ui| {
|
||||||
if ui.button("Open…").clicked() {
|
if ui.button("Open…").clicked() {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ impl Default for DemoGroups {
|
||||||
Box::<super::multi_touch::MultiTouch>::default(),
|
Box::<super::multi_touch::MultiTouch>::default(),
|
||||||
Box::<super::painting::Painting>::default(),
|
Box::<super::painting::Painting>::default(),
|
||||||
Box::<super::panels::Panels>::default(),
|
Box::<super::panels::Panels>::default(),
|
||||||
|
Box::<super::popups::PopupsDemo>::default(),
|
||||||
Box::<super::scene::SceneDemo>::default(),
|
Box::<super::scene::SceneDemo>::default(),
|
||||||
Box::<super::screenshot::Screenshot>::default(),
|
Box::<super::screenshot::Screenshot>::default(),
|
||||||
Box::<super::scrolling::Scrolling>::default(),
|
Box::<super::scrolling::Scrolling>::default(),
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ pub mod paint_bezier;
|
||||||
pub mod painting;
|
pub mod painting;
|
||||||
pub mod panels;
|
pub mod panels;
|
||||||
pub mod password;
|
pub mod password;
|
||||||
|
mod popups;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod screenshot;
|
pub mod screenshot;
|
||||||
pub mod scrolling;
|
pub mod scrolling;
|
||||||
|
|
|
||||||
|
|
@ -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("You can select this text.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.label("This tooltip shows at the mouse cursor.")
|
||||||
|
.on_hover_text_at_pointer("Move me around!!");
|
||||||
|
|
||||||
ui.separator(); // ---------------------------------------------------------
|
ui.separator(); // ---------------------------------------------------------
|
||||||
|
|
||||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d4cc8e0919fed5bd1ef981658626dba728435ab95da8ee96ced1fb4838d535ff
|
oid sha256:eb2bc4a38f20ed0f5fced36e8e56936bee328b24a0a45127d5d3739d40331cb7
|
||||||
size 11741
|
size 15514
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4806984f9c801a054cea80b89664293680abaa57cf0a95cf9682f111e3794fc1
|
||||||
|
size 25080
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c80158ac9c823f94d2830d1423236ad441dc7da31e748b6815c69663fa2a03d0
|
oid sha256:92b70683a685869274749d057de174896e18dae5cb67e70221c3efdb7106cdda
|
||||||
size 59662
|
size 63684
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
use kittest::Queryable;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_interactive_tooltip() {
|
||||||
|
struct State {
|
||||||
|
link_clicked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut harness = egui_kittest::Harness::new_ui_state(
|
||||||
|
|ui, state| {
|
||||||
|
ui.label("I have a tooltip").on_hover_ui(|ui| {
|
||||||
|
if ui.link("link").clicked() {
|
||||||
|
state.link_clicked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
link_clicked: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.get_by_label_contains("tooltip").hover();
|
||||||
|
harness.run();
|
||||||
|
harness.get_by_label("link").hover();
|
||||||
|
harness.run();
|
||||||
|
harness.get_by_label("link").simulate_click();
|
||||||
|
|
||||||
|
harness.run();
|
||||||
|
|
||||||
|
assert!(harness.state().link_clicked);
|
||||||
|
}
|
||||||
|
|
@ -50,6 +50,16 @@ impl Align {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the inverse alignment.
|
||||||
|
/// `Min` becomes `Max`, `Center` stays the same, `Max` becomes `Min`.
|
||||||
|
pub fn flip(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Min => Self::Max,
|
||||||
|
Self::Center => Self::Center,
|
||||||
|
Self::Max => Self::Min,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a range of given size within a specified range.
|
/// Returns a range of given size within a specified range.
|
||||||
///
|
///
|
||||||
/// If the requested `size` is bigger than the size of `range`, then the returned
|
/// If the requested `size` is bigger than the size of `range`, then the returned
|
||||||
|
|
@ -170,6 +180,24 @@ impl Align2 {
|
||||||
vec2(self.x().to_sign(), self.y().to_sign())
|
vec2(self.x().to_sign(), self.y().to_sign())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flip on the x-axis
|
||||||
|
/// e.g. `TOP_LEFT` -> `TOP_RIGHT`
|
||||||
|
pub fn flip_x(self) -> Self {
|
||||||
|
Self([self.x().flip(), self.y()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flip on the y-axis
|
||||||
|
/// e.g. `TOP_LEFT` -> `BOTTOM_LEFT`
|
||||||
|
pub fn flip_y(self) -> Self {
|
||||||
|
Self([self.x(), self.y().flip()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flip on both axes
|
||||||
|
/// e.g. `TOP_LEFT` -> `BOTTOM_RIGHT`
|
||||||
|
pub fn flip(self) -> Self {
|
||||||
|
Self([self.x().flip(), self.y().flip()])
|
||||||
|
}
|
||||||
|
|
||||||
/// Used e.g. to anchor a piece of text to a part of the rectangle.
|
/// Used e.g. to anchor a piece of text to a part of the rectangle.
|
||||||
/// Give a position within the rect, specified by the aligns
|
/// Give a position within the rect, specified by the aligns
|
||||||
pub fn anchor_rect(self, rect: Rect) -> Rect {
|
pub fn anchor_rect(self, rect: Rect) -> Rect {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ mod ordered_float;
|
||||||
mod pos2;
|
mod pos2;
|
||||||
mod range;
|
mod range;
|
||||||
mod rect;
|
mod rect;
|
||||||
|
mod rect_align;
|
||||||
mod rect_transform;
|
mod rect_transform;
|
||||||
mod rot2;
|
mod rot2;
|
||||||
pub mod smart_aim;
|
pub mod smart_aim;
|
||||||
|
|
@ -50,6 +51,7 @@ pub use self::{
|
||||||
pos2::*,
|
pos2::*,
|
||||||
range::Rangef,
|
range::Rangef,
|
||||||
rect::*,
|
rect::*,
|
||||||
|
rect_align::RectAlign,
|
||||||
rect_transform::*,
|
rect_transform::*,
|
||||||
rot2::*,
|
rot2::*,
|
||||||
ts_transform::*,
|
ts_transform::*,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
use crate::{Align2, Pos2, Rect, Vec2};
|
||||||
|
|
||||||
|
/// Position a child [`Rect`] relative to a parent [`Rect`].
|
||||||
|
///
|
||||||
|
/// The corner from [`RectAlign::child`] on the new rect will be aligned to
|
||||||
|
/// the corner from [`RectAlign::parent`] on the original rect.
|
||||||
|
///
|
||||||
|
/// There are helper constants for the 12 common menu positions:
|
||||||
|
/// ```text
|
||||||
|
/// ┌───────────┐ ┌────────┐ ┌─────────┐
|
||||||
|
/// │ TOP_START │ │ TOP │ │ TOP_END │
|
||||||
|
/// └───────────┘ └────────┘ └─────────┘
|
||||||
|
/// ┌──────────┐ ┌────────────────────────────────────┐ ┌───────────┐
|
||||||
|
/// │LEFT_START│ │ │ │RIGHT_START│
|
||||||
|
/// └──────────┘ │ │ └───────────┘
|
||||||
|
/// ┌──────────┐ │ │ ┌───────────┐
|
||||||
|
/// │ LEFT │ │ some_rect │ │ RIGHT │
|
||||||
|
/// └──────────┘ │ │ └───────────┘
|
||||||
|
/// ┌──────────┐ │ │ ┌───────────┐
|
||||||
|
/// │ LEFT_END │ │ │ │ RIGHT_END │
|
||||||
|
/// └──────────┘ └────────────────────────────────────┘ └───────────┘
|
||||||
|
/// ┌────────────┐ ┌──────┐ ┌──────────┐
|
||||||
|
/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│
|
||||||
|
/// └────────────┘ └──────┘ └──────────┘
|
||||||
|
/// ```
|
||||||
|
// There is no `new` function on purpose, since writing out `parent` and `child` is more
|
||||||
|
// reasonable.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct RectAlign {
|
||||||
|
/// The alignment in the parent (original) rect.
|
||||||
|
pub parent: Align2,
|
||||||
|
|
||||||
|
/// The alignment in the child (new) rect.
|
||||||
|
pub child: Align2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RectAlign {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::BOTTOM_START
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectAlign {
|
||||||
|
/// Along the top edge, leftmost.
|
||||||
|
pub const TOP_START: Self = Self {
|
||||||
|
parent: Align2::LEFT_TOP,
|
||||||
|
child: Align2::LEFT_BOTTOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the top edge, centered.
|
||||||
|
pub const TOP: Self = Self {
|
||||||
|
parent: Align2::CENTER_TOP,
|
||||||
|
child: Align2::CENTER_BOTTOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the top edge, rightmost.
|
||||||
|
pub const TOP_END: Self = Self {
|
||||||
|
parent: Align2::RIGHT_TOP,
|
||||||
|
child: Align2::RIGHT_BOTTOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the right edge, topmost.
|
||||||
|
pub const RIGHT_START: Self = Self {
|
||||||
|
parent: Align2::RIGHT_TOP,
|
||||||
|
child: Align2::LEFT_TOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the right edge, centered.
|
||||||
|
pub const RIGHT: Self = Self {
|
||||||
|
parent: Align2::RIGHT_CENTER,
|
||||||
|
child: Align2::LEFT_CENTER,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the right edge, bottommost.
|
||||||
|
pub const RIGHT_END: Self = Self {
|
||||||
|
parent: Align2::RIGHT_BOTTOM,
|
||||||
|
child: Align2::LEFT_BOTTOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the bottom edge, rightmost.
|
||||||
|
pub const BOTTOM_END: Self = Self {
|
||||||
|
parent: Align2::RIGHT_BOTTOM,
|
||||||
|
child: Align2::RIGHT_TOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the bottom edge, centered.
|
||||||
|
pub const BOTTOM: Self = Self {
|
||||||
|
parent: Align2::CENTER_BOTTOM,
|
||||||
|
child: Align2::CENTER_TOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the bottom edge, leftmost.
|
||||||
|
pub const BOTTOM_START: Self = Self {
|
||||||
|
parent: Align2::LEFT_BOTTOM,
|
||||||
|
child: Align2::LEFT_TOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the left edge, bottommost.
|
||||||
|
pub const LEFT_END: Self = Self {
|
||||||
|
parent: Align2::LEFT_BOTTOM,
|
||||||
|
child: Align2::RIGHT_BOTTOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the left edge, centered.
|
||||||
|
pub const LEFT: Self = Self {
|
||||||
|
parent: Align2::LEFT_CENTER,
|
||||||
|
child: Align2::RIGHT_CENTER,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Along the left edge, topmost.
|
||||||
|
pub const LEFT_START: Self = Self {
|
||||||
|
parent: Align2::LEFT_TOP,
|
||||||
|
child: Align2::RIGHT_TOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The 12 most common menu positions as an array, for use with [`RectAlign::find_best_align`].
|
||||||
|
pub const MENU_ALIGNS: [Self; 12] = [
|
||||||
|
Self::BOTTOM_START,
|
||||||
|
Self::BOTTOM_END,
|
||||||
|
Self::TOP_START,
|
||||||
|
Self::TOP_END,
|
||||||
|
Self::RIGHT_END,
|
||||||
|
Self::RIGHT_START,
|
||||||
|
Self::LEFT_END,
|
||||||
|
Self::LEFT_START,
|
||||||
|
// These come last on purpose, we prefer the corner ones
|
||||||
|
Self::TOP,
|
||||||
|
Self::RIGHT,
|
||||||
|
Self::BOTTOM,
|
||||||
|
Self::LEFT,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Align in the parent rect.
|
||||||
|
pub fn parent(&self) -> Align2 {
|
||||||
|
self.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Align in the child rect.
|
||||||
|
pub fn child(&self) -> Align2 {
|
||||||
|
self.child
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an [`Align2`] to an [`RectAlign`], positioning the child rect inside the parent.
|
||||||
|
pub fn from_align2(align: Align2) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: align,
|
||||||
|
child: align,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The center of the child rect will be aligned to a corner of the parent rect.
|
||||||
|
pub fn over_corner(align: Align2) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: align,
|
||||||
|
child: Align2::CENTER_CENTER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position the child rect outside the parent rect.
|
||||||
|
pub fn outside(align: Align2) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: align,
|
||||||
|
child: align.flip(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the child rect based on a size and some optional gap.
|
||||||
|
pub fn align_rect(&self, parent_rect: &Rect, size: Vec2, gap: f32) -> Rect {
|
||||||
|
let (pivot, anchor) = self.pivot_pos(parent_rect, gap);
|
||||||
|
pivot.anchor_size(anchor, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [`Align2`] and a [`Pos2`] that you can e.g. use with `Area::fixed_pos`
|
||||||
|
/// and `Area::pivot` to align an `Area` to some rect.
|
||||||
|
pub fn pivot_pos(&self, parent_rect: &Rect, gap: f32) -> (Align2, Pos2) {
|
||||||
|
(self.child(), self.anchor(parent_rect, gap))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a sign vector (-1, 0 or 1 in each direction) that can be used as an offset to the
|
||||||
|
/// child rect, creating a gap between the rects while keeping the edges aligned.
|
||||||
|
pub fn gap_vector(&self) -> Vec2 {
|
||||||
|
let mut gap = -self.child.to_sign();
|
||||||
|
|
||||||
|
// Align the edges in these cases
|
||||||
|
match *self {
|
||||||
|
Self::TOP_START | Self::TOP_END | Self::BOTTOM_START | Self::BOTTOM_END => {
|
||||||
|
gap.x = 0.0;
|
||||||
|
}
|
||||||
|
Self::LEFT_START | Self::LEFT_END | Self::RIGHT_START | Self::RIGHT_END => {
|
||||||
|
gap.y = 0.0;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
gap
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculator the anchor point for the child rect, based on the parent rect and an optional gap.
|
||||||
|
pub fn anchor(&self, parent_rect: &Rect, gap: f32) -> Pos2 {
|
||||||
|
let pos = self.parent.pos_in_rect(parent_rect);
|
||||||
|
|
||||||
|
let offset = self.gap_vector() * gap;
|
||||||
|
|
||||||
|
pos + offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flip the alignment on the x-axis.
|
||||||
|
pub fn flip_x(self) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: self.parent.flip_x(),
|
||||||
|
child: self.child.flip_x(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flip the alignment on the y-axis.
|
||||||
|
pub fn flip_y(self) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: self.parent.flip_y(),
|
||||||
|
child: self.child.flip_y(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flip the alignment on both axes.
|
||||||
|
pub fn flip(self) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: self.parent.flip(),
|
||||||
|
child: self.child.flip(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the 3 alternative [`RectAlign`]s that are flipped in various ways, for use
|
||||||
|
/// with [`RectAlign::find_best_align`].
|
||||||
|
pub fn symmetries(self) -> [Self; 3] {
|
||||||
|
[self.flip_x(), self.flip_y(), self.flip()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look for the [`RectAlign`] that fits best in the available space.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// - [`RectAlign::symmetries`] to calculate alternatives
|
||||||
|
/// - [`RectAlign::MENU_ALIGNS`] for the 12 common menu positions
|
||||||
|
pub fn find_best_align(
|
||||||
|
mut values_to_try: impl Iterator<Item = Self>,
|
||||||
|
available_space: Rect,
|
||||||
|
parent_rect: Rect,
|
||||||
|
gap: f32,
|
||||||
|
size: Vec2,
|
||||||
|
) -> Self {
|
||||||
|
let area = size.x * size.y;
|
||||||
|
|
||||||
|
let blocked_area = |pos: Self| {
|
||||||
|
let rect = pos.align_rect(&parent_rect, size, gap);
|
||||||
|
area - available_space.intersect(rect).area()
|
||||||
|
};
|
||||||
|
|
||||||
|
let first = values_to_try.next().unwrap_or_default();
|
||||||
|
|
||||||
|
if blocked_area(first) == 0.0 {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best_area = blocked_area(first);
|
||||||
|
let mut best = first;
|
||||||
|
|
||||||
|
for align in values_to_try {
|
||||||
|
let blocked = blocked_area(align);
|
||||||
|
if blocked == 0.0 {
|
||||||
|
return align;
|
||||||
|
}
|
||||||
|
if blocked < best_area {
|
||||||
|
best = align;
|
||||||
|
best_area = blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
|
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
|
||||||
|
|
||||||
use eframe::egui::{popup_below_widget, CentralPanel, ComboBox, Id, PopupCloseBehavior};
|
use eframe::egui::{CentralPanel, ComboBox, Popup, PopupCloseBehavior};
|
||||||
|
|
||||||
fn main() -> Result<(), eframe::Error> {
|
fn main() -> Result<(), eframe::Error> {
|
||||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||||
|
|
@ -42,23 +42,14 @@ impl eframe::App for MyApp {
|
||||||
|
|
||||||
ui.label("PopupCloseBehavior::IgnoreClicks popup");
|
ui.label("PopupCloseBehavior::IgnoreClicks popup");
|
||||||
let response = ui.button("Open");
|
let response = ui.button("Open");
|
||||||
let popup_id = Id::new("popup_id");
|
|
||||||
|
|
||||||
if response.clicked() {
|
Popup::menu(&response)
|
||||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||||
}
|
.show(|ui| {
|
||||||
|
|
||||||
popup_below_widget(
|
|
||||||
ui,
|
|
||||||
popup_id,
|
|
||||||
&response,
|
|
||||||
PopupCloseBehavior::IgnoreClicks,
|
|
||||||
|ui| {
|
|
||||||
ui.set_min_width(310.0);
|
ui.set_min_width(310.0);
|
||||||
ui.label("This popup will be open until you press the button again");
|
ui.label("This popup will be open until you press the button again");
|
||||||
ui.checkbox(&mut self.checkbox, "Checkbox");
|
ui.checkbox(&mut self.checkbox, "Checkbox");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue