use std::sync::Arc; use glam::{Vec2, IVec2}; use tracing::{warn, error}; use winit::{window::{Window, Fullscreen}, dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, error::ExternalError}; pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel}; 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: IVec2, /// 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: glam::i32::IVec2::new(800, 600), 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 { #[allow(dead_code)] create_options: WindowOptions, } /// Convert an Vec2 to a LogicalPosition fn vec2_to_logical_pos(pos: Vec2) -> LogicalPosition { LogicalPosition { x: pos.x, y: pos.y } } /// Convert an IVec2 to a LogicalSize fn ivec2_to_logical_size(size: IVec2) -> LogicalSize { LogicalSize { width: size.x, height: size.y } } /// Convert an Option to an Option> fn ivec2_to_logical_size_op(size: Option) -> Option> { size.map(ivec2_to_logical_size) } /// Convert an Option to an Option> fn vec2_to_logical_size_op(size: Option) -> Option> { size.map(|size| LogicalSize { width: size.x, height: size.y } ) } /// Set the cursor grab of a window depending on the platform. /// This will also modify the parameter `grab` to ensure it matches what the platform can support fn set_cursor_grab(window: &Window, grab: &mut CursorGrabMode) -> anyhow::Result<()> { if *grab != CursorGrabMode::None { if cfg!(unix) { *grab = CursorGrabMode::Confined; // TODO: Find a way to see if winit is using x11 or wayland. wayland supports Locked } else if cfg!(wasm) { *grab = CursorGrabMode::Locked; } else if cfg!(windows) { *grab = CursorGrabMode::Confined; // NOTE: May support Locked later } else if cfg!(target_os = "macos") { *grab = CursorGrabMode::Locked; // NOTE: May support Confined later } else if cfg!(any(target_os = "android", target_os = "ios", target_os = "orbital")) { warn!("CursorGrabMode is not supported on Android, IOS, or Oribital, skipping"); return Ok(()) } } window.set_cursor_grab(*grab)?; Ok(()) } /// if the window is set to confine the cursor, and the cursor is invisible, /// set the cursor position to the center of the screen. fn center_mouse(window: &Window, options: &WindowOptions) { if options.cursor_grab == CursorGrabMode::Confined && !options.cursor_visible { let size = window.inner_size(); let middle = PhysicalPosition { x: size.width / 2, y: size.height / 2, }; window.set_cursor_position(middle).unwrap(); } } fn window_updater_system(world: &mut edict::World) -> anyhow::Result<()> { if let (Some(window), Some(opts)) = (world.get_resource::>(), world.get_resource::>()) { // if the options changed, update the window if opts.peek_changed() { drop(opts); // avoid attempting to get a RefMut when we already have a Ref out. // now we can get it mutable, this will trigger the ChangeTracker, so it will be reset at the end of this scope. let mut opts = world.get_resource_mut::>().unwrap(); window.set_content_protected(opts.content_protected); set_cursor_grab(&window, &mut opts.cursor_grab)?; match window.set_cursor_hittest(opts.cursor_hittest) { Ok(()) => {}, Err(ExternalError::NotSupported(_)) => { /* ignore */ }, Err(e) => { error!("OS error when setting cursor hittest: {:?}", e); } } window.set_cursor_icon(opts.cursor_icon); // TODO: Handle unsupported platforms window.set_cursor_visible(opts.cursor_visible); // TODO: Handle unsupported platforms window.set_decorations(opts.decorations); // TODO: Handle unsupported platforms window.set_enabled_buttons(opts.enabled_buttons); // TODO: Handle unsupported platforms // 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(ivec2_to_logical_size(opts.inner_size)); if opts.max_inner_size.is_some() { window.set_max_inner_size(ivec2_to_logical_size_op(opts.max_inner_size)); } if opts.min_inner_size.is_some() { window.set_min_inner_size(ivec2_to_logical_size_op(opts.min_inner_size)); } window.set_maximized(opts.maximized); window.set_minimized(opts.minimized); 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); // reset the tracker after we mutably used it opts.reset(); center_mouse(&window, &opts); } else { center_mouse(&window, &opts); } } 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, &[]); } }