pub mod events;
pub mod highlight;
pub mod layout;
pub mod query;
pub mod term;
use std::time::Duration;
use bevy_app::{Plugin, PreStartup, Update};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
children,
component::Component,
entity::Entity,
event::{EntityEvent, Event},
lifecycle::HookContext,
observer::{Observer, On},
query::{AnyOf, Has, Or, QueryData, With},
resource::Resource,
schedule::{IntoScheduleConfigs, common_conditions::resource_changed},
system::{Commands, Query, Res},
world::World,
};
use bevy_log::error;
use bevy_math::{Rect, Vec2};
use bevy_picking::{
Pickable,
events::{Move, Out, Pointer, Press},
pointer::PointerButton,
};
use bevy_platform::collections::HashMap;
use bevy_text::TextSpan;
use bevy_time::{Time, Timer, TimerMode};
use bevy_ui::{
Display, GlobalZIndex, GridAutoFlow, Node, PositionType, RelativeCursorPosition, UiRect, Val,
widget::Text,
};
use bevy_window::Window;
use tiny_bail::prelude::*;
pub mod prelude {
pub use super::{
ActivationMethod, NestedTooltipPlugin, Tooltip, TooltipConfiguration, TooltipMap,
TooltipSpawned, TooltipsContent, TooltipsData,
events::{TooltipHighlighting, TooltipLocked},
highlight::{TooltipHighlight, TooltipHighlightLink},
layout::{TooltipStringText, TooltipTextNode, TooltipTitleNode, TooltipTitleText},
query::{TooltipEntities, TooltipEntitiesParam},
term::{TooltipTermLink, TooltipTermLinkRecursive},
};
}
use prelude::*;
use crate::{highlight::HighlightPlugin, term::hover_time_spawn};
pub struct NestedTooltipPlugin;
impl Plugin for NestedTooltipPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_plugins(HighlightPlugin)
.init_resource::<TooltipConfiguration>()
.init_resource::<TooltipReference>()
.add_systems(PreStartup, setup_component_hooks)
.add_systems(Update, tick_timers)
.add_systems(
Update,
update_settings.run_if(resource_changed::<TooltipConfiguration>),
)
.add_observer(spawn_time_done);
}
}
#[derive(Resource, Debug)]
pub struct TooltipConfiguration {
pub activation_method: ActivationMethod,
pub interaction_wait_for_time: Duration,
pub starting_z_index: i32,
}
impl Default for TooltipConfiguration {
fn default() -> Self {
Self {
activation_method: Default::default(),
interaction_wait_for_time: Duration::from_secs_f64(0.8),
starting_z_index: 3,
}
}
}
#[derive(Debug, Clone)]
pub enum ActivationMethod {
MiddleMouse,
Hover { time: Duration },
}
impl Default for ActivationMethod {
fn default() -> Self {
ActivationMethod::Hover {
time: Duration::from_secs_f64(0.9),
}
}
}
#[derive(Resource, Debug)]
pub struct TooltipReference {
tooltip_node: Node,
}
impl TooltipReference {
pub fn new(tooltip_node: Node) -> Self {
Self { tooltip_node }
}
}
impl Default for TooltipReference {
fn default() -> Self {
Self {
tooltip_node: Node {
position_type: PositionType::Absolute,
display: Display::Grid,
grid_auto_flow: GridAutoFlow::Row,
max_width: Val::Vw(35.),
min_height: Val::Vh(5.),
max_height: Val::Vh(20.),
border: UiRect::all(Val::Px(1.)),
..Default::default()
},
}
}
}
#[derive(Debug, Component)]
#[require(RelativeCursorPosition)]
pub struct Tooltip {
entity: Entity,
}
impl Tooltip {
pub fn entity(&self) -> Entity {
self.entity
}
}
#[derive(Debug, Component)]
struct ToolTipDebounced;
#[derive(Debug, EntityEvent)]
pub struct TooltipSpawned {
pub entity: Entity,
}
#[derive(Debug, Component)]
pub struct TooltipWaitForHover {
timer: Timer,
}
#[derive(Debug, Component)]
#[relationship_target(relationship = TooltipsNestedOf)]
pub struct TooltipsNested(Entity);
#[derive(Debug, Component)]
#[relationship(relationship_target = TooltipsNested)]
pub struct TooltipsNestedOf(Entity);
#[derive(Debug, Component)]
pub struct TooltipLinkTimer {
timer: Timer,
}
#[derive(Event)]
struct TooltipLinkTimeElapsed {
term_entity: Entity,
}
#[derive(Resource, Debug, Deref, DerefMut, Clone)]
pub struct TooltipMap {
pub map: HashMap<String, TooltipsData>,
}
#[derive(Debug, Clone)]
pub struct TooltipsData {
pub title: String,
pub content: Vec<TooltipsContent>,
}
impl TooltipsData {
pub fn new(title: impl ToString, content: Vec<TooltipsContent>) -> Self {
Self {
title: title.to_string(),
content,
}
}
}
#[derive(Debug, Clone)]
pub enum TooltipsContent {
String(String),
Term(String),
Highlight(String),
}
#[derive(Component)]
struct NestedTooltipsMiddleMouseObserver;
#[derive(Component)]
struct NestedTooltipsHoverObserver;
fn setup_component_hooks(world: &mut World) {
world
.register_component_hooks::<TooltipTermLink>()
.on_insert(|mut world, HookContext { entity, .. }| {
let config = rq!(world.get_resource::<TooltipConfiguration>());
match config.activation_method {
ActivationMethod::MiddleMouse => {
let middle_observe = Observer::new(middle_mouse_spawn).with_entity(entity);
world
.commands()
.spawn((middle_observe, NestedTooltipsMiddleMouseObserver));
}
ActivationMethod::Hover { .. } => {
let hover_spawn_observer = Observer::new(hover_time_spawn).with_entity(entity);
let hover_cancel_observer =
Observer::new(hover_cancel_spawn).with_entity(entity);
world
.commands()
.spawn((hover_spawn_observer, NestedTooltipsHoverObserver));
world
.commands()
.spawn((hover_cancel_observer, NestedTooltipsHoverObserver));
}
}
});
world
.register_component_hooks::<TooltipTermLinkRecursive>()
.on_insert(|mut world, HookContext { entity, .. }| {
let config = rq!(world.get_resource::<TooltipConfiguration>());
match config.activation_method {
ActivationMethod::MiddleMouse => {
let middle_observe = Observer::new(middle_mouse_spawn).with_entity(entity);
world
.commands()
.spawn((middle_observe, NestedTooltipsMiddleMouseObserver));
}
ActivationMethod::Hover { .. } => {
let hover_spawn_observer = Observer::new(hover_time_spawn).with_entity(entity);
let hover_cancel_observer =
Observer::new(hover_cancel_spawn).with_entity(entity);
world
.commands()
.spawn((hover_spawn_observer, NestedTooltipsHoverObserver));
world
.commands()
.spawn((hover_cancel_observer, NestedTooltipsHoverObserver));
}
}
});
world.register_component_hooks::<Tooltip>().on_insert(
|mut world, HookContext { entity, .. }| {
world
.commands()
.entity(entity)
.observe(toggle_lock)
.observe(hover_debounce)
.observe(hover_despawn);
},
);
}
#[allow(clippy::type_complexity)]
fn update_settings(
config: Res<TooltipConfiguration>,
term_links: Query<Entity, Or<(With<TooltipTermLink>, With<TooltipTermLinkRecursive>)>>,
mut commands: Commands,
) {
match config.activation_method {
ActivationMethod::MiddleMouse => {
let mut middle_observe = Observer::new(middle_mouse_spawn);
for entity in term_links {
middle_observe.watch_entity(entity);
}
commands.spawn((middle_observe, NestedTooltipsMiddleMouseObserver));
}
ActivationMethod::Hover { .. } => {
let mut hover_spawn_observer = Observer::new(hover_time_spawn);
let mut hover_cancel_observer = Observer::new(hover_cancel_spawn);
for entity in term_links {
hover_spawn_observer.watch_entity(entity);
hover_cancel_observer.watch_entity(entity);
}
commands.spawn((hover_spawn_observer, NestedTooltipsHoverObserver));
commands.spawn((hover_cancel_observer, NestedTooltipsHoverObserver));
}
}
}
#[derive(QueryData)]
#[query_data(mutable)]
struct HoverLinkQuery {
link: AnyOf<(&'static TooltipTermLink, &'static TooltipTermLinkRecursive)>,
timer: Option<&'static mut TooltipLinkTimer>,
}
fn hover_cancel_spawn(hover: On<Pointer<Out>>, mut commands: Commands) {
r!(commands.get_entity(hover.entity)).remove::<TooltipLinkTimer>();
}
#[derive(QueryData)]
#[query_data(mutable)]
struct SpawnLinksQuery {
entity: Entity,
link: AnyOf<(&'static TooltipTermLink, &'static TooltipTermLinkRecursive)>,
spawn_timer: &'static mut TooltipLinkTimer,
}
#[derive(QueryData)]
#[query_data(mutable)]
struct HoverWaitQuery {
entity: Entity,
tooltip: &'static Tooltip,
wait_for: &'static mut TooltipWaitForHover,
}
fn tick_timers(
mut links_query: Query<SpawnLinksQuery>,
mut wait_for_query: Query<HoverWaitQuery>,
time_res: Res<Time>,
mut commands: Commands,
) {
for mut links_item in &mut links_query {
links_item.spawn_timer.timer.tick(time_res.delta());
if links_item.spawn_timer.timer.is_finished() {
commands.trigger(TooltipLinkTimeElapsed {
term_entity: links_item.entity,
});
c!(commands.get_entity(links_item.entity)).remove::<TooltipLinkTimer>();
}
}
for mut wait_for_item in &mut wait_for_query {
wait_for_item.wait_for.timer.tick(time_res.delta());
if wait_for_item.wait_for.timer.is_finished() {
c!(commands.get_entity(wait_for_item.entity)).try_despawn();
}
}
}
#[allow(clippy::too_many_arguments)]
fn spawn_time_done(
term: On<TooltipLinkTimeElapsed>,
links_query: Query<AnyOf<(&TooltipTermLink, &TooltipTermLinkRecursive)>>,
existing_tooltips_query: Query<(Entity, &Tooltip)>,
window_query: Query<&Window>,
tooltips_map: Res<TooltipMap>,
tooltip_reference: Res<TooltipReference>,
tooltip_configuration: Res<TooltipConfiguration>,
mut commands: Commands,
) {
spawn_tooltip(
term.term_entity,
links_query,
existing_tooltips_query,
window_query,
tooltips_map,
tooltip_reference,
tooltip_configuration,
&mut commands,
);
}
#[derive(QueryData)]
struct TooltipDebounceQuery {
tooltip: &'static Tooltip,
debounced: Has<ToolTipDebounced>,
cursor: &'static RelativeCursorPosition,
}
fn hover_debounce(
hover: On<Pointer<Move>>,
tooltip_query: Query<TooltipDebounceQuery>,
mut commands: Commands,
) {
const DEBOUNCE_DIST: f32 = 0.48;
let tooltip_item = r!(tooltip_query.get(hover.entity));
if tooltip_item.debounced {
return;
}
let normalised = rq!(tooltip_item.cursor.normalized);
let bounds = Rect {
min: Vec2::new(-DEBOUNCE_DIST, -DEBOUNCE_DIST),
max: Vec2::new(DEBOUNCE_DIST, DEBOUNCE_DIST),
};
if bounds.contains(normalised) {
r!(commands.get_entity(hover.entity))
.insert(ToolTipDebounced)
.remove::<TooltipWaitForHover>();
}
}
#[derive(QueryData)]
struct TooltipQuery {
tooltip: &'static Tooltip,
relative_cursor: &'static RelativeCursorPosition,
has_nested: Has<TooltipsNested>,
locked: Has<TooltipLocked>,
debounced: Has<ToolTipDebounced>,
}
fn hover_despawn(
hover: On<Pointer<Out>>,
tooltip_query: Query<TooltipQuery>,
mut commands: Commands,
) {
let tooltip_item = r!(tooltip_query.get(hover.entity));
if tooltip_item.has_nested || tooltip_item.locked || !tooltip_item.debounced {
return;
}
if tooltip_item.relative_cursor.cursor_over {
return;
}
r!(commands.get_entity(hover.entity)).despawn();
}
#[allow(clippy::too_many_arguments)]
fn middle_mouse_spawn(
mut press: On<Pointer<Press>>,
links_query: Query<AnyOf<(&TooltipTermLink, &TooltipTermLinkRecursive)>>,
existing_tooltips_query: Query<(Entity, &Tooltip)>,
window_query: Query<&Window>,
tooltips_map: Res<TooltipMap>,
tooltip_reference: Res<TooltipReference>,
tooltip_configuration: Res<TooltipConfiguration>,
mut commands: Commands,
) {
press.propagate(false);
if press.button != PointerButton::Middle {
return;
}
spawn_tooltip(
press.entity,
links_query,
existing_tooltips_query,
window_query,
tooltips_map,
tooltip_reference,
tooltip_configuration,
&mut commands,
);
}
#[allow(clippy::too_many_arguments)]
fn spawn_tooltip(
term_entity: Entity,
links_query: Query<'_, '_, AnyOf<(&TooltipTermLink, &TooltipTermLinkRecursive)>>,
existing_tooltips_query: Query<(Entity, &Tooltip)>,
window_query: Query<'_, '_, &Window>,
tooltips_map: Res<'_, TooltipMap>,
tooltip_reference: Res<'_, TooltipReference>,
tooltip_configuration: Res<TooltipConfiguration>,
commands: &mut Commands<'_, '_>,
) {
for (_, tooltip) in existing_tooltips_query {
if tooltip.entity == term_entity {
return;
}
}
let link_item = r!(links_query.get(term_entity));
let (tooltip_term, nested) = match link_item {
(None, None) => {
error!("Bevy invariant failed");
return;
}
(None, Some(s)) => (s.linked_string.clone(), Some(s.parent_entity)),
(Some(s), None) => (s.linked_string.clone(), None),
(Some(_), Some(_)) => {
error!("Nested tooltips has a bug");
return;
}
};
let zindex = match nested {
None => {
for (entity, _) in existing_tooltips_query {
c!(commands.get_entity(entity)).try_despawn();
}
GlobalZIndex(tooltip_configuration.starting_z_index)
}
Some(_) => GlobalZIndex(
existing_tooltips_query.count() as i32 + tooltip_configuration.starting_z_index,
),
};
let tooltip_data = r!(tooltips_map.get(&tooltip_term));
let design_node = position_tooltip(window_query, tooltip_reference);
let mut tooltip_commands = commands.spawn((
design_node,
Tooltip {
entity: term_entity,
},
TooltipWaitForHover {
timer: Timer::new(
tooltip_configuration.interaction_wait_for_time,
TimerMode::Once,
),
},
zindex,
Pickable {
should_block_lower: true,
is_hoverable: true,
},
children![(
TooltipTitleNode,
Node {
display: Display::Flex,
..Default::default()
},
children![(TooltipTitleText, Text::new(tooltip_data.title.clone()))]
)],
));
if let Some(nested) = nested {
tooltip_commands.insert(TooltipsNestedOf(nested));
}
tooltip_commands.with_children(|parent| {
let parent_entity = parent.target_entity();
parent
.spawn((
TooltipTextNode,
Node {
display: Display::Flex,
width: Val::Percent(100.),
..Default::default()
},
Text::new(""),
))
.with_children(|text| {
for c in &tooltip_data.content {
match c.clone() {
TooltipsContent::String(s) => {
text.spawn((TooltipStringText, TextSpan::new(s)));
}
TooltipsContent::Term(s) => {
text.spawn((
TooltipTermLinkRecursive::new(parent_entity, s.clone()),
TextSpan::new(s),
));
}
TooltipsContent::Highlight(s) => {
text.spawn((TooltipHighlightLink(s.clone()), TextSpan::new(s)));
}
}
}
});
});
let tooltip_id = tooltip_commands.id();
commands.trigger(TooltipSpawned { entity: tooltip_id });
}
fn position_tooltip(
window_query: Query<'_, '_, &Window>,
tooltip_reference: Res<'_, TooltipReference>,
) -> Node {
let mut design_node = tooltip_reference.tooltip_node.clone();
let window = r!(window_query.single());
let cursor_position = r!(window.cursor_position());
let window_size = window.size();
let half_window_size = window_size / 2.0;
let offset = 8.0;
let (left, right) = if cursor_position.x > half_window_size.x {
(
Val::Auto,
Val::Px(window_size.x - cursor_position.x + offset),
)
} else {
(Val::Px(cursor_position.x + offset), Val::Auto)
};
let (top, bottom) = if cursor_position.y > half_window_size.y {
(
Val::Auto,
Val::Px(window_size.y - cursor_position.y + offset),
)
} else {
(Val::Px(cursor_position.y + offset), Val::Auto)
};
design_node.left = left;
design_node.right = right;
design_node.top = top;
design_node.bottom = bottom;
design_node
}
#[derive(QueryData)]
struct LockTooltipQuery {
tooltip: &'static Tooltip,
locked: Has<TooltipLocked>,
}
fn toggle_lock(
press: On<Pointer<Press>>,
tooltip_query: Query<LockTooltipQuery>,
mut commands: Commands,
) {
if press.button == PointerButton::Middle {
let tooltip_item = r!(tooltip_query.get(press.entity));
if tooltip_item.locked {
r!(commands.get_entity(press.entity)).remove::<TooltipLocked>();
} else {
r!(commands.get_entity(press.entity)).insert(TooltipLocked);
}
}
}