diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b89807..6fde513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - **Initial release** - Added `TooltipPlugin` plugin +- Added `TooltipSet` system set - Added `PrimaryTooltip` resource - Added `Tooltip` component - Added `bevy_reflect` feature \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 757cdb6..7bdfe27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ bevy_ui = { version = "0.14", default-features = false, features = [ "bevy_text", ] } bevy_window = { version = "0.14", default-features = false } -tiny_bail = "0.2" +tiny_bail = "0.3" [lints.rust] missing_docs = "deny" diff --git a/src/context.rs b/src/context.rs index b2d6c74..8a603f3 100644 --- a/src/context.rs +++ b/src/context.rs @@ -19,7 +19,7 @@ use bevy_ui::{Interaction, UiStack}; use bevy_window::{PrimaryWindow, Window, WindowRef}; use tiny_bail::prelude::*; -use crate::{PrimaryTooltip, Tooltip, TooltipActivation, TooltipEntity, TooltipTransfer}; +use crate::{PrimaryTooltip, Tooltip, TooltipEntity, TooltipSet}; pub(super) fn plugin(app: &mut App) { app.register_type::(); @@ -33,7 +33,8 @@ pub(super) fn plugin(app: &mut App) { hide_tooltip.run_if(on_event::()), show_tooltip.run_if(on_event::()), ) - .chain(), + .chain() + .in_set(TooltipSet::Content), ); } @@ -53,12 +54,8 @@ pub(crate) struct TooltipContext { timer: u16, /// The current cursor position or activation point. pub(crate) cursor_pos: Vec2, - /// The current activation conditions. - activation: TooltipActivation, - /// The current transfer conditions. - transfer: TooltipTransfer, - /// The tooltip container entity. - entity: TooltipEntity, + /// The current tooltip parameters. + pub(crate) tooltip: Tooltip, } impl Default for TooltipContext { @@ -68,9 +65,7 @@ impl Default for TooltipContext { target: Entity::PLACEHOLDER, timer: 0, cursor_pos: Vec2::ZERO, - activation: TooltipActivation::IMMEDIATE, - transfer: TooltipTransfer::NONE, - entity: TooltipEntity::Custom(Entity::PLACEHOLDER), + tooltip: Tooltip::custom(Entity::PLACEHOLDER), } } } @@ -89,7 +84,7 @@ fn update_tooltip_context( ) { let old_active = matches!(ctx.state, TooltipState::Active); let old_target = ctx.target; - let old_entity = match ctx.entity { + let old_entity = match ctx.tooltip.entity { TooltipEntity::Primary(_) => primary.container, TooltipEntity::Custom(id) => id, }; @@ -111,14 +106,14 @@ fn update_tooltip_context( // 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.tooltip.activation.reset_delay_on_cursor_move { - ctx.timer = ctx.activation.delay; + ctx.timer = ctx.tooltip.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.dismiss_radius + && ctx.cursor_pos.distance_squared(cursor_pos) > ctx.tooltip.activation.dismiss_radius { ctx.state = TooltipState::Dismissed; } @@ -147,7 +142,7 @@ fn update_tooltip_context( Interaction::Pressed => { ctx.target = entity; ctx.state = TooltipState::Dismissed; - ctx.transfer = tooltip.transfer; + ctx.tooltip.transfer = tooltip.transfer; found_target = true; break; } @@ -164,8 +159,8 @@ fn update_tooltip_context( 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.tooltip.transfer.layer >= tooltip.transfer.layer + && (matches!((ctx.tooltip.transfer.group, tooltip.transfer.group), (Some(x), Some(y)) if x == y) || ctx.target == entity)) { TooltipState::Active @@ -173,21 +168,20 @@ fn update_tooltip_context( TooltipState::Delayed }; ctx.timer = tooltip.activation.delay; - ctx.activation = tooltip.activation; - ctx.activation.dismiss_radius *= ctx.activation.dismiss_radius; - ctx.transfer = tooltip.transfer; - ctx.entity = tooltip.entity.clone(); + ctx.tooltip = tooltip.clone(); + ctx.tooltip.activation.dismiss_radius *= ctx.tooltip.activation.dismiss_radius; found_target = true; break; } // There is no longer a target entity. if !found_target && !matches!(ctx.state, TooltipState::Inactive) { - ctx.timer = if matches!(ctx.state, TooltipState::Active) || !ctx.transfer.from_active { - ctx.transfer.timeout - } else { - 0 - }; + ctx.timer = + if matches!(ctx.state, TooltipState::Active) || !ctx.tooltip.transfer.from_active { + ctx.tooltip.transfer.timeout + } else { + 0 + }; ctx.state = TooltipState::Inactive; } @@ -240,7 +234,7 @@ fn show_tooltip( mut text_query: Query<&mut Text>, mut visibility_query: Query<&mut Visibility>, ) { - let entity = match &mut ctx.entity { + let entity = match &mut ctx.tooltip.entity { TooltipEntity::Primary(ref mut text) => { if let Ok(mut primary_text) = text_query.get_mut(primary.text) { *primary_text = std::mem::take(text); diff --git a/src/lib.rs b/src/lib.rs index 9e80f7b..7a14354 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,40 +44,47 @@ mod placement; pub mod prelude { pub use super::{ PrimaryTooltip, Tooltip, TooltipActivation, TooltipEntity, TooltipPlacement, TooltipPlugin, - TooltipTransfer, + TooltipSet, TooltipTransfer, }; } -use bevy_app::Plugin; +use bevy_app::{Plugin, PostUpdate, PreUpdate}; use bevy_color::Color; 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, + schedule::{IntoSystemSetConfigs as _, SystemSet}, + system::Resource, + world::World, +}; use bevy_hierarchy::BuildWorldChildren as _; use bevy_render::view::Visibility; -use bevy_text::{Text, TextSection, TextStyle}; +use bevy_text::{JustifyText, Text, TextSection, TextStyle}; +use bevy_transform::TransformSystem; use bevy_ui::{ node_bundles::{NodeBundle, TextBundle}, - PositionType, Style, UiRect, Val, ZIndex, + PositionType, Style, UiRect, UiSystem, Val, ZIndex, }; pub use placement::TooltipPlacement; /// A [`Plugin`] that sets up the tooltip widget system. -#[derive(Default)] pub struct TooltipPlugin { /// Set a custom entity for [`PrimaryTooltip::container`], or spawn the default container /// entity if `None`. /// /// This entity should include all of the components of [`NodeBundle`], with /// [`Visibility::Hidden`] and [`Style::position_type`] set to [`PositionType::Absolute`]. - pub container: Option, + pub container: Entity, /// Set a custom entity for [`PrimaryTooltip::text`], or spawn the default text entity if /// `None`. /// - /// This entity should include all of the components of [`TextBundle`]. - pub text: Option, + /// This entity should include all of the components of [`TextBundle`] and be a child of + /// [`Self::container`]. + pub text: Entity, } impl Plugin for TooltipPlugin { @@ -88,10 +95,29 @@ impl Plugin for TooltipPlugin { app.register_type::(); + app.configure_sets(PreUpdate, (UiSystem::Focus, TooltipSet::Content).chain()); + app.configure_sets( + PostUpdate, + ( + UiSystem::Layout, + TooltipSet::Placement, + TransformSystem::TransformPropagate, + ) + .chain(), + ); app.add_plugins((context::plugin, placement::plugin)); } } +impl Default for TooltipPlugin { + fn default() -> Self { + Self { + container: Entity::PLACEHOLDER, + text: Entity::PLACEHOLDER, + } + } +} + /// A [`Resource`] containing the [`Entity`] IDs of the global primary tooltip. /// /// See [`TooltipPlugin`] to set up a custom primary tooltip. @@ -109,14 +135,17 @@ pub struct PrimaryTooltip { } impl PrimaryTooltip { - fn new(world: &mut World, container: Option, text: Option) -> Self { - let container = container.unwrap_or_else(|| { + fn new(world: &mut World, container: Entity, text: Entity) -> Self { + let container = if container != Entity::PLACEHOLDER { + container + } else { world .spawn(( Name::new("PrimaryTooltip"), NodeBundle { style: Style { position_type: PositionType::Absolute, + max_width: Val::Vw(40.0), padding: UiRect::all(Val::Px(8.0)), ..Default::default() }, @@ -127,14 +156,16 @@ impl PrimaryTooltip { }, )) .id() - }); + }; - let text = text.unwrap_or_else(|| { + let text = if text != Entity::PLACEHOLDER { + text + } else { world .spawn((Name::new("Text"), TextBundle::default())) .set_parent(container) .id() - }); + }; Self { container, text } } @@ -151,7 +182,7 @@ impl PrimaryTooltip { /// - [`TooltipActivation::IDLE`] /// - [`TooltipTransfer::NONE`] /// - [`TooltipPlacement::CURSOR`] -#[derive(Component)] +#[derive(Component, Clone, Debug)] #[cfg_attr( feature = "bevy_reflect", derive(bevy_reflect::Reflect), @@ -179,6 +210,11 @@ impl Tooltip { } } + /// Use a custom tooltip entity and default behavior. + pub fn custom(entity: Entity) -> Self { + Self::new(TooltipEntity::Custom(entity)) + } + /// Use the primary tooltip entity with a single [`TextSection`] and default behavior. pub fn from_section(value: impl Into, style: TextStyle) -> Self { Self::new(TooltipEntity::Primary(Text::from_section(value, style))) @@ -194,9 +230,16 @@ impl Tooltip { Self::new(TooltipEntity::Primary(text.into())) } - /// Use a custom tooltip entity and default behavior. - pub fn custom(entity: Entity) -> Self { - Self::new(TooltipEntity::Custom(entity)) + /// Set the [`JustifyText`]. + /// + /// NOTE: This does nothing with a custom tooltip. + pub fn with_justify_text(mut self, justify_text: JustifyText) -> Self { + match &mut self.entity { + TooltipEntity::Primary(text) => text.justify = justify_text, + // TODO: Warn? + _ => (), + } + self } /// Set a custom [`TooltipActivation`]. @@ -341,3 +384,12 @@ pub enum TooltipEntity { /// Use a fully custom entity as the tooltip. Custom(Entity), } + +/// A [`SystemSet`] for tooltip systems. +#[derive(SystemSet, Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum TooltipSet { + /// Update and show / hide the tooltip content (runs in [`PreUpdate`]). + Content, + /// Position the tooltip using its calculated size (runs in [`PostUpdate`]). + Placement, +} diff --git a/src/placement.rs b/src/placement.rs index ee8e90d..8640eca 100644 --- a/src/placement.rs +++ b/src/placement.rs @@ -9,25 +9,17 @@ use bevy_hierarchy::Parent; use bevy_math::Vec2; use bevy_render::camera::Camera; use bevy_sprite::Anchor; -use bevy_transform::{ - components::{GlobalTransform, Transform}, - TransformSystem, -}; -use bevy_ui::{IsDefaultUiCamera, Node, Style, TargetCamera, UiRect, UiSystem, Val}; +use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_ui::{IsDefaultUiCamera, Node, Style, TargetCamera, UiRect, Val}; use tiny_bail::prelude::*; use crate::{ context::{TooltipContext, TooltipState}, - PrimaryTooltip, Tooltip, TooltipEntity, + PrimaryTooltip, TooltipEntity, TooltipSet, }; pub(super) fn plugin(app: &mut App) { - app.add_systems( - PostUpdate, - place_tooltip - .after(UiSystem::Layout) - .before(TransformSystem::TransformPropagate), - ); + app.add_systems(PostUpdate, place_tooltip.in_set(TooltipSet::Placement)); } /// The tooltip placement configuration. @@ -119,16 +111,16 @@ fn place_tooltip( target_camera_query: Query<&TargetCamera>, parent_query: Query<&Parent>, default_camera_query: Query<(Entity, &Camera), With>, - target_query: Query<(&Tooltip, &GlobalTransform, &Node)>, + target_query: Query<(&GlobalTransform, &Node)>, mut tooltip_query: Query<(&mut Style, &mut Transform, &GlobalTransform, &Node)>, ) { rq!(matches!(ctx.state, TooltipState::Active)); - let (tooltip, target_gt, target_node) = r!(target_query.get(ctx.target)); - let entity = match &tooltip.entity { + let (target_gt, target_node) = rq!(target_query.get(ctx.target)); + let entity = match &ctx.tooltip.entity { TooltipEntity::Primary(_) => primary.container, &TooltipEntity::Custom(id) => id, }; - let (mut style, mut transform, gt, node) = r!(tooltip_query.get_mut(entity)); + let (mut style, mut transform, gt, node) = or_return!(tooltip_query.get_mut(entity)); // Identify the target camera and viewport rect. let (camera_entity, camera) = if let Ok(camera) = camera_query.get_single() { @@ -149,8 +141,10 @@ fn place_tooltip( // Insert instead of mutate because the tooltip entity might not spawn with a `TargetCamera` component. commands.entity(entity).insert(TargetCamera(camera_entity)); + let placement = &ctx.tooltip.placement; + // Calculate target position. - let mut pos = if let Some(target_anchor) = tooltip.placement.target_anchor { + let mut pos = if let Some(target_anchor) = placement.target_anchor { let target_rect = target_node.logical_rect(target_gt); target_rect.center() - target_rect.size() * target_anchor.as_vec() * Vec2::new(-1.0, 1.0) } else { @@ -160,21 +154,13 @@ fn place_tooltip( // Apply tooltip anchor to target position. let tooltip_rect = node.logical_rect(gt); let tooltip_anchor = - tooltip_rect.size() * tooltip.placement.tooltip_anchor.as_vec() * Vec2::new(-1.0, 1.0); + tooltip_rect.size() * placement.tooltip_anchor.as_vec() * Vec2::new(-1.0, 1.0); pos += tooltip_anchor; // Resolve offset `Val`s. let size = viewport.size(); - let offset_x = tooltip - .placement - .offset_x - .resolve(size.x, size) - .unwrap_or_default(); - let offset_y = tooltip - .placement - .offset_y - .resolve(size.y, size) - .unwrap_or_default(); + let offset_x = placement.offset_x.resolve(size.x, size).unwrap_or_default(); + let offset_y = placement.offset_y.resolve(size.y, size).unwrap_or_default(); // Apply offset. pos += Vec2::new(offset_x, offset_y); @@ -185,7 +171,7 @@ fn place_tooltip( right, top, bottom, - } = tooltip.placement.clamp_padding; + } = placement.clamp_padding; let left = left.resolve(size.x, size).unwrap_or_default(); let right = right.resolve(size.x, size).unwrap_or_default(); let top = top.resolve(size.x, size).unwrap_or_default(); @@ -218,6 +204,24 @@ fn place_tooltip( // 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. + pos = round_layout_coords(pos); transform.translation.x = pos.x; transform.translation.y = pos.y; } + +/// Taken from `bevy_ui`, used in `ui_layout_system`. +fn round_ties_up(value: f32) -> f32 { + if value.fract() != -0.5 { + value.round() + } else { + value.ceil() + } +} + +/// Taken from `bevy_ui`, used in `ui_layout_system`. +fn round_layout_coords(value: Vec2) -> Vec2 { + Vec2 { + x: round_ties_up(value.x), + y: round_ties_up(value.y), + } +}