Skip to content

Commit a792f37

Browse files
committed
Relative cursor position (#7199)
# Objective Add useful information about cursor position relative to a UI node. Fixes #7079. ## Solution - Added a new `RelativeCursorPosition` component --- ## Changelog - Added - `RelativeCursorPosition` - an example showcasing the new component Co-authored-by: Dawid Piotrowski <[email protected]>
1 parent 517deda commit a792f37

File tree

4 files changed

+148
-8
lines changed

4 files changed

+148
-8
lines changed

Cargo.toml

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

1458+
[[example]]
1459+
name = "relative_cursor_position"
1460+
path = "examples/ui/relative_cursor_position.rs"
1461+
1462+
[package.metadata.example.relative_cursor_position]
1463+
name = "Relative Cursor Position"
1464+
description = "Showcases the RelativeCursorPosition component"
1465+
category = "UI (User Interface)"
1466+
wasm = true
1467+
14581468
[[example]]
14591469
name = "text"
14601470
path = "examples/ui/text.rs"

crates/bevy_ui/src/focus.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
2+
use bevy_derive::{Deref, DerefMut};
23
use bevy_ecs::{
34
change_detection::DetectChangesMut,
45
entity::Entity,
@@ -52,6 +53,39 @@ impl Default for Interaction {
5253
}
5354
}
5455

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+
5589
/// Describes whether the node should block interactions with lower nodes
5690
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
5791
#[reflect(Component, Serialize, Deserialize, PartialEq)]
@@ -86,6 +120,7 @@ pub struct NodeQuery {
86120
node: &'static Node,
87121
global_transform: &'static GlobalTransform,
88122
interaction: Option<&'static mut Interaction>,
123+
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
89124
focus_policy: Option<&'static FocusPolicy>,
90125
calculated_clip: Option<&'static CalculatedClip>,
91126
computed_visibility: Option<&'static ComputedVisibility>,
@@ -175,20 +210,34 @@ pub fn ui_focus_system(
175210
let ui_position = position.truncate();
176211
let extents = node.node.size() / 2.0;
177212
let mut min = ui_position - extents;
178-
let mut max = ui_position + extents;
179213
if let Some(clip) = node.calculated_clip {
180214
min = Vec2::max(min, clip.clip.min);
181-
max = Vec2::min(max, clip.clip.max);
182215
}
183-
// if the current cursor position is within the bounds of the node, consider it for
216+
217+
// The mouse position relative to the node
218+
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
219+
let relative_cursor_position = cursor_position.map(|cursor_position| {
220+
Vec2::new(
221+
(cursor_position.x - min.x) / node.node.size().x,
222+
(cursor_position.y - min.y) / node.node.size().y,
223+
)
224+
});
225+
226+
// If the current cursor position is within the bounds of the node, consider it for
184227
// clicking
185-
let contains_cursor = if let Some(cursor_position) = cursor_position {
186-
(min.x..max.x).contains(&cursor_position.x)
187-
&& (min.y..max.y).contains(&cursor_position.y)
188-
} else {
189-
false
228+
let relative_cursor_position_component = RelativeCursorPosition {
229+
normalized: relative_cursor_position,
190230
};
191231

232+
let contains_cursor = relative_cursor_position_component.mouse_over();
233+
234+
// Save the relative cursor position to the correct component
235+
if let Some(mut node_relative_cursor_position_component) =
236+
node.relative_cursor_position
237+
{
238+
*node_relative_cursor_position_component = relative_cursor_position_component;
239+
}
240+
192241
if contains_cursor {
193242
Some(*entity)
194243
} else {

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ Example | Description
312312
--- | ---
313313
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
314314
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
315+
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
315316
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
316317
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
317318
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Showcases the `RelativeCursorPosition` component, used to check the position of the cursor relative to a UI node.
2+
3+
use bevy::{prelude::*, ui::RelativeCursorPosition, winit::WinitSettings};
4+
5+
fn main() {
6+
App::new()
7+
.add_plugins(DefaultPlugins)
8+
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
9+
.insert_resource(WinitSettings::desktop_app())
10+
.add_startup_system(setup)
11+
.add_system(relative_cursor_position_system)
12+
.run();
13+
}
14+
15+
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
16+
commands.spawn(Camera2dBundle::default());
17+
18+
commands
19+
.spawn(NodeBundle {
20+
style: Style {
21+
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
22+
align_items: AlignItems::Center,
23+
justify_content: JustifyContent::Center,
24+
flex_direction: FlexDirection::Column,
25+
..default()
26+
},
27+
..default()
28+
})
29+
.with_children(|parent| {
30+
parent
31+
.spawn(NodeBundle {
32+
style: Style {
33+
size: Size::new(Val::Px(250.0), Val::Px(250.0)),
34+
margin: UiRect::new(Val::Px(0.), Val::Px(0.), Val::Px(0.), Val::Px(15.)),
35+
..default()
36+
},
37+
background_color: Color::rgb(235., 35., 12.).into(),
38+
..default()
39+
})
40+
.insert(RelativeCursorPosition::default());
41+
42+
parent.spawn(TextBundle {
43+
text: Text::from_section(
44+
"(0.0, 0.0)",
45+
TextStyle {
46+
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
47+
font_size: 40.0,
48+
color: Color::rgb(0.9, 0.9, 0.9),
49+
},
50+
),
51+
..default()
52+
});
53+
});
54+
}
55+
56+
/// This systems polls the relative cursor position and displays its value in a text component.
57+
fn relative_cursor_position_system(
58+
relative_cursor_position_query: Query<&RelativeCursorPosition>,
59+
mut output_query: Query<&mut Text>,
60+
) {
61+
let relative_cursor_position = relative_cursor_position_query.single();
62+
63+
let mut output = output_query.single_mut();
64+
65+
output.sections[0].value =
66+
if let Some(relative_cursor_position) = relative_cursor_position.normalized {
67+
format!(
68+
"({:.1}, {:.1})",
69+
relative_cursor_position.x, relative_cursor_position.y
70+
)
71+
} else {
72+
"unknown".to_string()
73+
};
74+
75+
output.sections[0].style.color = if relative_cursor_position.mouse_over() {
76+
Color::rgb(0.1, 0.9, 0.1)
77+
} else {
78+
Color::rgb(0.9, 0.1, 0.1)
79+
};
80+
}

0 commit comments

Comments
 (0)