485 lines
17 KiB
Rust
485 lines
17 KiB
Rust
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 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,
|
||
|
||
/// 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 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,
|
||
|
||
/// 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.
|
||
///
|
||
/// (Apple’s 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 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,
|
||
|
||
/// 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, &[]);
|
||
}
|
||
}
|