Skip to content

Commit

Permalink
Remove RelativeCursorPosition, add clicked position to Interaction::C…
Browse files Browse the repository at this point in the history
…licked, improve readability of ui_focus_system
  • Loading branch information
valaphee committed Feb 15, 2023
1 parent 8d51f4a commit 093cbc9
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 219 deletions.
190 changes: 57 additions & 133 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
Expand Down Expand Up @@ -32,11 +31,11 @@ use smallvec::SmallVec;
///
/// 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.
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[derive(Component, Copy, Clone, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize, PartialEq)]
pub enum Interaction {
/// The node has been clicked
Clicked,
Clicked(Vec2),
/// The node has been hovered over
Hovered,
/// Nothing has happened
Expand All @@ -53,39 +52,6 @@ impl Default for Interaction {
}
}

/// 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.)
/// A None value means that the cursor position is unknown.
///
/// It can be used alongside interaction to get the position of the press.
#[derive(
Component,
Deref,
DerefMut,
Copy,
Clone,
Default,
PartialEq,
Debug,
Reflect,
Serialize,
Deserialize,
)]
#[reflect(Component, Serialize, Deserialize, PartialEq)]
pub struct RelativeCursorPosition {
/// Cursor position relative to size and position of the Node.
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| (0.0..1.).contains(&position.x) && (0.0..1.).contains(&position.y))
.unwrap_or(false)
}
}

/// Describes whether the node should block interactions with lower nodes
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize, PartialEq)]
Expand Down Expand Up @@ -120,7 +86,6 @@ pub struct NodeQuery {
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>,
computed_visibility: Option<&'static ComputedVisibility>,
Expand Down Expand Up @@ -154,19 +119,17 @@ pub fn ui_focus_system(
if mouse_released {
for node in node_query.iter_mut() {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Clicked {
if matches!(*interaction, Interaction::Clicked(_)) {
*interaction = Interaction::None;
}
}
}
}

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

let is_ui_disabled =
|camera_ui| matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. }));

let cursor_position = camera
.iter()
.filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui))
Expand All @@ -189,113 +152,74 @@ pub fn ui_focus_system(
})
.or_else(|| touches_input.first_pressed_position());

