Skip to content

Commit 579e565

Browse files
committed
Remove RelativeCursorPosition, add clicked position to Interaction::Clicked, improve readability of ui_focus_system
1 parent 8d51f4a commit 579e565

File tree

8 files changed

+65
-230
lines changed

8 files changed

+65
-230
lines changed

Cargo.toml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,16 +1533,6 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
15331533
category = "UI (User Interface)"
15341534
wasm = true
15351535

1536-
[[example]]
1537-
name = "relative_cursor_position"
1538-
path = "examples/ui/relative_cursor_position.rs"
1539-
1540-
[package.metadata.example.relative_cursor_position]
1541-
name = "Relative Cursor Position"
1542-
description = "Showcases the RelativeCursorPosition component"
1543-
category = "UI (User Interface)"
1544-
wasm = true
1545-
15461536
[[example]]
15471537
name = "text"
15481538
path = "examples/ui/text.rs"

crates/bevy_ui/src/focus.rs

Lines changed: 57 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
2-
use bevy_derive::{Deref, DerefMut};
32
use bevy_ecs::{
43
change_detection::DetectChangesMut,
54
entity::Entity,
@@ -32,11 +31,11 @@ use smallvec::SmallVec;
3231
///
3332
/// Note that you can also control the visibility of a node using the [`Display`](crate::ui_node::Display) property,
3433
/// which fully collapses it during layout calculations.
35-
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
34+
#[derive(Component, Copy, Clone, PartialEq, Debug, Reflect, Serialize, Deserialize)]
3635
#[reflect(Component, Serialize, Deserialize, PartialEq)]
3736
pub enum Interaction {
3837
/// The node has been clicked
39-
Clicked,
38+
Clicked(Vec2),
4039
/// The node has been hovered over
4140
Hovered,
4241
/// Nothing has happened
@@ -53,39 +52,6 @@ impl Default for Interaction {
5352
}
5453
}
5554

56-
/// 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
57-
/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.)
58-
/// A None value means that the cursor position is unknown.
59-
///
60-
/// It can be used alongside interaction to get the position of the press.
61-
#[derive(
62-
Component,
63-
Deref,
64-
DerefMut,
65-
Copy,
66-
Clone,
67-
Default,
68-
PartialEq,
69-
Debug,
70-
Reflect,
71-
Serialize,
72-
Deserialize,
73-
)]
74-
#[reflect(Component, Serialize, Deserialize, PartialEq)]
75-
pub struct RelativeCursorPosition {
76-
/// Cursor position relative to size and position of the Node.
77-
pub normalized: Option<Vec2>,
78-
}
79-
80-
impl RelativeCursorPosition {
81-
/// A helper function to check if the mouse is over the node
82-
pub fn mouse_over(&self) -> bool {
83-
self.normalized
84-
.map(|position| (0.0..1.).contains(&position.x) && (0.0..1.).contains(&position.y))
85-
.unwrap_or(false)
86-
}
87-
}
88-
8955
/// Describes whether the node should block interactions with lower nodes
9056
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
9157
#[reflect(Component, Serialize, Deserialize, PartialEq)]
@@ -120,7 +86,6 @@ pub struct NodeQuery {
12086
node: &'static Node,
12187
global_transform: &'static GlobalTransform,
12288
interaction: Option<&'static mut Interaction>,
123-
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
12489
focus_policy: Option<&'static FocusPolicy>,
12590
calculated_clip: Option<&'static CalculatedClip>,
12691
computed_visibility: Option<&'static ComputedVisibility>,
@@ -154,19 +119,17 @@ pub fn ui_focus_system(
154119
if mouse_released {
155120
for node in node_query.iter_mut() {
156121
if let Some(mut interaction) = node.interaction {
157-
if *interaction == Interaction::Clicked {
122+
if matches!(*interaction, Interaction::Clicked(_)) {
158123
*interaction = Interaction::None;
159124
}
160125
}
161126
}
162127
}
163-
164128
let mouse_clicked =
165129
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();
166130

167131
let is_ui_disabled =
168132
|camera_ui| matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. }));
169-
170133
let cursor_position = camera
171134
.iter()
172135
.filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui))
@@ -189,113 +152,74 @@ pub fn ui_focus_system(
189152
})
190153
.or_else(|| touches_input.first_pressed_position());
191154

