Skip to content

Commit

Permalink
Relative cursor position (#7199)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
Pietrek14 and Pietrek14 committed Jan 16, 2023
1 parent 517deda commit a792f37
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 8 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
category = "UI (User Interface)"
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]]
name = "text"
path = "examples/ui/text.rs"
Expand Down
65 changes: 57 additions & 8 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
Expand Down Expand Up @@ -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
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize, PartialEq)]
Expand Down Expand Up @@ -86,6 +120,7 @@ pub struct NodeQuery {
node: &'static Node,
global_transform: &'static GlobalTransform,
interaction: Option<&'static mut Interaction>,
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
computed_visibility: Option<&'static ComputedVisibility>,
Expand Down Expand Up @@ -175,20 +210,34 @@ pub fn ui_focus_system(
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
let mut min = ui_position - extents;
let mut max = ui_position + extents;
if let Some(clip) = node.calculated_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

// 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
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 relative_cursor_position_component = RelativeCursorPosition {
normalized: relative_cursor_position,
};

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 {
Some(*entity)
} else {
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ Example | Description
--- | ---
[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)
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
Expand Down
80 changes: 80 additions & 0 deletions examples/ui/relative_cursor_position.rs
Original file line number Diff line number Diff line change
@@ -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)
};
}

0 comments on commit a792f37

Please sign in to comment.