Skip to content
Snippets Groups Projects
Unverified Commit 8ceca4d3 authored by John's avatar John Committed by GitHub
Browse files

Merge pull request #65 from MrGVSV/ui-detection

Added UI Detection
parents 1fb46b26 2f4414ae
No related branches found
No related tags found
No related merge requests found
......@@ -17,4 +17,41 @@ impl BevyContext {
Self { kayak_context }
}
/// Returns true if the cursor is currently over a valid widget
///
/// For the purposes of this method, a valid widget is one which has the means to display a visual component on its own.
/// This means widgets specified with `RenderCommand::Empty`, `RenderCommand::Layout`, or `RenderCommand::Clip`
/// do not meet the requirements to "contain" the cursor.
pub fn contains_cursor(&self) -> bool {
if let Ok(kayak_context) = self.kayak_context.read() {
kayak_context.contains_cursor()
} else {
false
}
}
/// Returns true if the cursor may be needed by a widget or it's already in use by one
///
/// This is useful for checking if certain events (such as a click) would "matter" to the UI at all. Example widgets
/// include buttons, sliders, and text boxes.
pub fn wants_cursor(&self) -> bool {
if let Ok(kayak_context) = self.kayak_context.read() {
kayak_context.wants_cursor()
} else {
false
}
}
/// Returns true if the cursor is currently in use by a widget
///
/// This is most often useful for checking drag events as it will still return true even if the drag continues outside
/// the widget bounds (as long as it started within it).
pub fn has_cursor(&self) -> bool {
if let Ok(kayak_context) = self.kayak_context.read() {
kayak_context.has_cursor()
} else {
false
}
}
}
//! This example showcases how to handle world interactions in a way that considers Kayak.
//!
//! Specifically, it demonstrates how to determine if a click should affect the world or if
//! it should be left to be handled by the UI. This concept is very important when it comes
//! to designing input handling, as an incorrect implementation could lead to unexpected
//! behavior.
use bevy::{
math::{const_vec2, Vec3Swizzles, Vec4Swizzles},
prelude::{
App as BevyApp, AssetServer, Color as BevyColor, Commands, Component, CursorMoved,
EventReader, GlobalTransform, Input, MouseButton, OrthographicCameraBundle, Query, Res,
ResMut, Sprite, SpriteBundle, Transform, Vec2, Windows, With, Without,
},
window::WindowDescriptor,
DefaultPlugins,
};
use kayak_ui::{
bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle},
core::{
render, rsx,
styles::{Style, StyleProp, Units},
use_state, widget, EventType, Index, OnEvent,
},
widgets::{App, Button, Text, Window},
};
const TILE_SIZE: Vec2 = const_vec2!([50.0, 50.0]);
const COLORS: &[BevyColor] = &[BevyColor::TEAL, BevyColor::MAROON, BevyColor::INDIGO];
/// This is the system that sets the active tile's target position
///
/// To prevent the tile from being moved to a position under our UI, we can use the `BevyContext` resource
/// to filter out clicks that occur over the UI
fn set_active_tile_target(
mut tile: Query<&mut ActiveTile>,
cursor: Res<Input<MouseButton>>,
context: Res<BevyContext>,
camera_transform: Query<&GlobalTransform, With<WorldCamera>>,
windows: Res<Windows>,
) {
if !cursor.just_pressed(MouseButton::Left) {
// Only run this system when the mouse button is clicked
return;
}
if context.contains_cursor() {
// This is the important bit:
// If the cursor is over a part of the UI, then we should not allow clicks to pass through to the world
return;
}
// If you wanted to allow clicks through the UI as long as the cursor is not on a focusable widget (such as Buttons),
// you could use `context.wants_cursor()` instead:
//
// ```
// if context.wants_cursor() {
// return;
// }
// ```
let world_pos = cursor_to_world(&windows, &camera_transform.single());
let tile_pos = world_to_tile(world_pos);
let mut tile = tile.single_mut();
tile.target = tile_pos;
}
#[widget]
fn ControlPanel() {
let text_styles = Style {
left: StyleProp::Value(Units::Stretch(1.0)),
right: StyleProp::Value(Units::Stretch(1.0)),
..Default::default()
};
let button_styles = Style {
min_width: StyleProp::Value(Units::Pixels(150.0)),
width: StyleProp::Value(Units::Auto),
height: StyleProp::Value(Units::Auto),
left: StyleProp::Value(Units::Stretch(1.0)),
right: StyleProp::Value(Units::Stretch(1.0)),
top: StyleProp::Value(Units::Pixels(16.0)),
bottom: StyleProp::Value(Units::Pixels(8.0)),
padding_top: StyleProp::Value(Units::Pixels(8.0)),
padding_bottom: StyleProp::Value(Units::Pixels(8.0)),
padding_left: StyleProp::Value(Units::Pixels(48.0)),
padding_right: StyleProp::Value(Units::Pixels(48.0)),
..Default::default()
};
let (color_index, set_color_index, ..) = use_state!(0);
let current_index =
context.query_world::<Res<ActiveColor>, _, usize>(|active_color| active_color.index);
if color_index != current_index {
context.query_world::<ResMut<ActiveColor>, _, ()>(|mut active_color| {
active_color.index = color_index
});
}
let on_change_color = OnEvent::new(move |_, event| match event.event_type {
EventType::Click => {
// Cycle the color
set_color_index((color_index + 1) % COLORS.len());
}
_ => {}
});
rsx! {
<>
<Window position={(0.0, 570.0)} size={(1270.0, 150.0)} title={"Square Mover: The Game".to_string()}>
<Text size={13.0} content={"You can check if the cursor is over the UI or on a focusable widget using the BevyContext resource.".to_string()} styles={Some(text_styles)} />
<Button on_event={Some(on_change_color)} styles={Some(button_styles)}>
<Text size={16.0} content={"Change Tile Color".to_string()} />
</Button>
<Text size={11.0} content={"Go ahead and click the button! The tile won't move.".to_string()} styles={Some(text_styles)} />
</Window>
</>
}
}
fn startup(
mut commands: Commands,
mut font_mapping: ResMut<FontMapping>,
asset_server: Res<AssetServer>,
) {
commands.spawn_bundle(UICameraBundle::new());
font_mapping.add("Roboto", asset_server.load("roboto.kayak_font"));
let context = BevyContext::new(|context| {
render! {
<App>
<ControlPanel />
</App>
}
});
commands.insert_resource(context);
}
fn main() {
BevyApp::new()
.insert_resource(WindowDescriptor {
width: 1270.0,
height: 720.0,
title: String::from("UI Example"),
resizable: false,
..Default::default()
})
.insert_resource(ActiveColor { index: 0 })
.add_plugins(DefaultPlugins)
.add_plugin(BevyKayakUIPlugin)
.add_startup_system(startup)
.add_startup_system(world_setup)
.add_system(move_ghost_tile)
.add_system(set_active_tile_target)
.add_system(move_active_tile)
.add_system(on_color_change)
.run();
}
// ! === Unnecessary Details Below === ! //
// Below this point are mainly implementation details. The main purpose of this example is to show how to know
// when to allow or disallow world interaction through `BevyContext` (see the `set_active_tile_target` function)
/// A resource used to control the color of the tiles
struct ActiveColor {
index: usize,
}
/// A component used to control the "Active Tile" that moves to the clicked positions
#[derive(Default, Component)]
struct ActiveTile {
target: Vec2,
}
/// A component used to control the "Ghost Tile" that follows the user's cursor
#[derive(Component)]
struct GhostTile;
/// A component used to mark the "world camera" (differentiating it from other cameras possibly in the scene)
#[derive(Component)]
struct WorldCamera;
/// A system that moves the active tile to its target position
fn move_active_tile(mut tile: Query<(&mut Transform, &ActiveTile)>) {
let (mut transform, tile) = tile.single_mut();
let curr_pos = transform.translation.xy();
let next_pos = curr_pos.lerp(tile.target, 0.1);
transform.translation.x = next_pos.x;
transform.translation.y = next_pos.y;
}
/// A system that moves the ghost tile to the cursor's position
fn move_ghost_tile(
mut tile: Query<&mut Transform, With<GhostTile>>,
mut cursor_moved: EventReader<CursorMoved>,
camera_transform: Query<&GlobalTransform, With<WorldCamera>>,
windows: Res<Windows>,
) {
for _ in cursor_moved.iter() {
let world_pos = cursor_to_world(&windows, &camera_transform.single());
let tile_pos = world_to_tile(world_pos);
let mut ghost = tile.single_mut();
ghost.translation.x = tile_pos.x;
ghost.translation.y = tile_pos.y;
}
}
/// A system that updates the tiles' color
fn on_color_change(
mut active_tile: Query<&mut Sprite, (With<ActiveTile>, Without<GhostTile>)>,
mut ghost_tile: Query<&mut Sprite, (With<GhostTile>, Without<ActiveTile>)>,
active_color: Res<ActiveColor>,
) {
if !active_color.is_changed() {
return;
}
let mut active_tile = active_tile.single_mut();
active_tile.color = COLORS[active_color.index];
let mut ghost_tile = ghost_tile.single_mut();
ghost_tile.color = ghost_color(COLORS[active_color.index]);
}
/// A system that sets up the world
fn world_setup(mut commands: Commands, active_color: Res<ActiveColor>) {
commands
.spawn_bundle(OrthographicCameraBundle::new_2d())
.insert(WorldCamera);
commands
.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: COLORS[active_color.index],
custom_size: Some(TILE_SIZE),
..Default::default()
},
..Default::default()
})
.insert(ActiveTile::default());
commands
.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: ghost_color(COLORS[active_color.index]),
custom_size: Some(TILE_SIZE),
..Default::default()
},
..Default::default()
})
.insert(GhostTile);
}
/// Get the world position of the cursor in 2D space
fn cursor_to_world(windows: &Windows, camera_transform: &GlobalTransform) -> Vec2 {
let window = windows.get_primary().unwrap();
let size = Vec2::new(window.width(), window.height());
let mut pos = window.cursor_position().unwrap_or_default();
pos -= size / 2.0;
let point = camera_transform.compute_matrix() * pos.extend(0.0).extend(1.0);
point.xy()
}
/// Converts a world coordinate to a rounded tile coordinate
fn world_to_tile(world_pos: Vec2) -> Vec2 {
let extents = TILE_SIZE / 2.0;
let world_pos = world_pos - extents;
(world_pos / TILE_SIZE).ceil() * TILE_SIZE
}
/// Get the ghost tile color for a given color
fn ghost_color(color: BevyColor) -> BevyColor {
let mut c = color;
c.set_a(0.35);
c
}
......@@ -508,4 +508,29 @@ impl KayakContext {
pub fn get_last_clicked_widget(&self) -> Binding<Index> {
self.event_dispatcher.last_clicked.clone()
}
/// Returns true if the cursor is currently over a valid widget
///
/// For the purposes of this method, a valid widget is one which has the means to display a visual component on its own.
/// This means widgets specified with `RenderCommand::Empty`, `RenderCommand::Layout`, or `RenderCommand::Clip`
/// do not meet the requirements to "contain" the cursor.
pub fn contains_cursor(&self) -> bool {
self.event_dispatcher.contains_cursor()
}
/// Returns true if the cursor may be needed by a widget or it's already in use by one
///
/// This is useful for checking if certain events (such as a click) would "matter" to the UI at all. Example widgets
/// include buttons, sliders, and text boxes.
pub fn wants_cursor(&self) -> bool {
self.event_dispatcher.wants_cursor()
}
/// Returns true if the cursor is currently in use by a widget
///
/// This is most often useful for checking drag events as it will still return true even if the drag continues outside
/// the widget bounds (as long as it started within it).
pub fn has_cursor(&self) -> bool {
self.event_dispatcher.has_cursor()
}
}
use crate::flo_binding::{Binding, MutableBound};
use crate::layout_cache::Rect;
use crate::render_command::RenderCommand;
use crate::widget_manager::WidgetManager;
use crate::{
Event, EventType, Index, InputEvent, InputEventCategory, KayakContext, KeyCode, KeyboardEvent,
KeyboardModifiers, PointerEvents,
KeyboardModifiers, PointerEvents, Widget,
};
use std::collections::{HashMap, HashSet};
......@@ -41,6 +42,9 @@ pub(crate) struct EventDispatcher {
previous_events: EventMap,
keyboard_modifiers: KeyboardModifiers,
pub last_clicked: Binding<Index>,
contains_cursor: Option<bool>,
wants_cursor: Option<bool>,
has_cursor: Option<Index>,
}
impl EventDispatcher {
......@@ -52,6 +56,9 @@ impl EventDispatcher {
next_mouse_position: Default::default(),
previous_events: Default::default(),
keyboard_modifiers: Default::default(),
contains_cursor: None,
wants_cursor: None,
has_cursor: None,
}
}
......@@ -67,6 +74,34 @@ impl EventDispatcher {
self.current_mouse_position
}
/// Returns true if the cursor is currently over a valid widget
///
/// For the purposes of this method, a valid widget is one which has the means to display a visual component on its own.
/// This means widgets specified with [`RenderCommand::Empty`], [`RenderCommand::Layout`], or [`RenderCommand::Clip`]
/// do not meet the requirements to "contain" the cursor.
#[allow(dead_code)]
pub fn contains_cursor(&self) -> bool {
self.contains_cursor.unwrap_or_default()
}
/// Returns true if the cursor may be needed by a widget or it's already in use by one
///
/// This is useful for checking if certain events (such as a click) would "matter" to the UI at all. Example widgets
/// include buttons, sliders, and text boxes.
#[allow(dead_code)]
pub fn wants_cursor(&self) -> bool {
self.wants_cursor.unwrap_or_default() || self.has_cursor.is_some()
}
/// Returns true if the cursor is currently in use by a widget
///
/// This is most often useful for checking drag events as it will still return true even if the drag continues outside
/// the widget bounds (as long as it started within it).
#[allow(dead_code)]
pub fn has_cursor(&self) -> bool {
self.has_cursor.is_some()
}
/// Process and dispatch an [InputEvent](crate::InputEvent)
#[allow(dead_code)]
pub fn process_event(&mut self, input_event: InputEvent, context: &mut KayakContext) {
......@@ -161,6 +196,12 @@ impl EventDispatcher {
return event_stream;
};
// === Setup Cursor States === //
let old_contains_cursor = self.contains_cursor;
let old_wants_cursor = self.wants_cursor;
self.contains_cursor = None;
self.wants_cursor = None;
// === Mouse Events === //
let mut stack: Vec<TreeNode> = vec![(root, 0)];
while stack.len() > 0 {
......@@ -247,9 +288,18 @@ impl EventDispatcher {
}
}
// Apply changes
// === Process Cursor States === //
self.current_mouse_position = self.next_mouse_position;
if self.contains_cursor.is_none() {
// No change -> revert
self.contains_cursor = old_contains_cursor;
}
if self.wants_cursor.is_none() {
// No change -> revert
self.wants_cursor = old_wants_cursor;
}
event_stream
}
......@@ -275,6 +325,22 @@ impl EventDispatcher {
event_stream.push(Event::new(node, EventType::MouseIn));
}
}
if self.contains_cursor.is_none() || !self.contains_cursor.unwrap_or_default() {
if let Some(widget) = widget_manager.current_widgets.get(node).unwrap() {
// Check if the cursor moved onto a widget that qualifies as one that can contain it
if Self::can_contain_cursor(widget) {
self.contains_cursor = Some(is_contained);
}
}
}
if self.wants_cursor.is_none() || !self.wants_cursor.unwrap_or_default() {
let focusable = widget_manager.get_focusable(node);
// Check if the cursor moved onto a focusable widget (i.e. one that would want it)
if matches!(focusable, Some(true)) {
self.wants_cursor = Some(is_contained);
}
}
// Check for hover eligibility
if is_contained {
......@@ -298,12 +364,23 @@ impl EventDispatcher {
Self::update_state(states, (node, depth), layout, EventType::Focus);
}
}
if self.has_cursor.is_none() {
let widget = widget_manager.current_widgets.get(node).unwrap();
if let Some(widget) = widget {
// Check if the cursor moved onto a widget that qualifies as one that can contain it
if Self::can_contain_cursor(widget) {
self.has_cursor = Some(node);
}
}
}
}
}
}
InputEvent::MouseLeftRelease => {
// Reset global mouse pressed
self.is_mouse_pressed = false;
self.has_cursor = None;
if let Some(layout) = widget_manager.get_layout(&node) {
if layout.contains(&self.current_mouse_position) {
......@@ -410,6 +487,22 @@ impl EventDispatcher {
entry.insert(event_type)
}
/// Checks if the given widget is eligible to "contain" the cursor (i.e. the cursor is considered contained when hovering over it)
///
/// Currently a valid widget is defined as one where:
/// * RenderCommands is neither `Empty` nor `Layout` nor `Clip`
fn can_contain_cursor(widget: &Box<dyn Widget>) -> bool {
if let Some(styles) = widget.get_styles() {
let cmds = styles.render_command.resolve();
!matches!(
cmds,
RenderCommand::Empty | RenderCommand::Layout | RenderCommand::Clip
)
} else {
false
}
}
/// Executes default actions for events
fn execute_default(&mut self, event: Event, context: &mut KayakContext) {
match event.event_type {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment