1use crate::{
2 ui_transform::UiGlobalTransform, ComputedNode, ComputedUiTargetCamera, Node, OverrideClip,
3 UiStack,
4};
5use bevy_camera::{visibility::InheritedVisibility, Camera, NormalizedRenderTarget, RenderTarget};
6use 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};
15use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
16use bevy_math::Vec2;
17use bevy_platform::collections::HashMap;
18use bevy_reflect::{std_traits::ReflectDefault, Reflect};
19use bevy_window::{PrimaryWindow, Window};
20
21use smallvec::SmallVec;
22
23#[cfg(feature = "serialize")]
24use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
25
26#[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)]
52pub enum Interaction {
53 Pressed,
57 Hovered,
59 None,
61}
62
63impl Interaction {
64 const DEFAULT: Self = Self::None;
65}
66
67impl Default for Interaction {
68 fn default() -> Self {
69 Self::DEFAULT
70 }
71}
72
73#[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)]
86pub struct RelativeCursorPosition {
87 pub cursor_over: bool,
89 pub normalized: Option<Vec2>,
92}
93
94impl RelativeCursorPosition {
95 pub fn cursor_over(&self) -> bool {
97 self.cursor_over
98 }
99}
100
101#[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)]
109pub enum FocusPolicy {
110 Block,
112 Pass,
114}
115
116impl FocusPolicy {
117 const DEFAULT: Self = Self::Pass;
118}
119
120impl Default for FocusPolicy {
121 fn default() -> Self {
122 Self::DEFAULT
123 }
124}
125
126#[derive(Default)]
128pub struct State {
129 entities_to_reset: SmallVec<[Entity; 1]>,
130}
131
132#[derive(QueryData)]
134#[query_data(mutable)]
135pub 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
146pub 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 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 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 hovered_nodes.clear();
221 for uinodes in ui_stack
223 .partition
224 .iter()
225 .rev()
226 .map(|range| &ui_stack.uinodes[range.clone()])
227 {
228 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 if !inherited_visibility.get() {
251 if let Some(mut interaction) = node.interaction {
253 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 let normalized_cursor_position = cursor_position.and_then(|cursor_position| {
268 node.node.normalize_point(*node.transform, *cursor_position)
272 });
273
274 let relative_cursor_position_component = RelativeCursorPosition {
277 cursor_over: contains_cursor,
278 normalized: normalized_cursor_position,
279 };
280
281 if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
283 {
284 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 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 if *interaction != Interaction::Pressed {
312 *interaction = Interaction::Pressed;
313 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 => { }
329 }
330 }
331 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 if *interaction != Interaction::Pressed {
338 interaction.set_if_neq(Interaction::None);
339 }
340 }
341 }
342}
343
344pub 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 return false;
361 }
362 return clip_check_recursive(point, parent, clipping_query, child_of_query);
363 }
364 true
366}