diff --git a/bevy_kayak_ui/src/cursor.rs b/bevy_kayak_ui/src/cursor.rs new file mode 100644 index 0000000000000000000000000000000000000000..338df80827354e96f95b5a08904f14c13144bdcc --- /dev/null +++ b/bevy_kayak_ui/src/cursor.rs @@ -0,0 +1,41 @@ +use kayak_core::CursorIcon; + +pub fn convert_cursor_icon(cursor_icon: CursorIcon) -> bevy::prelude::CursorIcon { + match cursor_icon { + CursorIcon::Default => bevy::prelude::CursorIcon::Default, + CursorIcon::Crosshair => bevy::prelude::CursorIcon::Crosshair, + CursorIcon::Hand => bevy::prelude::CursorIcon::Hand, + CursorIcon::Arrow => bevy::prelude::CursorIcon::Arrow, + CursorIcon::Move => bevy::prelude::CursorIcon::Move, + CursorIcon::Text => bevy::prelude::CursorIcon::Text, + CursorIcon::Wait => bevy::prelude::CursorIcon::Wait, + CursorIcon::Help => bevy::prelude::CursorIcon::Help, + CursorIcon::Progress => bevy::prelude::CursorIcon::Progress, + CursorIcon::NotAllowed => bevy::prelude::CursorIcon::NotAllowed, + CursorIcon::ContextMenu => bevy::prelude::CursorIcon::ContextMenu, + CursorIcon::Cell => bevy::prelude::CursorIcon::Cell, + CursorIcon::VerticalText => bevy::prelude::CursorIcon::VerticalText, + CursorIcon::Alias => bevy::prelude::CursorIcon::Alias, + CursorIcon::Copy => bevy::prelude::CursorIcon::Copy, + CursorIcon::NoDrop => bevy::prelude::CursorIcon::NoDrop, + CursorIcon::Grab => bevy::prelude::CursorIcon::Grab, + CursorIcon::Grabbing => bevy::prelude::CursorIcon::Grabbing, + CursorIcon::AllScroll => bevy::prelude::CursorIcon::AllScroll, + CursorIcon::ZoomIn => bevy::prelude::CursorIcon::ZoomIn, + CursorIcon::ZoomOut => bevy::prelude::CursorIcon::ZoomOut, + CursorIcon::EResize => bevy::prelude::CursorIcon::EResize, + CursorIcon::NResize => bevy::prelude::CursorIcon::NResize, + CursorIcon::NeResize => bevy::prelude::CursorIcon::NeResize, + CursorIcon::NwResize => bevy::prelude::CursorIcon::NwResize, + CursorIcon::SResize => bevy::prelude::CursorIcon::SResize, + CursorIcon::SeResize => bevy::prelude::CursorIcon::SeResize, + CursorIcon::SwResize => bevy::prelude::CursorIcon::SwResize, + CursorIcon::WResize => bevy::prelude::CursorIcon::WResize, + CursorIcon::EwResize => bevy::prelude::CursorIcon::EwResize, + CursorIcon::NsResize => bevy::prelude::CursorIcon::NsResize, + CursorIcon::NeswResize => bevy::prelude::CursorIcon::NeswResize, + CursorIcon::NwseResize => bevy::prelude::CursorIcon::NwseResize, + CursorIcon::ColResize => bevy::prelude::CursorIcon::ColResize, + CursorIcon::RowResize => bevy::prelude::CursorIcon::RowResize, + } +} \ No newline at end of file diff --git a/bevy_kayak_ui/src/lib.rs b/bevy_kayak_ui/src/lib.rs index 86a10a0f0ce6caea5718ebcbc034f3dfcb90bd1d..21313338b858fdc139336db11526c54049c9671d 100644 --- a/bevy_kayak_ui/src/lib.rs +++ b/bevy_kayak_ui/src/lib.rs @@ -10,12 +10,14 @@ mod bevy_context; mod camera; mod key; mod render; +mod cursor; pub use bevy_context::BevyContext; pub use camera::*; use kayak_core::{bind, Binding, InputEvent, MutableBound}; pub use render::unified::font::FontMapping; pub use render::unified::image::ImageManager; +use crate::cursor::convert_cursor_icon; #[derive(Default)] pub struct BevyKayakUIPlugin; @@ -40,8 +42,15 @@ pub fn update(world: &mut World) { if let Ok(mut context) = bevy_context.kayak_context.write() { context.set_global_state(std::mem::take(world)); context.render(); - *world = context.take_global_state::<World>().unwrap() + *world = context.take_global_state::<World>().unwrap(); + + if let Some(ref mut windows) = world.get_resource_mut::<Windows>() { + if let Some(window) = windows.get_primary_mut() { + window.set_cursor_icon(convert_cursor_icon(context.cursor_icon())); + } + } } + world.insert_resource(bevy_context); } } diff --git a/kayak_core/src/context.rs b/kayak_core/src/context.rs index 5e447244e26bb7f782908b0bec868271d7292406..fde229aa8d3465fd23e3b5abf57b6467b5280da4 100644 --- a/kayak_core/src/context.rs +++ b/kayak_core/src/context.rs @@ -1,5 +1,5 @@ use crate::assets::AssetStorage; -use crate::{Binding, Changeable, KayakContextRef}; +use crate::{Binding, Changeable, CursorIcon, KayakContextRef}; use std::collections::HashMap; use std::path::PathBuf; @@ -27,6 +27,7 @@ pub struct KayakContext { widget_state_lifetimes: HashMap<crate::Index, HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>>, widget_states: HashMap<crate::Index, resources::Resources>, + cursor_icon: CursorIcon } impl std::fmt::Debug for KayakContext { @@ -42,6 +43,7 @@ impl KayakContext { assets: resources::Resources::default(), current_effect_index: 0, current_state_index: 0, + cursor_icon: CursorIcon::Default, event_dispatcher: EventDispatcher::new(), global_bindings: HashMap::new(), global_state: resources::Resources::default(), @@ -394,6 +396,7 @@ impl KayakContext { // self.widget_manager.dirty_nodes.clear(); self.widget_manager.render(); self.widget_manager.calculate_layout(); + self.update_cursor(); } /// Processes the given input events @@ -557,4 +560,26 @@ impl KayakContext { pub fn force_release_cursor(&mut self) -> Option<Index> { self.event_dispatcher.force_release_cursor() } + + pub fn cursor_icon(&self) -> CursorIcon { + self.cursor_icon + } + + pub(crate) fn set_cursor_icon(&mut self, icon: CursorIcon) { + self.cursor_icon = icon; + } + + fn update_cursor(&mut self) { + if self.event_dispatcher.hovered.is_none() { + return; + } + + let hovered = self.event_dispatcher.hovered.unwrap(); + if let Some(node) = self.widget_manager.nodes.get(hovered) { + if let Some(node) = node { + let icon = node.resolved_styles.cursor.resolve(); + self.cursor_icon = icon; + } + } + } } diff --git a/kayak_core/src/cursor_icon.rs b/kayak_core/src/cursor_icon.rs new file mode 100644 index 0000000000000000000000000000000000000000..82b790e83c0ef49eec8a7c3cee9067d4e6496d5b --- /dev/null +++ b/kayak_core/src/cursor_icon.rs @@ -0,0 +1,44 @@ +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum CursorIcon { + Default, + Crosshair, + Hand, + Arrow, + Move, + Text, + Wait, + Help, + Progress, + NotAllowed, + ContextMenu, + Cell, + VerticalText, + Alias, + Copy, + NoDrop, + Grab, + Grabbing, + AllScroll, + ZoomIn, + ZoomOut, + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, +} + +impl Default for CursorIcon { + fn default() -> Self { + CursorIcon::Default + } +} \ No newline at end of file diff --git a/kayak_core/src/event_dispatcher.rs b/kayak_core/src/event_dispatcher.rs index 689ea42377241e37eb254f8c37dcb0722969f1d7..2131b562ade394e8e4976bf7e9207d92044b4cae 100644 --- a/kayak_core/src/event_dispatcher.rs +++ b/kayak_core/src/event_dispatcher.rs @@ -48,6 +48,7 @@ pub(crate) struct EventDispatcher { wants_cursor: Option<bool>, has_cursor: Option<Index>, pub cursor_capture: Option<Index>, + pub hovered: Option<Index> } impl EventDispatcher { @@ -64,6 +65,7 @@ impl EventDispatcher { wants_cursor: None, has_cursor: None, cursor_capture: None, + hovered: None } } @@ -144,6 +146,12 @@ impl EventDispatcher { self.has_cursor.is_some() } + /// The currently hovered node + #[allow(dead_code)] + pub fn hovered(&self) -> Option<Index> { + self.hovered + } + /// Process and dispatch an [InputEvent](crate::InputEvent) #[allow(dead_code)] pub fn process_event(&mut self, input_event: InputEvent, context: &mut KayakContext) { @@ -252,8 +260,10 @@ impl EventDispatcher { }; // === Setup Cursor States === // + let old_hovered = self.hovered; let old_contains_cursor = self.contains_cursor; let old_wants_cursor = self.wants_cursor; + self.hovered = None; self.contains_cursor = None; self.wants_cursor = None; self.next_mouse_position = self.current_mouse_position; @@ -375,6 +385,9 @@ impl EventDispatcher { } widget_manager.focus_tree.focus(node); } + EventType::Hover(..) => { + self.hovered = Some(node); + } _ => {} } } @@ -393,6 +406,10 @@ impl EventDispatcher { self.current_mouse_position = self.next_mouse_position; self.is_mouse_pressed = self.next_mouse_pressed; + if self.hovered.is_none() { + // No change -> revert + self.hovered = old_hovered; + } if self.contains_cursor.is_none() { // No change -> revert self.contains_cursor = old_contains_cursor; @@ -700,6 +717,7 @@ impl EventDispatcher { self.contains_cursor = from.contains_cursor; self.wants_cursor = from.wants_cursor; self.has_cursor = from.has_cursor; + self.hovered = from.hovered; // Do not include: // self.cursor_capture = from.cursor_capture; diff --git a/kayak_core/src/lib.rs b/kayak_core/src/lib.rs index 02bc713dddc60d011931ecdb3eb0687b0781b623..b0a21bb9daa8c9c2485722478f8b25acfddcc4e6 100644 --- a/kayak_core/src/lib.rs +++ b/kayak_core/src/lib.rs @@ -25,6 +25,7 @@ pub mod tree; mod vec; pub mod widget; pub mod widget_manager; +mod cursor_icon; use std::sync::{Arc, RwLock}; @@ -34,6 +35,7 @@ pub use color::Color; pub use context::*; pub use context_ref::KayakContextRef; pub use cursor::PointerEvents; +pub use cursor_icon::CursorIcon; pub use event::*; pub use focus_tree::FocusTree; pub use fragment::{Fragment, FragmentProps}; diff --git a/kayak_core/src/styles.rs b/kayak_core/src/styles.rs index 3929618113e42e3ec855fe304b2ddedf7b71e077..9c5934c2d6d4be2e3a4f4c228360ec96597c2738 100644 --- a/kayak_core/src/styles.rs +++ b/kayak_core/src/styles.rs @@ -1,7 +1,7 @@ pub use morphorm::{LayoutType, PositionType, Units}; use crate::cursor::PointerEvents; -use crate::{color::Color, render_command::RenderCommand}; +use crate::{color::Color, CursorIcon, render_command::RenderCommand}; #[derive(Debug, Clone, PartialEq)] pub enum StyleProp<T: Default + Clone> { @@ -142,6 +142,7 @@ define_styles! { pub border: StyleProp<(f32, f32, f32, f32)>, pub bottom: StyleProp<Units>, pub color: StyleProp<Color>, + pub cursor: StyleProp<CursorIcon>, pub height: StyleProp<Units>, pub layout_type: StyleProp<LayoutType>, pub left: StyleProp<Units>, @@ -179,6 +180,7 @@ impl Style { border_radius: StyleProp::Default, bottom: StyleProp::Default, color: StyleProp::Inherit, + cursor: StyleProp::Inherit, height: StyleProp::Default, layout_type: StyleProp::Default, left: StyleProp::Default, diff --git a/src/widgets/button.rs b/src/widgets/button.rs index d42de1f9059ac7e737b6f0674cdfbf01b8d953e6..2e31db769774a01e9debf59c6fea6a45b02cae33 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,3 +1,4 @@ +use kayak_core::CursorIcon; use crate::core::{ render_command::RenderCommand, rsx, @@ -51,6 +52,7 @@ pub fn Button(props: ButtonProps) { height: StyleProp::Value(Units::Pixels(45.0)), padding_left: StyleProp::Value(Units::Stretch(1.0)), padding_right: StyleProp::Value(Units::Stretch(1.0)), + cursor: CursorIcon::Hand.into(), ..Default::default() }), ); diff --git a/src/widgets/text.rs b/src/widgets/text.rs index d73d3833ee68b1bba1ff8268172c0341d4981870..529ac4325794665ed010f9abcfe218ef1421df75 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -1,4 +1,4 @@ -use kayak_core::{styles::Units, Binding, Bound}; +use kayak_core::{styles::Units, Binding, Bound, CursorIcon}; use kayak_font::{CoordinateSystem, KayakFont}; use crate::core::{ @@ -73,11 +73,13 @@ pub fn Text(props: TextProps) { font: font_name.clone().unwrap_or("Roboto".into()), }; + let styles = props.styles.clone().unwrap_or_default(); props.styles = Some(Style { render_command: StyleProp::Value(render_command), width: StyleProp::Value(Units::Pixels(layout_size.0)), height: StyleProp::Value(Units::Pixels(layout_size.1)), - ..props.styles.clone().unwrap_or_default() + cursor: StyleProp::select(&[&styles.cursor, &StyleProp::Value(CursorIcon::Text)]).clone(), + ..styles }); } else { context.mark_dirty(); diff --git a/src/widgets/text_box.rs b/src/widgets/text_box.rs index 1a7514fccedf136742ca07cb35b9f3b20225db90..eebabc03e114b11564aecdc54301d03e919cd609 100644 --- a/src/widgets/text_box.rs +++ b/src/widgets/text_box.rs @@ -5,6 +5,7 @@ use crate::core::{ widget, Bound, Children, Color, EventType, MutableBound, OnEvent, WidgetProps, }; use std::sync::{Arc, RwLock}; +use kayak_core::CursorIcon; use crate::widgets::{Background, Clip, Text}; @@ -94,6 +95,7 @@ pub fn TextBox(props: TextBoxProps) { top: Units::Pixels(0.0).into(), bottom: Units::Pixels(0.0).into(), height: Units::Pixels(26.0).into(), + cursor: CursorIcon::Text.into(), ..Default::default() }), ); diff --git a/src/widgets/window.rs b/src/widgets/window.rs index 8a932e3640b77dad4b024058c70a1a2fcb0068ed..a1ff69047f04a797c914243526e746bc20e685f1 100644 --- a/src/widgets/window.rs +++ b/src/widgets/window.rs @@ -1,3 +1,4 @@ +use kayak_core::CursorIcon; use crate::core::{ color::Color, render_command::RenderCommand, @@ -88,9 +89,16 @@ pub fn Window(props: WindowProps) { ..Style::default() }; + let cursor = if is_dragging { + CursorIcon::Grabbing + } else { + CursorIcon::Grab + }; + let title_background_styles = Style { background_color: StyleProp::Value(Color::new(0.0781, 0.0898, 0.101, 1.0)), border_radius: StyleProp::Value((5.0, 0.0, 0.0, 5.0)), + cursor: cursor.into(), height: StyleProp::Value(Units::Pixels(24.0)), width: StyleProp::Value(Units::Stretch(1.0)), left: StyleProp::Value(Units::Pixels(0.0)), @@ -103,6 +111,7 @@ pub fn Window(props: WindowProps) { let title_text_styles = Style { height: StyleProp::Value(Units::Pixels(25.0)), + cursor: StyleProp::Inherit, ..Style::default() };