1pub mod highlight;
128pub mod layout;
129pub mod query;
130pub mod react;
131pub mod term;
132
133use std::{fmt::Debug, time::Duration};
134
135use bevy_app::{Plugin, PreStartup, Update};
136use bevy_derive::{Deref, DerefMut};
137use bevy_ecs::{
138 component::Component,
139 entity::Entity,
140 event::{EntityEvent, Event},
141 hierarchy::Children,
142 lifecycle::HookContext,
143 observer::{Observer, On},
144 query::{AnyOf, Has, Or, QueryData, With},
145 resource::Resource,
146 schedule::{IntoScheduleConfigs, common_conditions::resource_changed},
147 system::{Commands, Query, Res},
148 template::template,
149 world::{DeferredWorld, World},
150};
151use bevy_scene::{CommandsSceneExt, Scene, bsn, template_value};
152
153use bevy_log::error;
154use bevy_math::{Rect, Vec2};
155use bevy_picking::{
156 Pickable,
157 events::{Click, Drag, Move, Out, Over, Pointer, Press},
158 pointer::PointerButton,
159};
160use bevy_platform::collections::HashMap;
161use bevy_text::TextSpan;
162use bevy_time::{Time, Timer, TimerMode};
163use bevy_ui::{
164 ComputedNode, Display, GlobalZIndex, GridAutoFlow, Node, PositionType, RelativeCursorPosition,
165 UiRect, Val, widget::Text,
166};
167use bevy_window::Window;
168use tiny_bail::prelude::*;
169
170pub mod prelude {
172 pub use super::{
173 ActivationMethod, ArbitraryTooltip, NestedTooltipPlugin, Tooltip, TooltipConfiguration,
174 TooltipMap, TooltipSpawned, TooltipsContent, TooltipsContentDetail, TooltipsData,
175 highlight::{TooltipHighlight, TooltipHighlightLink},
176 layout::{TooltipStringText, TooltipTextNode, TooltipTitleNode, TooltipTitleText},
177 query::{TooltipEntities, TooltipEntitiesParam},
178 react::{SpawnArbitraryTooltip, SpawnTooltip, TooltipHighlighting, TooltipLocked},
179 term::{TooltipTermLink, TooltipTermLinkRecursive},
180 };
181}
182use prelude::*;
183
184use crate::{highlight::HighlightPlugin, react::SpawnArbitraryTooltip, term::hover_time_spawn};
185
186pub struct NestedTooltipPlugin;
188
189impl Plugin for NestedTooltipPlugin {
190 fn build(&self, app: &mut bevy_app::App) {
191 app.add_plugins(HighlightPlugin)
192 .init_resource::<TooltipConfiguration>()
193 .init_resource::<TooltipReference>()
194 .init_resource::<TooltipMap>()
195 .add_systems(PreStartup, setup_component_hooks)
196 .add_systems(Update, tick_timers)
197 .add_systems(
198 Update,
199 update_settings.run_if(resource_changed::<TooltipConfiguration>),
200 )
201 .add_observer(spawn_time_done)
202 .add_observer(requested_spawn)
203 .add_observer(arbitrary_spawn);
204 }
205}
206
207#[derive(Resource, Debug)]
209pub struct TooltipConfiguration {
210 pub activation_method: ActivationMethod,
212
213 pub interaction_wait_for_time: Duration,
215
216 pub starting_z_index: i32,
219}
220
221impl Default for TooltipConfiguration {
222 fn default() -> Self {
223 Self {
224 activation_method: Default::default(),
225 interaction_wait_for_time: Duration::from_secs_f64(0.8),
226 starting_z_index: 3,
227 }
228 }
229}
230
231#[derive(Debug, Clone)]
234pub enum ActivationMethod {
235 MiddleMouse,
237 Hover { time: Duration },
239}
240
241impl Default for ActivationMethod {
242 fn default() -> Self {
243 ActivationMethod::Hover {
244 time: Duration::from_secs_f64(0.9),
245 }
246 }
247}
248
249#[derive(Resource, Debug)]
253pub struct TooltipReference {
254 pub tooltip_node: Node,
256}
257
258impl TooltipReference {
259 pub fn new(tooltip_node: Node) -> Self {
260 Self { tooltip_node }
261 }
262}
263
264impl Default for TooltipReference {
265 fn default() -> Self {
266 Self {
267 tooltip_node: Node {
268 position_type: PositionType::Absolute,
269 display: Display::Grid,
270 grid_auto_flow: GridAutoFlow::Row,
271 max_width: Val::Vw(35.),
272 min_height: Val::Vh(5.),
273 max_height: Val::Vh(20.),
274 border: UiRect::all(Val::Px(1.)),
275 ..Default::default()
276 },
277 }
278 }
279}
280
281#[derive(Debug, Component)]
285#[require(RelativeCursorPosition, TooltipPointerPresence)]
286pub struct Tooltip {
287 from_entity: Entity,
288}
289
290impl Tooltip {
291 pub fn entity(&self) -> Entity {
293 self.from_entity
294 }
295}
296
297#[derive(Debug, Component)]
300struct TooltipDebounced;
301
302#[derive(Debug, EntityEvent)]
304pub struct TooltipSpawned {
305 pub entity: Entity,
306}
307
308#[derive(Debug, Component)]
311pub struct TooltipWaitForHover {
312 timer: Timer,
313}
314
315#[derive(Debug, Component)]
317#[relationship_target(relationship = TooltipsNestedOf)]
318pub struct TooltipsNested(Entity);
319
320#[derive(Debug, Component)]
322#[relationship(relationship_target = TooltipsNested)]
323pub struct TooltipsNestedOf(Entity);
324
325#[derive(Debug, Component)]
328pub struct TooltipLinkTimer {
329 timer: Timer,
330}
331
332#[derive(Event)]
334struct TooltipLinkTimeElapsed {
335 term_entity: Entity,
336}
337
338#[derive(Component, Debug, Default, PartialEq)]
342#[component(on_insert = tooltip_presence)]
343enum TooltipPointerPresence {
344 #[default]
345 On,
346 Left,
347}
348
349fn tooltip_presence(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
350 r!(world.commands().get_entity(entity))
351 .observe(pointer_left_link)
352 .observe(pointer_over_link);
353}
354
355#[derive(Resource, Default, Debug, Deref, DerefMut)]
361pub struct TooltipMap {
362 pub map: HashMap<String, TooltipsData>,
363}
364
365#[derive(Debug)]
367pub struct TooltipsData {
368 pub title: String,
370 pub content: Vec<TooltipsContent>,
372}
373
374impl TooltipsData {
375 pub fn new(title: impl ToString, content: impl IntoTooltipsContent) -> Self {
376 Self {
377 title: title.to_string(),
378 content: content.into_tooltips_content(),
379 }
380 }
381}
382
383pub trait IntoTooltipsContent {
386 fn into_tooltips_content(self) -> Vec<TooltipsContent>;
388}
389
390#[derive(Clone)]
394pub enum TooltipsContent {
395 String(String),
397 Term(TooltipsContentDetail),
399 Highlight(TooltipsContentDetail),
401 Custom(TooltipsContentDetail, fn(&str) -> Box<dyn Scene>),
404}
405
406impl IntoTooltipsContent for Vec<TooltipsContent> {
407 fn into_tooltips_content(self) -> Vec<TooltipsContent> {
408 self
409 }
410}
411
412impl IntoTooltipsContent for String {
413 fn into_tooltips_content(self) -> Vec<TooltipsContent> {
414 vec![TooltipsContent::String(self)]
415 }
416}
417
418#[derive(Clone, Debug)]
421pub struct TooltipsContentDetail {
422 pub link: String,
424 pub text: String,
427}
428
429impl TooltipsContentDetail {
430 pub fn new(link: impl Into<String>) -> Self {
433 let link = link.into();
434 let text = link.clone();
435 Self { link, text }
436 }
437
438 pub fn with_alias(link: impl Into<String>, text: impl Into<String>) -> Self {
441 let link = link.into();
442 let text = text.into();
443 Self { link, text }
444 }
445}
446
447impl Debug for TooltipsContent {
448 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449 match self {
450 Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
451 Self::Term(arg0) => f.debug_tuple("Term").field(arg0).finish(),
452 Self::Highlight(arg0) => f.debug_tuple("Highlight").field(arg0).finish(),
453 Self::Custom(arg0, _) => f.debug_tuple("Custom").field(arg0).finish(),
454 }
455 }
456}
457
458#[derive(Component, Clone, Debug, Default)]
460pub struct ArbitraryTooltip;
461
462#[derive(Component)]
464struct NestedTooltipsMiddleMouseObserver;
465
466#[derive(Component)]
468struct NestedTooltipsHoverObserver;
469
470fn setup_component_hooks(world: &mut World) {
474 world
475 .register_component_hooks::<TooltipTermLink>()
476 .on_insert(|mut world, HookContext { entity, .. }| {
477 let config = rq!(world.get_resource::<TooltipConfiguration>());
478
479 match config.activation_method {
480 ActivationMethod::MiddleMouse => {
481 let middle_observe = Observer::new(middle_mouse_spawn).with_entity(entity);
482 world
483 .commands()
484 .spawn((middle_observe, NestedTooltipsMiddleMouseObserver));
485 }
486 ActivationMethod::Hover { .. } => {
487 let hover_spawn_observer = Observer::new(hover_time_spawn).with_entity(entity);
488 let hover_cancel_observer =
489 Observer::new(hover_cancel_spawn).with_entity(entity);
490
491 world
492 .commands()
493 .spawn((hover_spawn_observer, NestedTooltipsHoverObserver));
494 world
495 .commands()
496 .spawn((hover_cancel_observer, NestedTooltipsHoverObserver));
497 }
498 }
499 });
500
501 world
502 .register_component_hooks::<TooltipTermLinkRecursive>()
503 .on_insert(|mut world, HookContext { entity, .. }| {
504 let config = rq!(world.get_resource::<TooltipConfiguration>());
505
506 match config.activation_method {
507 ActivationMethod::MiddleMouse => {
508 let middle_observe = Observer::new(middle_mouse_spawn).with_entity(entity);
509 world
510 .commands()
511 .spawn((middle_observe, NestedTooltipsMiddleMouseObserver));
512 }
513 ActivationMethod::Hover { .. } => {
514 let hover_spawn_observer = Observer::new(hover_time_spawn).with_entity(entity);
515 let hover_cancel_observer =
516 Observer::new(hover_cancel_spawn).with_entity(entity);
517
518 world
519 .commands()
520 .spawn((hover_spawn_observer, NestedTooltipsHoverObserver));
521 world
522 .commands()
523 .spawn((hover_cancel_observer, NestedTooltipsHoverObserver));
524 }
525 }
526 });
527
528 world.register_component_hooks::<Tooltip>().on_insert(
529 |mut world, HookContext { entity, .. }| {
530 world
531 .commands()
532 .entity(entity)
533 .observe(toggle_lock)
534 .observe(move_locked_tooltip)
535 .observe(hover_debounce)
536 .observe(hover_despawn);
537 },
538 );
539}
540
541#[allow(clippy::type_complexity)]
544fn update_settings(
545 config: Res<TooltipConfiguration>,
546 term_links: Query<Entity, Or<(With<TooltipTermLink>, With<TooltipTermLinkRecursive>)>>,
547 mut commands: Commands,
548) {
549 match config.activation_method {
550 ActivationMethod::MiddleMouse => {
551 let mut middle_observe = Observer::new(middle_mouse_spawn);
552 for entity in term_links {
553 middle_observe.watch_entity(entity);
554 }
555 commands.spawn((middle_observe, NestedTooltipsMiddleMouseObserver));
556 }
557 ActivationMethod::Hover { .. } => {
558 let mut hover_spawn_observer = Observer::new(hover_time_spawn);
559 let mut hover_cancel_observer = Observer::new(hover_cancel_spawn);
560
561 for entity in term_links {
562 hover_spawn_observer.watch_entity(entity);
563 hover_cancel_observer.watch_entity(entity);
564 }
565 commands.spawn((hover_spawn_observer, NestedTooltipsHoverObserver));
566 commands.spawn((hover_cancel_observer, NestedTooltipsHoverObserver));
567 }
568 }
569}
570
571#[derive(QueryData)]
572#[query_data(mutable)]
573struct HoverLinkQuery {
574 link: AnyOf<(&'static TooltipTermLink, &'static TooltipTermLinkRecursive)>,
575 timer: Option<&'static mut TooltipLinkTimer>,
576}
577
578#[track_caller]
580fn hover_cancel_spawn(hover: On<Pointer<Out>>, mut commands: Commands) {
581 rq!(commands.get_entity(hover.entity)).remove::<TooltipLinkTimer>();
582}
583
584#[derive(QueryData)]
585#[query_data(mutable)]
586struct SpawnLinksQuery {
587 entity: Entity,
588 link: AnyOf<(&'static TooltipTermLink, &'static TooltipTermLinkRecursive)>,
589 spawn_timer: &'static mut TooltipLinkTimer,
590}
591
592#[derive(QueryData)]
593#[query_data(mutable)]
594struct HoverWaitQuery {
595 entity: Entity,
596 tooltip: &'static Tooltip,
597 wait_for: &'static mut TooltipWaitForHover,
598}
599
600fn tick_timers(
602 mut links_query: Query<SpawnLinksQuery>,
603 mut wait_for_query: Query<HoverWaitQuery>,
604 pointer_presence_query: Query<&TooltipPointerPresence>,
605 time_res: Res<Time>,
606 mut commands: Commands,
607) {
608 for mut links_item in &mut links_query {
609 links_item.spawn_timer.timer.tick(time_res.delta());
610 if links_item.spawn_timer.timer.is_finished() {
611 commands.trigger(TooltipLinkTimeElapsed {
612 term_entity: links_item.entity,
613 });
614 c!(commands.get_entity(links_item.entity)).remove::<TooltipLinkTimer>();
615 }
616 }
617 for mut wait_for_item in &mut wait_for_query {
618 if let Ok(pointer) = pointer_presence_query.get(wait_for_item.tooltip.from_entity)
620 && *pointer == TooltipPointerPresence::On
621 {
622 continue;
623 }
624 wait_for_item.wait_for.timer.tick(time_res.delta());
625 if wait_for_item.wait_for.timer.is_finished() {
626 c!(commands.get_entity(wait_for_item.entity)).try_despawn();
627 }
628 }
629}
630
631#[derive(QueryData)]
632struct ExistingTooltipQuery {
633 entity: Entity,
634 tooltip: &'static Tooltip,
635}
636
637#[allow(clippy::too_many_arguments)]
639fn spawn_time_done(
640 term: On<TooltipLinkTimeElapsed>,
641 links_query: Query<AnyOf<(&TooltipTermLink, &TooltipTermLinkRecursive)>>,
642 existing_tooltips_query: Query<ExistingTooltipQuery>,
643 window_query: Query<&Window>,
644 tooltips_map: Res<TooltipMap>,
645 tooltip_reference: Res<TooltipReference>,
646 tooltip_configuration: Res<TooltipConfiguration>,
647 mut commands: Commands,
648) {
649 let term_entity = term.term_entity;
650
651 for tooltip_item in existing_tooltips_query {
652 let tooltip = tooltip_item.tooltip;
653 if tooltip.from_entity == term_entity {
654 return;
655 }
656 }
657
658 let link_item = r!(links_query.get(term_entity));
659 let (tooltip_term, nested) = match link_item {
660 (None, None) => {
662 error!("Bevy invariant failed");
663 return;
664 }
665 (None, Some(s)) => (s.linked_string.clone(), Some(s.parent_entity)),
666 (Some(s), None) => (s.linked_string.clone(), None),
667 (Some(_), Some(_)) => {
669 error!("Nested tooltips has a bug");
670 return;
671 }
672 };
673
674 let zindex = match nested {
676 None => {
677 for tooltip_item in existing_tooltips_query {
678 let entity = tooltip_item.entity;
679 c!(commands.get_entity(entity)).try_despawn();
680 }
681 GlobalZIndex(tooltip_configuration.starting_z_index)
682 }
683 Some(_) => GlobalZIndex(
684 existing_tooltips_query.count() as i32 + tooltip_configuration.starting_z_index,
685 ),
686 };
687
688 let Some(tooltip_data) = tooltips_map.get(&tooltip_term) else {
689 error!("Could not find {tooltip_term} in tooltips");
690 return;
691 };
692
693 spawn_tooltip(
694 term.term_entity,
695 tooltip_data,
696 zindex,
697 (),
698 window_query,
699 &tooltip_reference,
700 &tooltip_configuration,
701 &mut commands,
702 );
703}
704
705#[derive(QueryData)]
706struct TooltipDebounceQuery {
707 tooltip: &'static Tooltip,
708 debounced: Has<TooltipDebounced>,
709 cursor: &'static RelativeCursorPosition,
710}
711
712fn hover_debounce(
716 hover: On<Pointer<Move>>,
717 tooltip_query: Query<TooltipDebounceQuery>,
718 mut commands: Commands,
719) {
720 const DEBOUNCE_DIST: f32 = 0.48;
723 let tooltip_item = r!(tooltip_query.get(hover.entity));
724 if tooltip_item.debounced {
725 return;
726 }
727 let normalised = rq!(tooltip_item.cursor.normalized);
728 let bounds = Rect {
729 min: Vec2::new(-DEBOUNCE_DIST, -DEBOUNCE_DIST),
730 max: Vec2::new(DEBOUNCE_DIST, DEBOUNCE_DIST),
731 };
732
733 if bounds.contains(normalised) {
734 r!(commands.get_entity(hover.entity))
735 .insert(TooltipDebounced)
736 .remove::<TooltipWaitForHover>();
737 }
738}
739
740#[derive(QueryData)]
741struct TooltipQuery {
742 tooltip: &'static Tooltip,
743 relative_cursor: &'static RelativeCursorPosition,
744 has_nested: Has<TooltipsNested>,
745 locked: Has<TooltipLocked>,
746 debounced: Has<TooltipDebounced>,
747}
748
749#[allow(clippy::type_complexity)]
752fn hover_despawn(
753 hover: On<Pointer<Out>>,
754 tooltip_query: Query<TooltipQuery>,
755 link_query: Query<&'static TooltipPointerPresence>,
756 mut commands: Commands,
757) {
758 let tooltip_item = r!(tooltip_query.get(hover.entity));
759
760 if tooltip_item.has_nested || tooltip_item.locked || !tooltip_item.debounced {
762 return;
763 }
764
765 if tooltip_item.relative_cursor.cursor_over {
766 return;
767 }
768
769 if let Ok(presence) = link_query.get(tooltip_item.tooltip.from_entity)
771 && *presence == TooltipPointerPresence::On
772 {
773 return;
774 }
775
776 r!(commands.get_entity(hover.entity)).despawn();
777}
778
779#[allow(clippy::too_many_arguments)]
781fn middle_mouse_spawn(
782 mut press: On<Pointer<Click>>,
783 links_query: Query<AnyOf<(&TooltipTermLink, &TooltipTermLinkRecursive)>>,
784 existing_tooltips_query: Query<ExistingTooltipQuery>,
785 window_query: Query<&Window>,
786 tooltips_map: Res<TooltipMap>,
787 tooltip_reference: Res<TooltipReference>,
788 tooltip_configuration: Res<TooltipConfiguration>,
789 mut commands: Commands,
790) {
791 press.propagate(false);
793 if press.button != PointerButton::Middle {
794 return;
795 }
796
797 let term_entity = press.entity;
798 for tooltip_item in existing_tooltips_query {
799 let tooltip = tooltip_item.tooltip;
800 if tooltip.from_entity == press.entity {
801 return;
802 }
803 }
804
805 let link_item = r!(links_query.get(term_entity));
806 let (tooltip_term, nested) = match link_item {
807 (None, None) => {
809 error!("Bevy invariant failed");
810 return;
811 }
812 (None, Some(s)) => (s.linked_string.clone(), Some(s.parent_entity)),
813 (Some(s), None) => (s.linked_string.clone(), None),
814 (Some(_), Some(_)) => {
816 error!("Nested tooltips has a bug");
817 return;
818 }
819 };
820
821 let zindex = match nested {
823 None => {
824 for tooltip_item in existing_tooltips_query {
825 let entity = tooltip_item.entity;
826 c!(commands.get_entity(entity)).try_despawn();
827 }
828 GlobalZIndex(tooltip_configuration.starting_z_index)
829 }
830 Some(_) => GlobalZIndex(
831 existing_tooltips_query.count() as i32 + tooltip_configuration.starting_z_index,
832 ),
833 };
834
835 let Some(tooltip_data) = tooltips_map.get(&tooltip_term) else {
836 error!("Could not find {tooltip_term} in tooltips");
837 return;
838 };
839
840 spawn_tooltip(
841 press.entity,
842 tooltip_data,
843 zindex,
844 (),
845 window_query,
846 &tooltip_reference,
847 &tooltip_configuration,
848 &mut commands,
849 );
850}
851
852fn requested_spawn(
853 tooltip_spawn: On<SpawnTooltip>,
854 existing_tooltips_query: Query<ExistingTooltipQuery>,
855 window_query: Query<&Window>,
856 tooltips_map: Res<TooltipMap>,
857 tooltip_reference: Res<TooltipReference>,
858 tooltip_configuration: Res<TooltipConfiguration>,
859 mut commands: Commands,
860) {
861 for tooltip_item in existing_tooltips_query {
863 let tooltip = tooltip_item.tooltip;
864 if tooltip.from_entity == tooltip_spawn.entity {
865 return;
866 }
867 }
868
869 let tooltip_term = &tooltip_spawn.term;
870 let Some(tooltip_data) = tooltips_map.get(tooltip_term) else {
871 error!("Could not find {tooltip_term} in tooltips");
872 return;
873 };
874
875 spawn_tooltip(
876 tooltip_spawn.entity,
877 tooltip_data,
878 GlobalZIndex(tooltip_configuration.starting_z_index),
879 (),
880 window_query,
881 &tooltip_reference,
882 &tooltip_configuration,
883 &mut commands,
884 );
885}
886
887fn arbitrary_spawn(
888 tooltip_spawn: On<SpawnArbitraryTooltip>,
889 existing_tooltips_query: Query<ExistingTooltipQuery>,
890 window_query: Query<&Window>,
891 tooltip_reference: Res<TooltipReference>,
892 tooltip_configuration: Res<TooltipConfiguration>,
893 mut commands: Commands,
894) {
895 for tooltip_item in existing_tooltips_query {
897 let tooltip = tooltip_item.tooltip;
898 if tooltip.from_entity == tooltip_spawn.entity {
899 c!(commands.get_entity(tooltip_item.entity)).despawn();
900 }
901 }
902
903 spawn_tooltip(
904 tooltip_spawn.entity,
905 &tooltip_spawn.tooltips_data,
906 GlobalZIndex(tooltip_configuration.starting_z_index),
907 bsn! { ArbitraryTooltip },
908 window_query,
909 &tooltip_reference,
910 &tooltip_configuration,
911 &mut commands,
912 );
913}
914
915#[allow(clippy::too_many_arguments)]
918fn spawn_tooltip(
919 term_entity: Entity,
920 tooltip_data: &TooltipsData,
921 zindex: GlobalZIndex,
922 additional: impl Scene,
923 window_query: Query<&Window>,
924 tooltip_reference: &TooltipReference,
925 tooltip_configuration: &TooltipConfiguration,
926 commands: &mut Commands,
927) {
928 let design_node = position_tooltip(window_query, tooltip_reference);
929
930 let wait_for = tooltip_configuration.interaction_wait_for_time.clone();
931 let title = tooltip_data.title.clone();
932
933 let tooltip_commands = commands.spawn_scene(bsn! {
934 #tooltip
935 additional
936 template_value(design_node)
937 template(move|_|Ok(Tooltip {
938 from_entity: term_entity,
939 }))
940 template(move|_|Ok(TooltipWaitForHover {
941 timer: Timer::new(
942 wait_for,
943 TimerMode::Once,
944 ),
945 }))
946 template_value(zindex)
947 Pickable {
948 should_block_lower: true,
949 is_hoverable: true,
950 }
951 Children[(
952 TooltipTitleNode
953 Node {
954 display: Display::Flex,
955 }
956 Children[
957 (
958 TooltipTitleText
959 Text::new(title)
960 )
961 ]
962 ),
963 (
964 TooltipTextNode
965 Node {
966 display: Display::Flex,
967 width: Val::Percent(100.),
968 }
969 Text::new("")
970 Children[
971 {tooltip_data.content.iter().map(|item| -> Box<dyn Scene> {
972 match item {
973 TooltipsContent::String(s) => {
974 let s = s.clone();
975 Box::new(bsn! {
976 TooltipStringText
977 TextSpan::new(s)
978 })
979 },
980 TooltipsContent::Term(s) => {
981 let link = s.link.clone();
982 let text = s.text.clone();
983 Box::new(bsn! {
984 TooltipTermLinkRecursive{
985 parent_entity: #tooltip,
986 linked_string:{link.clone()}
987 }
988 TextSpan::new(text)
989 })
990 },
991 TooltipsContent::Highlight(s) => {
992 let link = s.link.clone();
993 let text = s.text.clone();
994 Box::new(bsn! {
995 template(move |_|{
996 Ok(TooltipHighlightLink(link.clone()))
997 })
998 TextSpan::new(text)
999 })
1000 },
1001 TooltipsContent::Custom(s, scene) => {
1002 let text = s.text.clone();
1003 Box::new(bsn! {
1004 TextSpan::new(text.clone())
1005 {scene(&s.link)}
1006 })
1007 },
1008 }
1009 }).collect::<Vec<_>>()}
1010 ]
1011 )]
1012 });
1013
1014 let tooltip_id = tooltip_commands.id();
1015
1016 r!(commands.get_entity(term_entity)).insert(TooltipPointerPresence::On);
1017
1018 commands.trigger(TooltipSpawned { entity: tooltip_id });
1019}
1020
1021fn position_tooltip(window_query: Query<&Window>, tooltip_reference: &TooltipReference) -> Node {
1023 let mut design_node = tooltip_reference.tooltip_node.clone();
1024 let window = r!(window_query.single());
1025 let cursor_position = r!(window.cursor_position());
1026
1027 let window_size = window.size();
1028 let half_window_size = window_size / 2.0;
1029 let offset = 8.0;
1030 let (left, right) = if cursor_position.x > half_window_size.x {
1031 (
1032 Val::Auto,
1033 Val::Px(window_size.x - cursor_position.x + offset),
1034 )
1035 } else {
1036 (Val::Px(cursor_position.x + offset), Val::Auto)
1037 };
1038 let (top, bottom) = if cursor_position.y > half_window_size.y {
1039 (
1040 Val::Auto,
1041 Val::Px(window_size.y - cursor_position.y + offset),
1042 )
1043 } else {
1044 (Val::Px(cursor_position.y + offset), Val::Auto)
1045 };
1046
1047 design_node.left = left;
1048 design_node.right = right;
1049 design_node.top = top;
1050 design_node.bottom = bottom;
1051 design_node
1052}
1053
1054fn pointer_left_link(
1056 hover: On<Pointer<Out>>,
1057 mut prescence_query: Query<&mut TooltipPointerPresence>,
1058) {
1059 let mut prescene_item = r!(prescence_query.get_mut(hover.entity));
1060 *prescene_item = TooltipPointerPresence::Left;
1061}
1062fn pointer_over_link(
1063 hover: On<Pointer<Over>>,
1064 mut prescence_query: Query<&mut TooltipPointerPresence>,
1065) {
1066 let mut prescene_item = r!(prescence_query.get_mut(hover.entity));
1067 *prescene_item = TooltipPointerPresence::On;
1068}
1069
1070#[derive(QueryData)]
1071struct LockTooltipQuery {
1072 tooltip: &'static Tooltip,
1073 locked: Has<TooltipLocked>,
1074}
1075
1076fn toggle_lock(
1078 press: On<Pointer<Press>>,
1079 tooltip_query: Query<LockTooltipQuery>,
1080 mut commands: Commands,
1081) {
1082 if press.button == PointerButton::Middle {
1083 let tooltip_item = r!(tooltip_query.get(press.entity));
1084 if tooltip_item.locked {
1085 r!(commands.get_entity(press.entity)).remove::<TooltipLocked>();
1086 } else {
1087 r!(commands.get_entity(press.entity)).insert(TooltipLocked);
1088 }
1089 }
1090}
1091
1092#[derive(QueryData)]
1093#[query_data(mutable)]
1094struct MoveLockedQuery {
1095 tooltip: &'static Tooltip,
1096 locked: Has<TooltipLocked>,
1097 node: &'static mut Node,
1098 computed_node: &'static ComputedNode,
1099}
1100
1101fn move_locked_tooltip(drag: On<Pointer<Drag>>, mut tooltip_query: Query<MoveLockedQuery>) {
1103 if drag.button != PointerButton::Primary {
1104 return;
1105 }
1106 let tooltip_item = r!(tooltip_query.get_mut(drag.entity));
1107 if !tooltip_item.locked {
1108 return;
1109 }
1110
1111 let width = tooltip_item.computed_node.size.x / 3.;
1112 let height = tooltip_item.computed_node.size.y / 3.;
1113
1114 let x = drag.pointer_location.position.x - width;
1115 let y = drag.pointer_location.position.y - height;
1116
1117 let mut node = tooltip_item.node;
1118
1119 node.left = Val::Px(x);
1120 node.right = Val::Auto;
1121 node.top = Val::Px(y);
1122 node.bottom = Val::Auto;
1123}