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 crate::{
|
||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
|
||||
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
||||
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
|
||||
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
|
||||
};
|
||||
|
||||
/// State of an [`Area`] that is persisted between frames.
|
||||
|
|
@ -120,6 +120,7 @@ pub struct Area {
|
|||
anchor: Option<(Align2, Vec2)>,
|
||||
new_pos: Option<Pos2>,
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl WidgetWithState for Area {
|
||||
|
|
@ -145,6 +146,7 @@ impl Area {
|
|||
pivot: Align2::LEFT_TOP,
|
||||
anchor: None,
|
||||
fade_in: true,
|
||||
layout: Layout::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -339,6 +341,13 @@ impl Area {
|
|||
self.fade_in = fade_in;
|
||||
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 {
|
||||
|
|
@ -358,6 +367,7 @@ pub(crate) struct Prepared {
|
|||
sizing_pass: bool,
|
||||
|
||||
fade_in: bool,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
|
|
@ -390,6 +400,7 @@ impl Area {
|
|||
constrain,
|
||||
constrain_rect,
|
||||
fade_in,
|
||||
layout,
|
||||
} = self;
|
||||
|
||||
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
|
||||
|
|
@ -516,6 +527,7 @@ impl Area {
|
|||
constrain_rect,
|
||||
sizing_pass,
|
||||
fade_in,
|
||||
layout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -543,7 +555,8 @@ impl Prepared {
|
|||
let mut ui_builder = UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(self.kind))
|
||||
.layer_id(self.layer_id)
|
||||
.max_rect(max_rect);
|
||||
.max_rect(max_rect)
|
||||
.layout(self.layout);
|
||||
|
||||
if !self.enabled {
|
||||
ui_builder = ui_builder.disabled();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use epaint::Shape;
|
||||
|
||||
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,
|
||||
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
|
||||
};
|
||||
|
|
@ -9,15 +9,8 @@ use crate::{
|
|||
#[allow(unused_imports)] // Documentation
|
||||
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
|
||||
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.
|
||||
///
|
||||
|
|
@ -135,7 +128,6 @@ impl ComboBox {
|
|||
/// rect: egui::Rect,
|
||||
/// visuals: &egui::style::WidgetVisuals,
|
||||
/// _is_open: bool,
|
||||
/// _above_or_below: egui::AboveOrBelow,
|
||||
/// ) {
|
||||
/// let rect = egui::Rect::from_center_size(
|
||||
/// rect.center(),
|
||||
|
|
@ -154,10 +146,8 @@ impl ComboBox {
|
|||
/// .show_ui(ui, |_ui| {});
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn icon(
|
||||
mut self,
|
||||
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
|
||||
) -> Self {
|
||||
#[inline]
|
||||
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self {
|
||||
self.icon = Some(Box::new(icon_fn));
|
||||
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 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 close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick);
|
||||
|
|
@ -385,15 +359,9 @@ fn combo_box_dyn<'c, R>(
|
|||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
is_popup_open,
|
||||
above_or_below,
|
||||
);
|
||||
} else {
|
||||
paint_default_icon(
|
||||
ui.painter(),
|
||||
icon_rect.expand(visuals.expansion),
|
||||
visuals,
|
||||
above_or_below,
|
||||
);
|
||||
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||
}
|
||||
|
||||
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 inner = crate::popup::popup_above_or_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
&button_response,
|
||||
above_or_below,
|
||||
close_behavior,
|
||||
|ui| {
|
||||
let inner = Popup::menu(&button_response)
|
||||
.id(popup_id)
|
||||
.width(button_response.rect.width())
|
||||
.close_behavior(close_behavior)
|
||||
.show(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
|
||||
ScrollArea::vertical()
|
||||
.max_height(height)
|
||||
.show(ui, |ui| {
|
||||
|
|
@ -427,8 +391,8 @@ fn combo_box_dyn<'c, R>(
|
|||
menu_contents(ui)
|
||||
})
|
||||
.inner
|
||||
},
|
||||
);
|
||||
})
|
||||
.map(|r| r.inner);
|
||||
|
||||
InnerResponse {
|
||||
inner,
|
||||
|
|
@ -484,33 +448,19 @@ fn button_frame(
|
|||
response
|
||||
}
|
||||
|
||||
fn paint_default_icon(
|
||||
painter: &Painter,
|
||||
rect: Rect,
|
||||
visuals: &WidgetVisuals,
|
||||
above_or_below: AboveOrBelow,
|
||||
) {
|
||||
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||
let rect = Rect::from_center_size(
|
||||
rect.center(),
|
||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||
);
|
||||
|
||||
match above_or_below {
|
||||
AboveOrBelow::Above => {
|
||||
// Upward pointing triangle
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Downward pointing triangle
|
||||
// Previously, we would show an up arrow when we expected the popup to open upwards
|
||||
// (due to lack of space below the button), but this could look weird in edge cases, so this
|
||||
// feature was removed. (See https://github.com/emilk/egui/pull/5713#issuecomment-2654420245)
|
||||
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;
|
||||
pub mod frame;
|
||||
pub mod modal;
|
||||
pub mod old_popup;
|
||||
pub mod panel;
|
||||
pub mod popup;
|
||||
mod popup;
|
||||
pub(crate) mod resize;
|
||||
mod scene;
|
||||
pub mod scroll_area;
|
||||
mod sides;
|
||||
mod tooltip;
|
||||
pub(crate) mod window;
|
||||
|
||||
pub use {
|
||||
|
|
@ -21,11 +23,13 @@ pub use {
|
|||
combo_box::*,
|
||||
frame::Frame,
|
||||
modal::{Modal, ModalResponse},
|
||||
old_popup::*,
|
||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||
popup::*,
|
||||
resize::Resize,
|
||||
scene::Scene,
|
||||
scroll_area::ScrollArea,
|
||||
sides::Sides,
|
||||
tooltip::*,
|
||||
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::{
|
||||
pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id,
|
||||
InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2,
|
||||
Widget, WidgetText,
|
||||
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
|
||||
Sense, Ui, UiKind,
|
||||
};
|
||||
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 {
|
||||
Id::new("when_was_a_toolip_last_shown")
|
||||
/// Show the popup relative to the mouse pointer.
|
||||
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 {
|
||||
let when_was_a_toolip_last_shown =
|
||||
ctx.data(|d| d.get_temp::<f64>(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
|
||||
impl From<Rect> for PopupAnchor {
|
||||
fn from(rect: Rect) -> Self {
|
||||
Self::ParentRect(rect)
|
||||
}
|
||||
}
|
||||
|
||||
fn remember_that_tooltip_was_shown(ctx: &Context) {
|
||||
let now = ctx.input(|i| i.time);
|
||||
ctx.data_mut(|data| data.insert_temp::<f64>(when_was_a_toolip_last_shown_id(), now));
|
||||
impl From<Pos2> for PopupAnchor {
|
||||
fn from(pos: Pos2) -> Self {
|
||||
Self::Position(pos)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// 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(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;
|
||||
impl From<&Response> for PopupAnchor {
|
||||
fn from(response: &Response) -> Self {
|
||||
let mut widget_rect = response.rect;
|
||||
if let Some(to_global) = response.ctx.layer_transform_to_global(response.layer_id) {
|
||||
widget_rect = to_global * 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;
|
||||
Self::ParentRect(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?
|
||||
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)
|
||||
});
|
||||
tooltip_id(widget_id, tooltip_count)
|
||||
}
|
||||
|
||||
pub fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
|
||||
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,
|
||||
);
|
||||
impl PopupAnchor {
|
||||
/// Get the rect the popup should be shown relative to.
|
||||
/// Returns `Rect::from_pos` for [`PopupAnchor::Pointer`], [`PopupAnchor::PointerFixed`]
|
||||
/// and [`PopupAnchor::Position`] (so the rect will be zero-sized).
|
||||
pub fn rect(self, popup_id: Id, ctx: &Context) -> Option<Rect> {
|
||||
match self {
|
||||
Self::ParentRect(rect) => Some(rect),
|
||||
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
|
||||
Self::PointerFixed => ctx
|
||||
.memory(|mem| mem.popup_position(popup_id))
|
||||
.map(Rect::from_pos),
|
||||
Self::Position(pos) => Some(Rect::from_pos(pos)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PopupCloseBehavior {
|
||||
/// Popup will be closed on click anywhere, inside or outside the popup.
|
||||
///
|
||||
|
|
@ -344,114 +85,480 @@ pub enum PopupCloseBehavior {
|
|||
IgnoreClicks,
|
||||
}
|
||||
|
||||
/// Helper for [`popup_above_or_below_widget`].
|
||||
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,
|
||||
)
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SetOpenCommand {
|
||||
/// Set the open state to the given value
|
||||
Bool(bool),
|
||||
|
||||
/// Toggle the open state
|
||||
Toggle,
|
||||
}
|
||||
|
||||
/// 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::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;
|
||||
impl From<bool> for SetOpenCommand {
|
||||
fn from(b: bool) -> Self {
|
||||
Self::Bool(b)
|
||||
}
|
||||
}
|
||||
|
||||
let (mut pos, pivot) = match above_or_below {
|
||||
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
|
||||
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
|
||||
};
|
||||
/// How do we determine if the popup should be open or closed
|
||||
enum OpenKind<'a> {
|
||||
/// Always open
|
||||
Open,
|
||||
|
||||
if let Some(to_global) = parent_ui
|
||||
.ctx()
|
||||
.layer_transform_to_global(parent_ui.layer_id())
|
||||
{
|
||||
pos = to_global * pos;
|
||||
}
|
||||
/// Always closed
|
||||
Closed,
|
||||
|
||||
let frame = Frame::popup(parent_ui.style());
|
||||
let frame_margin = frame.total_margin();
|
||||
let inner_width = (widget_response.rect.width() - frame_margin.sum().x).max(0.0);
|
||||
/// Open if the bool is true
|
||||
Bool(&'a mut bool, PopupCloseBehavior),
|
||||
|
||||
parent_ui.ctx().pass_state_mut(|fs| {
|
||||
fs.layers
|
||||
.entry(parent_ui.layer_id())
|
||||
.or_default()
|
||||
.open_popups
|
||||
.insert(popup_id)
|
||||
});
|
||||
/// Store the open state via [`crate::Memory`]
|
||||
Memory {
|
||||
set: Option<SetOpenCommand>,
|
||||
close_behavior: PopupCloseBehavior,
|
||||
},
|
||||
}
|
||||
|
||||
let response = Area::new(popup_id)
|
||||
.kind(UiKind::Popup)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(pos)
|
||||
.default_width(inner_width)
|
||||
.pivot(pivot)
|
||||
.show(parent_ui.ctx(), |ui| {
|
||||
frame
|
||||
.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()
|
||||
impl<'a> OpenKind<'a> {
|
||||
/// Returns `true` if the popup should be open
|
||||
fn is_open(&self, id: Id, ctx: &Context) -> bool {
|
||||
match self {
|
||||
OpenKind::Open => true,
|
||||
OpenKind::Closed => false,
|
||||
OpenKind::Bool(open, _) => **open,
|
||||
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
|
||||
}
|
||||
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::{Color32, Rgba};
|
||||
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::{
|
||||
mutex,
|
||||
|
|
|
|||
|
|
@ -89,8 +89,12 @@ pub struct Memory {
|
|||
|
||||
/// Which popup-window is open (if any)?
|
||||
/// 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))]
|
||||
popup: Option<Id>,
|
||||
popup: Option<(Id, Option<Pos2>)>,
|
||||
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
everything_is_visible: bool,
|
||||
|
|
@ -1070,7 +1074,7 @@ impl Memory {
|
|||
impl Memory {
|
||||
/// Is the given popup open?
|
||||
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?
|
||||
|
|
@ -1080,7 +1084,18 @@ impl Memory {
|
|||
|
||||
/// Open the given popup and close all others.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc};
|
|||
|
||||
use crate::{
|
||||
emath::{Align, Pos2, Rect, Vec2},
|
||||
menu, pass_state, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui,
|
||||
WidgetRect, WidgetText,
|
||||
menu, pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense,
|
||||
Tooltip, Ui, WidgetRect, WidgetText,
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -550,36 +550,22 @@ impl Response {
|
|||
/// ```
|
||||
#[doc(alias = "tooltip")]
|
||||
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||
if self.flags.contains(Flags::ENABLED) && self.should_show_hover_ui() {
|
||||
self.show_tooltip_ui(add_contents);
|
||||
}
|
||||
Tooltip::for_enabled(&self).show(add_contents);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show this UI when hovering if the widget is disabled.
|
||||
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
|
||||
if !self.enabled() && self.should_show_hover_ui() {
|
||||
crate::containers::show_tooltip_for(
|
||||
&self.ctx,
|
||||
self.layer_id,
|
||||
self.id,
|
||||
&self.rect,
|
||||
add_contents,
|
||||
);
|
||||
}
|
||||
Tooltip::for_disabled(&self).show(add_contents);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if self.enabled() && self.should_show_hover_ui() {
|
||||
crate::containers::show_tooltip_at_pointer(
|
||||
&self.ctx,
|
||||
self.layer_id,
|
||||
self.id,
|
||||
add_contents,
|
||||
);
|
||||
}
|
||||
Tooltip::for_enabled(&self)
|
||||
.at_pointer()
|
||||
.gap(12.0)
|
||||
.show(add_contents);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -587,13 +573,9 @@ impl Response {
|
|||
///
|
||||
/// 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)) {
|
||||
crate::containers::show_tooltip_for(
|
||||
&self.ctx,
|
||||
self.layer_id,
|
||||
self.id,
|
||||
&self.rect,
|
||||
add_contents,
|
||||
);
|
||||
Popup::from_response(self)
|
||||
.kind(PopupKind::Tooltip)
|
||||
.show(add_contents);
|
||||
}
|
||||
|
||||
/// 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?
|
||||
pub fn is_tooltip_open(&self) -> bool {
|
||||
crate::popup::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
|
||||
Tooltip::was_tooltip_open_last_frame(&self.ctx, self.id)
|
||||
}
|
||||
|
||||
/// Like `on_hover_text`, but show the text next to cursor.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use egui::{ComboBox, Popup};
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {}
|
||||
|
|
@ -32,6 +34,20 @@ impl crate::View for ContextMenus {
|
|||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let response = ui.button("New menu");
|
||||
Popup::menu(&response).show(Self::nested_menus);
|
||||
|
||||
let response = ui.button("New context menu");
|
||||
Popup::context_menu(&response).show(Self::nested_menus);
|
||||
|
||||
ComboBox::new("Hi", "Hi").show_ui(ui, |ui| {
|
||||
_ = ui.selectable_label(false, "I have some long text that should be wrapped");
|
||||
_ = ui.selectable_label(false, "Short");
|
||||
_ = ui.selectable_label(false, "Medium length");
|
||||
});
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
|
@ -51,6 +67,7 @@ impl ContextMenus {
|
|||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
ui.menu_button("Recursive", Self::nested_menus)
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open…").clicked() {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ impl Default for DemoGroups {
|
|||
Box::<super::multi_touch::MultiTouch>::default(),
|
||||
Box::<super::painting::Painting>::default(),
|
||||
Box::<super::panels::Panels>::default(),
|
||||
Box::<super::popups::PopupsDemo>::default(),
|
||||
Box::<super::scene::SceneDemo>::default(),
|
||||
Box::<super::screenshot::Screenshot>::default(),
|
||||
Box::<super::scrolling::Scrolling>::default(),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub mod paint_bezier;
|
|||
pub mod painting;
|
||||
pub mod panels;
|
||||
pub mod password;
|
||||
mod popups;
|
||||
pub mod scene;
|
||||
pub mod screenshot;
|
||||
pub mod scrolling;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
use egui::{vec2, Align2, ComboBox, Frame, Id, Popup, PopupCloseBehavior, RectAlign, Tooltip, Ui};
|
||||
|
||||
/// Showcase [`Popup`].
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct PopupsDemo {
|
||||
align4: RectAlign,
|
||||
gap: f32,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
close_behavior: PopupCloseBehavior,
|
||||
popup_open: bool,
|
||||
}
|
||||
|
||||
impl PopupsDemo {
|
||||
fn apply_options<'a>(&self, popup: Popup<'a>) -> Popup<'a> {
|
||||
popup
|
||||
.align(self.align4)
|
||||
.gap(self.gap)
|
||||
.close_behavior(self.close_behavior)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PopupsDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
align4: RectAlign::default(),
|
||||
gap: 4.0,
|
||||
close_behavior: PopupCloseBehavior::CloseOnClick,
|
||||
popup_open: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Demo for PopupsDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"\u{2755} Popups"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.default_width(250.0)
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for PopupsDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.style_mut().spacing.item_spacing.x = 0.0;
|
||||
let align_combobox = |ui: &mut Ui, label: &str, align: &mut Align2| {
|
||||
let aligns = [
|
||||
(Align2::LEFT_TOP, "Left top"),
|
||||
(Align2::LEFT_CENTER, "Left center"),
|
||||
(Align2::LEFT_BOTTOM, "Left bottom"),
|
||||
(Align2::CENTER_TOP, "Center top"),
|
||||
(Align2::CENTER_CENTER, "Center center"),
|
||||
(Align2::CENTER_BOTTOM, "Center bottom"),
|
||||
(Align2::RIGHT_TOP, "Right top"),
|
||||
(Align2::RIGHT_CENTER, "Right center"),
|
||||
(Align2::RIGHT_BOTTOM, "Right bottom"),
|
||||
];
|
||||
|
||||
ui.label(label);
|
||||
ComboBox::new(label, "")
|
||||
.selected_text(aligns.iter().find(|(a, _)| a == align).unwrap().1)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align2, name) in &aligns {
|
||||
ui.selectable_value(align, *align2, *name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ui.label("Align4(");
|
||||
align_combobox(ui, "parent: ", &mut self.align4.parent);
|
||||
ui.label(", ");
|
||||
align_combobox(ui, "child: ", &mut self.align4.child);
|
||||
ui.label(") ");
|
||||
|
||||
let presets = [
|
||||
(RectAlign::TOP_START, "Top start"),
|
||||
(RectAlign::TOP, "Top"),
|
||||
(RectAlign::TOP_END, "Top end"),
|
||||
(RectAlign::RIGHT_START, "Right start"),
|
||||
(RectAlign::RIGHT, "Right Center"),
|
||||
(RectAlign::RIGHT_END, "Right end"),
|
||||
(RectAlign::BOTTOM_START, "Bottom start"),
|
||||
(RectAlign::BOTTOM, "Bottom"),
|
||||
(RectAlign::BOTTOM_END, "Bottom end"),
|
||||
(RectAlign::LEFT_START, "Left start"),
|
||||
(RectAlign::LEFT, "Left"),
|
||||
(RectAlign::LEFT_END, "Left end"),
|
||||
];
|
||||
|
||||
ui.label(" Presets: ");
|
||||
|
||||
ComboBox::new("Preset", "")
|
||||
.selected_text(
|
||||
presets
|
||||
.iter()
|
||||
.find(|(a, _)| a == &self.align4)
|
||||
.map_or("Select", |(_, name)| *name),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (align4, name) in &presets {
|
||||
ui.selectable_value(&mut self.align4, *align4, *name);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Gap:");
|
||||
ui.add(egui::DragValue::new(&mut self.gap));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Close behavior:");
|
||||
ui.selectable_value(
|
||||
&mut self.close_behavior,
|
||||
PopupCloseBehavior::CloseOnClick,
|
||||
"Close on click",
|
||||
)
|
||||
.on_hover_text("Closes when the user clicks anywhere (inside or outside)");
|
||||
ui.selectable_value(
|
||||
&mut self.close_behavior,
|
||||
PopupCloseBehavior::CloseOnClickOutside,
|
||||
"Close on click outside",
|
||||
)
|
||||
.on_hover_text("Closes when the user clicks outside the popup");
|
||||
ui.selectable_value(
|
||||
&mut self.close_behavior,
|
||||
PopupCloseBehavior::IgnoreClicks,
|
||||
"Ignore clicks",
|
||||
)
|
||||
.on_hover_text("Close only when the button is clicked again");
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.popup_open, "Show popup");
|
||||
|
||||
let response = Frame::group(ui.style())
|
||||
.inner_margin(vec2(0.0, 25.0))
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| ui.button("Click, right-click and hover me!"))
|
||||
.inner
|
||||
})
|
||||
.inner;
|
||||
|
||||
self.apply_options(Popup::menu(&response).id(Id::new("menu")))
|
||||
.show(|ui| {
|
||||
_ = ui.button("Menu item 1");
|
||||
_ = ui.button("Menu item 2");
|
||||
});
|
||||
|
||||
self.apply_options(Popup::context_menu(&response).id(Id::new("context_menu")))
|
||||
.show(|ui| {
|
||||
_ = ui.button("Context menu item 1");
|
||||
_ = ui.button("Context menu item 2");
|
||||
});
|
||||
|
||||
if self.popup_open {
|
||||
self.apply_options(Popup::from_response(&response).id(Id::new("popup")))
|
||||
.show(|ui| {
|
||||
ui.label("Popup contents");
|
||||
});
|
||||
}
|
||||
|
||||
let mut tooltip = Tooltip::for_enabled(&response);
|
||||
tooltip.popup = self.apply_options(tooltip.popup);
|
||||
tooltip.show(|ui| {
|
||||
ui.label("Tooltips are popups, too!");
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,9 @@ impl Tooltips {
|
|||
ui.label("You can select this text.");
|
||||
});
|
||||
|
||||
ui.label("This tooltip shows at the mouse cursor.")
|
||||
.on_hover_text_at_pointer("Move me around!!");
|
||||
|
||||
ui.separator(); // ---------------------------------------------------------
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4cc8e0919fed5bd1ef981658626dba728435ab95da8ee96ced1fb4838d535ff
|
||||
size 11741
|
||||
oid sha256:eb2bc4a38f20ed0f5fced36e8e56936bee328b24a0a45127d5d3739d40331cb7
|
||||
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
|
||||
oid sha256:c80158ac9c823f94d2830d1423236ad441dc7da31e748b6815c69663fa2a03d0
|
||||
size 59662
|
||||
oid sha256:92b70683a685869274749d057de174896e18dae5cb67e70221c3efdb7106cdda
|
||||
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.
|
||||
///
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Give a position within the rect, specified by the aligns
|
||||
pub fn anchor_rect(self, rect: Rect) -> Rect {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ mod ordered_float;
|
|||
mod pos2;
|
||||
mod range;
|
||||
mod rect;
|
||||
mod rect_align;
|
||||
mod rect_transform;
|
||||
mod rot2;
|
||||
pub mod smart_aim;
|
||||
|
|
@ -50,6 +51,7 @@ pub use self::{
|
|||
pos2::*,
|
||||
range::Rangef,
|
||||
rect::*,
|
||||
rect_align::RectAlign,
|
||||
rect_transform::*,
|
||||
rot2::*,
|
||||
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
|
||||
#![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> {
|
||||
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");
|
||||
let response = ui.button("Open");
|
||||
let popup_id = Id::new("popup_id");
|
||||
|
||||
if response.clicked() {
|
||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
}
|
||||
|
||||
popup_below_widget(
|
||||
ui,
|
||||
popup_id,
|
||||
&response,
|
||||
PopupCloseBehavior::IgnoreClicks,
|
||||
|ui| {
|
||||
Popup::menu(&response)
|
||||
.close_behavior(PopupCloseBehavior::IgnoreClicks)
|
||||
.show(|ui| {
|
||||
ui.set_min_width(310.0);
|
||||
ui.label("This popup will be open until you press the button again");
|
||||
ui.checkbox(&mut self.checkbox, "Checkbox");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue