Skip to content

Commit 995eddb

Browse files
emilkhacknus
authored andcommitted
Improve tooltip positioning (emilk#4579)
This simplifies and improves the tooltip positioning * Closes emilk#4568 ### For a follow-up PR * [ ] Test if it closes emilk#4471 * [ ] Add an API to close emilk#890
1 parent 5a0d924 commit 995eddb

File tree

3 files changed

+132
-187
lines changed

3 files changed

+132
-187
lines changed

crates/egui/src/containers/area.rs

+1-10
Original file line numberDiff line numberDiff line change
@@ -525,20 +525,11 @@ impl Prepared {
525525
enabled: _,
526526
constrain: _,
527527
constrain_rect: _,
528-
sizing_pass,
528+
sizing_pass: _,
529529
} = self;
530530

531531
state.size = content_ui.min_size();
532532

533-
if sizing_pass {
534-
// If during the sizing pass we measure our width to `123.45` and
535-
// then try to wrap to exactly that next frame,
536-
// we may accidentally wrap the last letter of some text.
537-
// We only do this after the initial sizing pass though;
538-
// otherwise we could end up with for-ever expanding areas.
539-
state.size = state.size.ceil();
540-
}
541-
542533
ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
543534

544535
move_response

crates/egui/src/containers/popup.rs

+110-169
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,8 @@
11
//! Show popup windows, tooltips, context menus etc.
22
3-
use crate::*;
4-
5-
// ----------------------------------------------------------------------------
6-
7-
/// Same state for all tooltips.
8-
#[derive(Clone, Debug, Default)]
9-
pub(crate) struct TooltipState {
10-
last_common_id: Option<Id>,
11-
individual_ids_and_sizes: ahash::HashMap<usize, (Id, Vec2)>,
12-
}
13-
14-
impl TooltipState {
15-
pub fn load(ctx: &Context) -> Option<Self> {
16-
ctx.data_mut(|d| d.get_temp(Id::NULL))
17-
}
18-
19-
fn store(self, ctx: &Context) {
20-
ctx.data_mut(|d| d.insert_temp(Id::NULL, self));
21-
}
3+
use frame_state::PerWidgetTooltipState;
224

23-
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
24-
if self.last_common_id == Some(common_id) {
25-
Some(self.individual_ids_and_sizes.get(&index)?.1)
26-
} else {
27-
None
28-
}
29-
}
30-
31-
fn set_individual_tooltip(
32-
&mut self,
33-
common_id: Id,
34-
index: usize,
35-
individual_id: Id,
36-
size: Vec2,
37-
) {
38-
if self.last_common_id != Some(common_id) {
39-
self.last_common_id = Some(common_id);
40-
self.individual_ids_and_sizes.clear();
41-
}
42-
43-
self.individual_ids_and_sizes
44-
.insert(index, (individual_id, size));
45-
}
46-
}
5+
use crate::*;
476

487
// ----------------------------------------------------------------------------
498

@@ -94,10 +53,8 @@ pub fn show_tooltip_at_pointer<R>(
9453
id: Id,
9554
add_contents: impl FnOnce(&mut Ui) -> R,
9655
) -> Option<R> {
97-
let suggested_pos = ctx
98-
.input(|i| i.pointer.hover_pos())
99-
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
100-
show_tooltip_at(ctx, id, suggested_pos, add_contents)
56+
ctx.input(|i| i.pointer.hover_pos())
57+
.map(|pointer_pos| show_tooltip_at(ctx, id, pointer_pos + vec2(16.0, 16.0), add_contents))
10158
}
10259

