1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
use crate::{CalculatedClip, DefaultUiCamera, Node, TargetCamera, UiScale, UiStack};
use bevy_ecs::{
    change_detection::DetectChangesMut,
    entity::Entity,
    prelude::{Component, With},
    query::QueryData,
    reflect::ReflectComponent,
    system::{Local, Query, Res},
};
use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
use bevy_math::{Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::ViewVisibility};
use bevy_transform::components::GlobalTransform;

use bevy_utils::{smallvec::SmallVec, HashMap};
use bevy_window::{PrimaryWindow, Window};

#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};

/// Describes what type of input interaction has occurred for a UI node.
///
/// This is commonly queried with a `Changed<Interaction>` filter.
///
/// Updated in [`ui_focus_system`].
///
/// If a UI node has both [`Interaction`] and [`ViewVisibility`] components,
/// [`Interaction`] will always be [`Interaction::None`]
/// when [`ViewVisibility::get()`] is false.
/// This ensures that hidden UI nodes are not interactable,
/// and do not end up stuck in an active state if hidden at the wrong time.
///
/// Note that you can also control the visibility of a node using the [`Display`](crate::ui_node::Display) property,
/// which fully collapses it during layout calculations.
///
/// # See also
///
/// - [`ButtonBundle`](crate::node_bundles::ButtonBundle) which includes this component
/// - [`RelativeCursorPosition`] to obtain the position of the cursor relative to current node
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq)]
#[cfg_attr(
    feature = "serialize",
    derive(serde::Serialize, serde::Deserialize),
    reflect(Serialize, Deserialize)
)]
pub enum Interaction {
    /// The node has been pressed.
    ///
    /// Note: This does not capture click/press-release action.
    Pressed,
    /// The node has been hovered over
    Hovered,
    /// Nothing has happened
    None,
}

impl Interaction {
    const DEFAULT: Self = Self::None;
}

impl Default for Interaction {
    fn default() -> Self {
        Self::DEFAULT
    }
}

/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right
/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.)
///
/// It can be used alongside [`Interaction`] to get the position of the press.
///
/// The component is updated when it is in the same entity with [`Node`].
#[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq)]
#[cfg_attr(
    feature = "serialize",
    derive(serde::Serialize, serde::Deserialize),
    reflect(Serialize, Deserialize)
)]
pub struct RelativeCursorPosition {
    /// Visible area of the Node relative to the size of the entire Node.
    pub normalized_visible_node_rect: Rect,
    /// Cursor position relative to the size and position of the Node.
    /// A None value indicates that the cursor position is unknown.
    pub normalized: Option<Vec2>,
}

impl RelativeCursorPosition {
    /// A helper function to check if the mouse is over the node
    pub fn mouse_over(&self) -> bool {
        self.normalized
            .map(|position| self.normalized_visible_node_rect.contains(position))
            .unwrap_or(false)
    }
}

/// Describes whether the node should block interactions with lower nodes
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq)]
#[cfg_attr(
    feature = "serialize",
    derive(serde::Serialize, serde::Deserialize),
    reflect(Serialize, Deserialize)
)]
pub enum FocusPolicy {
    /// Blocks interaction
    Block,
    /// Lets interaction pass through
    Pass,
}

impl FocusPolicy {
    const DEFAULT: Self = Self::Pass;
}

impl Default for FocusPolicy {
    fn default() -> Self {
        Self::DEFAULT
    }
}

/// Contains entities whose Interaction should be set to None
#[derive(Default)]
pub struct State {
    entities_to_reset: SmallVec<[Entity; 1]>,
}

/// Main query for [`ui_focus_system`]
#[derive(QueryData)]
#[query_data(mutable)]
pub struct NodeQuery {
    entity: Entity,
    node: &'static Node,
    global_transform: &'static GlobalTransform,
    interaction: Option<&'static mut Interaction>,
    relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
    focus_policy: Option<&'static FocusPolicy>,
    calculated_clip: Option<&'static CalculatedClip>,
    view_visibility: Option<&'static ViewVisibility>,
    target_camera: Option<&'static TargetCamera>,
}

