Skip to content

Commit

Permalink
Write tooltip activation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
benfrankel committed Aug 17, 2024
1 parent da444a9 commit 4864372
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 16 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ bevy_app = { version = "0.14", default-features = false }
bevy_core = { version = "0.14", default-features = false }
bevy_ecs = { version = "0.14", default-features = false }
bevy_hierarchy = { version = "0.14", default-features = false }
bevy_math = { version = "0.14", default-features = false }
bevy_reflect = { version = "0.14", optional = true, default-features = false }
bevy_render = { version = "0.14", default-features = false }
bevy_sprite = { version = "0.14", default-features = false }
bevy_text = { version = "0.14", default-features = false }
bevy_time = { version = "0.14", default-features = false }
bevy_transform = { version = "0.14", default-features = false }
bevy_ui = { version = "0.14", default-features = false, features = [
"bevy_text",
] }
bevy_window = { version = "0.14", default-features = false }
tiny_bail = "0.1"

[lints.rust]
missing_docs = "deny"
Expand Down
206 changes: 190 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
//! TODO

use core::f32;

use bevy_app::Plugin;
use bevy_app::{Plugin, PreUpdate};
use bevy_core::Name;
#[cfg(feature = "bevy_reflect")]
use bevy_ecs::reflect::{ReflectComponent, ReflectResource};
use bevy_ecs::{component::Component, entity::Entity, system::Resource, world::World};
use bevy_ecs::{
component::Component,
entity::Entity,
query::With,
system::{Query, Res, ResMut, Resource},
world::World,
};
use bevy_hierarchy::BuildWorldChildren as _;
use bevy_render::view::Visibility;
use bevy_math::Vec2;
use bevy_render::{
camera::{Camera, RenderTarget},
view::Visibility,
};
use bevy_sprite::Anchor;
use bevy_text::{Text, TextSection, TextStyle};
use bevy_time::Time;
use bevy_transform::components::GlobalTransform;
use bevy_ui::{
node_bundles::{NodeBundle, TextBundle},
PositionType, Style, UiRect, Val, ZIndex,
Interaction, PositionType, Style, UiRect, UiStack, Val, ZIndex,
};
use bevy_window::{PrimaryWindow, Window, WindowRef};
use tiny_bail::prelude::*;

/// TODO
#[derive(Default)]
pub struct TooltipPlugin {
/// TODO
/// Set a custom entity for [`PrimaryTooltip::container`], or spawn a default entity if `None`.
pub container_entity: Option<Entity>,
/// TODO
/// Set a custom entity for [`PrimaryTooltip::text`], or spawn a default entity if `None`.
pub text_entity: Option<Entity>,
}

Expand All @@ -32,12 +44,17 @@ impl Plugin for TooltipPlugin {
PrimaryTooltip::new(app.world_mut(), self.container_entity, self.text_entity);
app.insert_resource(primary_tooltip);

app.register_type::<TooltipContext>();
app.init_resource::<TooltipContext>();
// TODO: Make sure this runs after `Interaction` is updated.
app.add_systems(PreUpdate, update_tooltip_context);

app.register_type::<Tooltip>();
}
}

/// TODO
#[derive(Resource)]
#[derive(Resource, Copy, Clone, Debug)]
#[cfg_attr(
feature = "bevy_reflect",
derive(bevy_reflect::Reflect),
Expand Down Expand Up @@ -82,6 +99,110 @@ impl PrimaryTooltip {
}
}

fn update_tooltip_context(
mut ctx: ResMut<TooltipContext>,
time: Res<Time>,
ui_stack: Res<UiStack>,
primary_window_query: Query<Entity, With<PrimaryWindow>>,
window_query: Query<&Window>,
camera_query: Query<(&Camera, &GlobalTransform)>,
interaction_query: Query<(&Tooltip, &Interaction)>,
) {
// TODO: Is this needed? Send an event? Show / hide the tooltip?
let _old_state = ctx.state;

// Detect cursor movement.
for (camera, camera_gt) in &camera_query {
let RenderTarget::Window(window) = camera.target else {
continue;
};
let window = match window {
WindowRef::Primary => cq!(primary_window_query.get_single()),
WindowRef::Entity(id) => id,
};
let window = c!(window_query.get(window));
cq!(window.focused);
let cursor_pos = c!(window
.cursor_position()
.and_then(|cursor| camera.viewport_to_world_2d(camera_gt, cursor)));

// Reset activation delay on cursor move.
if ctx.cursor_pos != cursor_pos
&& matches!(ctx.state, TooltipState::Delayed)
&& ctx.activation.reset_delay_on_cursor_move
{
ctx.timer = ctx.activation.delay;
}

// Dismiss tooltip if cursor has left the activation radius.
if matches!(ctx.state, TooltipState::Active)
&& ctx.cursor_pos.distance_squared(cursor_pos) > ctx.activation.radius
{
ctx.state = TooltipState::Dismissed;
}

// Update cursor position.
if !matches!(ctx.state, TooltipState::Active) {
ctx.cursor_pos = cursor_pos;
}

break;
}

// Tick timer for transfer timeout / activation delay.
if matches!(ctx.state, TooltipState::Inactive | TooltipState::Delayed) {
ctx.timer = ctx.timer.saturating_sub(time.delta().as_millis() as u16);
if matches!(ctx.state, TooltipState::Delayed) && ctx.timer == 0 {
ctx.state = TooltipState::Active;
}
}

// Find the highest entity in the `UiStack` that has a tooltip and is being interacted with.
for &entity in ui_stack.uinodes.iter().rev() {
let (tooltip, interaction) = cq!(interaction_query.get(entity));
match interaction {
Interaction::Pressed => {
ctx.target = entity;
ctx.state = TooltipState::Dismissed;
ctx.transfer = tooltip.transfer;
return;
}
Interaction::Hovered => (),
Interaction::None => continue,
};
r!(matches!(ctx.state, TooltipState::Inactive) || ctx.target != entity);

// Switch to the new target entity.
ctx.target = entity;
ctx.state = if tooltip.activation.delay == 0
|| (matches!(ctx.state, TooltipState::Inactive)
&& ctx.timer > 0
&& ctx.transfer.layer >= tooltip.transfer.layer
&& (matches!((ctx.transfer.group, tooltip.transfer.group), (Some(x), Some(y)) if x == y)
|| ctx.target == entity))
{
TooltipState::Active
} else {
TooltipState::Delayed
};
ctx.timer = tooltip.activation.delay;
ctx.activation = tooltip.activation;
ctx.transfer = tooltip.transfer;

return;
}

// There is no target entity.
if !matches!(ctx.state, TooltipState::Inactive) {
ctx.state = TooltipState::Inactive;
ctx.timer = if matches!(ctx.state, TooltipState::Active) || !ctx.transfer.from_active {
ctx.transfer.timeout
} else {
0
};
}
}

