From 9ec11c6c25fcb4c9aec5fd16bc75d0eeb8591c33 Mon Sep 17 00:00:00 2001 From: pubrrr Date: Sun, 20 Mar 2022 12:04:25 +0100 Subject: [PATCH 1/6] document current behavior by unit tests --- crates/bevy_ui/src/focus.rs | 410 +++++++++++++++++++++++++++++++++++- 1 file changed, 405 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index bf01512d57b80..c9f53f39009e3 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -4,7 +4,7 @@ use bevy_ecs::{ entity::Entity, prelude::Component, reflect::ReflectComponent, - system::{Local, Query, Res}, + system::{Local, Query, Res, Resource}, }; use bevy_input::{mouse::MouseButton, touch::Touches, Input}; use bevy_math::Vec2; @@ -59,10 +59,34 @@ pub struct State { /// 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, + state: Local, windows: Res, mouse_button_input: Res>, touches_input: Res, + node_query: Query<( + Entity, + &Node, + &GlobalTransform, + Option<&mut Interaction>, + Option<&FocusPolicy>, + Option<&CalculatedClip>, + )>, +) { + focus_ui( + state, + windows, + mouse_button_input, + touches_input, + node_query, + ) +} + +#[allow(clippy::type_complexity)] +fn focus_ui( + mut state: Local, + windows: Res, + mouse_button_input: Res>, + touches_input: Res, mut node_query: Query<( Entity, &Node, @@ -72,9 +96,7 @@ pub fn ui_focus_system( Option<&CalculatedClip>, )>, ) { - let cursor_position = windows - .get_primary() - .and_then(|window| window.cursor_position()); + let cursor_position = windows.get_cursor_position(); // reset entities that were both clicked and released in the last frame for entity in state.entities_to_reset.drain(..) { @@ -175,3 +197,381 @@ pub fn ui_focus_system( } } } + +trait CursorResource: Resource { + fn get_cursor_position(&self) -> Option; +} + +impl CursorResource for Windows { + fn get_cursor_position(&self) -> Option { + self.get_primary() + .and_then(|window| window.cursor_position()) + } +} + +#[cfg(test)] +mod tests { + use bevy_app::App; + use bevy_ecs::event::Events; + use bevy_ecs::prelude::ParallelSystemDescriptorCoercion; + use bevy_input::touch::{touch_screen_input_system, TouchInput, TouchPhase}; + use bevy_math::Vec3; + + use super::*; + + const NODE_SIZE: f32 = 5.0; + + #[test] + fn test_sets_hovered_nodes() { + let test_sets = vec![ + vec![(None, Interaction::None)], + vec![(Some((0., 0.)), Interaction::None)], + vec![(Some((10., 10.)), Interaction::Hovered)], + vec![ + (Some((10., 10.)), Interaction::Hovered), + (Some((0., 0.)), Interaction::None), + ], + vec![ + (Some((10., 10.)), Interaction::Hovered), + (None, Interaction::None), + ], + ]; + + for test_set in test_sets { + test_hovered_nodes(test_set); + } + } + + fn test_hovered_nodes(test_set: Vec<(Option<(f32, f32)>, Interaction)>) { + let mut app = TestApp::new(); + let entity = app.spawn_node_entity_at(10., 10.); + + for (cursor_position, expected_interaction) in test_set { + app.set_cursor_position(cursor_position); + + app.run_step(); + + let interaction = app.get_interaction(entity); + assert_eq!( + &expected_interaction, interaction, + "for position {:?}", + cursor_position, + ); + } + } + + #[test] + fn test_sets_clicked_nodes() { + let test_sets_mouse = vec![ + vec![(None, Interaction::None)], + vec![(Some((0., 0.)), Interaction::None)], + vec![(Some((10., 10.)), Interaction::Clicked)], + vec![ + (Some((10., 10.)), Interaction::Clicked), + (Some((0., 0.)), Interaction::Clicked), + ], + vec![ + (Some((10., 10.)), Interaction::Clicked), + (None, Interaction::None), + ], + ]; + let test_sets_touch = vec![ + vec![(None, Interaction::None)], + vec![(Some((0., 0.)), Interaction::None)], + vec![(Some((10., 10.)), Interaction::Clicked)], + vec![ + (Some((10., 10.)), Interaction::Clicked), + (Some((0., 0.)), Interaction::None), + ], + vec![ + (Some((10., 10.)), Interaction::Clicked), + (None, Interaction::None), + ], + ]; + + for mouse_test_set in test_sets_mouse { + test_clicked_nodes(mouse_test_set, false); + } + for touch_test_set in test_sets_touch { + test_clicked_nodes(touch_test_set, true); + } + } + + fn test_clicked_nodes(test_set: Vec<(Option<(f32, f32)>, Interaction)>, touch: bool) { + let mut app = TestApp::new(); + let entity = app.spawn_node_entity_at(10., 10.); + + for (cursor_position, expected_interaction) in test_set { + app.set_cursor_position(cursor_position); + if touch { + app.set_screen_touched(); + } else { + app.set_mouse_clicked(); + } + + app.run_step(); + + let interaction = app.get_interaction(entity); + assert_eq!( + &expected_interaction, interaction, + "for position {:?}", + cursor_position, + ); + } + } + + #[test] + fn test_sets_click_stacked_nodes() { + let test_sets = vec![ + (None, Interaction::None), + (Some(FocusPolicy::Block), Interaction::None), + (Some(FocusPolicy::Pass), Interaction::Clicked), + ]; + + for (focus_policy, expected_interaction) in test_sets { + test_click_stacked_nodes(focus_policy, expected_interaction); + } + } + + fn test_click_stacked_nodes( + focus_policy: Option, + expected_interaction: Interaction, + ) { + let mut app = TestApp::new(); + let background_entity = app.spawn_node_entity_with_z_at(10., 10., 0., focus_policy); + let foreground_entity = app.spawn_node_entity_with_z_at(10., 10., 5., focus_policy); + + app.set_cursor_position(Some((10., 10.))); + app.set_mouse_clicked(); + + app.run_step(); + + assert_eq!( + &Interaction::Clicked, + app.get_interaction(foreground_entity) + ); + assert_eq!( + &expected_interaction, + app.get_interaction(background_entity) + ); + } + + #[test] + fn hover_one_node_then_click_the_other_where_both_overlap() { + let mut app = TestApp::new(); + let background_node_position = 8.; + let background_entity = app.spawn_node_entity_with_z_at( + background_node_position, + background_node_position, + 0., + Some(FocusPolicy::Block), + ); + let foreground_entity = + app.spawn_node_entity_with_z_at(10., 10., 5., Some(FocusPolicy::Block)); + + app.set_cursor_position(Some((6., 6.))); + + app.run_step(); + + assert_eq!(&Interaction::None, app.get_interaction(foreground_entity)); + assert_eq!( + &Interaction::Hovered, + app.get_interaction(background_entity) + ); + + app.set_cursor_position(Some((background_node_position, background_node_position))); + app.set_mouse_clicked(); + + app.run_step(); + + assert_eq!( + &Interaction::Clicked, + app.get_interaction(foreground_entity) + ); + assert_eq!(&Interaction::None, app.get_interaction(background_entity)); + } + + #[test] + fn click_then_move_away_and_release_mouse_button() { + let mut app = TestApp::new(); + let entity = app.spawn_node_entity_at(10., 10.); + + app.set_cursor_position(Some((10., 10.))); + app.set_mouse_clicked(); + + app.run_step(); + assert_eq!(&Interaction::Clicked, app.get_interaction(entity)); + + app.set_cursor_position(Some((0., 0.))); + + app.run_step(); + assert_eq!(&Interaction::Clicked, app.get_interaction(entity)); + + app.set_mouse_released(); + + app.run_step(); + assert_eq!(&Interaction::None, app.get_interaction(entity)); + } + + #[test] + fn click_and_keep_pressed() { + let mut app = TestApp::new(); + let entity = app.spawn_node_entity_at(10., 10.); + app.set_cursor_position(Some((10., 10.))); + app.set_mouse_clicked(); + + app.run_step(); + assert_eq!(&Interaction::Clicked, app.get_interaction(entity)); + + app.run_step(); + assert_eq!(&Interaction::Clicked, app.get_interaction(entity)); + } + + #[test] + fn click_and_release() { + let mut app = TestApp::new(); + let entity = app.spawn_node_entity_at(10., 10.); + app.set_cursor_position(Some((10., 10.))); + + app.set_mouse_clicked(); + app.run_step(); + assert_eq!(&Interaction::Clicked, app.get_interaction(entity)); + + app.set_mouse_released(); + app.run_step(); + assert_eq!(&Interaction::Hovered, app.get_interaction(entity)); + } + + #[test] + fn click_and_release_in_single_frame() { + let mut app = TestApp::new(); + let entity = app.spawn_node_entity_at(10., 10.); + app.set_cursor_position(Some((10., 10.))); + + app.set_mouse_clicked(); + app.set_mouse_released(); + app.run_step(); + assert_eq!(&Interaction::Clicked, app.get_interaction(entity)); + + app.run_step(); + assert_eq!(&Interaction::Hovered, app.get_interaction(entity)); + } + + struct TestApp { + app: App, + } + + impl TestApp { + fn new() -> TestApp { + let mut app = App::new(); + app.init_resource::>() + .init_resource::() + .add_event::() + .add_system(focus_ui::.label("under_test")) + .add_system(touch_screen_input_system.before("under_test")); + + TestApp { app } + } + + fn set_cursor_position(&mut self, cursor_position: Option<(f32, f32)>) { + let cursor_position = cursor_position.map(|(x, y)| Vec2::new(x, y)); + self.app.insert_resource(WindowsDouble { cursor_position }); + } + + fn set_screen_touched(&mut self) { + self.app + .world + .get_resource_mut::>() + .unwrap() + .send(TouchInput { + phase: TouchPhase::Ended, + position: Default::default(), + force: None, + id: 0, + }) + } + + fn set_mouse_clicked(&mut self) { + let mut mouse_input = self + .app + .world + .get_resource_mut::>() + .unwrap(); + mouse_input.press(MouseButton::Left); + } + + fn set_mouse_released(&mut self) { + let mut mouse_input = self + .app + .world + .get_resource_mut::>() + .unwrap(); + mouse_input.release(MouseButton::Left); + } + + fn spawn_node_entity_at(&mut self, x: f32, y: f32) -> Entity { + self.app + .world + .spawn() + .insert(GlobalTransform { + translation: Vec3::new(x, y, 0.0), + ..GlobalTransform::default() + }) + .insert(Node { + size: Vec2::new(NODE_SIZE, NODE_SIZE), + }) + .insert(Interaction::None) + .id() + } + + fn spawn_node_entity_with_z_at( + &mut self, + x: f32, + y: f32, + z: f32, + focus_policy: Option, + ) -> Entity { + let mut entity = self.app.world.spawn(); + if let Some(focus_policy) = focus_policy { + entity.insert(focus_policy); + } + + entity + .insert(GlobalTransform { + translation: Vec3::new(x, y, z), + ..GlobalTransform::default() + }) + .insert(Node { + size: Vec2::new(NODE_SIZE, NODE_SIZE), + }) + .insert(Interaction::None) + .id() + } + + fn run_step(&mut self) { + self.app.schedule.run_once(&mut self.app.world); + + let mut mouse_input = self + .app + .world + .get_resource_mut::>() + .unwrap(); + mouse_input.clear(); + } + + fn get_interaction(&self, entity: Entity) -> &Interaction { + self.app.world.get::(entity).unwrap() + } + } + + #[derive(Debug, Default)] + struct WindowsDouble { + cursor_position: Option, + } + + impl CursorResource for WindowsDouble { + fn get_cursor_position(&self) -> Option { + self.cursor_position + } + } +} From a4e9c8f3d3033125a5453d5f89f2c58dfd437e97 Mon Sep 17 00:00:00 2001 From: pubrrr Date: Wed, 23 Mar 2022 21:46:17 +0100 Subject: [PATCH 2/6] add a test for change detection --- crates/bevy_ui/src/focus.rs | 78 ++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index c9f53f39009e3..b12bdcc93e4e6 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -214,6 +214,7 @@ mod tests { use bevy_app::App; use bevy_ecs::event::Events; use bevy_ecs::prelude::ParallelSystemDescriptorCoercion; + use bevy_ecs::query::Changed; use bevy_input::touch::{touch_screen_input_system, TouchInput, TouchPhase}; use bevy_math::Vec3; @@ -457,6 +458,35 @@ mod tests { assert_eq!(&Interaction::Hovered, app.get_interaction(entity)); } + #[test] + fn change_detection_journey() { + let mut app = TestApp::new(); + app.spawn_node_entity_at(10., 10.); + + app.run_step(); + + app.expect_no_changed_interaction("mouse does still not touch target"); + app.run_step(); + + app.set_cursor_position(Some((10., 10.))); + app.expect_changed_interaction("mouse hovers target"); + app.run_step(); + + app.expect_no_changed_interaction("mouse still hovers target"); + app.run_step(); + + app.set_mouse_clicked(); + app.expect_changed_interaction("mouse clicked target"); + app.run_step(); + + app.expect_no_changed_interaction("mouse button still clicked"); + app.run_step(); + + app.set_cursor_position(Some((0., 0.))); + app.expect_no_changed_interaction("mouse dragged away, but button still clicked"); + app.run_step(); + } + struct TestApp { app: App, } @@ -466,9 +496,12 @@ mod tests { let mut app = App::new(); app.init_resource::>() .init_resource::() + .init_resource::() + .init_resource::() .add_event::() + .add_system(touch_screen_input_system.before("under_test")) .add_system(focus_ui::.label("under_test")) - .add_system(touch_screen_input_system.before("under_test")); + .add_system(watch_changes.after("under_test")); TestApp { app } } @@ -562,6 +595,49 @@ mod tests { fn get_interaction(&self, entity: Entity) -> &Interaction { self.app.world.get::(entity).unwrap() } + + fn expect_changed_interaction(&mut self, message: &str) { + let mut changed_interaction_expectation = self + .app + .world + .get_resource_mut::() + .unwrap(); + changed_interaction_expectation.0 = Some(true); + changed_interaction_expectation.1 = message.to_string(); + } + + fn expect_no_changed_interaction(&mut self, message: &str) { + let mut changed_interaction_expectation = self + .app + .world + .get_resource_mut::() + .unwrap(); + changed_interaction_expectation.0 = Some(false); + changed_interaction_expectation.1 = message.to_string(); + } + } + + #[derive(Default)] + struct ChangedInteractionExpectation(Option, String); + + fn watch_changes( + query: Query>, + expected_changed_interaction: Res, + ) { + match expected_changed_interaction.0 { + Some(true) => assert!( + query.iter().count() > 0, + "{}", + expected_changed_interaction.1.as_str() + ), + Some(false) => assert_eq!( + query.iter().count(), + 0, + "{}", + expected_changed_interaction.1.as_str() + ), + None => {} + } } #[derive(Debug, Default)] From af21aacdf7322ddcfeeedc5b07799f09c13ab781 Mon Sep 17 00:00:00 2001 From: pubrrr Date: Sun, 20 Mar 2022 12:48:19 +0100 Subject: [PATCH 3/6] the focus system does nothing if interaction is none as it is the only mutable object --- crates/bevy_ui/src/focus.rs | 56 ++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index b12bdcc93e4e6..16a12482b45e9 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -67,7 +67,7 @@ pub fn ui_focus_system( Entity, &Node, &GlobalTransform, - Option<&mut Interaction>, + &mut Interaction, Option<&FocusPolicy>, Option<&CalculatedClip>, )>, @@ -91,7 +91,7 @@ fn focus_ui( Entity, &Node, &GlobalTransform, - Option<&mut Interaction>, + &mut Interaction, Option<&FocusPolicy>, Option<&CalculatedClip>, )>, @@ -108,13 +108,11 @@ fn focus_ui( 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 + for (_entity, _node, _global_transform, mut interaction, _focus_policy, _clip) in node_query.iter_mut() { - if let Some(mut interaction) = interaction { - if *interaction == Interaction::Clicked { - *interaction = Interaction::None; - } + if *interaction == Interaction::Clicked { + *interaction = Interaction::None; } } } @@ -125,7 +123,7 @@ fn focus_ui( let mut moused_over_z_sorted_nodes = node_query .iter_mut() .filter_map( - |(entity, node, global_transform, interaction, focus_policy, clip)| { + |(entity, node, global_transform, mut interaction, focus_policy, clip)| { let position = global_transform.translation; let ui_position = position.truncate(); let extents = node.size / 2.0; @@ -147,12 +145,10 @@ fn focus_ui( if contains_cursor { Some((entity, focus_policy, interaction, FloatOrd(position.z))) } else { - if let Some(mut interaction) = interaction { - if *interaction == Interaction::Hovered - || (cursor_position.is_none() && *interaction != Interaction::None) - { - *interaction = Interaction::None; - } + if *interaction == Interaction::Hovered + || (cursor_position.is_none() && *interaction != Interaction::None) + { + *interaction = Interaction::None; } None } @@ -164,21 +160,19 @@ fn focus_ui( 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); - } + for (entity, focus_policy, mut interaction, _) in moused_over_z_sorted_nodes.by_ref() { + 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; } + } else if *interaction == Interaction::None { + *interaction = Interaction::Hovered; } match focus_policy.cloned().unwrap_or(FocusPolicy::Block) { @@ -189,11 +183,9 @@ fn focus_ui( } } // 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; - } + for (_entity, _focus_policy, mut interaction, _) in moused_over_z_sorted_nodes { + if *interaction != Interaction::None { + *interaction = Interaction::None; } } } From cd22b81880c21bafa635ee59b7bb490f07b01ab7 Mon Sep 17 00:00:00 2001 From: pubrrr Date: Sun, 20 Mar 2022 13:02:36 +0100 Subject: [PATCH 4/6] simplify logic: if there is no cursor, then there is no interaction --- crates/bevy_ui/src/focus.rs | 41 ++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 16a12482b45e9..7d79ce67b54b1 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -56,8 +56,8 @@ 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)] +/// The system that sets Interaction for all UI elements based on the mouse and touch cursor +/// activity pub fn ui_focus_system( state: Local, windows: Res, @@ -96,7 +96,13 @@ fn focus_ui( Option<&CalculatedClip>, )>, ) { - let cursor_position = windows.get_cursor_position(); + let cursor_position = match windows.get_cursor_position() { + None => { + set_all_interactions_to_none(node_query); + return; + } + Some(cursor_position) => cursor_position, + }; // reset entities that were both clicked and released in the last frame for entity in state.entities_to_reset.drain(..) { @@ -135,19 +141,13 @@ fn focus_ui( } // if the current cursor position is within the bounds of the node, consider it for // clicking - let contains_cursor = if let Some(cursor_position) = cursor_position { - (min.x..max.x).contains(&cursor_position.x) - && (min.y..max.y).contains(&cursor_position.y) - } else { - false - }; + let contains_cursor = (min.x..max.x).contains(&cursor_position.x) + && (min.y..max.y).contains(&cursor_position.y); if contains_cursor { Some((entity, focus_policy, interaction, FloatOrd(position.z))) } else { - if *interaction == Interaction::Hovered - || (cursor_position.is_none() && *interaction != Interaction::None) - { + if *interaction == Interaction::Hovered { *interaction = Interaction::None; } None @@ -190,6 +190,23 @@ fn focus_ui( } } +fn set_all_interactions_to_none( + mut node_query: Query<( + Entity, + &Node, + &GlobalTransform, + &mut Interaction, + Option<&FocusPolicy>, + Option<&CalculatedClip>, + )>, +) { + for (_entity, _node, _global_transform, mut interaction, _focus_policy, _clip) in + node_query.iter_mut() + { + *interaction = Interaction::None; + } +} + trait CursorResource: Resource { fn get_cursor_position(&self) -> Option; } From 7f6460e84a3d74727487a16a92f14f6656dcf43e Mon Sep 17 00:00:00 2001 From: pubrrr Date: Sun, 20 Mar 2022 15:30:12 +0100 Subject: [PATCH 5/6] refactor - remove now unnecessary logic, write interaction state in fewer spots: - reset everything that should be reset - find hovered nodes - write Hovered/Clicked states --- crates/bevy_ui/src/focus.rs | 90 +++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 7d79ce67b54b1..025b27c344ef9 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -56,8 +56,17 @@ pub struct State { entities_to_reset: SmallVec<[Entity; 1]>, } -/// The system that sets Interaction for all UI elements based on the mouse and touch cursor -/// activity +pub type NodeQuery<'a> = ( + Entity, + &'a Node, + &'a GlobalTransform, + &'a mut Interaction, + Option<&'a FocusPolicy>, + Option<&'a CalculatedClip>, +); + +/// The system that sets Interaction for all UI elements based on the mouse cursor activity +#[allow(clippy::type_complexity)] pub fn ui_focus_system( state: Local, windows: Res, @@ -96,32 +105,21 @@ fn focus_ui( Option<&CalculatedClip>, )>, ) { + reset_interactions( + &mut node_query, + &mouse_button_input, + &touches_input, + &windows.get_cursor_position(), + &mut state, + ); + let cursor_position = match windows.get_cursor_position() { - None => { - set_all_interactions_to_none(node_query); - return; - } + None => return, Some(cursor_position) => cursor_position, }; - // 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, mut interaction, _focus_policy, _clip) in - node_query.iter_mut() - { - if *interaction == Interaction::Clicked { - *interaction = Interaction::None; - } - } - } let mouse_clicked = mouse_button_input.just_pressed(MouseButton::Left) || touches_input.just_released(0); @@ -129,7 +127,7 @@ fn focus_ui( let mut moused_over_z_sorted_nodes = node_query .iter_mut() .filter_map( - |(entity, node, global_transform, mut interaction, focus_policy, clip)| { + |(entity, node, global_transform, interaction, focus_policy, clip)| { let position = global_transform.translation; let ui_position = position.truncate(); let extents = node.size / 2.0; @@ -147,9 +145,6 @@ fn focus_ui( if contains_cursor { Some((entity, focus_policy, interaction, FloatOrd(position.z))) } else { - if *interaction == Interaction::Hovered { - *interaction = Interaction::None; - } None } }, @@ -158,19 +153,14 @@ fn focus_ui( 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, mut interaction, _) in moused_over_z_sorted_nodes.by_ref() { + for (entity, focus_policy, mut interaction, _) in moused_over_z_sorted_nodes { 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); - } + // if the mouse was simultaneously released, reset this Interaction in the next frame + if *interaction != Interaction::Clicked && mouse_released { + state.entities_to_reset.push(entity); } + *interaction = Interaction::Clicked; } else if *interaction == Interaction::None { *interaction = Interaction::Hovered; } @@ -182,16 +172,10 @@ fn focus_ui( FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ } } } - // reset lower nodes to None - for (_entity, _focus_policy, mut interaction, _) in moused_over_z_sorted_nodes { - if *interaction != Interaction::None { - *interaction = Interaction::None; - } - } } -fn set_all_interactions_to_none( - mut node_query: Query<( +fn reset_interactions( + node_query: &mut Query<( Entity, &Node, &GlobalTransform, @@ -199,12 +183,30 @@ fn set_all_interactions_to_none( Option<&FocusPolicy>, Option<&CalculatedClip>, )>, + mouse_button_input: &Input, + touches_input: &Touches, + cursor_position: &Option, + state: &mut State, ) { + let mouse_release = + mouse_button_input.just_released(MouseButton::Left) || touches_input.just_released(0); + let input_should_leave_button_clicked = cursor_position.is_some() && !mouse_release; + for (_entity, _node, _global_transform, mut interaction, _focus_policy, _clip) in node_query.iter_mut() { + if input_should_leave_button_clicked && *interaction == Interaction::Clicked { + continue; + } *interaction = Interaction::None; } + + // 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; + } + } } trait CursorResource: Resource { From c6704a838a86f377912f1f53569bd1b6a62bc42f Mon Sep 17 00:00:00 2001 From: pubrrr Date: Sun, 20 Mar 2022 15:57:36 +0100 Subject: [PATCH 6/6] refactor - extract some functions --- crates/bevy_ui/src/focus.rs | 95 ++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 025b27c344ef9..b0ec69cfd834e 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -2,12 +2,12 @@ use crate::{CalculatedClip, Node}; use bevy_core::FloatOrd; use bevy_ecs::{ entity::Entity, - prelude::Component, + prelude::{Component, Mut}, reflect::ReflectComponent, system::{Local, Query, Res, Resource}, }; use bevy_input::{mouse::MouseButton, touch::Touches, Input}; -use bevy_math::Vec2; +use bevy_math::{Vec2, Vec3}; use bevy_reflect::{Reflect, ReflectDeserialize}; use bevy_transform::components::GlobalTransform; use bevy_window::Windows; @@ -118,31 +118,13 @@ fn focus_ui( Some(cursor_position) => cursor_position, }; - let mouse_released = - mouse_button_input.just_released(MouseButton::Left) || touches_input.just_released(0); - - 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 - let contains_cursor = (min.x..max.x).contains(&cursor_position.x) - && (min.y..max.y).contains(&cursor_position.y); - if contains_cursor { + if contains_cursor(&cursor_position, node, position, clip) { Some((entity, focus_policy, interaction, FloatOrd(position.z))) } else { None @@ -153,25 +135,30 @@ fn focus_ui( moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| -*z); - // set Clicked or Hovered on top nodes - for (entity, focus_policy, mut interaction, _) in moused_over_z_sorted_nodes { - if mouse_clicked { - // if the mouse was simultaneously released, reset this Interaction in the next frame - if *interaction != Interaction::Clicked && mouse_released { - state.entities_to_reset.push(entity); - } - *interaction = Interaction::Clicked; - } else if *interaction == Interaction::None { - *interaction = Interaction::Hovered; - } + set_top_nodes_as_clicked_or_hovered( + moused_over_z_sorted_nodes, + mouse_button_input, + touches_input, + state, + ) +} - match focus_policy.cloned().unwrap_or(FocusPolicy::Block) { - FocusPolicy::Block => { - break; - } - FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ } - } +fn contains_cursor( + cursor_position: &Vec2, + node: &Node, + position: Vec3, + clip: Option<&CalculatedClip>, +) -> bool { + 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); } + + (min.x..max.x).contains(&cursor_position.x) && (min.y..max.y).contains(&cursor_position.y) } fn reset_interactions( @@ -209,6 +196,38 @@ fn reset_interactions( } } +fn set_top_nodes_as_clicked_or_hovered( + moused_over_z_sorted_nodes: Vec<(Entity, Option<&FocusPolicy>, Mut, FloatOrd)>, + mouse_button_input: Res>, + touches_input: Res, + mut state: Local, +) { + let mouse_released = + mouse_button_input.just_released(MouseButton::Left) || touches_input.just_released(0); + + let mouse_clicked = + mouse_button_input.just_pressed(MouseButton::Left) || touches_input.just_released(0); + + for (entity, focus_policy, mut interaction, _) in moused_over_z_sorted_nodes { + if mouse_clicked { + // if the mouse was simultaneously released, reset this Interaction in the next frame + if *interaction != Interaction::Clicked && mouse_released { + state.entities_to_reset.push(entity); + } + *interaction = Interaction::Clicked; + } 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 */ } + } + } +} + trait CursorResource: Resource { fn get_cursor_position(&self) -> Option; }