Skip to content

Commit 779ed34

Browse files
committed
Support advanced tooltip positioning
1 parent 5a3f59a commit 779ed34

File tree

3 files changed

+134
-53
lines changed

3 files changed

+134
-53
lines changed

src/game/card.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use bevy::ecs::system::EntityCommand;
22
use bevy::ecs::system::SystemState;
33
use bevy::prelude::*;
4+
use bevy::sprite::Anchor;
45
use bevy::utils::HashMap;
56
use serde::Deserialize;
67
use serde::Serialize;
@@ -172,7 +173,8 @@ fn card(key: impl Into<String>, active: bool) -> impl EntityCommand {
172173
Interaction::default(),
173174
Tooltip {
174175
text: tooltip_text,
175-
side: TooltipSide::Top,
176+
self_anchor: Anchor::TopCenter,
177+
tooltip_anchor: Anchor::BottomCenter,
176178
offset: Vec2::ZERO,
177179
},
178180
))

src/ui.rs

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ pub mod prelude {
2121
pub use super::interaction::InteractionTable;
2222
pub use super::interaction::IsDisabled;
2323
pub use super::tooltip::Tooltip;
24-
pub use super::tooltip::TooltipSide;
2524
pub use super::widget;
2625
pub use super::UiRoot;
2726
pub use crate::core::theme::ThemeColor;

src/ui/tooltip.rs

+131-51
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
use bevy::ecs::entity::Entities;
12
use bevy::prelude::*;
3+
use bevy::sprite::Anchor;
24

35
use crate::core::window::WindowRoot;
6+
use crate::core::PostTransformSet;
47
use crate::core::UpdateSet;
58
use crate::ui::prelude::*;
69
use crate::util::prelude::*;
710

811
pub(super) fn plugin(app: &mut App) {
9-
app.configure::<(TooltipRoot, Tooltip)>();
12+
app.configure::<(TooltipRoot, Tooltip, TooltipHover)>();
1013
}
1114

1215
#[derive(Resource, Reflect)]
@@ -40,6 +43,7 @@ impl FromWorld for TooltipRoot {
4043
..default()
4144
},
4245
ThemeColor::Popup.target::<BackgroundColor>(),
46+
Selection::default(),
4347
))
4448
.id();
4549

@@ -66,75 +70,151 @@ impl FromWorld for TooltipRoot {
6670
}
6771
}
6872

69-
#[derive(Reflect)]
70-
pub enum TooltipSide {
71-
Left,
72-
Right,
73-
Top,
74-
Bottom,
75-
}
76-
7773
#[derive(Component, Reflect)]
7874
#[reflect(Component)]
7975
pub struct Tooltip {
8076
pub text: String,
81-
pub side: TooltipSide,
82-
// TODO: Val
77+
pub self_anchor: Anchor,
78+
pub tooltip_anchor: Anchor,
79+
// TODO: Val?
8380
pub offset: Vec2,
8481
}
8582

8683
impl Configure for Tooltip {
8784
fn configure(app: &mut App) {
8885
app.register_type::<Self>();
89-
app.add_systems(Update, show_tooltip_on_hover.in_set(UpdateSet::RecordInput));
86+
app.add_systems(
87+
Update,
88+
(
89+
update_tooltip_selection,
90+
update_tooltip_visibility,
91+
update_tooltip_text,
92+
)
93+
.in_set(UpdateSet::SyncLate)
94+
.after(detect_tooltip_hover)
95+
.run_if(on_event::<TooltipHover>()),
96+
);
97+
app.add_systems(
98+
PostUpdate,
99+
update_tooltip_position
100+
.in_set(PostTransformSet::Blend)
101+
.run_if(on_event::<TooltipHover>()),
102+
);
103+
}
104+
}
105+
106+
fn update_tooltip_selection(
107+
mut events: EventReader<TooltipHover>,
108+
tooltip_root: Res<TooltipRoot>,
109+
mut tooltip_query: Query<&mut Selection>,
110+
) {
111+
let event = r!(events.read().last());
112+
let mut selection = r!(tooltip_query.get_mut(tooltip_root.container));
113+
114+
selection.0 = event.0.unwrap_or(Entity::PLACEHOLDER);
115+
}
116+
117+
fn update_tooltip_visibility(
118+
mut events: EventReader<TooltipHover>,
119+
tooltip_root: Res<TooltipRoot>,
120+
mut tooltip_query: Query<&mut Visibility>,
121+
) {
122+
let event = r!(events.read().last());
123+
let mut visibility = r!(tooltip_query.get_mut(tooltip_root.container));
124+
125+
*visibility = match event.0 {
126+
Some(_) => Visibility::Inherited,
127+
None => Visibility::Hidden,
128+
};
129+
}
130+
131+
fn update_tooltip_text(
132+
mut events: EventReader<TooltipHover>,
133+
selected_query: Query<&Tooltip>,
134+
tooltip_root: Res<TooltipRoot>,
135+
mut tooltip_query: Query<&mut Text>,
136+
) {
137+
let event = r!(events.read().last());
138+
let entity = rq!(event.0);
139+
let tooltip = r!(selected_query.get(entity));
140+
let mut text = r!(tooltip_query.get_mut(tooltip_root.text));
141+
142+
text.sections[0].value.clone_from(&tooltip.text);
143+
}
144+
145+
fn update_tooltip_position(
146+
mut events: EventReader<TooltipHover>,
147+
selected_query: Query<(&Tooltip, &GlobalTransform, &Node)>,
148+
tooltip_root: Res<TooltipRoot>,
149+
mut tooltip_query: Query<(&mut Style, &mut Transform, &GlobalTransform, &Node)>,
150+
) {
151+
let event = r!(events.read().last());
152+
let entity = rq!(event.0);
153+
let (tooltip, selected_gt, selected_node) = r!(selected_query.get(entity));
154+
let (mut style, mut transform, gt, node) = r!(tooltip_query.get_mut(tooltip_root.container));
155+
156+
// Convert `self_anchor` to a window-space offset.
157+
let self_rect = selected_node.logical_rect(selected_gt);
158+
let self_anchor = self_rect.size() * tooltip.self_anchor.as_vec();
159+
160+
// Convert `tooltip_anchor` to a window-space offset.
161+
let tooltip_rect = node.logical_rect(gt);
162+
let tooltip_anchor = tooltip_rect.size() * tooltip.tooltip_anchor.as_vec();
163+
164+
// Calculate the combined anchor (adjusted by bonus offset).
165+
let anchor = tooltip_anchor - self_anchor + tooltip.offset;
166+
167+
// Convert to absolute position.
168+
let center = self_rect.center() + anchor;
169+
let top_left = center - tooltip_rect.half_size();
170+
style.top = Px(top_left.y);
171+
style.left = Px(top_left.x);
172+
173+
// This system has to run after `UiSystem::Layout` so that its size is calculated
174+
// from the updated text. However, that means that `Style` positioning will be
175+
// delayed by 1 frame. As a workaround, update the `Transform` directly as well.
176+
transform.translation.x = center.x;
177+
transform.translation.y = center.y;
178+
}
179+
180+
/// A buffered event sent when an entity with tooltip is hovered.
181+
#[derive(Event)]
182+
struct TooltipHover(Option<Entity>);
183+
184+
impl Configure for TooltipHover {
185+
fn configure(app: &mut App) {
186+
app.add_event::<TooltipHover>();
187+
app.add_systems(Update, detect_tooltip_hover.in_set(UpdateSet::SyncLate));
90188
}
91189
}
92190

