lyra-engine/src/render/window.rs

342 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 doesnt 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 cursors 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 its recommended to account for screen scaling and pick a multiple of that, i.e. 32x32.
/// * X11: Has no universal guidelines for icon sizes, so youre at the whims of the WM. That said, its 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, &[]);
}
}