From fec6c3a096dffe2ffdd63f9bdd5e2d3f20539fb3 Mon Sep 17 00:00:00 2001 From: StarToaster <startoaster23@gmail.com> Date: Thu, 27 Oct 2022 19:38:38 -0400 Subject: [PATCH] Fixed a few issues. Added new bevy scene interaction example. --- examples/bevy_scene.rs | 272 +++++++++++++++++++++++++++++++++++++++ examples/context.rs | 3 + examples/quads.rs | 3 + examples/simple_state.rs | 3 + examples/text_box.rs | 3 + src/camera/camera.rs | 6 +- src/event_dispatcher.rs | 10 +- src/lib.rs | 2 +- 8 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 examples/bevy_scene.rs diff --git a/examples/bevy_scene.rs b/examples/bevy_scene.rs new file mode 100644 index 0000000..7804645 --- /dev/null +++ b/examples/bevy_scene.rs @@ -0,0 +1,272 @@ +use bevy::{ + math::{Vec3Swizzles, Vec4Swizzles}, + prelude::*, +}; +use kayak_ui::prelude::{widgets::*, *}; + +const TILE_SIZE: Vec2 = Vec2::from_array([50.0, 50.0]); +const COLORS: &[Color] = &[Color::TEAL, Color::MAROON, Color::INDIGO]; + +// ! === 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 +#[derive(Resource)] +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; + +/// 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>>, + event_context: Res<EventDispatcher>, + 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 event_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; +} + +/// 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((Camera2dBundle::default(), WorldCamera)); + commands + .spawn(SpriteBundle { + sprite: Sprite { + color: COLORS[active_color.index], + custom_size: Some(TILE_SIZE), + ..Default::default() + }, + ..Default::default() + }) + .insert(ActiveTile::default()); + commands + .spawn(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.y = size.y - pos.y; + 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: Color) -> Color { + let mut c = color; + c.set_a(0.35); + c +} + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = KayakRootContext::new(); + + let handle_change_color = OnEvent::new( + move |In((event_dispatcher_context, _, event, _)): In<( + EventDispatcherContext, + WidgetState, + Event, + Entity, + )>, + mut active_color: ResMut<ActiveColor>| { + match event.event_type { + EventType::Click(..) => { + active_color.index = (active_color.index + 1) % COLORS.len(); + } + _ => {} + } + (event_dispatcher_context, event) + }, + ); + + let text_styles = KStyle { + left: StyleProp::Value(Units::Stretch(1.0)), + right: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }; + let button_styles = KStyle { + 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: StyleProp::Value(Edge::axis(Units::Pixels(8.0), Units::Pixels(48.0))), + ..Default::default() + }; + + let parent_id = None; + rsx! { + <KayakAppBundle> + <WindowBundle + window={KWindow { + draggable: true, + initial_position: Vec2::new(50.0, 50.0), + size: Vec2::new(300.0, 200.0), + title: "Square Mover: The Game".to_string(), + ..Default::default() + }} + > + <TextWidgetBundle + text={TextProps { + 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(), + ..Default::default() + }} + styles={text_styles.clone()} + /> + <KButtonBundle + on_event={handle_change_color} + styles={button_styles} + > + <TextWidgetBundle + text={TextProps { + size: 16.0, + content: "Change Tile Color".to_string(), + ..Default::default() + }} + /> + </KButtonBundle> + <TextWidgetBundle + text={TextProps { + size: 11.0, + content: "Go ahead and click the button! The tile won't move.".to_string(), + ..Default::default() + }} + styles={text_styles} + /> + </WindowBundle> + </KayakAppBundle> + } + + commands.insert_resource(widget_context); +} + +fn main() { + App::new() + .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) + .insert_resource(ActiveColor { index: 0 }) + .add_plugins(DefaultPlugins) + .add_plugin(KayakContextPlugin) + .add_plugin(KayakWidgets) + .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() +} diff --git a/examples/context.rs b/examples/context.rs index 3a2e654..5630b1f 100644 --- a/examples/context.rs +++ b/examples/context.rs @@ -339,6 +339,9 @@ fn startup( ) { font_mapping.set_default(asset_server.load("roboto.kayak_font")); + // Camera 2D forces a clear pass in bevy. + // We do this because our scene is not rendering anything else. + commands.spawn(Camera2dBundle::default()); commands.spawn(UICameraBundle::new()); let mut widget_context = KayakRootContext::new(); diff --git a/examples/quads.rs b/examples/quads.rs index 1681416..514c9a9 100644 --- a/examples/quads.rs +++ b/examples/quads.rs @@ -83,6 +83,9 @@ fn startup( ) { font_mapping.set_default(asset_server.load("roboto.kayak_font")); + // Camera 2D forces a clear pass in bevy. + // We do this because our scene is not rendering anything else. + commands.spawn(Camera2dBundle::default()); commands.spawn(UICameraBundle::new()); let mut widget_context = KayakRootContext::new(); diff --git a/examples/simple_state.rs b/examples/simple_state.rs index 00fe63c..eee326a 100644 --- a/examples/simple_state.rs +++ b/examples/simple_state.rs @@ -63,6 +63,9 @@ fn startup( ) { font_mapping.set_default(asset_server.load("roboto.kayak_font")); + // Camera 2D forces a clear pass in bevy. + // We do this because our scene is not rendering anything else. + commands.spawn(Camera2dBundle::default()); commands.spawn(UICameraBundle::new()); let mut widget_context = KayakRootContext::new(); diff --git a/examples/text_box.rs b/examples/text_box.rs index 0e3f409..723f96e 100644 --- a/examples/text_box.rs +++ b/examples/text_box.rs @@ -90,6 +90,9 @@ fn startup( ) { font_mapping.set_default(asset_server.load("roboto.kayak_font")); + // Camera 2D forces a clear pass in bevy. + // We do this because our scene is not rendering anything else. + commands.spawn(Camera2dBundle::default()); commands.spawn(UICameraBundle::new()); let mut widget_context = KayakRootContext::new(); diff --git a/src/camera/camera.rs b/src/camera/camera.rs index f04d3ab..edd4761 100644 --- a/src/camera/camera.rs +++ b/src/camera/camera.rs @@ -1,6 +1,6 @@ use bevy::{ ecs::query::QueryItem, - prelude::{Bundle, Camera2d, Component, GlobalTransform, Transform, With}, + prelude::{Bundle, Component, GlobalTransform, Transform, With}, render::{ camera::{Camera, CameraProjection, CameraRenderGraph, WindowOrigin}, extract_component::ExtractComponent, @@ -28,7 +28,7 @@ impl ExtractComponent for CameraUiKayak { #[derive(Bundle)] pub struct UICameraBundle { pub camera: Camera, - pub camera_2d: Camera2d, + // pub camera_2d: Camera2d, pub camera_render_graph: CameraRenderGraph, pub orthographic_projection: UIOrthographicProjection, pub visible_entities: VisibleEntities, @@ -71,7 +71,7 @@ impl UICameraBundle { frustum, visible_entities: VisibleEntities::default(), transform, - camera_2d: Camera2d::default(), + // camera_2d: Camera2d::default(), global_transform: Default::default(), marker: CameraUiKayak, } diff --git a/src/event_dispatcher.rs b/src/event_dispatcher.rs index cb70a75..53a6340 100644 --- a/src/event_dispatcher.rs +++ b/src/event_dispatcher.rs @@ -44,7 +44,7 @@ impl Default for EventState { } #[derive(Resource, Debug, Clone)] -pub(crate) struct EventDispatcher { +pub struct EventDispatcher { is_mouse_pressed: bool, next_mouse_pressed: bool, current_mouse_position: (f32, f32), @@ -55,12 +55,12 @@ pub(crate) struct EventDispatcher { contains_cursor: Option<bool>, wants_cursor: Option<bool>, has_cursor: Option<WrappedIndex>, - pub cursor_capture: Option<WrappedIndex>, - pub hovered: Option<WrappedIndex>, + pub(crate) cursor_capture: Option<WrappedIndex>, + pub(crate) hovered: Option<WrappedIndex>, } impl EventDispatcher { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self { // last_clicked: Binding::new(WrappedIndex(Entity::from_raw(0))), is_mouse_pressed: Default::default(), @@ -175,7 +175,7 @@ impl EventDispatcher { // } /// Process and dispatch a set of [InputEvents](crate::InputEvent) - pub fn process_events( + pub(crate) fn process_events( &mut self, input_events: Vec<InputEvent>, context: &mut KayakRootContext, diff --git a/src/lib.rs b/src/lib.rs index 2c93120..5415530 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub mod prelude { pub use crate::clone_component::PreviousWidget; pub use crate::context::*; pub use crate::event::*; - pub use crate::event_dispatcher::EventDispatcherContext; + pub use crate::event_dispatcher::{EventDispatcher, EventDispatcherContext}; pub use crate::focus_tree::Focusable; pub use crate::input_event::*; pub use crate::keyboard_event::*; -- GitLab