Skip to content

Commit

Permalink
Support advanced tooltip positioning
Browse files Browse the repository at this point in the history
  • Loading branch information
benfrankel committed Jul 26, 2024
1 parent 5a3f59a commit 779ed34
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 53 deletions.
4 changes: 3 additions & 1 deletion src/game/card.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::ecs::system::EntityCommand;
use bevy::ecs::system::SystemState;
use bevy::prelude::*;
use bevy::sprite::Anchor;
use bevy::utils::HashMap;
use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -172,7 +173,8 @@ fn card(key: impl Into<String>, active: bool) -> impl EntityCommand {
Interaction::default(),
Tooltip {
text: tooltip_text,
side: TooltipSide::Top,
self_anchor: Anchor::TopCenter,
tooltip_anchor: Anchor::BottomCenter,
offset: Vec2::ZERO,
},
))
Expand Down
1 change: 0 additions & 1 deletion src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ pub mod prelude {
pub use super::interaction::InteractionTable;
pub use super::interaction::IsDisabled;
pub use super::tooltip::Tooltip;
pub use super::tooltip::TooltipSide;
pub use super::widget;
pub use super::UiRoot;
pub use crate::core::theme::ThemeColor;
Expand Down
182 changes: 131 additions & 51 deletions src/ui/tooltip.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use bevy::ecs::entity::Entities;
use bevy::prelude::*;
use bevy::sprite::Anchor;

use crate::core::window::WindowRoot;
use crate::core::PostTransformSet;
use crate::core::UpdateSet;
use crate::ui::prelude::*;
use crate::util::prelude::*;

pub(super) fn plugin(app: &mut App) {
app.configure::<(TooltipRoot, Tooltip)>();
app.configure::<(TooltipRoot, Tooltip, TooltipHover)>();
}

#[derive(Resource, Reflect)]
Expand Down Expand Up @@ -40,6 +43,7 @@ impl FromWorld for TooltipRoot {
..default()
},
ThemeColor::Popup.target::<BackgroundColor>(),
Selection::default(),
))
.id();

Expand All @@ -66,75 +70,151 @@ impl FromWorld for TooltipRoot {
}
}

#[derive(Reflect)]
pub enum TooltipSide {
Left,
Right,
Top,
Bottom,
}

#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct Tooltip {
pub text: String,
pub side: TooltipSide,
// TODO: Val
pub self_anchor: Anchor,
pub tooltip_anchor: Anchor,
// TODO: Val?
pub offset: Vec2,
}

impl Configure for Tooltip {
fn configure(app: &mut App) {
app.register_type::<Self>();
app.add_systems(Update, show_tooltip_on_hover.in_set(UpdateSet::RecordInput));
app.add_systems(
Update,
(
update_tooltip_selection,
update_tooltip_visibility,
update_tooltip_text,
)
.in_set(UpdateSet::SyncLate)
.after(detect_tooltip_hover)
.run_if(on_event::<TooltipHover>()),
);
app.add_systems(
PostUpdate,
update_tooltip_position
.in_set(PostTransformSet::Blend)
.run_if(on_event::<TooltipHover>()),
);
}
}

fn update_tooltip_selection(
mut events: EventReader<TooltipHover>,
tooltip_root: Res<TooltipRoot>,
mut tooltip_query: Query<&mut Selection>,
) {
let event = r!(events.read().last());
let mut selection = r!(tooltip_query.get_mut(tooltip_root.container));

selection.0 = event.0.unwrap_or(Entity::PLACEHOLDER);
}

fn update_tooltip_visibility(
mut events: EventReader<TooltipHover>,
tooltip_root: Res<TooltipRoot>,
mut tooltip_query: Query<&mut Visibility>,
) {
let event = r!(events.read().last());
let mut visibility = r!(tooltip_query.get_mut(tooltip_root.container));

*visibility = match event.0 {
Some(_) => Visibility::Inherited,
None => Visibility::Hidden,
};
}

fn update_tooltip_text(
mut events: EventReader<TooltipHover>,
selected_query: Query<&Tooltip>,
tooltip_root: Res<TooltipRoot>,
mut tooltip_query: Query<&mut Text>,
) {
let event = r!(events.read().last());
let entity = rq!(event.0);
let tooltip = r!(selected_query.get(entity));
let mut text = r!(tooltip_query.get_mut(tooltip_root.text));

text.sections[0].value.clone_from(&tooltip.text);
}