192-
// prepare an iterator that contains all the nodes that have the cursor in their rect,
193-
// from the top node to the bottom one. this will also reset the interaction to `None`
194-
// for all nodes encountered that are no longer hovered.
195-
let mut moused_over_nodes = ui_stack
196-
.uinodes
197-
.iter()
198-
// reverse the iterator to traverse the tree from closest nodes to furthest
199-
.rev()
200-
.filter_map(|entity| {
201-
if let Ok(node) = node_query.get_mut(*entity) {
202-
// Nodes that are not rendered should not be interactable
203-
if let Some(computed_visibility) = node.computed_visibility {
204-
if !computed_visibility.is_visible() {
205-
// Reset their interaction to None to avoid strange stuck state
206-
if let Some(mut interaction) = node.interaction {
207-
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
208-
interaction.set_if_neq(Interaction::None);
209-
}
210-
211-
return None;
155+
// Iterate through all nodes from top to bottom
156+
let mut focus_blocked = false;
157+
for entity in ui_stack.uinodes.iter().rev() {
158+
if let Ok(node) = node_query.get_mut(*entity) {
159+
// Nodes that are not rendered should not be interactable
160+
if let Some(computed_visibility) = node.computed_visibility {
161+
if !computed_visibility.is_visible() {
162+
// Reset their interaction to None to avoid strange stuck state
163+
if let Some(mut interaction) = node.interaction {
164+
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
165+
interaction.set_if_neq(Interaction::None);
212166
}
213-
}
214167

215-
let position = node.global_transform.translation();
216-
let ui_position = position.truncate();
217-
let extents = node.node.size() / 2.0;
218-
let mut min = ui_position - extents;
219-
if let Some(clip) = node.calculated_clip {
220-
min = Vec2::max(min, clip.clip.min);
168+
continue;
221169
}
170+
}
222171

223-
// The mouse position relative to the node
224-
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
225-
let relative_cursor_position = cursor_position.map(|cursor_position| {
226-
Vec2::new(
227-
(cursor_position.x - min.x) / node.node.size().x,
228-
(cursor_position.y - min.y) / node.node.size().y,
229-
)
230-
});
231-
232-
// If the current cursor position is within the bounds of the node, consider it for
233-
// clicking
234-
let relative_cursor_position_component = RelativeCursorPosition {
235-
normalized: relative_cursor_position,
236-
};
237-
238-
let contains_cursor = relative_cursor_position_component.mouse_over();
239-
240-
// Save the relative cursor position to the correct component
241-
if let Some(mut node_relative_cursor_position_component) =
242-
node.relative_cursor_position
243-
{
244-
*node_relative_cursor_position_component = relative_cursor_position_component;
245-
}
172+
let position = node.global_transform.translation();
173+
let ui_position = position.truncate();
174+
let extents = node.node.size() / 2.0;
175+
let mut min = ui_position - extents;
176+
let mut max = ui_position + extents;
177+
if let Some(clip) = node.calculated_clip {
178+
min = Vec2::max(min, clip.clip.min);
179+
max = Vec2::max(max, clip.clip.max);
180+
}
246181

182+
let contains_cursor = cursor_position
183+
.map(|cursor_position| {
184+
(min.x..max.x).contains(&cursor_position.x)
185+
&& (min.y..max.y).contains(&cursor_position.y)
186+
})
187+
.unwrap_or(false);
188+
if let Some(mut interaction) = node.interaction {
247189
if contains_cursor {
248-
Some(*entity)
249-
} else {
250-
if let Some(mut interaction) = node.interaction {
251-
if *interaction == Interaction::Hovered || (cursor_position.is_none()) {
190+
if focus_blocked {
191+
// don't reset clicked nodes because they're handled separately
192+
if !matches!(*interaction, Interaction::Clicked(_)) {
252193
interaction.set_if_neq(Interaction::None);
253194
}
195+
} else if mouse_clicked {
196+
// only consider nodes with Interaction "clickable"
197+
if !matches!(*interaction, Interaction::Clicked(_)) {
198+
*interaction = Interaction::Clicked(Vec2::new(
199+
cursor_position.unwrap().x - min.x,
200+
cursor_position.unwrap().y - min.y,
201+
));
202+
// if the mouse was simultaneously released, reset this Interaction in the next
203+
// frame
204+
if mouse_released {
205+
state.entities_to_reset.push(node.entity);
206+
}
207+
}
208+
} else if *interaction == Interaction::None {
209+
*interaction = Interaction::Hovered;
254210
}
255-
None
211+
} else if *interaction == Interaction::Hovered || cursor_position.is_none() {
212+
interaction.set_if_neq(Interaction::None);
256213
}
257-
} else {
258-
None
259214
}
260-
})
261-
.collect::<Vec<Entity>>()
262-
.into_iter();
263215

264-
// set Clicked or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
265-
// the iteration will stop on it because it "captures" the interaction.
266-
let mut iter = node_query.iter_many_mut(moused_over_nodes.by_ref());
267-
while let Some(node) = iter.fetch_next() {
268-
if let Some(mut interaction) = node.interaction {
269-
if mouse_clicked {
270-
// only consider nodes with Interaction "clickable"
271-
if *interaction != Interaction::Clicked {
272-
*interaction = Interaction::Clicked;
273-
// if the mouse was simultaneously released, reset this Interaction in the next
274-
// frame
275-
if mouse_released {
276-
state.entities_to_reset.push(node.entity);
216+
if contains_cursor && !focus_blocked {
217+
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
218+
FocusPolicy::Block => {
219+
focus_blocked = true;
277220
}
221+
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
278222
}
279-
} else if *interaction == Interaction::None {
280-
*interaction = Interaction::Hovered;
281-
}
282-
}
283-
284-
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
285-
FocusPolicy::Block => {
286-
break;
287-
}
288-
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
289-
}
290-
}
291-
// reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
292-
// `moused_over_nodes` after the previous loop is exited.
293-
let mut iter = node_query.iter_many_mut(moused_over_nodes);
294-
while let Some(node) = iter.fetch_next() {
295-
if let Some(mut interaction) = node.interaction {
296-
// don't reset clicked nodes because they're handled separately
297-
if *interaction != Interaction::Clicked {
298-
interaction.set_if_neq(Interaction::None);
299223
}
300224
}
301225
}

examples/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,6 @@ Example | Description
319319
--- | ---
320320
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
321321
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
322-
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
323322
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
324323
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
325324
[Text Layout](../examples/ui/text_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout text

examples/ecs/state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ fn menu(
9595
) {
9696
for (interaction, mut color) in &mut interaction_query {
9797
match *interaction {
98-
Interaction::Clicked => {
98+
Interaction::Clicked(_) => {
9999
*color = PRESSED_BUTTON.into();
100100
next_state.set(AppState::InGame);
101101
}

examples/games/game_menu.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,9 @@ mod menu {
362362
) {
363363
for (interaction, mut color, selected) in &mut interaction_query {
364364
*color = match (*interaction, selected) {
365-
(Interaction::Clicked, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
365+
(Interaction::Clicked(_), _) | (Interaction::None, Some(_)) => {
366+
PRESSED_BUTTON.into()
367+
}
366368
(Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
367369
(Interaction::Hovered, None) => HOVERED_BUTTON.into(),
368370
(Interaction::None, None) => NORMAL_BUTTON.into(),
@@ -379,7 +381,7 @@ mod menu {
379381
mut setting: ResMut<T>,
380382
) {
381383
for (interaction, button_setting, entity) in &interaction_query {
382-
if *interaction == Interaction::Clicked && *setting != *button_setting {
384+
if matches!(*interaction, Interaction::Clicked(_)) && *setting != *button_setting {
383385
let (previous_button, mut previous_color) = selected_query.single_mut();
384386
*previous_color = NORMAL_BUTTON.into();
385387
commands.entity(previous_button).remove::<SelectedOption>();
@@ -796,7 +798,7 @@ mod menu {
796798
mut game_state: ResMut<NextState<GameState>>,
797799
) {
798800
for (interaction, menu_button_action) in &interaction_query {
799-
if *interaction == Interaction::Clicked {
801+
if matches!(*interaction, Interaction::Clicked(_)) {
800802
match menu_button_action {
801803
MenuButtonAction::Quit => app_exit_events.send(AppExit),
802804
MenuButtonAction::Play => {

examples/mobile/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ fn button_handler(
135135
) {
136136
for (interaction, mut color) in &mut interaction_query {
137137
match *interaction {
138-
Interaction::Clicked => {
138+
Interaction::Clicked(_) => {
139139
*color = Color::BLUE.into();
140140
}
141141
Interaction::Hovered => {

examples/ui/button.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fn button_system(
2727
for (interaction, mut color, children) in &mut interaction_query {
2828
let mut text = text_query.get_mut(children[0]).unwrap();
2929
match *interaction {
30-
Interaction::Clicked => {
30+
Interaction::Clicked(_) => {
3131
text.sections[0].value = "Press".to_string();
3232
*color = PRESSED_BUTTON.into();
3333
}

0 commit comments

Comments
 (0)