lyra-engine/lyra-game/src/render/window.rs
SeanOMik 45fd190409
update wgpu and winit to latest versions
need to make a WinitPlugin though, so no window currently
2024-09-18 19:47:55 -04:00

485 lines
17 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::{IVec2, Vec2};
use lyra_ecs::World;
use tracing::{error, warn};
use winit::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
error::ExternalError,
window::{Fullscreen, Window},
};
pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel};
use crate::{change_tracker::Ct, input::InputEvent, plugin::Plugin, EventQueue};
#[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,
}
#[derive(Default, Clone, Copy)]
pub struct Area {
position: Position,
size: Size,
}
#[derive(Clone, Copy)]
pub enum Size {
Physical { x: i32, y: i32 },
Logical { x: f64, y: f64 },
}
impl Default for Size {
fn default() -> Self {
Self::Physical { x: 0, y: 0 }
}
}
impl Into<winit::dpi::Size> for Size {
fn into(self) -> winit::dpi::Size {
match self {
Size::Physical { x, y } => winit::dpi::PhysicalSize::new(x, y).into(),
Size::Logical { x, y } => winit::dpi::LogicalSize::new(x, y).into(),
}
}
}
impl Size {
pub fn new_physical(x: i32, y: i32) -> Self {
Self::Physical { x, y }
}
pub fn new_logical(x: f64, y: f64) -> Self {
Self::Logical { x, y }
}
}
#[derive(Clone, Copy)]
pub enum Position {
Physical { x: i32, y: i32 },
Logical { x: f64, y: f64 },
}
impl Default for Position {
fn default() -> Self {
Self::Physical { x: 0, y: 0 }
}
}
impl Into<winit::dpi::Position> for Position {
fn into(self) -> winit::dpi::Position {
match self {
Position::Physical { x, y } => winit::dpi::PhysicalPosition::new(x, y).into(),
Position::Logical { x, y } => winit::dpi::LogicalPosition::new(x, y).into(),
}
}
}
impl Position {
pub fn new_physical(x: i32, y: i32) -> Self {
Self::Physical { x, y }
}
pub fn new_logical(x: f64, y: f64) -> Self {
Self::Logical { x, y }
}
}
/// 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,
/// Modifies the cursor icon of the window.
///
/// Platform-specific:
/// * iOS / Android / Orbital: Unsupported.
/// * Web: Custom cursors have to be loaded and decoded first, until then the previous
/// cursor is shown.
pub cursor: winit::window::Cursor,
/// 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,
/// Set the IME cursor editing area, where the `position` is the top left corner of that
/// area and `size` is the size of this area starting from the position. An example of such
/// area could be a input field in the UI or line in the editor.
///
/// The windowing system could place a candidate box close to that area, but try to not
/// obscure the specified area, so the user input to it stays visible.
///
/// The candidate box 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.
///
/// (Apples official term is “candidate window”, see their chinese and japanese guides).
///
/// Platform-specific
/// * **X11:** - area is not supported, only position.
/// * **iOS / Android / Web / Orbital:** Unsupported.
pub ime_cursor_area: Area,
/// Modifies the inner size of the window.
///
/// Platform-specific:
/// * iOS / Android: Unsupported.
/// * Web: Sets the size of the canvas element.
pub inner_size: Size,
/// Sets a maximum dimension size for the window.
///
/// Platform-specific:
/// * iOS / Android / Web / Orbital: Unsupported.
pub max_inner_size: Option<Size>,
/// Sets a minimum dimension size for the window.
///
/// Platform-specific:
/// * iOS / Android / Web / Orbital: Unsupported.
pub min_inner_size: Option<Size>,
/// 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,
/// Get/set the window's focused state.
pub focused: bool,
/// Get whether or not the cursor is inside the window.
pub cursor_inside_window: bool,
}
impl Default for WindowOptions {
fn default() -> Self {
Self {
content_protected: false,
cursor_grab: CursorGrabMode::None,
cursor_hittest: true,
cursor: Default::default(),
cursor_visible: true,
decorations: true,
enabled_buttons: WindowButtons::all(),
mode: WindowMode::Windowed,
ime_allowed: false,
ime_cursor_area: Area::default(),
inner_size: Size::new_physical(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,
focused: false,
cursor_inside_window: false,
}
}
}
#[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, the cursor is invisible,
/// and the window is focused, 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 && options.focused
{
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 World) -> anyhow::Result<()> {
if let (Some(window), Some(opts)) = (
world.try_get_resource::<Arc<Window>>(),
world.try_get_resource::<Ct<WindowOptions>>(),
) {
// if the options changed, update the window
if opts.peek_changed() {
drop(opts); // drop the Ref, we're about to get a RefMut
// 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>>();
if opts.focused {
window.focus_window();
}
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(opts.cursor.clone()); // 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_cursor_area(opts.ime_cursor_area.position, opts.ime_cursor_area.size);
window.request_inner_size(opts.inner_size);
if opts.max_inner_size.is_some() {
window.set_max_inner_size(opts.max_inner_size);
}
if opts.min_inner_size.is_some() {
window.set_min_inner_size(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 {
drop(opts); // drop the Ref, we're about to get a RefMut
let mut opts = world.get_resource_mut::<Ct<WindowOptions>>();
if let Some(event_queue) = world.try_get_resource_mut::<EventQueue>() {
if let Some(events) = event_queue.read_events::<InputEvent>() {
for ev in events {
match ev {
InputEvent::CursorEntered { .. } => {
opts.cursor_inside_window = true;
}
InputEvent::CursorLeft { .. } => {
opts.cursor_inside_window = false;
}
_ => {}
}
}
}
}
// update the stored state of the window to match the actual window
opts.focused = window.has_focus();
opts.reset();
center_mouse(&window, &opts);
}
}
Ok(())
}
impl Plugin for WindowPlugin {
fn setup(&self, app: &mut crate::game::App) {
let window_options = WindowOptions::default();
app.world.add_resource(Ct::new(window_options));
app.with_system("window_updater", window_updater_system, &[]);
}
}