diff --git a/Cargo.toml b/Cargo.toml index 48b7e55bb5d88..898cd1f2e33ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index fab9a0121c1a7..0c71fe790e770 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -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, @@ -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, +} + +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)] @@ -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>, @@ -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 { diff --git a/examples/README.md b/examples/README.md index 7817465d9e8b8..83f6c129044f1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs new file mode 100644 index 0000000000000..a02a5b3a2d7d5 --- /dev/null +++ b/examples/ui/relative_cursor_position.rs @@ -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) { + 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) + }; +}