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:
Dawid Piotrowski 2023-01-16 17:17:45 +00:00
parent 517deda215
commit a792f37040
4 changed files with 148 additions and 8 deletions

View file

@ -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"

View file

@ -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 {

View file

@ -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

View 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)
};
}