use bevy::ecs::lifecycle::HookContext;
use bevy::ecs::world::DeferredWorld;
use bevy::input_focus::{InputFocus, InputFocusVisible};
use bevy::picking::hover::Hovered;
use bevy::prelude::*;
use bevy::ui::{ComputedUiRenderTargetInfo, OverrideClip, UiGlobalTransform};
use bevy::ui_widgets::popover::Popover;
use crate::shortcut::Shortcut;
use crate::skin::Role;
use crate::text::TextInputOf;
#[derive(Component, Clone, Debug, PartialEq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[component(on_insert = queue_box_sync, on_remove = queue_box_sync)]
pub struct Tooltip {
pub text: String,
pub shortcut: Option<Shortcut>,
}
impl Tooltip {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
shortcut: None,
}
}
pub fn with_shortcut(mut self, shortcut: Shortcut) -> Self {
self.shortcut = Some(shortcut);
self
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq)]
pub struct TooltipBox {
pub(crate) in_row: bool,
pub(crate) room: f32,
pub(crate) cap: f32,
}
fn queue_box_sync(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
world
.commands()
.queue(move |world: &mut World| sync_box(world, entity));
}
fn sync_box(world: &mut World, entity: Entity) {
let Ok(owner) = world.get_entity(entity) else {
return;
};
let tip = owner.get::<Tooltip>().cloned();
let boxes: Vec<Entity> = owner
.get::<Children>()
.into_iter()
.flat_map(|children| children.iter())
.filter(|child| world.get::<TooltipBox>(*child).is_some())
.collect();
match (tip, boxes.is_empty()) {
(Some(tip), true) => {
world.entity_mut(entity).insert_if_new(Hovered::default());
let tip_box = world
.spawn((
Node {
position_type: PositionType::Absolute,
display: Display::None,
..default()
},
Role::Tooltip,
Popover::default(),
OverrideClip,
BoxShadow::default(),
GlobalZIndex(200),
TooltipBox::default(),
ChildOf(entity),
children![(Text::new(tip.text), Role::Label)],
))
.id();
if let Some(shortcut) = tip.shortcut {
world.spawn((keycap(shortcut), ChildOf(tip_box)));
}
}
(None, false) => {
for tip in boxes {
world.entity_mut(tip).despawn();
}
}
_ => {}
}
}
fn keycap(shortcut: Shortcut) -> impl Bundle {
(
Text::new(shortcut.label()),
TextLayout::new(Justify::Center, LineBreak::NoWrap),
Role::PromptKey,
)
}
pub fn update_tooltip_text(
tooltips: Query<(&Tooltip, &Children), Changed<Tooltip>>,
boxes: Query<&Children, With<TooltipBox>>,
mut texts: Query<(&Role, &mut Text)>,
mut commands: Commands,
) {
for (tooltip, children) in &tooltips {
for (tip_box, lines) in children
.iter()
.filter_map(|child| Some((child, boxes.get(child).ok()?)))
{
let mut cap = None;
for line in lines {
let Ok((role, mut text)) = texts.get_mut(*line) else {
continue;
};
match *role {
Role::PromptKey => {
cap = Some(*line);
if let Some(shortcut) = tooltip.shortcut {
let label = shortcut.label();
if text.0 != label {
text.0 = label;
}
}
}
_ if text.0 != tooltip.text => text.0.clone_from(&tooltip.text),
_ => {}
}
}
match (tooltip.shortcut, cap) {
(Some(shortcut), None) => {
commands.spawn((keycap(shortcut), ChildOf(tip_box)));
}
(None, Some(cap)) => commands.entity(cap).despawn(),
_ => {}
}
}
}
}
pub fn show_tooltips(
focus: Option<Res<InputFocus>>,
visible: Option<Res<InputFocusVisible>>,
widgets: Query<Described, Without<TooltipBox>>,
flows: Query<&Node, Without<TooltipBox>>,
fields: Query<&TextInputOf>,
caps: Query<(&Role, &ComputedNode)>,
mut boxes: Query<(&ChildOf, &mut Node, &mut TooltipBox, &Children)>,
) {
let focused = visible
.is_some_and(|v| v.0)
.then(|| focus.and_then(|f| f.get()))
.flatten();
let focused_row = focused.and_then(|f| fields.get(f).ok()).map(|of| of.0);
for (owner, mut node, mut tip, lines) in &mut boxes {
let widget = owner.parent();
let Ok((hovered, parent, placed)) = widgets.get(widget) else {
continue;
};
let shown = hovered.is_some_and(|h| h.get())
|| focused == Some(widget)
|| focused_row == Some(widget);
let display = if shown { Display::Flex } else { Display::None };
if node.display != display {
node.display = display;
}
let in_row = parent
.and_then(|p| flows.get(p.parent()).ok())
.is_some_and(|n| {
matches!(
n.flex_direction,
FlexDirection::Row | FlexDirection::RowReverse
)
});
let room = placed.map_or(f32::INFINITY, |(computed, at, target)| {
let scale = computed.inverse_scale_factor;
let rect = Rect::from_center_size(at.translation * scale, computed.size() * scale);
let window = target.logical_size();
(window.x - rect.max.x).max(rect.min.x).floor()
});
let cap = lines
.iter()
.filter_map(|line| caps.get(line).ok())
.find(|(role, _)| **role == Role::PromptKey)
.map_or(0.0, |(_, computed)| {
let seen = (computed.size().x * computed.inverse_scale_factor).ceil();
if seen > 0.0 { seen } else { tip.cap }
});
tip.set_if_neq(TooltipBox { in_row, room, cap });
}
}
type Described = (
Option<&'static Hovered>,
Option<&'static ChildOf>,
Option<(
&'static ComputedNode,
&'static UiGlobalTransform,
&'static ComputedUiRenderTargetInfo,
)>,
);