10360
/// Show a tooltip under the given area.
@@ -106,21 +63,16 @@ pub fn show_tooltip_at_pointer<R>(
10663
pub fn show_tooltip_for<R>(
10764
ctx: &Context,
10865
id: Id,
109-
rect: &Rect,
66+
widget_rect: &Rect,
11067
add_contents: impl FnOnce(&mut Ui) -> R,
111-
) -> Option<R> {
112-
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
113-
let (above, position) = if ctx.input(|i| i.any_touches()) {
114-
(true, expanded_rect.left_top())
115-
} else {
116-
(false, expanded_rect.left_bottom())
117-
};
68+
) -> R {
69+
let is_touch_screen = ctx.input(|i| i.any_touches());
70+
let allow_placing_below = !is_touch_screen; // There is a finger below.
11871
show_tooltip_at_avoid_dyn(
11972
ctx,
12073
id,
121-
Some(position),
122-
above,
123-
expanded_rect,
74+
allow_placing_below,
75+
widget_rect,
12476
Box::new(add_contents),
12577
)
12678
}
@@ -131,101 +83,119 @@ pub fn show_tooltip_for<R>(
13183
pub fn show_tooltip_at<R>(
13284
ctx: &Context,
13385
id: Id,
134-
suggested_position: Option<Pos2>,
86+
suggested_position: Pos2,
13587
add_contents: impl FnOnce(&mut Ui) -> R,
136-
) -> Option<R> {
137-
let above = false;
138-
show_tooltip_at_avoid_dyn(
139-
ctx,
140-
id,
141-
suggested_position,
142-
above,
143-
Rect::NOTHING,
144-
Box::new(add_contents),
145-
)
88+
) -> R {
89+
let allow_placing_below = true;
90+
let rect = Rect::from_center_size(suggested_position, Vec2::ZERO);
91+
show_tooltip_at_avoid_dyn(ctx, id, allow_placing_below, &rect, Box::new(add_contents))
14692
}
14793

14894
fn show_tooltip_at_avoid_dyn<'c, R>(
14995
ctx: &Context,
150-
individual_id: Id,
151-
suggested_position: Option<Pos2>,
152-
above: bool,
153-
mut avoid_rect: Rect,
96+
widget_id: Id,
97+
allow_placing_below: bool,
98+
widget_rect: &Rect,
15499
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
155-
) -> Option<R> {
156-
let spacing = 4.0;
157-
100+
) -> R {
158101
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
159-
let mut frame_state =
160-
ctx.frame_state(|fs| fs.tooltip_state)
161-
.unwrap_or(crate::frame_state::TooltipFrameState {
162-
common_id: individual_id,
163-
rect: Rect::NOTHING,
164-
count: 0,
165-
});
102+
let mut state = ctx.frame_state(|fs| {
103+
fs.tooltip_state
104+
.widget_tooltips
105+
.get(&widget_id)
106+
.copied()
107+
.unwrap_or(PerWidgetTooltipState {
108+
bounding_rect: *widget_rect,
109+
tooltip_count: 0,
110+
})
111+
});
166112

167-
let mut position = if frame_state.rect.is_positive() {
168-
avoid_rect = avoid_rect.union(frame_state.rect);
169-
if above {
170-
frame_state.rect.left_top() - spacing * Vec2::Y
171-
} else {
172-
frame_state.rect.left_bottom() + spacing * Vec2::Y
173-
}
174-
} else if let Some(position) = suggested_position {
175-
position
176-
} else if ctx.memory(|mem| mem.everything_is_visible()) {
177-
Pos2::ZERO
178-
} else {
179-
return None; // No good place for a tooltip :(
180-
};
113+
let tooltip_area_id = tooltip_id(widget_id, state.tooltip_count);
114+
let expected_tooltip_size =
115+
AreaState::load(ctx, tooltip_area_id).map_or(vec2(64.0, 32.0), |area| area.size);
181116

182-
let mut long_state = TooltipState::load(ctx).unwrap_or_default();
183-
let expected_size =
184-
long_state.individual_tooltip_size(frame_state.common_id, frame_state.count);
185-
let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0));
117+
let screen_rect = ctx.screen_rect();
186118

187-
if above {
188-
position.y -= expected_size.y;
189-
}
119+
let (pivot, anchor) = find_tooltip_position(
120+
screen_rect,
121+
state.bounding_rect,
122+
allow_placing_below,
123+
expected_tooltip_size,
124+
);
190125

191-
position = position.at_most(ctx.screen_rect().max - expected_size);
126+
let InnerResponse { inner, response } = Area::new(tooltip_area_id)
127+
.order(Order::Tooltip)
128+
.pivot(pivot)
129+
.fixed_pos(anchor)
130+
.default_width(ctx.style().spacing.tooltip_width)
131+
.constrain_to(screen_rect)
132+
.interactable(false)
133+
.show(ctx, |ui| {
134+
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
135+
});
192136

193-
// check if we intersect the avoid_rect
194-
{
195-
let new_rect = Rect::from_min_size(position, expected_size);
137+
state.tooltip_count += 1;
138+
state.bounding_rect = state.bounding_rect.union(response.rect);
139+
ctx.frame_state_mut(|fs| fs.tooltip_state.widget_tooltips.insert(widget_id, state));
196140

197-
// Note: We use shrink so that we don't get false positives when the rects just touch
198-
if new_rect.shrink(1.0).intersects(avoid_rect) {
199-
if above {
200-
// place below instead:
201-
position = avoid_rect.left_bottom() + spacing * Vec2::Y;
202-
} else {
203-
// place above instead:
204-
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
205-
}
206-
}
207-
}
141+
inner
142+
}
208143

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