/// The system that sets Interaction for all UI elements based on the mouse cursor activity
///
/// Entities with a hidden [`ViewVisibility`] are always treated as released.
#[allow(clippy::too_many_arguments)]
pub fn ui_focus_system(
    mut state: Local<State>,
    camera_query: Query<(Entity, &Camera)>,
    default_ui_camera: DefaultUiCamera,
    primary_window: Query<Entity, With<PrimaryWindow>>,
    windows: Query<&Window>,
    mouse_button_input: Res<ButtonInput<MouseButton>>,
    touches_input: Res<Touches>,
    ui_scale: Res<UiScale>,
    ui_stack: Res<UiStack>,
    mut node_query: Query<NodeQuery>,
) {
    let primary_window = primary_window.iter().next();

    // reset entities that were both clicked and released in the last frame
    for entity in state.entities_to_reset.drain(..) {
        if let Ok(NodeQueryItem {
            interaction: Some(mut interaction),
            ..
        }) = node_query.get_mut(entity)
        {
            *interaction = Interaction::None;
        }
    }

    let mouse_released =
        mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
    if mouse_released {
        for node in &mut node_query {
            if let Some(mut interaction) = node.interaction {
                if *interaction == Interaction::Pressed {
                    *interaction = Interaction::None;
                }
            }
        }
    }

    let mouse_clicked =
        mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();

    let camera_cursor_positions: HashMap<Entity, Vec2> = camera_query
        .iter()
        .filter_map(|(entity, camera)| {
            // Interactions are only supported for cameras rendering to a window.
            let Some(NormalizedRenderTarget::Window(window_ref)) =
                camera.target.normalize(primary_window)
            else {
                return None;
            };

            let viewport_position = camera
                .logical_viewport_rect()
                .map(|rect| rect.min)
                .unwrap_or_default();
            windows
                .get(window_ref.entity())
                .ok()
                .and_then(|window| window.cursor_position())
                .or_else(|| touches_input.first_pressed_position())
                .map(|cursor_position| (entity, cursor_position - viewport_position))
        })
        // The cursor position returned by `Window` only takes into account the window scale factor and not `UiScale`.
        // To convert the cursor position to logical UI viewport coordinates we have to divide it by `UiScale`.
        .map(|(entity, cursor_position)| (entity, cursor_position / ui_scale.0))
        .collect();

    // prepare an iterator that contains all the nodes that have the cursor in their rect,
    // from the top node to the bottom one. this will also reset the interaction to `None`
    // for all nodes encountered that are no longer hovered.
    let mut hovered_nodes = ui_stack
        .uinodes
        .iter()
        // reverse the iterator to traverse the tree from closest nodes to furthest
        .rev()
        .filter_map(|entity| {
            let Ok(node) = node_query.get_mut(*entity) else {
                return None;
            };

            let view_visibility = node.view_visibility?;
            // Nodes that are not rendered should not be interactable
            if !view_visibility.get() {
                // Reset their interaction to None to avoid strange stuck state
                if let Some(mut interaction) = node.interaction {
                    // We cannot simply set the interaction to None, as that will trigger change detection repeatedly
                    interaction.set_if_neq(Interaction::None);
                }
                return None;
            }
            let camera_entity = node
                .target_camera
                .map(TargetCamera::entity)
                .or(default_ui_camera.get())?;

            let node_rect = node.node.logical_rect(node.global_transform);

            // Intersect with the calculated clip rect to find the bounds of the visible region of the node
            let visible_rect = node
                .calculated_clip
                .map(|clip| node_rect.intersect(clip.clip))
                .unwrap_or(node_rect);

            // The mouse position relative to the node
            // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
            // Coordinates are relative to the entire node, not just the visible region.
            let relative_cursor_position = camera_cursor_positions
                .get(&camera_entity)
                .map(|cursor_position| (*cursor_position - node_rect.min) / node_rect.size());

            // If the current cursor position is within the bounds of the node's visible area, consider it for
            // clicking
            let relative_cursor_position_component = RelativeCursorPosition {
                normalized_visible_node_rect: visible_rect.normalize(node_rect),
                normalized: relative_cursor_position,
            };

            let contains_cursor = relative_cursor_position_component.mouse_over();

            // Save the relative cursor position to the correct component
            if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
            {
                *node_relative_cursor_position_component = relative_cursor_position_component;
            }

            if contains_cursor {
                Some(*entity)
            } else {
                if let Some(mut interaction) = node.interaction {
                    if *interaction == Interaction::Hovered || (relative_cursor_position.is_none())
                    {
                        interaction.set_if_neq(Interaction::None);
                    }
                }
                None
            }
        })
        .collect::<Vec<Entity>>()
        .into_iter();

    // set Pressed or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
    // the iteration will stop on it because it "captures" the interaction.
    let mut iter = node_query.iter_many_mut(hovered_nodes.by_ref());
    while let Some(node) = iter.fetch_next() {
        if let Some(mut interaction) = node.interaction {
            if mouse_clicked {
                // only consider nodes with Interaction "pressed"
                if *interaction != Interaction::Pressed {
                    *interaction = Interaction::Pressed;
                    // if the mouse was simultaneously released, reset this Interaction in the next
                    // frame
                    if mouse_released {
                        state.entities_to_reset.push(node.entity);
                    }
                }
            } else if *interaction == Interaction::None {
                *interaction = Interaction::Hovered;
            }
        }

        match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
            FocusPolicy::Block => {
                break;
            }
            FocusPolicy::Pass => { /* allow the next node to be hovered/pressed */ }
        }
    }
    // reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
    // `moused_over_nodes` after the previous loop is exited.
    let mut iter = node_query.iter_many_mut(hovered_nodes);
    while let Some(node) = iter.fetch_next() {
        if let Some(mut interaction) = node.interaction {
            // don't reset pressed nodes because they're handled separately
            if *interaction != Interaction::Pressed {
                interaction.set_if_neq(Interaction::None);
            }
        }
    }
}