Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui/src/focus.rs
9367 views
1
use crate::{
2
ui_transform::UiGlobalTransform, ComputedNode, ComputedUiTargetCamera, Node, OverrideClip,
3
UiStack,
4
};
5
use bevy_camera::{visibility::InheritedVisibility, Camera, NormalizedRenderTarget, RenderTarget};
6
use bevy_ecs::{
7
change_detection::DetectChangesMut,
8
entity::{ContainsEntity, Entity},
9
hierarchy::ChildOf,
10
prelude::{Component, With},
11
query::{QueryData, Without},
12
reflect::ReflectComponent,
13
system::{Local, Query, Res},
14
};
15
use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
16
use bevy_math::Vec2;
17
use bevy_platform::collections::HashMap;
18
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
19
use bevy_window::{PrimaryWindow, Window};
20
21
use smallvec::SmallVec;
22
23
#[cfg(feature = "serialize")]
24
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
25
26
/// Describes what type of input interaction has occurred for a UI node.
27
///
28
/// This is commonly queried with a `Changed<Interaction>` filter.
29
///
30
/// Updated in [`ui_focus_system`].
31
///
32
/// If a UI node has both [`Interaction`] and [`InheritedVisibility`] components,
33
/// [`Interaction`] will always be [`Interaction::None`]
34
/// when [`InheritedVisibility::get()`] is false.
35
/// This ensures that hidden UI nodes are not interactable,
36
/// and do not end up stuck in an active state if hidden at the wrong time.
37
///
38
/// Note that you can also control the visibility of a node using the [`Display`](crate::ui_node::Display) property,
39
/// which fully collapses it during layout calculations.
40
///
41
/// # See also
42
///
43
/// - [`Button`](crate::widget::Button) which requires this component
44
/// - [`RelativeCursorPosition`] to obtain the position of the cursor relative to current node
45
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)]
46
#[reflect(Component, Default, PartialEq, Debug, Clone)]
47
#[cfg_attr(
48
feature = "serialize",
49
derive(serde::Serialize, serde::Deserialize),
50
reflect(Serialize, Deserialize)
51
)]
52
pub enum Interaction {
53
/// The node has been pressed.
54
///
55
/// Note: This does not capture click/press-release action.
56
Pressed,
57
/// The node has been hovered over
58
Hovered,
59
/// Nothing has happened
60
None,
61
}
62
63
impl Interaction {
64
const DEFAULT: Self = Self::None;
65
}
66
67
impl Default for Interaction {
68
fn default() -> Self {
69
Self::DEFAULT
70
}
71
}
72
73
/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right
74
/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5)
75
///
76
/// It can be used alongside [`Interaction`] to get the position of the press.
77
///
78
/// The component is updated when it is in the same entity with [`Node`].
79
#[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)]
80
#[reflect(Component, Default, PartialEq, Debug, Clone)]
81
#[cfg_attr(
82
feature = "serialize",
83
derive(serde::Serialize, serde::Deserialize),
84
reflect(Serialize, Deserialize)
85
)]
86
pub struct RelativeCursorPosition {
87
/// True if the cursor position is over an unclipped area of the Node.
88
pub cursor_over: bool,
89
/// Cursor position relative to the size and position of the Node.
90
/// A None value indicates that the cursor position is unknown.
91
pub normalized: Option<Vec2>,
92
}
93
94
impl RelativeCursorPosition {
95
/// A helper function to check if the mouse is over the node
96
pub fn cursor_over(&self) -> bool {
97
self.cursor_over
98
}
99
}
100
101
/// Describes whether the node should block interactions with lower nodes
102
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)]
103
#[reflect(Component, Default, PartialEq, Debug, Clone)]
104
#[cfg_attr(
105
feature = "serialize",
106
derive(serde::Serialize, serde::Deserialize),
107
reflect(Serialize, Deserialize)
108
)]
109
pub enum FocusPolicy {
110
/// Blocks interaction
111
Block,
112
/// Lets interaction pass through
113
Pass,
114
}
115
116
impl FocusPolicy {
117
const DEFAULT: Self = Self::Pass;
118
}
119
120
impl Default for FocusPolicy {
121
fn default() -> Self {
122
Self::DEFAULT
123
}
124
}
125
126
/// Contains entities whose Interaction should be set to None
127
#[derive(Default)]
128
pub struct State {
129
entities_to_reset: SmallVec<[Entity; 1]>,
130
}
131
132
/// Main query for [`ui_focus_system`]
133
#[derive(QueryData)]
134
#[query_data(mutable)]
135
pub struct NodeQuery {
136
entity: Entity,
137
node: &'static ComputedNode,
138
transform: &'static UiGlobalTransform,
139
interaction: Option<&'static mut Interaction>,
140
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
141
focus_policy: Option<&'static FocusPolicy>,
142
inherited_visibility: Option<&'static InheritedVisibility>,
143
target_camera: &'static ComputedUiTargetCamera,
144
}
145
146
/// The system that sets Interaction for all UI elements based on the mouse cursor activity
147
///
148
/// Entities with a hidden [`InheritedVisibility`] are always treated as released.
149
pub fn ui_focus_system(
150
mut hovered_nodes: Local<Vec<Entity>>,
151
mut state: Local<State>,
152
camera_query: Query<(Entity, &Camera, &RenderTarget)>,
153
primary_window: Query<Entity, With<PrimaryWindow>>,
154
windows: Query<&Window>,
155
mouse_button_input: Res<ButtonInput<MouseButton>>,
156
touches_input: Res<Touches>,
157
ui_stack: Res<UiStack>,
158
mut node_query: Query<NodeQuery>,
159
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
160
child_of_query: Query<&ChildOf, Without<OverrideClip>>,
161
) {
162
let primary_window = primary_window.iter().next();
163
164
// reset entities that were both clicked and released in the last frame
165
for entity in state.entities_to_reset.drain(..) {
166
if let Ok(NodeQueryItem {
167
interaction: Some(mut interaction),
168
..
169
}) = node_query.get_mut(entity)
170
{
171
*interaction = Interaction::None;
172
}
173
}
174
175
let mouse_released =
176
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
177
if mouse_released {
178
for node in &mut node_query {
179
if let Some(mut interaction) = node.interaction
180
&& *interaction == Interaction::Pressed
181
{
182
*interaction = Interaction::None;
183
}
184
}
185
}
186
187
let mouse_clicked =
188
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();
189
190
let camera_cursor_positions: HashMap<Entity, Vec2> = camera_query
191
.iter()
192
.filter_map(|(entity, camera, render_target)| {
193
// Interactions are only supported for cameras rendering to a window.
194
let Some(NormalizedRenderTarget::Window(window_ref)) =
195
render_target.normalize(primary_window)
196
else {
197
return None;
198
};
199
let window = windows.get(window_ref.entity()).ok()?;
200
201
let viewport_position = camera
202
.physical_viewport_rect()
203
.map(|rect| rect.min.as_vec2())
204
.unwrap_or_default();
205
window
206
.physical_cursor_position()
207
.or_else(|| {
208
touches_input
209
.first_pressed_position()
210
.map(|pos| pos * window.scale_factor())
211
})
212
.map(|cursor_position| (entity, cursor_position - viewport_position))
213
})
214
.collect();
215
216
// prepare an iterator that contains all the nodes that have the cursor in their rect,
217
// from the top node to the bottom one. this will also reset the interaction to `None`
218
// for all nodes encountered that are no longer hovered.
219
220
hovered_nodes.clear();
221
// reverse the iterator to traverse the tree from closest slice to furthest
222
for uinodes in ui_stack
223
.partition
224
.iter()
225
.rev()
226
.map(|range| &ui_stack.uinodes[range.clone()])
227
{
228
// Retrieve the first node and resolve its camera target.
229
// Only need to do this once per slice, as all the nodes in the slice share the same camera.
230
let Ok(root_node) = node_query.get_mut(uinodes[0]) else {
231
continue;
232
};
233
234
let Some(camera_entity) = root_node.target_camera.get() else {
235
continue;
236
};
237
238
let cursor_position = camera_cursor_positions.get(&camera_entity);
239
240
for entity in uinodes.iter().rev().cloned() {
241
let Ok(node) = node_query.get_mut(entity) else {
242
continue;
243
};
244
245
let Some(inherited_visibility) = node.inherited_visibility else {
246
continue;
247
};
248
249
// Nodes that are not rendered should not be interactable
250
if !inherited_visibility.get() {
251
// Reset their interaction to None to avoid strange stuck state
252
if let Some(mut interaction) = node.interaction {
253
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
254
interaction.set_if_neq(Interaction::None);
255
}
256
continue;
257
}
258
259
let contains_cursor = cursor_position.is_some_and(|point| {
260
node.node.contains_point(*node.transform, *point)
261
&& clip_check_recursive(*point, entity, &clipping_query, &child_of_query)
262
});
263
264
// The mouse position relative to the node
265
// (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner
266
// Coordinates are relative to the entire node, not just the visible region.
267
let normalized_cursor_position = cursor_position.and_then(|cursor_position| {
268
// ensure node size is non-zero in all dimensions, otherwise relative position will be
269
// +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
270
// false positives for mouse_over (#12395)
271
node.node.normalize_point(*node.transform, *cursor_position)
272
});
273
274
// If the current cursor position is within the bounds of the node's visible area, consider it for
275
// clicking
276
let relative_cursor_position_component = RelativeCursorPosition {
277
cursor_over: contains_cursor,
278
normalized: normalized_cursor_position,
279
};
280
281
// Save the relative cursor position to the correct component
282
if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
283
{
284
// Avoid triggering change detection when not necessary.
285
node_relative_cursor_position_component
286
.set_if_neq(relative_cursor_position_component);
287
}
288
289
if contains_cursor {
290
hovered_nodes.push(entity);
291
} else {
292
if let Some(mut interaction) = node.interaction
293
&& (*interaction == Interaction::Hovered
294
|| (normalized_cursor_position.is_none()))
295
{
296
interaction.set_if_neq(Interaction::None);
297
}
298
continue;
299
}
300
}
301
}
302
303
// set Pressed or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
304
// the iteration will stop on it because it "captures" the interaction.
305
let mut hovered_nodes = hovered_nodes.iter();
306
let mut iter = node_query.iter_many_mut(hovered_nodes.by_ref());
307
while let Some(node) = iter.fetch_next() {
308
if let Some(mut interaction) = node.interaction {
309
if mouse_clicked {
310
// only consider nodes with Interaction "pressed"
311
if *interaction != Interaction::Pressed {
312
*interaction = Interaction::Pressed;
313
// if the mouse was simultaneously released, reset this Interaction in the next
314
// frame
315
if mouse_released {
316
state.entities_to_reset.push(node.entity);
317
}
318
}
319
} else if *interaction == Interaction::None {
320
*interaction = Interaction::Hovered;
321
}
322
}
323
324
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
325
FocusPolicy::Block => {
326
break;
327
}
328
FocusPolicy::Pass => { /* allow the next node to be hovered/pressed */ }
329
}
330
}
331
// reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
332
// `moused_over_nodes` after the previous loop is exited.
333
let mut iter = node_query.iter_many_mut(hovered_nodes);
334
while let Some(node) = iter.fetch_next() {
335
if let Some(mut interaction) = node.interaction {
336
// don't reset pressed nodes because they're handled separately
337
if *interaction != Interaction::Pressed {
338
interaction.set_if_neq(Interaction::None);
339
}
340
}
341
}
342
}
343
344
/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node.
345
/// If `entity` has an [`OverrideClip`] component it ignores any inherited clipping and returns true.
346
pub fn clip_check_recursive(
347
point: Vec2,
348
entity: Entity,
349
clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>,
350
child_of_query: &Query<&ChildOf, Without<OverrideClip>>,
351
) -> bool {
352
if let Ok(child_of) = child_of_query.get(entity) {
353
let parent = child_of.0;
354
if let Ok((computed_node, transform, node)) = clipping_query.get(parent)
355
&& !computed_node
356
.resolve_clip_rect(node.overflow, node.overflow_clip_margin)
357
.contains(transform.inverse().transform_point2(point))
358
{
359
// The point is clipped and should be ignored by picking
360
return false;
361
}
362
return clip_check_recursive(point, parent, clipping_query, child_of_query);
363
}
364
// Reached root, point unclipped by all ancestors
365
true
366
}
367
368