211-
let area_id = frame_state.common_id.with(frame_state.count);
148+
/// Returns `(PIVOT, POS)` to mean: put the `PIVOT` corner of the tooltip at `POS`.
149+
///
150+
/// Note: the position might need to be constrained to the screen,
151+
/// (e.g. moved sideways if shown under the widget)
152+
/// but the `Area` will take care of that.
153+
fn find_tooltip_position(
154+
screen_rect: Rect,
155+
widget_rect: Rect,
156+
allow_placing_below: bool,
157+
tooltip_size: Vec2,
158+
) -> (Align2, Pos2) {
159+
let spacing = 4.0;
212160

213-
let InnerResponse { inner, response } =
214-
show_tooltip_area_dyn(ctx, area_id, position, add_contents);
161+
// Does it fit below?
162+
if allow_placing_below
163+
&& widget_rect.bottom() + spacing + tooltip_size.y <= screen_rect.bottom()
164+
{
165+
return (
166+
Align2::LEFT_TOP,
167+
widget_rect.left_bottom() + spacing * Vec2::DOWN,
168+
);
169+
}
215170

216-
long_state.set_individual_tooltip(
217-
frame_state.common_id,
218-
frame_state.count,
219-
individual_id,
220-
response.rect.size(),
221-
);
222-
long_state.store(ctx);
171+
// Does it fit above?
172+
if screen_rect.top() + tooltip_size.y + spacing <= widget_rect.top() {
173+
return (
174+
Align2::LEFT_BOTTOM,
175+
widget_rect.left_top() + spacing * Vec2::UP,
176+
);
177+
}
223178

224-
frame_state.count += 1;
225-
frame_state.rect = frame_state.rect.union(response.rect);
226-
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
179+
// Does it fit to the right?
180+
if widget_rect.right() + spacing + tooltip_size.x <= screen_rect.right() {
181+
return (
182+
Align2::LEFT_TOP,
183+
widget_rect.right_top() + spacing * Vec2::RIGHT,
184+
);
185+
}
227186

228-
Some(inner)
187+
// Does it fit to the left?
188+
if screen_rect.left() + tooltip_size.x + spacing <= widget_rect.left() {
189+
return (
190+
Align2::RIGHT_TOP,
191+
widget_rect.left_top() + spacing * Vec2::LEFT,
192+
);
193+
}
194+
195+
// It doesn't fit anywhere :(
196+
197+
// Just show it anyway:
198+
(Align2::LEFT_TOP, screen_rect.left_top())
229199
}
230200

231201
/// 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>) ->
249219
})
250220
}
251221

252-
/// Show a pop-over window.
253-
fn show_tooltip_area_dyn<'c, R>(
254-
ctx: &Context,
255-
area_id: Id,
256-
window_pos: Pos2,
257-
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
258-
) -> InnerResponse<R> {
259-
use containers::*;
260-
Area::new(area_id)
261-
.order(Order::Tooltip)
262-
.fixed_pos(window_pos)
263-
.default_width(ctx.style().spacing.tooltip_width)
264-
.constrain_to(ctx.screen_rect())
265-
.interactable(false)
266-
.show(ctx, |ui| {
267-
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
268-
})
269-
}
270-
271222
/// Was this popup visible last frame?
272-
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
273-
if let Some(state) = TooltipState::load(ctx) {
274-
if let Some(common_id) = state.last_common_id {
275-
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
276-
if *individual_id == tooltip_id {
277-
let area_id = common_id.with(count);
278-
let layer_id = LayerId::new(Order::Tooltip, area_id);
279-
if ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)) {
280-
return true;
281-
}
282-
}
283-
}
284-
}
285-
}
286-
287-
false
223+
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
224+
let primary_tooltip_area_id = tooltip_id(widget_id, 0);
225+
ctx.memory(|mem| {
226+
mem.areas()
227+
.visible_last_frame(&LayerId::new(Order::Tooltip, primary_tooltip_area_id))
228+
})
288229
}
289230

290231
/// Helper for [`popup_above_or_below_widget`].

0 commit comments

Comments
 (0)