diff --git a/src/change_tracker.rs b/src/change_tracker.rs index 211ad77..16ecd9d 100644 --- a/src/change_tracker.rs +++ b/src/change_tracker.rs @@ -35,9 +35,14 @@ impl Ct { pub fn reset(&mut self) { self.changed = false; } + + /// Triggers the change tracker to be true + pub fn trigger(&mut self) { + self.changed = true; + } /// Silently mutate the inner data, this wont be tracked - pub fn silent_mutate(&mut self) -> &mut T { + pub fn silently_mutate(&mut self) -> &mut T { &mut self.data } @@ -88,16 +93,20 @@ mod tests { #[test] fn silent_mutate() { let mut c = Ct::new(100); - let a = c.silent_mutate(); + let a = c.silently_mutate(); *a = 10; assert!(!c.changed()); } #[test] - fn reset() { + fn reset_trigger() { let mut c = Ct::new(100); *c = 10; c.reset(); assert!(!c.changed()); // should not be changed because of reset + + let mut c = Ct::new(100); + c.trigger(); + assert!(c.changed()); // should changed because of trigger } } \ No newline at end of file diff --git a/src/ecs/events.rs b/src/ecs/events.rs index 2374fd3..c8db0f7 100644 --- a/src/ecs/events.rs +++ b/src/ecs/events.rs @@ -1,6 +1,6 @@ use std::{collections::{HashMap, VecDeque}, any::{TypeId, Any}, cell::RefCell}; -use crate::castable_any::CastableAny; +use crate::{castable_any::CastableAny, plugin::Plugin}; pub trait Event: Clone + Send + Sync + 'static {} impl Event for T {} @@ -65,4 +65,13 @@ impl EventQueue { None } } +} + +#[derive(Default)] +pub struct EventsPlugin; + +impl Plugin for EventsPlugin { + fn setup(&self, game: &mut crate::game::Game) { + game.world().insert_resource(EventQueue::new()); + } } \ No newline at end of file diff --git a/src/game.rs b/src/game.rs index e670246..e719846 100755 --- a/src/game.rs +++ b/src/game.rs @@ -60,7 +60,7 @@ impl GameLoop { pub async fn on_init(&mut self) { // Create the EventQueue resource in the world - self.world.insert_resource(EventQueue::new()); + self.world.insert_resource(self.window.clone()); } pub fn run_sync(&mut self, event: Event<()>, control_flow: &mut ControlFlow) { diff --git a/src/input.rs b/src/input.rs index ce0e67a..a031c7f 100755 --- a/src/input.rs +++ b/src/input.rs @@ -3,9 +3,9 @@ use std::{collections::{HashMap, hash_map::DefaultHasher}, hash::{Hash, Hasher}, use gilrs_core::Gilrs; use glam::Vec2; use tracing::{warn, debug}; -use winit::event::{ElementState, MouseScrollDelta}; +use winit::{event::{ElementState, MouseScrollDelta}, window::{Window, CursorGrabMode}, dpi::PhysicalPosition}; -use crate::{ecs::{SimpleSystem, EventQueue}, input_event::InputEvent, plugin::Plugin}; +use crate::{ecs::{SimpleSystem, EventQueue}, input_event::InputEvent, plugin::Plugin, render::window::WindowOptions, change_tracker::Ct}; pub type KeyCode = winit::event::VirtualKeyCode; @@ -246,6 +246,8 @@ impl InputSystem { delta: Vec2::new(delta.0 as f32, delta.1 as f32) }; + debug!("delta: {:?}", delta); + event_queue.trigger_event(delta); }, InputEvent::CursorMoved { position, .. } => { @@ -253,6 +255,19 @@ impl InputSystem { pos: Vec2::new(position.x as f32, position.y as f32) }; + // if the window is set to confine the cursor, and the cursor is invisible, + // set the cursor position to the center of the screen. + let options = world.get_resource::>().unwrap(); + if options.cursor_grab == CursorGrabMode::Confined && options.cursor_visible == false { + let window = world.get_resource::>().unwrap(); + let size = window.inner_size(); + let middle = PhysicalPosition { + x: size.width / 2, + y: size.height / 2, + }; + window.set_cursor_position(middle).unwrap(); + } + event_queue.trigger_event(exact); }, InputEvent::CursorEntered { .. } => { diff --git a/src/plugin.rs b/src/plugin.rs index 8be0e8e..45d0143 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,12 +1,26 @@ use lyra_resource::ResourceManager; +use crate::ecs::EventsPlugin; use crate::game::Game; use crate::input::InputPlugin; +use crate::render::window::WindowPlugin; /// A Plugin is something you can add to a `Game` that can be used to define systems, or spawn initial entities. pub trait Plugin { /// Setup this plugin. This runs before the game has started fn setup(&self, game: &mut Game); + + fn is_ready(&self, game: &mut Game) -> bool { + true + } + + fn complete(&self, game: &mut Game) { + + } + + fn cleanup(&self, game: &mut Game) { + + } } impl

Plugin for P @@ -94,7 +108,9 @@ pub struct DefaultPlugins; impl Plugin for DefaultPlugins { fn setup(&self, game: &mut Game) { // setup input + EventsPlugin::default().setup(game); InputPlugin::default().setup(game); ResourceManagerPlugin::default().setup(game); + WindowPlugin::default().setup(game); } } \ No newline at end of file diff --git a/src/render/mod.rs b/src/render/mod.rs index 739a771..881372a 100755 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -8,4 +8,5 @@ pub mod mesh; pub mod texture; pub mod shader_loader; pub mod material; -pub mod camera; \ No newline at end of file +pub mod camera; +pub mod window; \ No newline at end of file diff --git a/src/render/window.rs b/src/render/window.rs new file mode 100644 index 0000000..bdd4dd3 --- /dev/null +++ b/src/render/window.rs @@ -0,0 +1,315 @@ +use std::sync::Arc; + +use glam::Vec2; +use tracing::{warn, error}; +use winit::{window::{Window, Fullscreen}, dpi::{LogicalPosition, LogicalSize}, error::ExternalError}; + +pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel}; + +//use winit::dpi::{LogicalPosition, PhysicalPosition}; + +use crate::{plugin::Plugin, change_tracker::Ct}; + +#[derive(Default, Clone)] +pub enum WindowMode { + /// The window will use the full size of the screen. + Fullscreen, + /// The window will be fullscreen with the full size of the screen without a border. + Borderless, + /// The window will not be fullscreen and will use the windows resolution size. + #[default] + Windowed, +} + +/// Options that the window will be created with. +#[derive(Clone)] +pub struct WindowOptions { + /// Prevents the window contents from being captured by other apps. + /// + /// Platform-specific: + /// * macOS: if false, NSWindowSharingNone is used but doesn’t completely prevent all apps + /// from reading the window content, for instance, QuickTime. + /// * iOS / Android / x11 / Wayland / Web / Orbital: Unsupported. + pub content_protected: bool, + + /// Set grabbing mode on the cursor preventing it from leaving the window. + pub cursor_grab: CursorGrabMode, + + /// Modifies whether the window catches cursor events. + + /// If true, the window will catch the cursor events. If false, events are passed through + /// the window such that any otherwindow behind it receives them. By default hittest is enabled. + /// + /// Platform-specific: + /// * iOS / Android / Web / X11 / Orbital: Unsupported. + pub cursor_hittest: bool, + + /// The cursor icon of the window. + /// + /// Platform-specific + /// * iOS / Android / Orbital: Unsupported. + pub cursor_icon: CursorIcon, + + /// The cursor’s visibility. + /// If false, this will hide the cursor. If true, this will show the cursor. + /// + /// Platform-specific: + /// * Windows: The cursor is only hidden within the confines of the window. + /// * X11: The cursor is only hidden within the confines of the window. + /// * Wayland: The cursor is only hidden within the confines of the window. + /// * macOS: The cursor is hidden as long as the window has input focus, even if + /// the cursor is outside of the window. + /// * iOS / Android / Orbital: Unsupported. + pub cursor_visible: bool, + + /// Turn window decorations on or off. + /// Enable/disable window decorations provided by the server or Winit. By default this is enabled. + /// + /// Platform-specific + /// * iOS / Android / Web: No effect. + pub decorations: bool, + + /// Sets the enabled window buttons. + /// + /// Platform-specific: + /// * Wayland / X11 / Orbital: Not implemented. + /// * Web / iOS / Android: Unsupported. + pub enabled_buttons: WindowButtons, + + /// The window mode. Can be used to set fullscreen and borderless. + pub mode: WindowMode, + + /// Sets whether the window should get IME events. + /// + /// If its allowed, the window will receive Ime events instead of KeyboardInput events. + /// This should only be allowed if the window is expecting text input. + /// + /// Platform-specific: + /// * iOS / Android / Web / Orbital: Unsupported. + pub ime_allowed: bool, + + /// Sets location of IME candidate box in client area coordinates relative to the top left. + /// + /// This is the window / popup / overlay that allows you to select the desired characters. + /// The look of this box may differ between input devices, even on the same platform. + pub ime_position: Vec2, + + /// Modifies the inner size of the window. + /// + /// Platform-specific: + /// * iOS / Android: Unsupported. + /// * Web: Sets the size of the canvas element. + pub inner_size: Vec2, + + /// Sets a maximum dimension size for the window. + /// + /// Platform-specific: + /// * iOS / Android / Web / Orbital: Unsupported. + pub max_inner_size: Option, + + /// Sets a minimum dimension size for the window. + /// + /// Platform-specific: + /// * iOS / Android / Web / Orbital: Unsupported. + pub min_inner_size: Option, + + /// Sets the window to maximized or back. + /// + /// Platform-specific: + /// * iOS / Android / Web / Orbital: Unsupported. + pub maximized: bool, + + /// Sets the window to minimized or back. + /// + /// Platform-specific: + /// * iOS / Android / Web / Orbital: Unsupported. + pub minimized: bool, + + /// Modifies the position of the window. + /// + /// Platform-specific: + /// * Web: Sets the top-left coordinates relative to the viewport. + /// * Android / Wayland: Unsupported. + //pub outer_position: Vec2, + + /// Sets whether the window is resizable or not. + /// + /// Platform-specific: + /// * X11: Due to a bug in XFCE, this has no effect on Xfwm. + /// * iOS / Android / Web: Unsupported. + pub resizeable: bool, + + /// Sets window resize increments. + /// This is a niche constraint hint usually employed by terminal emulators and other apps that need “blocky” resizes. + /// + /// Platform-specific: + /// * Wayland / Windows: Not implemented. + /// * iOS / Android / Web / Orbital: Unsupported. + pub resize_increments: Option, + + /// Sets the current window theme. Use None to fallback to system default. + /// + /// Platform-specific: + /// * macOS: This is an app-wide setting. + /// * x11: If None is used, it will default to Theme::Dark. + /// * iOS / Android / Web / x11 / Orbital: Unsupported. + pub theme: Option, + + /// Modifies the title of the window. + /// + /// Platform-specific: + /// * iOS / Android: Unsupported. + pub title: String, + + /// Sets the window icon. + /// On Windows and X11, this is typically the small icon in the top-left corner of the titlebar. + /// + /// Platform-specific + /// * iOS / Android / Web / Wayland / macOS / Orbital: Unsupported. + /// * Windows: Sets ICON_SMALL. The base size for a window icon is 16x16, but it’s recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// * X11: Has no universal guidelines for icon sizes, so you’re at the whims of the WM. That said, it’s usually in the same ballpark as on Windows. + pub icon: Option, + + /// Change the window level. + /// This is just a hint to the OS, and the system could ignore it. + pub level: WindowLevel, +} + +impl Default for WindowOptions { + fn default() -> Self { + Self { + content_protected: false, + cursor_grab: CursorGrabMode::None, + cursor_hittest: true, + cursor_icon: CursorIcon::Default, + cursor_visible: true, + decorations: true, + enabled_buttons: WindowButtons::all(), + mode: WindowMode::Windowed, + ime_allowed: false, + ime_position: Default::default(), + inner_size: Default::default(), + max_inner_size: None, + min_inner_size: None, + maximized: false, + minimized: false, + //outer_position: Default::default(), + resizeable: false, + resize_increments: None, + theme: None, + title: "Lyra Engine Game".to_string(), + icon: None, + level: WindowLevel::Normal, + } + } +} + +#[derive(Default)] +pub struct WindowPlugin { + create_options: WindowOptions, +} + +fn vec2_to_logical_pos(pos: Vec2) -> LogicalPosition { + LogicalPosition { x: pos.x, y: pos.y } +} + +fn vec2_to_logical_size(size: Vec2) -> LogicalSize { + LogicalSize { width: size.x, height: size.y } +} + +fn vec2_to_logical_pos_op(pos: Option) -> Option> { + pos.map(|pos| vec2_to_logical_pos(pos)) +} + +fn vec2_to_logical_size_op(size: Option) -> Option> { + size.map(|size| vec2_to_logical_size(size)) +} + +fn window_updater_system(world: &mut edict::World) -> anyhow::Result<()> { + if let (Some(window), Some(mut opts)) = (world.get_resource::>(), world.get_resource_mut::>()) { + // if the options changed, update the window + if opts.changed() { + window.set_content_protected(opts.content_protected); + match opts.cursor_grab { + CursorGrabMode::Confined => { + match window.set_cursor_grab(opts.cursor_grab) + .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) { + Ok(()) => {}, + Err(ExternalError::NotSupported(_)) => { + warn!("Setting CursorGrab is not supported on this platform"); + }, + Err(e) => { + error!("OS error when setting CursorGrab: {:?}", e); + } + } + }, + _ => { + match window.set_cursor_grab(opts.cursor_grab) { + Ok(()) => {}, + Err(ExternalError::NotSupported(_)) => { + warn!("Setting CursorGrab is not supported on this platform"); + }, + Err(e) => { + error!("OS error when setting CursorGrab: {:?}", e); + } + } + } + } + match window.set_cursor_hittest(opts.cursor_hittest) { + Ok(()) => {}, + Err(ExternalError::NotSupported(_)) => { + warn!("Setting cursor hittest is not supported on this platform"); + }, + Err(e) => { + error!("OS error when setting cursor hittest: {:?}", e); + } + } + window.set_cursor_icon(opts.cursor_icon); + window.set_cursor_visible(opts.cursor_visible); + window.set_decorations(opts.decorations); + window.set_enabled_buttons(opts.enabled_buttons); + + // Update the window mode. can only be done if the monitor is found + if let Some(monitor) = window.current_monitor() + .or_else(|| window.primary_monitor()) + .or_else(|| window.available_monitors().next()) { + match opts.mode { + WindowMode::Borderless => window.set_fullscreen(Some(Fullscreen::Borderless(Some(monitor)))), + WindowMode::Fullscreen => window.set_fullscreen(Some(Fullscreen::Exclusive(monitor.video_modes().next().unwrap()))), + WindowMode::Windowed => window.set_fullscreen(None), + } + } else { + warn!("Failure to get monitor handle, could not update WindowMode"); + } + + window.set_ime_allowed(opts.ime_allowed); + window.set_ime_position(vec2_to_logical_pos(opts.ime_position)); + //window.set_inner_size(vec2_to_logical_size(opts.inner_size)); // TODO + if opts.max_inner_size.is_some() { + window.set_max_inner_size(vec2_to_logical_size_op(opts.max_inner_size)); + } + if opts.min_inner_size.is_some() { + window.set_min_inner_size(vec2_to_logical_size_op(opts.min_inner_size)); + } + //window.set_maximized(opts.maximized); // TODO + //window.set_minimized(opts.minimized); // TODO + window.set_resizable(opts.resizeable); + window.set_resize_increments(vec2_to_logical_size_op(opts.resize_increments)); + window.set_theme(opts.theme); + window.set_title(&opts.title); + window.set_window_icon(opts.icon.clone()); + window.set_window_level(opts.level); + } + } + + Ok(()) +} + +impl Plugin for WindowPlugin { + fn setup(&self, game: &mut crate::game::Game) { + let window_options = WindowOptions::default(); + + game.world().insert_resource(Ct::new(window_options)); + game.with_system("window_updater", window_updater_system, &[]); + } +}