// TODO: Animation, wedge (like a speech bubble), easier content customization / icons.
/// TODO
#[derive(Component)]
Expand Down Expand Up @@ -152,22 +273,72 @@ impl Tooltip {
}

/// TODO
#[derive(Resource, Clone, Debug)]
#[cfg_attr(
feature = "bevy_reflect",
derive(bevy_reflect::Reflect),
reflect(Resource)
)]
struct TooltipContext {
/// The current state of the tooltip system.
state: TooltipState,
/// The current or previous target entity being interacted with.
target: Entity,
/// The remaining duration of the current activation delay or transfer timeout (in milliseconds).
timer: u16,
/// The current activation conditions.
activation: TooltipActivation,
/// The current transfer conditions.
transfer: TooltipTransfer,
/// The current cursor position or activation point.
cursor_pos: Vec2,
}

impl Default for TooltipContext {
fn default() -> Self {
Self {
state: TooltipState::Inactive,
target: Entity::PLACEHOLDER,
timer: 0,
activation: TooltipActivation::DEFAULT,
transfer: TooltipTransfer::DEFAULT,
cursor_pos: Vec2::ZERO,
}
}
}

/// TODO
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
enum TooltipState {
/// There is no target entity being interacted with, and no active tooltip.
Inactive,
/// A target entity is being hovered, but its tooltip is not active yet.
Delayed,
/// A target entity is being hovered, and its tooltip is active.
Active,
/// A target entity is being interacted with, but its tooltip has been dismissed.
Dismissed,
}

/// TODO
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct TooltipActivation {
/// The hover duration before the tooltip will activate (in milliseconds).
pub delay: u16,
/// Whether to reset the activation delay timer whenever the cursor moves.
pub reset_delay_on_cursor_move: bool,
/// The cursor distance from the activation point beyond which the tooltip will be dismissed.
pub dismiss_distance: f32,
/// The radius around the activation point beyond which the tooltip will be dismissed.
pub radius: f32,
}

impl TooltipActivation {
/// The default `TooltipActivation`.
pub const DEFAULT: Self = Self {
delay: 100,
reset_delay_on_cursor_move: false,
dismiss_distance: f32::INFINITY,
radius: f32::INFINITY,
};
}

Expand All @@ -178,14 +349,15 @@ impl Default for TooltipActivation {
}

/// TODO
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct TooltipTransfer {
/// Only transfer to elements in the same group, or to self if `None`.
pub group: Option<i8>,
/// Only transfer to elements in the same layer or lower.
pub layer: i8,
/// Only transfer within this duration after the cursor moves away from the old target (in milliseconds), or forever if `None`.
pub timeout: Option<u16>,
/// Only transfer within this duration after the cursor moves away from the old target (in milliseconds).
pub timeout: u16,
/// Only transfer if the old tooltip was active.
pub from_active: bool,
}
Expand All @@ -195,7 +367,7 @@ impl TooltipTransfer {
pub const DEFAULT: Self = Self {
group: None,
layer: 0,
timeout: Some(0),
timeout: 0,
from_active: true,
};
}
Expand All @@ -207,6 +379,7 @@ impl Default for TooltipTransfer {
}

/// TODO
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct TooltipPlacement {
/// The anchor point on the tooltip entity.
Expand Down Expand Up @@ -239,9 +412,10 @@ impl Default for TooltipPlacement {
}

/// TODO
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub enum TooltipEntity {
/// Use the primary tooltip entity with custom text.
/// Use the primary tooltip entity with custom [`Text`].
Primary(Text),
/// Use a fully custom entity as the tooltip.
Custom(Entity),
Expand Down

0 comments on commit 4864372

Please sign in to comment.