mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Add Popup and Tooltip, unifying the previous behaviours (#5713)
This introduces new `Tooltip` and `Popup` structs that unify and extend the old popups and tooltips. `Popup` handles the positioning and optionally stores state on whether the popup is open (for click based popups like `ComboBox`, menus, context menus). `Tooltip` is based on `Popup` and handles state of whether the tooltip should be shown (which turns out to be quite complex to handles all the edge cases). Both `Popup` and `Tooltip` can easily be constructed from a `Response` and then customized via builder methods. This also introduces `PositionAlign`, for aligning something outside of a `Rect` (in contrast to `Align2` for aligning inside a `Rect`). But I don't like the name, any suggestions? Inspired by [mui's tooltip positioning](https://mui.com/material-ui/react-tooltip/#positioned-tooltips). * Part of #4607 * [x] I have followed the instructions in the PR template TODOs: - [x] Automatic tooltip positioning based on available space - [x] Review / fix / remove all code TODOs - [x] ~Update the helper fns on `Response` to be consistent in naming and parameters (Some use tooltip, some hover_ui, some take &self, some take self)~ actually, I think the naming and parameter make sense on second thought - [x] Make sure all old code is marked deprecated For discussion during review: - the following check in `show_tooltip_for` still necessary?: ```rust let is_touch_screen = ctx.input(|i| i.any_touches()); let allow_placing_below = !is_touch_screen; // There is a finger below. TODO: Needed? ```
This commit is contained in:
@@ -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::*,
|
||||
|
||||
279
crates/emath/src/rect_align.rs
Normal file
279
crates/emath/src/rect_align.rs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user