fn update_tooltip_position(
mut events: EventReader<TooltipHover>,
selected_query: Query<(&Tooltip, &GlobalTransform, &Node)>,
tooltip_root: Res<TooltipRoot>,
mut tooltip_query: Query<(&mut Style, &mut Transform, &GlobalTransform, &Node)>,
) {
let event = r!(events.read().last());
let entity = rq!(event.0);
let (tooltip, selected_gt, selected_node) = r!(selected_query.get(entity));
let (mut style, mut transform, gt, node) = r!(tooltip_query.get_mut(tooltip_root.container));

// Convert `self_anchor` to a window-space offset.
let self_rect = selected_node.logical_rect(selected_gt);
let self_anchor = self_rect.size() * tooltip.self_anchor.as_vec();

// Convert `tooltip_anchor` to a window-space offset.
let tooltip_rect = node.logical_rect(gt);
let tooltip_anchor = tooltip_rect.size() * tooltip.tooltip_anchor.as_vec();

// Calculate the combined anchor (adjusted by bonus offset).
let anchor = tooltip_anchor - self_anchor + tooltip.offset;

// Convert to absolute position.
let center = self_rect.center() + anchor;
let top_left = center - tooltip_rect.half_size();
style.top = Px(top_left.y);
style.left = Px(top_left.x);

// This system has to run after `UiSystem::Layout` so that its size is calculated
// from the updated text. However, that means that `Style` positioning will be
// delayed by 1 frame. As a workaround, update the `Transform` directly as well.
transform.translation.x = center.x;
transform.translation.y = center.y;
}

/// A buffered event sent when an entity with tooltip is hovered.
#[derive(Event)]
struct TooltipHover(Option<Entity>);

impl Configure for TooltipHover {
fn configure(app: &mut App) {
app.add_event::<TooltipHover>();
app.add_systems(Update, detect_tooltip_hover.in_set(UpdateSet::SyncLate));
}
}

// TODO: Set text in an early system, then set position in a late system.
// That way the tooltip can use its own calculated size to support centering.
fn show_tooltip_on_hover(
window_root: Res<WindowRoot>,
window_query: Query<&Window>,
fn detect_tooltip_hover(
entities: &Entities,
mut events: EventWriter<TooltipHover>,
tooltip_root: Res<TooltipRoot>,
mut container_query: Query<(&mut Visibility, &mut Style)>,
mut text_query: Query<&mut Text>,
interaction_query: Query<(&Interaction, &Tooltip, &GlobalTransform, &Node)>,
tooltip_query: Query<&Selection>,
interaction_query: Query<
(Entity, &Interaction),
(With<Tooltip>, With<GlobalTransform>, With<Node>),
>,
) {
let (mut visibility, mut style) = r!(container_query.get_mut(tooltip_root.container));
let mut text = r!(text_query.get_mut(tooltip_root.text));
let window = r!(window_query.get(window_root.primary));
let width = window.width();
let height = window.height();

for (interaction, tooltip, gt, node) in &interaction_query {
// Skip nodes that are not hovered.
let selection = r!(tooltip_query.get(tooltip_root.container));

// TODO: Sorting by ZIndex would be nice, but not necessary.
for (entity, interaction) in &interaction_query {
if matches!(interaction, Interaction::None) {
*visibility = Visibility::Hidden;
continue;
}

// Set the tooltip text and make it visible.
*visibility = Visibility::Inherited;
text.sections[0].value.clone_from(&tooltip.text);

// Get the left, right, top, bottom of the target node.
let rect = node.logical_rect(gt);
let (left, right, top, bottom) = (
rect.min.x + tooltip.offset.x,
rect.max.x + tooltip.offset.x,
rect.min.y + tooltip.offset.y,
rect.max.y + tooltip.offset.y,
);

// Set the left, right, top, bottom of the tooltip node.
(style.left, style.right, style.top, style.bottom) = match tooltip.side {
TooltipSide::Left => (Auto, Px(width - left), Auto, Px(height - bottom)),
TooltipSide::Right => (Px(right), Auto, Auto, Px(height - bottom)),
TooltipSide::Top => (Px(left), Auto, Auto, Px(height - top)),
TooltipSide::Bottom => (Px(left), Auto, Px(bottom), Auto),
};

// Exit early (because there's only one tooltip).
// Show tooltip.
if selection.0 != entity {
events.send(TooltipHover(Some(entity)));
}
return;
}

// Hide tooltip.
if entities.contains(selection.0) {
events.send(TooltipHover(None));
}
}

0 comments on commit 779ed34

Please sign in to comment.