bevy_ui/
focus.rs

1use crate::{
2    ui_transform::UiGlobalTransform, ComputedNode, ComputedUiTargetCamera, Node, OverrideClip,
3    UiStack,
4};
5use bevy_camera::{visibility::InheritedVisibility, Camera, NormalizedRenderTarget};
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/// 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)]
52pub 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
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/// 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)]
86pub 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
94impl 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)]
109pub enum FocusPolicy {
110    /// Blocks interaction
111    Block,
112    /// Lets interaction pass through
113    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/// Contains entities whose Interaction should be set to None
127#[derive(Default)]
128pub 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)]
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
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.
149pub fn ui_focus_system(
150    mut state: Local<State>,
151    camera_query: Query<(Entity, &Camera)>,
152    primary_window: Query<Entity, With<PrimaryWindow>>,
153    windows: Query<&Window>,
154    mouse_button_input: Res<ButtonInput<MouseButton>>,
155    touches_input: Res<Touches>,
156    ui_stack: Res<UiStack>,
157    mut node_query: Query<NodeQuery>,
158    clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
159    child_of_query: Query<&ChildOf, Without<OverrideClip>>,
160) {
161    let primary_window = primary_window.iter().next();
162
163    // reset entities that were both clicked and released in the last frame
164    for entity in state.entities_to_reset.drain(..) {
165        if let Ok(NodeQueryItem {
166            interaction: Some(mut interaction),
167            ..
168        }) = node_query.get_mut(entity)
169        {
170            *interaction = Interaction::None;
171        }
172    }
173
174    let mouse_released =
175        mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
176    if mouse_released {
177        for node in &mut node_query {
178            if let Some(mut interaction) = node.interaction
179                && *interaction == Interaction::Pressed
180            {
181                *interaction = Interaction::None;
182            }
183        }
184    }
185
186    let mouse_clicked =
187        mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();
188
189    let camera_cursor_positions: HashMap<Entity, Vec2> = camera_query
190        .iter()
191        .filter_map(|(entity, camera)| {
192            // Interactions are only supported for cameras rendering to a window.
193            let Some(NormalizedRenderTarget::Window(window_ref)) =
194                camera.target.normalize(primary_window)
195            else {
196                return None;
197            };
198            let window = windows.get(window_ref.entity()).ok()?;
199
200            let viewport_position = camera
201                .physical_viewport_rect()
202                .map(|rect| rect.min.as_vec2())
203                .unwrap_or_default();
204            window
205                .physical_cursor_position()
206                .or_else(|| {
207                    touches_input
208                        .first_pressed_position()
209                        .map(|pos| pos * window.scale_factor())
210                })
211                .map(|cursor_position| (entity, cursor_position - viewport_position))
212        })
213        .collect();
214
215    // prepare an iterator that contains all the nodes that have the cursor in their rect,
216    // from the top node to the bottom one. this will also reset the interaction to `None`
217    // for all nodes encountered that are no longer hovered.
218    let mut hovered_nodes = ui_stack
219        .uinodes
220        .iter()
221        // reverse the iterator to traverse the tree from closest nodes to furthest
222        .rev()
223        .filter_map(|entity| {
224            let Ok(node) = node_query.get_mut(*entity) else {
225                return None;
226            };
227
228            let inherited_visibility = node.inherited_visibility?;
229            // Nodes that are not rendered should not be interactable
230            if !inherited_visibility.get() {
231                // Reset their interaction to None to avoid strange stuck state
232                if let Some(mut interaction) = node.interaction {
233                    // We cannot simply set the interaction to None, as that will trigger change detection repeatedly
234                    interaction.set_if_neq(Interaction::None);
235                }
236                return None;
237            }
238            let camera_entity = node.target_camera.get()?;
239
240            let cursor_position = camera_cursor_positions.get(&camera_entity);
241
242            let contains_cursor = cursor_position.is_some_and(|point| {
243                node.node.contains_point(*node.transform, *point)
244                    && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query)
245            });
246
247            // The mouse position relative to the node
248            // (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner
249            // Coordinates are relative to the entire node, not just the visible region.
250            let normalized_cursor_position = cursor_position.and_then(|cursor_position| {
251                // ensure node size is non-zero in all dimensions, otherwise relative position will be
252                // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
253                // false positives for mouse_over (#12395)
254                node.node.normalize_point(*node.transform, *cursor_position)
255            });
256
257            // If the current cursor position is within the bounds of the node's visible area, consider it for
258            // clicking
259            let relative_cursor_position_component = RelativeCursorPosition {
260                cursor_over: contains_cursor,
261                normalized: normalized_cursor_position,
262            };
263
264            // Save the relative cursor position to the correct component
265            if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
266            {
267                // Avoid triggering change detection when not necessary.
268                node_relative_cursor_position_component
269                    .set_if_neq(relative_cursor_position_component);
270            }
271
272            if contains_cursor {
273                Some(*entity)
274            } else {
275                if let Some(mut interaction) = node.interaction
276                    && (*interaction == Interaction::Hovered
277                        || (normalized_cursor_position.is_none()))
278                {
279                    interaction.set_if_neq(Interaction::None);
280                }
281                None
282            }
283        })
284        .collect::<Vec<Entity>>()
285        .into_iter();
286
287    // set Pressed or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
288    // the iteration will stop on it because it "captures" the interaction.
289    let mut iter = node_query.iter_many_mut(hovered_nodes.by_ref());
290    while let Some(node) = iter.fetch_next() {
291        if let Some(mut interaction) = node.interaction {
292            if mouse_clicked {
293                // only consider nodes with Interaction "pressed"
294                if *interaction != Interaction::Pressed {
295                    *interaction = Interaction::Pressed;
296                    // if the mouse was simultaneously released, reset this Interaction in the next
297                    // frame
298                    if mouse_released {
299                        state.entities_to_reset.push(node.entity);
300                    }
301                }
302            } else if *interaction == Interaction::None {
303                *interaction = Interaction::Hovered;
304            }
305        }
306
307        match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
308            FocusPolicy::Block => {
309                break;
310            }
311            FocusPolicy::Pass => { /* allow the next node to be hovered/pressed */ }
312        }
313    }
314    // reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
315    // `moused_over_nodes` after the previous loop is exited.
316    let mut iter = node_query.iter_many_mut(hovered_nodes);
317    while let Some(node) = iter.fetch_next() {
318        if let Some(mut interaction) = node.interaction {
319            // don't reset pressed nodes because they're handled separately
320            if *interaction != Interaction::Pressed {
321                interaction.set_if_neq(Interaction::None);
322            }
323        }
324    }
325}
326
327/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node.
328/// If `entity` has an [`OverrideClip`] component it ignores any inherited clipping and returns true.
329pub fn clip_check_recursive(
330    point: Vec2,
331    entity: Entity,
332    clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>,
333    child_of_query: &Query<&ChildOf, Without<OverrideClip>>,
334) -> bool {
335    if let Ok(child_of) = child_of_query.get(entity) {
336        let parent = child_of.0;
337        if let Ok((computed_node, transform, node)) = clipping_query.get(parent)
338            && !computed_node
339                .resolve_clip_rect(node.overflow, node.overflow_clip_margin)
340                .contains(transform.inverse().transform_point2(point))
341        {
342            // The point is clipped and should be ignored by picking
343            return false;
344        }
345        return clip_check_recursive(point, parent, clipping_query, child_of_query);
346    }
347    // Reached root, point unclipped by all ancestors
348    true
349}