342 lines
13 KiB
Rust
342 lines
13 KiB
Rust
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<IVec2>,
|
||
|
||
/// Sets a minimum dimension size for the window.
|
||
///
|
||
/// Platform-specific:
|
||
/// * iOS / Android / Web / Orbital: Unsupported.
|
||
pub min_inner_size: Option<IVec2>,
|
||
|
||
/// 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<Vec2>,
|
||
|
||
/// 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<Theme>,
|
||
|
||
/// 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<Icon>,
|
||
|
||
/// 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<f32>
|
||
fn vec2_to_logical_pos(pos: Vec2) -> LogicalPosition<f32> {
|
||
LogicalPosition { x: pos.x, y: pos.y }
|
||
}
|
||
|
||
/// Convert an IVec2 to a LogicalSize<i32>
|
||
fn ivec2_to_logical_size(size: IVec2) -> LogicalSize<i32> {
|
||
LogicalSize { width: size.x, height: size.y }
|
||
}
|
||
|
||
/// Convert an Option<IVec2> to an Option<LogicalSize<i32>>
|
||
fn ivec2_to_logical_size_op(size: Option<IVec2>) -> Option<LogicalSize<i32>> {
|
||
size.map(ivec2_to_logical_size)
|
||
}
|
||
|
||
/// Convert an Option<Vec2> to an Option<LogicalSize<f32>>
|
||
fn vec2_to_logical_size_op(size: Option<Vec2>) -> Option<LogicalSize<f32>> {
|
||
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::<Arc<Window>>(), world.get_resource::<Ct<WindowOptions>>()) {
|
||
// 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::<Ct<WindowOptions>>().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, &[]);
|
||
}
|
||
}
|