diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index b30062818dac1..776113b7c56bd 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -32,11 +32,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 @@ -154,19 +154,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)) @@ -189,113 +187,90 @@ 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, - ) - }); + 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); + } - // 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, - }; + // 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, + ) + }); - let contains_cursor = relative_cursor_position_component.mouse_over(); + // 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, + }; - // 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 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 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::>() - .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); } } } diff --git a/examples/ecs/state.rs b/examples/ecs/state.rs index 8e1802edd2d2e..b382f475deb0f 100644 --- a/examples/ecs/state.rs +++ b/examples/ecs/state.rs @@ -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); } diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index c7ea05559e347..cbdc94f651042 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -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(), @@ -379,7 +381,7 @@ mod menu { mut setting: ResMut, ) { 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::(); @@ -796,7 +798,7 @@ mod menu { mut game_state: ResMut>, ) { 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 => { diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 398ea2969fae1..226c7560364d2 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -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 => { diff --git a/examples/ui/button.rs b/examples/ui/button.rs index aa364baf8bc55..e944eee11c576 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -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(); }