use crate::{CalculatedClip, Node}; use bevy_core::FloatOrd; use bevy_ecs::{ entity::Entity, prelude::Component, reflect::ReflectComponent, system::{Local, Query, Res}, }; use bevy_input::{mouse::MouseButton, touch::Touches, Input}; use bevy_math::Vec2; use bevy_reflect::{Reflect, ReflectDeserialize}; use bevy_transform::components::GlobalTransform; use bevy_window::Windows; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; /// Describes what type of input interaction has occurred for a UI node. /// /// This is commonly queried with a `Changed` filter. #[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)] #[reflect_value(Component, Serialize, Deserialize, PartialEq)] pub enum Interaction { /// The node has been clicked Clicked, /// The node has been hovered over Hovered, /// Nothing has happened None, } impl Default for Interaction { fn default() -> Self { Interaction::None } } /// Describes whether the node should block interactions with lower nodes #[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)] #[reflect_value(Component, Serialize, Deserialize, PartialEq)] pub enum FocusPolicy { /// Blocks interaction Block, /// Lets interaction pass through Pass, } impl Default for FocusPolicy { fn default() -> Self { FocusPolicy::Block } } /// Contains entities whose Interaction should be set to None #[derive(Default)] pub struct State { entities_to_reset: SmallVec<[Entity; 1]>, } /// The system that sets Interaction for all UI elements based on the mouse cursor activity #[allow(clippy::type_complexity)] pub fn ui_focus_system( mut state: Local, windows: Res, mouse_button_input: Res>, touches_input: Res, mut node_query: Query<( Entity, &Node, &GlobalTransform, Option<&mut Interaction>, Option<&FocusPolicy>, Option<&CalculatedClip>, )>, ) { let cursor_position = if let Some(cursor_position) = windows .get_primary() .and_then(|window| window.cursor_position()) { cursor_position } else { return; }; // reset entities that were both clicked and released in the last frame for entity in state.entities_to_reset.drain(..) { if let Ok(mut interaction) = node_query.get_component_mut::(entity) { *interaction = Interaction::None; } } let mouse_released = mouse_button_input.just_released(MouseButton::Left) || touches_input.just_released(0); if mouse_released { for (_entity, _node, _global_transform, interaction, _focus_policy, _clip) in node_query.iter_mut() { if let Some(mut interaction) = interaction { if *interaction == Interaction::Clicked { *interaction = Interaction::None; } } } } let mouse_clicked = mouse_button_input.just_pressed(MouseButton::Left) || touches_input.just_released(0); let mut moused_over_z_sorted_nodes = node_query .iter_mut() .filter_map( |(entity, node, global_transform, interaction, focus_policy, clip)| { let position = global_transform.translation; let ui_position = position.truncate(); let extents = node.size / 2.0; let mut min = ui_position - extents; let mut max = ui_position + extents; if let Some(clip) = clip { min = Vec2::max(min, clip.clip.min); max = Vec2::min(max, clip.clip.max); } // if the current cursor position is within the bounds of the node, consider it for // clicking if (min.x..max.x).contains(&cursor_position.x) && (min.y..max.y).contains(&cursor_position.y) { Some((entity, focus_policy, interaction, FloatOrd(position.z))) } else { if let Some(mut interaction) = interaction { if *interaction == Interaction::Hovered { *interaction = Interaction::None; } } None } }, ) .collect::>(); moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z); let mut moused_over_z_sorted_nodes = moused_over_z_sorted_nodes.into_iter(); // set Clicked or Hovered on top nodes for (entity, focus_policy, interaction, _) in moused_over_z_sorted_nodes.by_ref() { if let Some(mut interaction) = interaction { if mouse_clicked { // only consider nodes with Interaction "clickable" if *interaction != Interaction::Clicked { *interaction = Interaction::Clicked; // if the mouse was simultaneously released, reset this Interaction in the next // frame if mouse_released { state.entities_to_reset.push(entity); } } } else if *interaction == Interaction::None { *interaction = Interaction::Hovered; } } match focus_policy.cloned().unwrap_or(FocusPolicy::Block) { FocusPolicy::Block => { break; } FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ } } } // reset lower nodes to None for (_entity, _focus_policy, interaction, _) in moused_over_z_sorted_nodes { if let Some(mut interaction) = interaction { if *interaction != Interaction::None { *interaction = Interaction::None; } } } }