93-
// TODO: Set text in an early system, then set position in a late system.
94-
// That way the tooltip can use its own calculated size to support centering.
95-
fn show_tooltip_on_hover(
96-
window_root: Res<WindowRoot>,
97-
window_query: Query<&Window>,
191+
fn detect_tooltip_hover(
192+
entities: &Entities,
193+
mut events: EventWriter<TooltipHover>,
98194
tooltip_root: Res<TooltipRoot>,
99-
mut container_query: Query<(&mut Visibility, &mut Style)>,
100-
mut text_query: Query<&mut Text>,
101-
interaction_query: Query<(&Interaction, &Tooltip, &GlobalTransform, &Node)>,
195+
tooltip_query: Query<&Selection>,
196+
interaction_query: Query<
197+
(Entity, &Interaction),
198+
(With<Tooltip>, With<GlobalTransform>, With<Node>),
199+
>,
102200
) {
103-
let (mut visibility, mut style) = r!(container_query.get_mut(tooltip_root.container));
104-
let mut text = r!(text_query.get_mut(tooltip_root.text));
105-
let window = r!(window_query.get(window_root.primary));
106-
let width = window.width();
107-
let height = window.height();
108-
109-
for (interaction, tooltip, gt, node) in &interaction_query {
110-
// Skip nodes that are not hovered.
201+
let selection = r!(tooltip_query.get(tooltip_root.container));
202+
203+
// TODO: Sorting by ZIndex would be nice, but not necessary.
204+
for (entity, interaction) in &interaction_query {
111205
if matches!(interaction, Interaction::None) {
112-
*visibility = Visibility::Hidden;
113206
continue;
114207
}
115208

116-
// Set the tooltip text and make it visible.
117-
*visibility = Visibility::Inherited;
118-
text.sections[0].value.clone_from(&tooltip.text);
119-
120-
// Get the left, right, top, bottom of the target node.
121-
let rect = node.logical_rect(gt);
122-
let (left, right, top, bottom) = (
123-
rect.min.x + tooltip.offset.x,
124-
rect.max.x + tooltip.offset.x,
125-
rect.min.y + tooltip.offset.y,
126-
rect.max.y + tooltip.offset.y,
127-
);
128-
129-
// Set the left, right, top, bottom of the tooltip node.
130-
(style.left, style.right, style.top, style.bottom) = match tooltip.side {
131-
TooltipSide::Left => (Auto, Px(width - left), Auto, Px(height - bottom)),
132-
TooltipSide::Right => (Px(right), Auto, Auto, Px(height - bottom)),
133-
TooltipSide::Top => (Px(left), Auto, Auto, Px(height - top)),
134-
TooltipSide::Bottom => (Px(left), Auto, Px(bottom), Auto),
135-
};
136-
137-
// Exit early (because there's only one tooltip).
209+
// Show tooltip.
210+
if selection.0 != entity {
211+
events.send(TooltipHover(Some(entity)));
212+
}
138213
return;
139214
}
215+
216+
// Hide tooltip.
217+
if entities.contains(selection.0) {
218+
events.send(TooltipHover(None));
219+
}
140220
}

0 commit comments

Comments
 (0)