Improve tooltip positioning (#4579)
This simplifies and improves the tooltip positioning * Closes https://github.com/emilk/egui/issues/4568 ### For a follow-up PR * [ ] Test if it closes https://github.com/emilk/egui/issues/4471 * [ ] Add an API to close https://github.com/emilk/egui/issues/890
This commit is contained in:
parent
cc3b3629b8
commit
00396145d1
|
@ -525,20 +525,11 @@ impl Prepared {
|
|||
enabled: _,
|
||||
constrain: _,
|
||||
constrain_rect: _,
|
||||
sizing_pass,
|
||||
sizing_pass: _,
|
||||
} = self;
|
||||
|
||||
state.size = content_ui.min_size();
|
||||
|
||||
if sizing_pass {
|
||||
// If during the sizing pass we measure our width to `123.45` and
|
||||
// then try to wrap to exactly that next frame,
|
||||
// we may accidentally wrap the last letter of some text.
|
||||
// We only do this after the initial sizing pass though;
|
||||
// otherwise we could end up with for-ever expanding areas.
|
||||
state.size = state.size.ceil();
|
||||
}
|
||||
|
||||
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
|
||||
|
||||
move_response
|
||||
|
|
|
@ -1,52 +1,11 @@
|
|||
//! Show popup windows, tooltips, context menus etc.
|
||||
|
||||
use frame_state::PerWidgetTooltipState;
|
||||
|
||||
use crate::*;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Same state for all tooltips.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct TooltipState {
|
||||
last_common_id: Option<Id>,
|
||||
individual_ids_and_sizes: ahash::HashMap<usize, (Id, Vec2)>,
|
||||
}
|
||||
|
||||
impl TooltipState {
|
||||
pub fn load(ctx: &Context) -> Option<Self> {
|
||||
ctx.data_mut(|d| d.get_temp(Id::NULL))
|
||||
}
|
||||
|
||||
fn store(self, ctx: &Context) {
|
||||
ctx.data_mut(|d| d.insert_temp(Id::NULL, self));
|
||||
}
|
||||
|
||||
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
||||
if self.last_common_id == Some(common_id) {
|
||||
Some(self.individual_ids_and_sizes.get(&index)?.1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_individual_tooltip(
|
||||
&mut self,
|
||||
common_id: Id,
|
||||
index: usize,
|
||||
individual_id: Id,
|
||||
size: Vec2,
|
||||
) {
|
||||
if self.last_common_id != Some(common_id) {
|
||||
self.last_common_id = Some(common_id);
|
||||
self.individual_ids_and_sizes.clear();
|
||||
}
|
||||
|
||||
self.individual_ids_and_sizes
|
||||
.insert(index, (individual_id, size));
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Show a tooltip at the current pointer position (if any).
|
||||
///
|
||||
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
||||
|
@ -94,10 +53,8 @@ pub fn show_tooltip_at_pointer<R>(
|
|||
id: Id,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let suggested_pos = ctx
|
||||
.input(|i| i.pointer.hover_pos())
|
||||
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
|
||||
show_tooltip_at(ctx, id, suggested_pos, add_contents)
|
||||
ctx.input(|i| i.pointer.hover_pos())
|
||||
.map(|pointer_pos| show_tooltip_at(ctx, id, pointer_pos + vec2(16.0, 16.0), add_contents))
|
||||
}
|
||||
|
||||
/// Show a tooltip under the given area.
|
||||
|
@ -106,21 +63,16 @@ pub fn show_tooltip_at_pointer<R>(
|
|||
pub fn show_tooltip_for<R>(
|
||||
ctx: &Context,
|
||||
id: Id,
|
||||
rect: &Rect,
|
||||
widget_rect: &Rect,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
|
||||
let (above, position) = if ctx.input(|i| i.any_touches()) {
|
||||
(true, expanded_rect.left_top())
|
||||
} else {
|
||||
(false, expanded_rect.left_bottom())
|
||||
};
|
||||
) -> 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_avoid_dyn(
|
||||
ctx,
|
||||
id,
|
||||
Some(position),
|
||||
above,
|
||||
expanded_rect,
|
||||
allow_placing_below,
|
||||
widget_rect,
|
||||
Box::new(add_contents),
|
||||
)
|
||||
}
|
||||
|
@ -131,101 +83,119 @@ pub fn show_tooltip_for<R>(
|
|||
pub fn show_tooltip_at<R>(
|
||||
ctx: &Context,
|
||||
id: Id,
|
||||
suggested_position: Option<Pos2>,
|
||||
suggested_position: Pos2,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let above = false;
|
||||
show_tooltip_at_avoid_dyn(
|
||||
ctx,
|
||||
id,
|
||||
suggested_position,
|
||||
above,
|
||||
Rect::NOTHING,
|
||||
Box::new(add_contents),
|
||||
)
|
||||
) -> R {
|
||||
let allow_placing_below = true;
|
||||
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
|
||||
show_tooltip_at_avoid_dyn(ctx, id, allow_placing_below, &rect, Box::new(add_contents))
|
||||
}
|
||||
|
||||
fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
ctx: &Context,
|
||||
individual_id: Id,
|
||||
suggested_position: Option<Pos2>,
|
||||
above: bool,
|
||||
mut avoid_rect: Rect,
|
||||
widget_id: Id,
|
||||
allow_placing_below: bool,
|
||||
widget_rect: &Rect,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> Option<R> {
|
||||
) -> R {
|
||||
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
||||
let mut state = ctx.frame_state(|fs| {
|
||||
fs.tooltip_state
|
||||
.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).map_or(vec2(64.0, 32.0), |area| area.size);
|
||||
|
||||
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)
|
||||
.order(Order::Tooltip)
|
||||
.pivot(pivot)
|
||||
.fixed_pos(anchor)
|
||||
.default_width(ctx.style().spacing.tooltip_width)
|
||||
.constrain_to(screen_rect)
|
||||
.interactable(false)
|
||||
.show(ctx, |ui| {
|
||||
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
|
||||
});
|
||||
|
||||
state.tooltip_count += 1;
|
||||
state.bounding_rect = state.bounding_rect.union(response.rect);
|
||||
ctx.frame_state_mut(|fs| fs.tooltip_state.widget_tooltips.insert(widget_id, state));
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
||||
let mut frame_state =
|
||||
ctx.frame_state(|fs| fs.tooltip_state)
|
||||
.unwrap_or(crate::frame_state::TooltipFrameState {
|
||||
common_id: individual_id,
|
||||
rect: Rect::NOTHING,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
let mut position = if frame_state.rect.is_positive() {
|
||||
avoid_rect = avoid_rect.union(frame_state.rect);
|
||||
if above {
|
||||
frame_state.rect.left_top() - spacing * Vec2::Y
|
||||
} else {
|
||||
frame_state.rect.left_bottom() + spacing * Vec2::Y
|
||||
}
|
||||
} else if let Some(position) = suggested_position {
|
||||
position
|
||||
} else if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
Pos2::ZERO
|
||||
} else {
|
||||
return None; // No good place for a tooltip :(
|
||||
};
|
||||
|
||||
let mut long_state = TooltipState::load(ctx).unwrap_or_default();
|
||||
let expected_size =
|
||||
long_state.individual_tooltip_size(frame_state.common_id, frame_state.count);
|
||||
let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0));
|
||||
|
||||
if above {
|
||||
position.y -= expected_size.y;
|
||||
}
|
||||
|
||||
position = position.at_most(ctx.screen_rect().max - expected_size);
|
||||
|
||||
// check if we intersect the avoid_rect
|
||||
// Does it fit below?
|
||||
if allow_placing_below
|
||||
&& widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
|
||||
{
|
||||
let new_rect = Rect::from_min_size(position, expected_size);
|
||||
|
||||
// Note: We use shrink so that we don't get false positives when the rects just touch
|
||||
if new_rect.shrink(1.0).intersects(avoid_rect) {
|
||||
if above {
|
||||
// place below instead:
|
||||
position = avoid_rect.left_bottom() + spacing * Vec2::Y;
|
||||
} else {
|
||||
// place above instead:
|
||||
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
|
||||
}
|
||||
}
|
||||
return (
|
||||
Align2::LEFT_TOP,
|
||||
widget_rect.left_bottom() + spacing * Vec2::DOWN,
|
||||
);
|
||||
}
|
||||
|
||||
let position = position.at_least(ctx.screen_rect().min);
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
let area_id = frame_state.common_id.with(frame_state.count);
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
let InnerResponse { inner, response } =
|
||||
show_tooltip_area_dyn(ctx, area_id, position, add_contents);
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
long_state.set_individual_tooltip(
|
||||
frame_state.common_id,
|
||||
frame_state.count,
|
||||
individual_id,
|
||||
response.rect.size(),
|
||||
);
|
||||
long_state.store(ctx);
|
||||
// It doesn't fit anywhere :(
|
||||
|
||||
frame_state.count += 1;
|
||||
frame_state.rect = frame_state.rect.union(response.rect);
|
||||
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
|
||||
|
||||
Some(inner)
|
||||
// Just show it anyway:
|
||||
(Align2::LEFT_TOP, screen_rect.left_top())
|
||||
}
|
||||
|
||||
/// Show some text at the current pointer position (if any).
|
||||
|
@ -249,42 +219,13 @@ pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) ->
|
|||
})
|
||||
}
|
||||
|
||||
/// Show a pop-over window.
|
||||
fn show_tooltip_area_dyn<'c, R>(
|
||||
ctx: &Context,
|
||||
area_id: Id,
|
||||
window_pos: Pos2,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> InnerResponse<R> {
|
||||
use containers::*;
|
||||
Area::new(area_id)
|
||||
.order(Order::Tooltip)
|
||||
.fixed_pos(window_pos)
|
||||
.default_width(ctx.style().spacing.tooltip_width)
|
||||
.constrain_to(ctx.screen_rect())
|
||||
.interactable(false)
|
||||
.show(ctx, |ui| {
|
||||
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
|
||||
})
|
||||
}
|
||||
|
||||
/// Was this popup visible last frame?
|
||||
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
||||
if let Some(state) = TooltipState::load(ctx) {
|
||||
if let Some(common_id) = state.last_common_id {
|
||||
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
|
||||
if *individual_id == tooltip_id {
|
||||
let area_id = common_id.with(count);
|
||||
let layer_id = LayerId::new(Order::Tooltip, area_id);
|
||||
if ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper for [`popup_above_or_below_widget`].
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
use crate::{id::IdSet, *};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct TooltipFrameState {
|
||||
pub common_id: Id,
|
||||
pub rect: Rect,
|
||||
pub count: usize,
|
||||
pub widget_tooltips: IdMap<PerWidgetTooltipState>,
|
||||
}
|
||||
|
||||
impl TooltipFrameState {
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.widget_tooltips.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct PerWidgetTooltipState {
|
||||
/// Bounding rectangle for all widget and all previous tooltips.
|
||||
pub bounding_rect: Rect,
|
||||
|
||||
/// How many tooltips have been shown for this widget this frame?
|
||||
pub tooltip_count: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
|
@ -35,8 +48,8 @@ pub(crate) struct FrameState {
|
|||
|
||||
/// If a tooltip has been shown this frame, where was it?
|
||||
/// This is used to prevent multiple tooltips to cover each other.
|
||||
/// Initialized to `None` at the start of each frame.
|
||||
pub(crate) tooltip_state: Option<TooltipFrameState>,
|
||||
/// Reset at the start of each frame.
|
||||
pub(crate) tooltip_state: TooltipFrameState,
|
||||
|
||||
/// The current scroll area should scroll to this range (horizontal, vertical).
|
||||
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
|
||||
|
@ -72,7 +85,7 @@ impl Default for FrameState {
|
|||
available_rect: Rect::NAN,
|
||||
unused_rect: Rect::NAN,
|
||||
used_by_panels: Rect::NAN,
|
||||
tooltip_state: None,
|
||||
tooltip_state: Default::default(),
|
||||
scroll_target: [None, None],
|
||||
scroll_delta: Vec2::default(),
|
||||
#[cfg(feature = "accesskit")]
|
||||
|
@ -110,7 +123,7 @@ impl FrameState {
|
|||
*available_rect = screen_rect;
|
||||
*unused_rect = screen_rect;
|
||||
*used_by_panels = Rect::NOTHING;
|
||||
*tooltip_state = None;
|
||||
tooltip_state.clear();
|
||||
*scroll_target = [None, None];
|
||||
*scroll_delta = Vec2::default();
|
||||
|
||||
|
|
Loading…
Reference in New Issue