1
+ use bevy:: ecs:: entity:: Entities ;
1
2
use bevy:: prelude:: * ;
3
+ use bevy:: sprite:: Anchor ;
2
4
3
5
use crate :: core:: window:: WindowRoot ;
6
+ use crate :: core:: PostTransformSet ;
4
7
use crate :: core:: UpdateSet ;
5
8
use crate :: ui:: prelude:: * ;
6
9
use crate :: util:: prelude:: * ;
7
10
8
11
pub ( super ) fn plugin ( app : & mut App ) {
9
- app. configure :: < ( TooltipRoot , Tooltip ) > ( ) ;
12
+ app. configure :: < ( TooltipRoot , Tooltip , TooltipHover ) > ( ) ;
10
13
}
11
14
12
15
#[ derive( Resource , Reflect ) ]
@@ -40,6 +43,7 @@ impl FromWorld for TooltipRoot {
40
43
..default ( )
41
44
} ,
42
45
ThemeColor :: Popup . target :: < BackgroundColor > ( ) ,
46
+ Selection :: default ( ) ,
43
47
) )
44
48
. id ( ) ;
45
49
@@ -66,75 +70,151 @@ impl FromWorld for TooltipRoot {
66
70
}
67
71
}
68
72
69
- #[ derive( Reflect ) ]
70
- pub enum TooltipSide {
71
- Left ,
72
- Right ,
73
- Top ,
74
- Bottom ,
75
- }
76
-
77
73
#[ derive( Component , Reflect ) ]
78
74
#[ reflect( Component ) ]
79
75
pub struct Tooltip {
80
76
pub text : String ,
81
- pub side : TooltipSide ,
82
- // TODO: Val
77
+ pub self_anchor : Anchor ,
78
+ pub tooltip_anchor : Anchor ,
79
+ // TODO: Val?
83
80
pub offset : Vec2 ,
84
81
}
85
82
86
83
impl Configure for Tooltip {
87
84
fn configure ( app : & mut App ) {
88
85
app. register_type :: < Self > ( ) ;
89
- app. add_systems ( Update , show_tooltip_on_hover. in_set ( UpdateSet :: RecordInput ) ) ;
86
+ app. add_systems (
87
+ Update ,
88
+ (
89
+ update_tooltip_selection,
90
+ update_tooltip_visibility,
91
+ update_tooltip_text,
92
+ )
93
+ . in_set ( UpdateSet :: SyncLate )
94
+ . after ( detect_tooltip_hover)
95
+ . run_if ( on_event :: < TooltipHover > ( ) ) ,
96
+ ) ;
97
+ app. add_systems (
98
+ PostUpdate ,
99
+ update_tooltip_position
100
+ . in_set ( PostTransformSet :: Blend )
101
+ . run_if ( on_event :: < TooltipHover > ( ) ) ,
102
+ ) ;
103
+ }
104
+ }
105
+
106
+ fn update_tooltip_selection (
107
+ mut events : EventReader < TooltipHover > ,
108
+ tooltip_root : Res < TooltipRoot > ,
109
+ mut tooltip_query : Query < & mut Selection > ,
110
+ ) {
111
+ let event = r ! ( events. read( ) . last( ) ) ;
112
+ let mut selection = r ! ( tooltip_query. get_mut( tooltip_root. container) ) ;
113
+
114
+ selection. 0 = event. 0 . unwrap_or ( Entity :: PLACEHOLDER ) ;
115
+ }
116
+
117
+ fn update_tooltip_visibility (
118
+ mut events : EventReader < TooltipHover > ,
119
+ tooltip_root : Res < TooltipRoot > ,
120
+ mut tooltip_query : Query < & mut Visibility > ,
121
+ ) {
122
+ let event = r ! ( events. read( ) . last( ) ) ;
123
+ let mut visibility = r ! ( tooltip_query. get_mut( tooltip_root. container) ) ;
124
+
125
+ * visibility = match event. 0 {
126
+ Some ( _) => Visibility :: Inherited ,
127
+ None => Visibility :: Hidden ,
128
+ } ;
129
+ }
130
+
131
+ fn update_tooltip_text (
132
+ mut events : EventReader < TooltipHover > ,
133
+ selected_query : Query < & Tooltip > ,
134
+ tooltip_root : Res < TooltipRoot > ,
135
+ mut tooltip_query : Query < & mut Text > ,
136
+ ) {
137
+ let event = r ! ( events. read( ) . last( ) ) ;
138
+ let entity = rq ! ( event. 0 ) ;
139
+ let tooltip = r ! ( selected_query. get( entity) ) ;
140
+ let mut text = r ! ( tooltip_query. get_mut( tooltip_root. text) ) ;
141
+
142
+ text. sections [ 0 ] . value . clone_from ( & tooltip. text ) ;
143
+ }
144
+
145
+ fn update_tooltip_position (
146
+ mut events : EventReader < TooltipHover > ,
147
+ selected_query : Query < ( & Tooltip , & GlobalTransform , & Node ) > ,
148
+ tooltip_root : Res < TooltipRoot > ,
149
+ mut tooltip_query : Query < ( & mut Style , & mut Transform , & GlobalTransform , & Node ) > ,
150
+ ) {
151
+ let event = r ! ( events. read( ) . last( ) ) ;
152
+ let entity = rq ! ( event. 0 ) ;
153
+ let ( tooltip, selected_gt, selected_node) = r ! ( selected_query. get( entity) ) ;
154
+ let ( mut style, mut transform, gt, node) = r ! ( tooltip_query. get_mut( tooltip_root. container) ) ;
155
+
156
+ // Convert `self_anchor` to a window-space offset.
157
+ let self_rect = selected_node. logical_rect ( selected_gt) ;
158
+ let self_anchor = self_rect. size ( ) * tooltip. self_anchor . as_vec ( ) ;
159
+
160
+ // Convert `tooltip_anchor` to a window-space offset.
161
+ let tooltip_rect = node. logical_rect ( gt) ;
162
+ let tooltip_anchor = tooltip_rect. size ( ) * tooltip. tooltip_anchor . as_vec ( ) ;
163
+
164
+ // Calculate the combined anchor (adjusted by bonus offset).
165
+ let anchor = tooltip_anchor - self_anchor + tooltip. offset ;
166
+
167
+ // Convert to absolute position.
168
+ let center = self_rect. center ( ) + anchor;
169
+ let top_left = center - tooltip_rect. half_size ( ) ;
170
+ style. top = Px ( top_left. y ) ;
171
+ style. left = Px ( top_left. x ) ;
172
+
173
+ // This system has to run after `UiSystem::Layout` so that its size is calculated
174
+ // from the updated text. However, that means that `Style` positioning will be
175
+ // delayed by 1 frame. As a workaround, update the `Transform` directly as well.
176
+ transform. translation . x = center. x ;
177
+ transform. translation . y = center. y ;
178
+ }
179
+
180
+ /// A buffered event sent when an entity with tooltip is hovered.
181
+ #[ derive( Event ) ]
182
+ struct TooltipHover ( Option < Entity > ) ;
183
+
184
+ impl Configure for TooltipHover {
185
+ fn configure ( app : & mut App ) {
186
+ app. add_event :: < TooltipHover > ( ) ;
187
+ app. add_systems ( Update , detect_tooltip_hover. in_set ( UpdateSet :: SyncLate ) ) ;
90
188
}
91
189
}
92
190
93
- // TODO: Set text in an early system, then set position in a late system.
94
- // That way the tooltip can use its own calculated size to support centering.
95
- fn show_tooltip_on_hover (
96
- window_root : Res < WindowRoot > ,
97
- window_query : Query < & Window > ,
191
+ fn detect_tooltip_hover (
192
+ entities : & Entities ,
193
+ mut events : EventWriter < TooltipHover > ,
98
194
tooltip_root : Res < TooltipRoot > ,
99
- mut container_query : Query < ( & mut Visibility , & mut Style ) > ,
100
- mut text_query : Query < & mut Text > ,
101
- interaction_query : Query < ( & Interaction , & Tooltip , & GlobalTransform , & Node ) > ,
195
+ tooltip_query : Query < & Selection > ,
196
+ interaction_query : Query <
197
+ ( Entity , & Interaction ) ,
198
+ ( With < Tooltip > , With < GlobalTransform > , With < Node > ) ,
199
+ > ,
102
200
) {
103
- let ( mut visibility, mut style) = r ! ( container_query. get_mut( tooltip_root. container) ) ;
104
- let mut text = r ! ( text_query. get_mut( tooltip_root. text) ) ;
105
- let window = r ! ( window_query. get( window_root. primary) ) ;
106
- let width = window. width ( ) ;
107
- let height = window. height ( ) ;
108
-
109
- for ( interaction, tooltip, gt, node) in & interaction_query {
110
- // Skip nodes that are not hovered.
201
+ let selection = r ! ( tooltip_query. get( tooltip_root. container) ) ;
202
+
203
+ // TODO: Sorting by ZIndex would be nice, but not necessary.
204
+ for ( entity, interaction) in & interaction_query {
111
205
if matches ! ( interaction, Interaction :: None ) {
112
- * visibility = Visibility :: Hidden ;
113
206
continue ;
114
207
}
115
208
116
- // Set the tooltip text and make it visible.
117
- * visibility = Visibility :: Inherited ;
118
- text. sections [ 0 ] . value . clone_from ( & tooltip. text ) ;
119
-
120
- // Get the left, right, top, bottom of the target node.
121
- let rect = node. logical_rect ( gt) ;
122
- let ( left, right, top, bottom) = (
123
- rect. min . x + tooltip. offset . x ,
124
- rect. max . x + tooltip. offset . x ,
125
- rect. min . y + tooltip. offset . y ,
126
- rect. max . y + tooltip. offset . y ,
127
- ) ;
128
-
129
- // Set the left, right, top, bottom of the tooltip node.
130
- ( style. left , style. right , style. top , style. bottom ) = match tooltip. side {
131
- TooltipSide :: Left => ( Auto , Px ( width - left) , Auto , Px ( height - bottom) ) ,
132
- TooltipSide :: Right => ( Px ( right) , Auto , Auto , Px ( height - bottom) ) ,
133
- TooltipSide :: Top => ( Px ( left) , Auto , Auto , Px ( height - top) ) ,
134
- TooltipSide :: Bottom => ( Px ( left) , Auto , Px ( bottom) , Auto ) ,
135
- } ;
136
-
137
- // Exit early (because there's only one tooltip).
209
+ // Show tooltip.
210
+ if selection. 0 != entity {
211
+ events. send ( TooltipHover ( Some ( entity) ) ) ;
212
+ }
138
213
return ;
139
214
}
215
+
216
+ // Hide tooltip.
217
+ if entities. contains ( selection. 0 ) {
218
+ events. send ( TooltipHover ( None ) ) ;
219
+ }
140
220
}
0 commit comments