// 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 moused_over_nodes = ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
.filter_map(|entity| {
if let Ok(node) = node_query.get_mut(*entity) {
// Nodes that are not rendered should not be interactable
if let Some(computed_visibility) = node.computed_visibility {
if !computed_visibility.is_visible() {
// 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;
// Iterate through all nodes from top to bottom
let mut focus_blocked = false;
for entity in ui_stack.uinodes.iter().rev() {
if let Ok(node) = node_query.get_mut(*entity) {
// Nodes that are not rendered should not be interactable
if let Some(computed_visibility) = node.computed_visibility {
if !computed_visibility.is_visible() {
// 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);
}
}

let position = node.global_transform.translation();
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
let mut min = ui_position - extents;
if let Some(clip) = node.calculated_clip {
min = Vec2::max(min, clip.clip.min);
continue;
}
}

// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
let relative_cursor_position = cursor_position.map(|cursor_position| {
Vec2::new(
(cursor_position.x - min.x) / node.node.size().x,
(cursor_position.y - min.y) / node.node.size().y,
)
});

// If the current cursor position is within the bounds of the node, consider it for
// clicking
let relative_cursor_position_component = RelativeCursorPosition {
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;
}
let position = node.global_transform.translation();
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
let mut min = ui_position - extents;
let mut max = ui_position + extents;
if let Some(clip) = node.calculated_clip {
min = Vec2::max(min, clip.clip.min);
max = Vec2::max(max, clip.clip.max);
}

let contains_cursor = cursor_position
.map(|cursor_position| {
(min.x..max.x).contains(&cursor_position.x)
&& (min.y..max.y).contains(&cursor_position.y)
})
.unwrap_or(false);
if let Some(mut interaction) = node.interaction {
if contains_cursor {
Some(*entity)
} else {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Hovered || (cursor_position.is_none()) {
if focus_blocked {
// don't reset clicked nodes because they're handled separately
if !matches!(*interaction, Interaction::Clicked(_)) {
interaction.set_if_neq(Interaction::None);
}
} else if mouse_clicked {
// only consider nodes with Interaction "clickable"
if !matches!(*interaction, Interaction::Clicked(_)) {
*interaction = Interaction::Clicked(Vec2::new(
cursor_position.unwrap().x - min.x,
cursor_position.unwrap().y - min.y,
));
// 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;
}
None
} else if *interaction == Interaction::Hovered || cursor_position.is_none() {
interaction.set_if_neq(Interaction::None);
}
} else {
None
}
})
.collect::<Vec<Entity>>()
.into_iter();

// set Clicked 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(moused_over_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 "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(node.entity);
if contains_cursor && !focus_blocked {
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
FocusPolicy::Block => {
focus_blocked = true;
}
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
}
} 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/clicked */ }
}
}
// 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(moused_over_nodes);
while let Some(node) = iter.fetch_next() {
if let Some(mut interaction) = node.interaction {
// don't reset clicked nodes because they're handled separately
if *interaction != Interaction::Clicked {
interaction.set_if_neq(Interaction::None);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/ecs/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ fn menu(
) {
for (interaction, mut color) in &mut interaction_query {
match *interaction {
Interaction::Clicked => {
Interaction::Clicked(_) => {
*color = PRESSED_BUTTON.into();
next_state.set(AppState::InGame);
}
Expand Down
8 changes: 5 additions & 3 deletions examples/games/game_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,9 @@ mod menu {
) {
for (interaction, mut color, selected) in &mut interaction_query {
*color = match (*interaction, selected) {
(Interaction::Clicked, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
(Interaction::Clicked(_), _) | (Interaction::None, Some(_)) => {
PRESSED_BUTTON.into()
}
(Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
(Interaction::Hovered, None) => HOVERED_BUTTON.into(),
(Interaction::None, None) => NORMAL_BUTTON.into(),
Expand All @@ -379,7 +381,7 @@ mod menu {
mut setting: ResMut<T>,
) {
for (interaction, button_setting, entity) in &interaction_query {
if *interaction == Interaction::Clicked && *setting != *button_setting {
if matches!(*interaction, Interaction::Clicked(_)) && *setting != *button_setting {
let (previous_button, mut previous_color) = selected_query.single_mut();
*previous_color = NORMAL_BUTTON.into();
commands.entity(previous_button).remove::<SelectedOption>();
Expand Down Expand Up @@ -796,7 +798,7 @@ mod menu {
mut game_state: ResMut<NextState<GameState>>,
) {
for (interaction, menu_button_action) in &interaction_query {
if *interaction == Interaction::Clicked {
if matches!(*interaction, Interaction::Clicked(_)) {
match menu_button_action {
MenuButtonAction::Quit => app_exit_events.send(AppExit),
MenuButtonAction::Play => {
Expand Down
2 changes: 1 addition & 1 deletion examples/mobile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ fn button_handler(
) {
for (interaction, mut color) in &mut interaction_query {
match *interaction {
Interaction::Clicked => {
Interaction::Clicked(_) => {
*color = Color::BLUE.into();
}
Interaction::Hovered => {
Expand Down
2 changes: 1 addition & 1 deletion examples/ui/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn button_system(
for (interaction, mut color, children) in &mut interaction_query {
let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Clicked => {
Interaction::Clicked(_) => {
text.sections[0].value = "Press".to_string();
*color = PRESSED_BUTTON.into();
}
Expand Down
80 changes: 0 additions & 80 deletions examples/ui/relative_cursor_position.rs

This file was deleted.

0 comments on commit 093cbc9

Please sign in to comment.