mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
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 <41804418+Pietrek14@users.noreply.github.com>
This commit is contained in:
parent
517deda215
commit
a792f37040
4 changed files with 148 additions and 8 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -1455,6 +1455,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
|
||||||
category = "UI (User Interface)"
|
category = "UI (User Interface)"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "relative_cursor_position"
|
||||||
|
path = "examples/ui/relative_cursor_position.rs"
|
||||||
|
|
||||||
|
[package.metadata.example.relative_cursor_position]
|
||||||
|
name = "Relative Cursor Position"
|
||||||
|
description = "Showcases the RelativeCursorPosition component"
|
||||||
|
category = "UI (User Interface)"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "text"
|
name = "text"
|
||||||
path = "examples/ui/text.rs"
|
path = "examples/ui/text.rs"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
|
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
change_detection::DetectChangesMut,
|
change_detection::DetectChangesMut,
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
|
@ -52,6 +53,39 @@ 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
|
/// Describes whether the node should block interactions with lower nodes
|
||||||
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
|
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
|
||||||
#[reflect(Component, Serialize, Deserialize, PartialEq)]
|
#[reflect(Component, Serialize, Deserialize, PartialEq)]
|
||||||
|
@ -86,6 +120,7 @@ pub struct NodeQuery {
|
||||||
node: &'static Node,
|
node: &'static Node,
|
||||||
global_transform: &'static GlobalTransform,
|
global_transform: &'static GlobalTransform,
|
||||||
interaction: Option<&'static mut Interaction>,
|
interaction: Option<&'static mut Interaction>,
|
||||||
|
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
|
||||||
focus_policy: Option<&'static FocusPolicy>,
|
focus_policy: Option<&'static FocusPolicy>,
|
||||||
calculated_clip: Option<&'static CalculatedClip>,
|
calculated_clip: Option<&'static CalculatedClip>,
|
||||||
computed_visibility: Option<&'static ComputedVisibility>,
|
computed_visibility: Option<&'static ComputedVisibility>,
|
||||||
|
@ -175,20 +210,34 @@ pub fn ui_focus_system(
|
||||||
let ui_position = position.truncate();
|
let ui_position = position.truncate();
|
||||||
let extents = node.node.size() / 2.0;
|
let extents = node.node.size() / 2.0;
|
||||||
let mut min = ui_position - extents;
|
let mut min = ui_position - extents;
|
||||||
let mut max = ui_position + extents;
|
|
||||||
if let Some(clip) = node.calculated_clip {
|
if let Some(clip) = node.calculated_clip {
|
||||||
min = Vec2::max(min, clip.clip.min);
|
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
|
|
||||||
|
// 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
|
// clicking
|
||||||
let contains_cursor = if let Some(cursor_position) = cursor_position {
|
let relative_cursor_position_component = RelativeCursorPosition {
|
||||||
(min.x..max.x).contains(&cursor_position.x)
|
normalized: relative_cursor_position,
|
||||||
&& (min.y..max.y).contains(&cursor_position.y)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 contains_cursor {
|
if contains_cursor {
|
||||||
Some(*entity)
|
Some(*entity)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -312,6 +312,7 @@ Example | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
|
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
|
||||||
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
|
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
|
||||||
|
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
||||||
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
|
||||||
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
|
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
|
||||||
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
|
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
|
||||||
|
|
80
examples/ui/relative_cursor_position.rs
Normal file
80
examples/ui/relative_cursor_position.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
//! Showcases the `RelativeCursorPosition` component, used to check the position of the cursor relative to a UI node.
|
||||||
|
|
||||||
|
use bevy::{prelude::*, ui::RelativeCursorPosition, winit::WinitSettings};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
|
.insert_resource(WinitSettings::desktop_app())
|
||||||
|
.add_startup_system(setup)
|
||||||
|
.add_system(relative_cursor_position_system)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
commands.spawn(Camera2dBundle::default());
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
size: Size::new(Val::Px(250.0), Val::Px(250.0)),
|
||||||
|
margin: UiRect::new(Val::Px(0.), Val::Px(0.), Val::Px(0.), Val::Px(15.)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
background_color: Color::rgb(235., 35., 12.).into(),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.insert(RelativeCursorPosition::default());
|
||||||
|
|
||||||
|
parent.spawn(TextBundle {
|
||||||
|
text: Text::from_section(
|
||||||
|
"(0.0, 0.0)",
|
||||||
|
TextStyle {
|
||||||
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||||
|
font_size: 40.0,
|
||||||
|
color: Color::rgb(0.9, 0.9, 0.9),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This systems polls the relative cursor position and displays its value in a text component.
|
||||||
|
fn relative_cursor_position_system(
|
||||||
|
relative_cursor_position_query: Query<&RelativeCursorPosition>,
|
||||||
|
mut output_query: Query<&mut Text>,
|
||||||
|
) {
|
||||||
|
let relative_cursor_position = relative_cursor_position_query.single();
|
||||||
|
|
||||||
|
let mut output = output_query.single_mut();
|
||||||
|
|
||||||
|
output.sections[0].value =
|
||||||
|
if let Some(relative_cursor_position) = relative_cursor_position.normalized {
|
||||||
|
format!(
|
||||||
|
"({:.1}, {:.1})",
|
||||||
|
relative_cursor_position.x, relative_cursor_position.y
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"unknown".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
output.sections[0].style.color = if relative_cursor_position.mouse_over() {
|
||||||
|
Color::rgb(0.1, 0.9, 0.1)
|
||||||
|
} else {
|
||||||
|
Color::rgb(0.9, 0.1, 0.1)
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue