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: _,
|
enabled: _,
|
||||||
constrain: _,
|
constrain: _,
|
||||||
constrain_rect: _,
|
constrain_rect: _,
|
||||||
sizing_pass,
|
sizing_pass: _,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
state.size = content_ui.min_size();
|
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));
|
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
|
||||||
|
|
||||||
move_response
|
move_response
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,11 @@
|
||||||
//! Show popup windows, tooltips, context menus etc.
|
//! Show popup windows, tooltips, context menus etc.
|
||||||
|
|
||||||
|
use frame_state::PerWidgetTooltipState;
|
||||||
|
|
||||||
use crate::*;
|
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).
|
/// Show a tooltip at the current pointer position (if any).
|
||||||
///
|
///
|
||||||
/// Most of the time it is easier to use [`Response::on_hover_ui`].
|
/// 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,
|
id: Id,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
let suggested_pos = ctx
|
ctx.input(|i| i.pointer.hover_pos())
|
||||||
.input(|i| i.pointer.hover_pos())
|
.map(|pointer_pos| show_tooltip_at(ctx, id, pointer_pos + vec2(16.0, 16.0), add_contents))
|
||||||
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
|
|
||||||
show_tooltip_at(ctx, id, suggested_pos, add_contents)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a tooltip under the given area.
|
/// Show a tooltip under the given area.
|
||||||
|
|
@ -106,21 +63,16 @@ pub fn show_tooltip_at_pointer<R>(
|
||||||
pub fn show_tooltip_for<R>(
|
pub fn show_tooltip_for<R>(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
id: Id,
|
id: Id,
|
||||||
rect: &Rect,
|
widget_rect: &Rect,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> R {
|
||||||
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
|
let is_touch_screen = ctx.input(|i| i.any_touches());
|
||||||
let (above, position) = if ctx.input(|i| i.any_touches()) {
|
let allow_placing_below = !is_touch_screen; // There is a finger below.
|
||||||
(true, expanded_rect.left_top())
|
|
||||||
} else {
|
|
||||||
(false, expanded_rect.left_bottom())
|
|
||||||
};
|
|
||||||
show_tooltip_at_avoid_dyn(
|
show_tooltip_at_avoid_dyn(
|
||||||
ctx,
|
ctx,
|
||||||
id,
|
id,
|
||||||
Some(position),
|
allow_placing_below,
|
||||||
above,
|
widget_rect,
|
||||||
expanded_rect,
|
|
||||||
Box::new(add_contents),
|
Box::new(add_contents),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -131,101 +83,119 @@ pub fn show_tooltip_for<R>(
|
||||||
pub fn show_tooltip_at<R>(
|
pub fn show_tooltip_at<R>(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
id: Id,
|
id: Id,
|
||||||
suggested_position: Option<Pos2>,
|
suggested_position: Pos2,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> R {
|
||||||
let above = false;
|
let allow_placing_below = true;
|
||||||
show_tooltip_at_avoid_dyn(
|
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
|
||||||
ctx,
|
show_tooltip_at_avoid_dyn(ctx, id, allow_placing_below, &rect, Box::new(add_contents))
|
||||||
id,
|
|
||||||
suggested_position,
|
|
||||||
above,
|
|
||||||
Rect::NOTHING,
|
|
||||||
Box::new(add_contents),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_tooltip_at_avoid_dyn<'c, R>(
|
fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
individual_id: Id,
|
widget_id: Id,
|
||||||
suggested_position: Option<Pos2>,
|
allow_placing_below: bool,
|
||||||
above: bool,
|
widget_rect: &Rect,
|
||||||
mut avoid_rect: Rect,
|
|
||||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
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;
|
let spacing = 4.0;
|
||||||
|
|
||||||
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
// Does it fit below?
|
||||||
let mut frame_state =
|
if allow_placing_below
|
||||||
ctx.frame_state(|fs| fs.tooltip_state)
|
&& widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
|
||||||
.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
|
|
||||||
{
|
{
|
||||||
let new_rect = Rect::from_min_size(position, expected_size);
|
return (
|
||||||
|
Align2::LEFT_TOP,
|
||||||
// Note: We use shrink so that we don't get false positives when the rects just touch
|
widget_rect.left_bottom() + spacing * Vec2::DOWN,
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 } =
|
// Does it fit to the left?
|
||||||
show_tooltip_area_dyn(ctx, area_id, position, add_contents);
|
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(
|
// It doesn't fit anywhere :(
|
||||||
frame_state.common_id,
|
|
||||||
frame_state.count,
|
|
||||||
individual_id,
|
|
||||||
response.rect.size(),
|
|
||||||
);
|
|
||||||
long_state.store(ctx);
|
|
||||||
|
|
||||||
frame_state.count += 1;
|
// Just show it anyway:
|
||||||
frame_state.rect = frame_state.rect.union(response.rect);
|
(Align2::LEFT_TOP, screen_rect.left_top())
|
||||||
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
|
|
||||||
|
|
||||||
Some(inner)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show some text at the current pointer position (if any).
|
/// 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?
|
/// Was this popup visible last frame?
|
||||||
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
|
||||||
if let Some(state) = TooltipState::load(ctx) {
|
let primary_tooltip_area_id = tooltip_id(widget_id, 0);
|
||||||
if let Some(common_id) = state.last_common_id {
|
ctx.memory(|mem| {
|
||||||
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
|
mem.areas()
|
||||||
if *individual_id == tooltip_id {
|
.visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper for [`popup_above_or_below_widget`].
|
/// Helper for [`popup_above_or_below_widget`].
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
use crate::{id::IdSet, *};
|
use crate::{id::IdSet, *};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct TooltipFrameState {
|
pub(crate) struct TooltipFrameState {
|
||||||
pub common_id: Id,
|
pub widget_tooltips: IdMap<PerWidgetTooltipState>,
|
||||||
pub rect: Rect,
|
}
|
||||||
pub count: usize,
|
|
||||||
|
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")]
|
#[cfg(feature = "accesskit")]
|
||||||
|
|
@ -35,8 +48,8 @@ pub(crate) struct FrameState {
|
||||||
|
|
||||||
/// If a tooltip has been shown this frame, where was it?
|
/// If a tooltip has been shown this frame, where was it?
|
||||||
/// This is used to prevent multiple tooltips to cover each other.
|
/// This is used to prevent multiple tooltips to cover each other.
|
||||||
/// Initialized to `None` at the start of each frame.
|
/// Reset at the start of each frame.
|
||||||
pub(crate) tooltip_state: Option<TooltipFrameState>,
|
pub(crate) tooltip_state: TooltipFrameState,
|
||||||
|
|
||||||
/// The current scroll area should scroll to this range (horizontal, vertical).
|
/// The current scroll area should scroll to this range (horizontal, vertical).
|
||||||
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
|
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
|
||||||
|
|
@ -72,7 +85,7 @@ impl Default for FrameState {
|
||||||
available_rect: Rect::NAN,
|
available_rect: Rect::NAN,
|
||||||
unused_rect: Rect::NAN,
|
unused_rect: Rect::NAN,
|
||||||
used_by_panels: Rect::NAN,
|
used_by_panels: Rect::NAN,
|
||||||
tooltip_state: None,
|
tooltip_state: Default::default(),
|
||||||
scroll_target: [None, None],
|
scroll_target: [None, None],
|
||||||
scroll_delta: Vec2::default(),
|
scroll_delta: Vec2::default(),
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
|
|
@ -110,7 +123,7 @@ impl FrameState {
|
||||||
*available_rect = screen_rect;
|
*available_rect = screen_rect;
|
||||||
*unused_rect = screen_rect;
|
*unused_rect = screen_rect;
|
||||||
*used_by_panels = Rect::NOTHING;
|
*used_by_panels = Rect::NOTHING;
|
||||||
*tooltip_state = None;
|
tooltip_state.clear();
|
||||||
*scroll_target = [None, None];
|
*scroll_target = [None, None];
|
||||||
*scroll_delta = Vec2::default();
|
*scroll_delta = Vec2::default();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue