Fix tooltips sometimes changing position each frame (#7304)

There was a bug in how we decide where to place a `Tooltip` (or other
`Popup`), which could lead to tooltips jumping around every frame,
especially if it changed size slightly.

The new code is simpler and bug-free.
This commit is contained in:
Emil Ernerfeldt 2025-07-07 12:02:01 +02:00 committed by GitHub
parent a811b975c2
commit 6d80707422
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 35 deletions

View File

@ -484,6 +484,7 @@ impl<'a> Popup<'a> {
self.gap,
expected_popup_size,
)
.unwrap_or_default()
}
/// Show the popup.

View File

@ -146,7 +146,7 @@ impl Align {
// ----------------------------------------------------------------------------
/// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Align2(pub [Align; 2]);
@ -298,3 +298,9 @@ impl std::ops::IndexMut<usize> for Align2 {
pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect {
Align2::CENTER_CENTER.align_size_within_rect(size, frame)
}
impl std::fmt::Debug for Align2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Align2({:?}, {:?})", self.x(), self.y())
}
}

View File

@ -7,9 +7,9 @@ use crate::{Align2, Pos2, Rect, Vec2};
///
/// There are helper constants for the 12 common menu positions:
/// ```text
/// ┌───────────┐ ┌────────┐ ┌─────────┐
/// │ TOP_START │ │ TOP │ │ TOP_END │
/// └───────────┘ └────────┘ └─────────┘
/// ┌───────────┐ ┌────────┐ ┌─────────┐
/// │ TOP_START │ │ TOP │ │ TOP_END │
/// └───────────┘ └────────┘ └─────────┘
/// ┌──────────┐ ┌────────────────────────────────────┐ ┌───────────┐
/// │LEFT_START│ │ │ │RIGHT_START│
/// └──────────┘ │ │ └───────────┘
@ -19,9 +19,9 @@ use crate::{Align2, Pos2, Rect, Vec2};
/// ┌──────────┐ │ │ ┌───────────┐
/// │ LEFT_END │ │ │ │ RIGHT_END │
/// └──────────┘ └────────────────────────────────────┘ └───────────┘
/// ┌────────────┐ ┌──────┐ ┌──────────┐
/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│
/// └────────────┘ └──────┘ └──────────┘
/// ┌────────────┐ ┌──────┐ ┌──────────┐
/// │BOTTOM_START│ │BOTTOM│ │BOTTOM_END│
/// └────────────┘ └──────┘ └──────────┘
/// ```
// There is no `new` function on purpose, since writing out `parent` and `child` is more
// reasonable.
@ -235,45 +235,34 @@ impl RectAlign {
[self.flip_x(), self.flip_y(), self.flip()]
}
/// Look for the [`RectAlign`] that fits best in the available space.
/// Look for the first alternative [`RectAlign`] that allows the child rect to fit
/// inside the `screen_rect`.
///
/// If no alternative fits, the first is returned.
/// If no alternatives are given, `None` is returned.
///
/// 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,
values_to_try: impl Iterator<Item = Self>,
screen_rect: 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;
expected_size: Vec2,
) -> Option<Self> {
let mut first_choice = None;
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;
first_choice = first_choice.or(Some(align)); // Remember the first alternative
let suggested_popup_rect = align.align_rect(&parent_rect, expected_size, gap);
if screen_rect.contains_rect(suggested_popup_rect) {
return Some(align);
}
}
best
first_choice
}
}