Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve tooltip positioning #4579

Merged
merged 2 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
279 changes: 110 additions & 169 deletions crates/egui/src/containers/popup.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,8 @@
//! Show popup windows, tooltips, context menus etc.

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));
}
use frame_state::PerWidgetTooltipState;

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));
}
}
use crate::*;

// ----------------------------------------------------------------------------

Expand Down Expand Up @@ -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.
Expand All @@ -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),
)
}
Expand All @@ -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> {
let spacing = 4.0;

) -> R {
// 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 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 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 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 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));
let screen_rect = ctx.screen_rect();

if above {
position.y -= expected_size.y;
}
let (pivot, anchor) = find_tooltip_position(
screen_rect,
state.bounding_rect,
allow_placing_below,
expected_tooltip_size,
);

position = position.at_most(ctx.screen_rect().max - expected_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
});

// check if we intersect the avoid_rect
{
let new_rect = Rect::from_min_size(position, expected_size);
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));

// 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);
}
}
}
inner
}

let position = position.at_least(ctx.screen_rect().min);
fn tooltip_id(widget_id: Id, tooltip_count: usize) -> Id {
widget_id.with(tooltip_count)
}

let area_id = frame_state.common_id.with(frame_state.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 InnerResponse { inner, response } =
show_tooltip_area_dyn(ctx, area_id, position, add_contents);
// 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,
);
}

long_state.set_individual_tooltip(
frame_state.common_id,
frame_state.count,
individual_id,
response.rect.size(),
);
long_state.store(ctx);
// 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,
);
}

frame_state.count += 1;
frame_state.rect = frame_state.rect.union(response.rect);
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
// 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,
);
}

Some(inner)
// 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).
Expand All @@ -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`].
Expand Down
Loading
Loading