Compare commits

..

2 Commits

Author SHA1 Message Date
SeanOMik a2c52a0bb8
ecs: fix Changed query; lua: lock and hide mouse in window
CI / build (push) Failing after 3m10s Details
2024-10-02 21:29:13 -04:00
SeanOMik 76b7cac699
lua: expose most fields for window component 2024-10-02 20:54:54 -04:00
18 changed files with 913 additions and 400 deletions

View File

@ -1,3 +1,5 @@
HAS_SETUP_WINDOW = false
---Return the userdata's name from its metatable. ---Return the userdata's name from its metatable.
--- ---
---Returns nil if the userdata doesn't have a metatable. ---Returns nil if the userdata doesn't have a metatable.
@ -28,11 +30,22 @@ function on_init()
print("spawned entity " .. tostring(e)) print("spawned entity " .. tostring(e))
end end
--[[ function on_first() function on_first()
print("Lua's first function was called") if not HAS_SETUP_WINDOW then
world:view(function (w)
if w.cursor_grab == CursorGrabMode.NONE then
w.cursor_grab = CursorGrabMode.LOCKED
w.cursor_visible = false
return w
else
HAS_SETUP_WINDOW = true
print("Window setup")
end
end, Window)
end
end end
function on_pre_update() --[[ function on_pre_update()
print("Lua's pre-update function was called") print("Lua's pre-update function was called")
end ]] end ]]

View File

@ -20,7 +20,7 @@ where
unsafe fn get_item(&mut self, entity: crate::world::ArchetypeEntityId) -> Self::Item { unsafe fn get_item(&mut self, entity: crate::world::ArchetypeEntityId) -> Self::Item {
let tick = self.col.entity_ticks[entity.0 as usize]; let tick = self.col.entity_ticks[entity.0 as usize];
tick >= self.tick *tick >= (*self.tick) - 1
} }
} }

View File

@ -112,7 +112,7 @@ impl<'a, T: ResourceObject> Res<'a, T> {
/// Returns a boolean indicating if the resource changed. /// Returns a boolean indicating if the resource changed.
pub fn changed(&self) -> bool { pub fn changed(&self) -> bool {
self.inner.tick >= self.world_tick *self.inner.tick - 1 >= *self.world_tick - 1
} }
/// The tick that this resource was last modified at /// The tick that this resource was last modified at
@ -236,7 +236,7 @@ impl<'a, T: ResourceObject> ResMut<'a, T> {
} }
pub fn changed(&self) -> bool { pub fn changed(&self) -> bool {
self.inner.tick > self.world_tick *self.inner.tick - 1 >= *self.world_tick - 1
} }
/// The tick that this resource was last modified at /// The tick that this resource was last modified at

View File

@ -89,6 +89,6 @@ impl ResourceData {
} }
pub fn changed(&self, tick: Tick) -> bool { pub fn changed(&self, tick: Tick) -> bool {
self.data.borrow().tick >= tick *self.data.borrow().tick >= *tick - 1
} }
} }

View File

@ -132,7 +132,33 @@ impl World {
B: Bundle B: Bundle
{ {
let tick = self.current_tick(); let tick = self.current_tick();
let record = self.entities.entity_record(entity).unwrap(); let record = self.entities.entity_record(entity);
if record.is_none() {
//let mut combined_column_infos: Vec<ComponentInfo> = bundle.info().columns.iter().map(|c| c.info).collect();
let new_arch_id = self.next_archetype_id.increment();
let mut archetype = Archetype::from_bundle_info(new_arch_id, bundle.info());
let mut dbun = DynamicBundle::new();
dbun.push_bundle(bundle);
let entity_arch_id = archetype.add_entity(entity, dbun, &tick);
self.archetypes.insert(new_arch_id, archetype);
// Create entity record and store it
let record = Record {
id: new_arch_id,
index: entity_arch_id,
};
self.entities.insert_entity_record(entity, record);
return;
}
let record = record.unwrap();
let current_arch = self.archetypes.get(&record.id).unwrap(); let current_arch = self.archetypes.get(&record.id).unwrap();
let current_arch_len = current_arch.len(); let current_arch_len = current_arch.len();
@ -167,6 +193,7 @@ impl World {
.map(|c| unsafe { (NonNull::new_unchecked(c.borrow_ptr().as_ptr()), c.info) }) .map(|c| unsafe { (NonNull::new_unchecked(c.borrow_ptr().as_ptr()), c.info) })
.collect(); .collect();
// try to find an archetype that this entity and its new components can fit into
if let Some(arch) = self.archetypes.values_mut().find(|a| a.is_archetype_for(&combined_column_types)) { if let Some(arch) = self.archetypes.values_mut().find(|a| a.is_archetype_for(&combined_column_types)) {
let mut dbun = DynamicBundle::new(); let mut dbun = DynamicBundle::new();
// move old entity components into new archetype columns // move old entity components into new archetype columns

View File

@ -2,10 +2,9 @@ use lyra_ecs::CommandQueue;
use lyra_resource::ResourceManager; use lyra_resource::ResourceManager;
use crate::game::App; use crate::game::App;
use crate::winit::WinitPlugin; use crate::winit::{WinitPlugin, WindowPlugin};
use crate::DeltaTimePlugin; use crate::DeltaTimePlugin;
use crate::input::InputPlugin; use crate::input::InputPlugin;
use crate::render::window::WindowPlugin;
/// A Plugin is something you can add to a `Game` that can be used to define systems, or spawn initial entities. /// A Plugin is something you can add to a `Game` that can be used to define systems, or spawn initial entities.
pub trait Plugin { pub trait Plugin {

View File

@ -9,7 +9,6 @@ pub mod texture;
pub mod shader_loader; pub mod shader_loader;
pub mod material; pub mod material;
pub mod camera; pub mod camera;
pub mod window;
pub mod transform_buffer_storage; pub mod transform_buffer_storage;
pub mod light; pub mod light;
//pub mod light_cull_compute; //pub mod light_cull_compute;

View File

@ -1,315 +1,6 @@
use std::{collections::VecDeque, sync::Arc};
use async_std::task::block_on; mod plugin;
use glam::{DVec2, IVec2, UVec2}; pub use plugin::*;
use lyra_ecs::Entity;
use rustc_hash::FxHashMap;
use tracing::{debug, error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowAttributes, WindowId},
};
use crate::{ mod window;
game::{App, WindowState}, pub use window::*;
plugin::Plugin,
render::{
renderer::BasicRenderer,
window::{LastWindow, PrimaryWindow, WindowOptions},
},
};
/// A struct that contains a [`DeviceEvent`](winit::event::DeviceEvent) with its source
/// [`DeviceId`](winit::event::DeviceId).
#[derive(Debug, Clone)]
pub struct DeviceEventPair {
pub device_src: winit::event::DeviceId,
pub event: winit::event::DeviceEvent,
}
pub struct WinitPlugin {
/// The primary window that will be created.
///
/// This will become `None` after the window is created. If you want to get the
/// primary world later, query for an entity with the [`PrimaryWindow`] and
/// [`WindowOptions`] components.
pub primary_window: Option<WindowOptions>,
}
impl Default for WinitPlugin {
fn default() -> Self {
Self {
primary_window: Some(WindowOptions::default()),
}
}
}
impl Plugin for WinitPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
app.set_run_fn(winit_app_runner);
app.register_event::<WindowEvent>();
app.register_event::<DeviceEventPair>();
if let Some(prim) = self.primary_window.take() {
app.add_resource(WinitWindows::with_window(prim));
} else {
app.add_resource(WinitWindows::default());
}
}
fn is_ready(&self, _app: &mut crate::game::App) -> bool {
true
}
fn complete(&self, _app: &mut crate::game::App) {}
fn cleanup(&self, _app: &mut crate::game::App) {}
}
#[derive(Default)]
pub struct WinitWindows {
pub windows: FxHashMap<WindowId, Arc<Window>>,
pub entity_to_window: FxHashMap<Entity, WindowId>,
pub window_to_entity: FxHashMap<WindowId, Entity>,
/// windows that will be created when the Winit runner first starts.
window_queue: VecDeque<WindowOptions>,
}
impl WinitWindows {
pub fn with_window(window: WindowOptions) -> Self {
Self {
window_queue: vec![window].into(),
..Default::default()
}
}
pub fn create_window(
&mut self,
event_loop: &ActiveEventLoop,
entity: Entity,
attr: WindowAttributes,
) -> Result<WindowId, winit::error::OsError> {
let win = event_loop.create_window(attr)?;
let id = win.id();
self.windows.insert(id, Arc::new(win));
self.entity_to_window.insert(entity, id);
self.window_to_entity.insert(id, entity);
Ok(id)
}
pub fn get_entity_window(&self, entity: Entity) -> Option<&Arc<Window>> {
self.entity_to_window
.get(&entity)
.and_then(|id| self.windows.get(id))
}
}
pub fn winit_app_runner(app: App) {
let evloop = EventLoop::new().expect("failed to create winit EventLoop");
let mut winit_runner = WinitRunner { app };
evloop.run_app(&mut winit_runner).expect("loop error");
}
struct WinitRunner {
app: App,
}
impl ApplicationHandler for WinitRunner {
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.app.update();
let renderer = self
.app
.renderer
.get_mut()
.expect("renderer was not initialized");
renderer.prepare(&mut self.app.world);
match renderer.render() {
Ok(_) => {}
// Reconfigure the surface if lost
//Err(wgpu::SurfaceError::Lost) => self.on_resize(.surface_size()),
// The system is out of memory, we should probably quit
Err(wgpu::SurfaceError::OutOfMemory) => {
error!("OOM");
event_loop.exit();
}
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
}
let windows = self.app.world.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource");
for window in windows.windows.values() {
window.request_redraw();
}
}
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let world = &mut self.app.world;
let mut windows = world.get_resource_mut::<WinitWindows>()
.expect("world missing WinitWindows resource");
let to_create_window = windows.window_queue.pop_front().unwrap_or_default();
let window_attr = to_create_window.as_attributes();
drop(windows);
let en = world.spawn((to_create_window.clone(), LastWindow { last: to_create_window }, PrimaryWindow));
let mut windows = world.get_resource_mut::<WinitWindows>()
.expect("world missing WinitWindows resource");
let wid = windows.create_window(event_loop, en, window_attr).unwrap();
let window = windows.windows.get(&wid).unwrap().clone();
drop(windows);
debug!("Created window after resume");
let renderer = block_on(BasicRenderer::create_with_window(world, window));
if self.app.renderer.set(Box::new(renderer)).is_err() {
warn!("renderer was re-initialized");
}
}
fn device_event(
&mut self,
_: &ActiveEventLoop,
device_src: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
self.app.push_event(DeviceEventPair { device_src, event });
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
/* let windows = self.app.world.get_resource::<WinitWindows>();
let window = match windows.windows.get(&window_id) {
Some(w) => w.clone(),
None => return,
};
drop(windows); */
self.app.push_event(event.clone());
match event {
WindowEvent::CursorMoved { position, .. } => {
let windows = self.app.world.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource");
let en = windows.window_to_entity.get(&window_id)
.expect("missing window entity");
// update the window and its cache so the sync system doesn't try to update the window
let (mut en_window, mut en_last_win) = self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*en).get().unwrap();
let pos = Some(DVec2::new(position.x, position.y));
en_window.set_physical_cursor_position(pos);
en_last_win.set_physical_cursor_position(pos);
},
WindowEvent::ActivationTokenDone { .. } => todo!(),
WindowEvent::Resized(physical_size) => {
self.app.on_resize(physical_size);
let (mut window, mut last_window) = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*e).get())
.unwrap();
// update the window and its cache so the sync system doesn't try to update the window
let size = UVec2::new(physical_size.width, physical_size.height);
window.set_physical_size(size);
last_window.set_physical_size(size);
},
// Mark the cursor as outside the window when it leaves
WindowEvent::CursorLeft { .. } => {
let (mut window, mut last_window) = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*e).get())
.unwrap();
window.set_physical_cursor_position(None);
last_window.set_physical_cursor_position(None);
},
WindowEvent::Moved(physical_position) => {
let mut state = self.app.world.get_resource_or_else(WindowState::new);
state.position = IVec2::new(physical_position.x, physical_position.y);
},
WindowEvent::CloseRequested => {
self.app.on_exit();
event_loop.exit();
},
WindowEvent::Destroyed => todo!(),
WindowEvent::DroppedFile(_path_buf) => todo!(),
WindowEvent::HoveredFile(_path_buf) => todo!(),
WindowEvent::HoveredFileCancelled => todo!(),
WindowEvent::Focused(focused) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.focused = focused;
},
WindowEvent::ModifiersChanged(modifiers) => {
debug!("modifiers changed: {:?}", modifiers)
},
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.scale_factor = scale_factor;
},
WindowEvent::ThemeChanged(theme) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.theme = Some(theme);
},
WindowEvent::Occluded(occ) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.occluded = occ;
},
WindowEvent::RedrawRequested => {
//debug!("should redraw");
},
_ => {}
}
}
}

View File

@ -0,0 +1,330 @@
use std::{collections::VecDeque, sync::Arc};
use async_std::task::block_on;
use glam::{DVec2, IVec2, UVec2};
use lyra_ecs::Entity;
use rustc_hash::FxHashMap;
use tracing::{debug, error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowAttributes, WindowId},
};
use crate::{
game::{App, WindowState},
plugin::Plugin,
render::renderer::BasicRenderer, winit::{FullscreenMode, LastWindow, PrimaryWindow},
};
use super::WindowOptions;
/// A struct that contains a [`DeviceEvent`](winit::event::DeviceEvent) with its source
/// [`DeviceId`](winit::event::DeviceId).
#[derive(Debug, Clone)]
pub struct DeviceEventPair {
pub device_src: winit::event::DeviceId,
pub event: winit::event::DeviceEvent,
}
pub struct WinitPlugin {
/// The primary window that will be created.
///
/// This will become `None` after the window is created. If you want to get the
/// primary world later, query for an entity with the [`PrimaryWindow`] and
/// [`WindowOptions`] components.
pub primary_window: Option<WindowOptions>,
}
impl Default for WinitPlugin {
fn default() -> Self {
Self {
primary_window: Some(WindowOptions::default()),
}
}
}
impl Plugin for WinitPlugin {
fn setup(&mut self, app: &mut crate::game::App) {
app.set_run_fn(winit_app_runner);
app.register_event::<WindowEvent>();
app.register_event::<DeviceEventPair>();
if let Some(prim) = self.primary_window.take() {
app.add_resource(WinitWindows::with_window(prim));
} else {
app.add_resource(WinitWindows::default());
}
}
fn is_ready(&self, _app: &mut crate::game::App) -> bool {
true
}
fn complete(&self, _app: &mut crate::game::App) {}
fn cleanup(&self, _app: &mut crate::game::App) {}
}
#[derive(Default)]
pub struct WinitWindows {
pub windows: FxHashMap<WindowId, Arc<Window>>,
pub entity_to_window: FxHashMap<Entity, WindowId>,
pub window_to_entity: FxHashMap<WindowId, Entity>,
/// windows that will be created when the Winit runner first starts.
window_queue: VecDeque<WindowOptions>,
}
impl WinitWindows {
pub fn with_window(window: WindowOptions) -> Self {
Self {
window_queue: vec![window].into(),
..Default::default()
}
}
pub fn create_window(
&mut self,
event_loop: &ActiveEventLoop,
entity: Entity,
attr: WindowAttributes,
) -> Result<WindowId, winit::error::OsError> {
let win = event_loop.create_window(attr)?;
let id = win.id();
self.windows.insert(id, Arc::new(win));
self.entity_to_window.insert(entity, id);
self.window_to_entity.insert(id, entity);
Ok(id)
}
pub fn get_entity_window(&self, entity: Entity) -> Option<&Arc<Window>> {
self.entity_to_window
.get(&entity)
.and_then(|id| self.windows.get(id))
}
}
pub fn winit_app_runner(app: App) {
let evloop = EventLoop::new().expect("failed to create winit EventLoop");
let mut winit_runner = WinitRunner { app };
evloop.run_app(&mut winit_runner).expect("loop error");
}
struct WinitRunner {
app: App,
}
impl ApplicationHandler for WinitRunner {
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.app.update();
let renderer = self
.app
.renderer
.get_mut()
.expect("renderer was not initialized");
renderer.prepare(&mut self.app.world);
match renderer.render() {
Ok(_) => {}
// Reconfigure the surface if lost
//Err(wgpu::SurfaceError::Lost) => self.on_resize(.surface_size()),
// The system is out of memory, we should probably quit
Err(wgpu::SurfaceError::OutOfMemory) => {
error!("OOM");
event_loop.exit();
}
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
}
let windows = self.app.world.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource");
for window in windows.windows.values() {
window.request_redraw();
}
}
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let world = &mut self.app.world;
let en = world.reserve_entity();
let mut windows = world.get_resource_mut::<WinitWindows>()
.expect("world missing WinitWindows resource");
let mut to_create_window = windows.window_queue.pop_front().unwrap_or_default();
let window_attr = to_create_window.as_attributes();
//drop(windows);
//let en = world.spawn((to_create_window, last, PrimaryWindow));
//let mut windows = world.get_resource_mut::<WinitWindows>()
//.expect("world missing WinitWindows resource");
let wid = windows.create_window(event_loop, en, window_attr).unwrap();
let window = windows.windows.get(&wid).unwrap().clone();
drop(windows);
// update fields that default to `None`
to_create_window.position = window.outer_position()
.or_else(|_| window.inner_position())
.ok()
.map(|p| IVec2::new(p.x, p.y));
// See [`WindowOptions::as_attributes`], it defaults to Windowed fullscreen mode, so we
// must trigger an update in the sync system;
let mut last = LastWindow { last: to_create_window.clone() };
last.last.fullscreen_mode = FullscreenMode::Windowed;
world.insert(en, (to_create_window, last, PrimaryWindow));
debug!("Created window after resume");
let renderer = block_on(BasicRenderer::create_with_window(world, window));
if self.app.renderer.set(Box::new(renderer)).is_err() {
warn!("renderer was re-initialized");
}
}
fn device_event(
&mut self,
_: &ActiveEventLoop,
device_src: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
self.app.push_event(DeviceEventPair { device_src, event });
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
/* let windows = self.app.world.get_resource::<WinitWindows>();
let window = match windows.windows.get(&window_id) {
Some(w) => w.clone(),
None => return,
};
drop(windows); */
self.app.push_event(event.clone());
match event {
WindowEvent::CursorMoved { position, .. } => {
let windows = self.app.world.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource");
let en = windows.window_to_entity.get(&window_id)
.expect("missing window entity");
// update the window and its cache so the sync system doesn't try to update the window
let (mut en_window, mut en_last_win) = self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*en).get().unwrap();
let pos = Some(DVec2::new(position.x, position.y));
en_window.set_physical_cursor_position(pos);
en_last_win.set_physical_cursor_position(pos);
},
WindowEvent::ActivationTokenDone { .. } => todo!(),
WindowEvent::Resized(physical_size) => {
self.app.on_resize(physical_size);
let (mut window, mut last_window) = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*e).get())
.unwrap();
// update the window and its cache so the sync system doesn't try to update the window
let size = UVec2::new(physical_size.width, physical_size.height);
window.set_physical_size(size);
last_window.set_physical_size(size);
},
// Mark the cursor as outside the window when it leaves
WindowEvent::CursorLeft { .. } => {
let (mut window, mut last_window) = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<(&mut WindowOptions, &mut LastWindow)>(*e).get())
.unwrap();
window.set_physical_cursor_position(None);
last_window.set_physical_cursor_position(None);
},
WindowEvent::Moved(physical_position) => {
let mut state = self.app.world.get_resource_or_else(WindowState::new);
state.position = IVec2::new(physical_position.x, physical_position.y);
},
WindowEvent::CloseRequested => {
self.app.on_exit();
event_loop.exit();
},
WindowEvent::Destroyed => todo!(),
WindowEvent::DroppedFile(_path_buf) => todo!(),
WindowEvent::HoveredFile(_path_buf) => todo!(),
WindowEvent::HoveredFileCancelled => todo!(),
WindowEvent::Focused(focused) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.focused = focused;
},
WindowEvent::ModifiersChanged(modifiers) => {
debug!("modifiers changed: {:?}", modifiers)
},
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.scale_factor = scale_factor;
},
WindowEvent::ThemeChanged(theme) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.theme = Some(theme);
},
WindowEvent::Occluded(occ) => {
let mut window_opts = self
.app
.world
.get_resource::<WinitWindows>()
.expect("world missing WinitWindows resource")
.window_to_entity
.get(&window_id)
.and_then(|e| self.app.world.view_one::<&mut WindowOptions>(*e).get())
.unwrap();
window_opts.occluded = occ;
},
WindowEvent::RedrawRequested => {
//debug!("should redraw");
},
_ => {}
}
}
}

View File

@ -3,13 +3,14 @@ use std::ops::{Deref, DerefMut};
use glam::{DVec2, IVec2, UVec2, Vec2}; use glam::{DVec2, IVec2, UVec2, Vec2};
use lyra_ecs::{query::{filter::Changed, Entities, Res, View}, Component}; use lyra_ecs::{query::{filter::Changed, Entities, Res, View}, Component};
use lyra_math::Area; use lyra_math::Area;
use lyra_reflect::Reflect;
use lyra_resource::Image; use lyra_resource::Image;
use tracing::error; use tracing::{error, warn};
use winit::{dpi::{PhysicalPosition, PhysicalSize, Position, Size}, window::{CustomCursor, Fullscreen, Window}}; use winit::{dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, monitor::{MonitorHandle, VideoModeHandle}, window::{CustomCursor, Window}};
pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel}; pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel};
use crate::{plugin::Plugin, winit::WinitWindows}; use crate::{plugin::Plugin, winit::WinitWindows, lyra_engine};
/// Flag component that /// Flag component that
#[derive(Clone, Component)] #[derive(Clone, Component)]
@ -21,6 +22,35 @@ pub enum CursorAppearance {
Custom(CustomCursor) Custom(CustomCursor)
} }
#[derive(Clone, Debug, Default, PartialEq, Eq, Reflect)]
pub enum FullscreenMode{
#[default]
Windowed,
BorderlessFullscreen,
SizedFullscreen,
Fullscreen,
}
impl FullscreenMode {
pub fn as_winit_fullscreen(&self, monitor: MonitorHandle, physical_size: UVec2) -> Option<winit::window::Fullscreen> {
match &self {
FullscreenMode::Windowed => None,
FullscreenMode::BorderlessFullscreen => Some(winit::window::Fullscreen::Borderless(None)),
// find closest video mode for full screen sizes
_ => {
let closest = find_closest_video_mode(monitor, physical_size);
if let Some(closest) = closest {
Some(winit::window::Fullscreen::Exclusive(closest))
} else {
warn!("Could not find closest video mode, falling back to windowed.");
None
}
}
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Cursor { pub struct Cursor {
/// Modifies the cursor icon of the window. /// Modifies the cursor icon of the window.
@ -28,13 +58,13 @@ pub struct Cursor {
/// Platform-specific /// Platform-specific
/// * **iOS / Android / Orbital:** Unsupported. /// * **iOS / Android / Orbital:** Unsupported.
/// * **Web:** Custom cursors have to be loaded and decoded first, until then the previous cursor is shown. /// * **Web:** Custom cursors have to be loaded and decoded first, until then the previous cursor is shown.
appearance: CursorAppearance, pub appearance: CursorAppearance,
/// Gets/sets the window's cursor grab mode /// Gets/sets the window's cursor grab mode
/// ///
/// # Tip: /// # Tip:
/// First try confining the cursor, and if it fails, try locking it instead. /// First try confining the cursor, and if it fails, try locking it instead.
grab: CursorGrabMode, pub grab: CursorGrabMode,
/// Gets/sets whether the window catches cursor events. /// Gets/sets whether the window catches cursor events.
/// ///
@ -43,7 +73,7 @@ pub struct Cursor {
/// ///
/// Platform-specific /// Platform-specific
/// * **iOS / Android / Web / Orbital:** Unsupported. /// * **iOS / Android / Web / Orbital:** Unsupported.
hittest: bool, pub hittest: bool,
/// Gets/sets the cursor's visibility /// Gets/sets the cursor's visibility
/// ///
@ -52,18 +82,19 @@ pub struct Cursor {
/// * **macOS:** The cursor is hidden as long as the window has input focus, even if the /// * **macOS:** The cursor is hidden as long as the window has input focus, even if the
/// cursor is outside of the window. /// cursor is outside of the window.
/// * **iOS / Android:** Unsupported. /// * **iOS / Android:** Unsupported.
visible: bool, pub visible: bool,
//cursor_position: Option<PhysicalPosition<i32>>, //cursor_position: Option<PhysicalPosition<i32>>,
} }
/// Options that the window will be created with. /// Options that the window will be created with.
#[derive(Clone, Component)] #[derive(Clone, Component, Reflect)]
pub struct WindowOptions { pub struct WindowOptions {
/// The enabled window buttons. /// The enabled window buttons.
/// ///
/// Platform-specific /// Platform-specific
/// * **Wayland / X11 / Orbital:** Not implemented. Always set to [`WindowButtons::all`]. /// * **Wayland / X11 / Orbital:** Not implemented. Always set to [`WindowButtons::all`].
/// * **Web / iOS / Android:** Unsupported. Always set to [`WindowButtons::all`]. /// * **Web / iOS / Android:** Unsupported. Always set to [`WindowButtons::all`].
#[reflect(skip)]
pub enabled_buttons: WindowButtons, pub enabled_buttons: WindowButtons,
/// Gets or sets if the window is in focus. /// Gets or sets if the window is in focus.
@ -73,15 +104,7 @@ pub struct WindowOptions {
pub focused: bool, pub focused: bool,
/// Gets or sets the fullscreen setting. /// Gets or sets the fullscreen setting.
/// pub fullscreen_mode: FullscreenMode,
/// If this is `None`, the window is windowed.
///
/// Platform-specific
/// * **iOS:** Can only be called on the main thread.
/// * **Android / Orbital:** Will always return None.
/// * **Wayland:** Can return Borderless(None) when there are no monitors.
/// * **Web:** Can only return None or Borderless(None).
pub fullscreen: Option<Fullscreen>,
/// Gets/sets the position of the top-left hand corner of the window relative to /// Gets/sets the position of the top-left hand corner of the window relative to
/// the top-left hand corner of the desktop. /// the top-left hand corner of the desktop.
@ -100,6 +123,7 @@ pub struct WindowOptions {
/// * **Web:** Value is the top-left coordinates relative to the viewport. Note: this will be /// * **Web:** Value is the top-left coordinates relative to the viewport. Note: this will be
/// the same value as [`WindowOptions::outer_position`]. /// the same value as [`WindowOptions::outer_position`].
/// * **Android / Wayland:** Unsupported. /// * **Android / Wayland:** Unsupported.
#[reflect(skip)]
pub position: Option<IVec2>, pub position: Option<IVec2>,
/// Gets/sets the size of the view in the window. /// Gets/sets the size of the view in the window.
@ -108,6 +132,7 @@ pub struct WindowOptions {
/// ///
/// Platform-specific /// Platform-specific
/// * **Web:** The size of the canvas element. Doesnt account for CSS `transform`. /// * **Web:** The size of the canvas element. Doesnt account for CSS `transform`.
#[reflect(skip)]
physical_size: UVec2, physical_size: UVec2,
/// Gets/sets if the window has decorations. /// Gets/sets if the window has decorations.
@ -161,6 +186,7 @@ pub struct WindowOptions {
/// * **macOS:** Increments are converted to logical size and then macOS rounds them to whole numbers. /// * **macOS:** Increments are converted to logical size and then macOS rounds them to whole numbers.
/// * **Wayland:** Not implemented, always `None`. /// * **Wayland:** Not implemented, always `None`.
/// * **iOS / Android / Web / Orbital:** Unsupported. /// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
pub resize_increments: Option<Size>, pub resize_increments: Option<Size>,
/// Gets the scale factor. /// Gets the scale factor.
@ -177,15 +203,7 @@ pub struct WindowOptions {
/// * **Wayland:** Only works with org_kde_kwin_blur_manager protocol. /// * **Wayland:** Only works with org_kde_kwin_blur_manager protocol.
pub blur: bool, pub blur: bool,
/// Prevents the window contents from being captured by other apps. #[reflect(skip)]
///
/// Platform-specific
/// * **macOS:** if false, [`NSWindowSharingNone`](https://developer.apple.com/documentation/appkit/nswindowsharingtype/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,
pub cursor: Cursor, pub cursor: Cursor,
/// Sets whether the window should get IME events /// Sets whether the window should get IME events
@ -210,19 +228,26 @@ pub struct WindowOptions {
/// Platform-specific /// Platform-specific
/// * **X11:** - area is not supported, only position. /// * **X11:** - area is not supported, only position.
/// * **iOS / Android / Web / Orbital:** Unsupported. /// * **iOS / Android / Web / Orbital:** Unsupported.
pub physical_ime_cursor_area: Option<Area<Vec2, Vec2>>, #[reflect(skip)]
physical_ime_cursor_area: Option<Area<Vec2, Vec2>>,
/// Gets/sets the minimum size of the window. /// Gets/sets the minimum size of the window.
/// ///
/// Platform-specific /// Units are in logical pixels.
/// * **iOS / Android / Orbital:** Unsupported.
pub min_size: Option<Size>,
/// Gets/sets the maximum size of the window.
/// ///
/// Platform-specific /// Platform-specific
/// * **iOS / Android / Orbital:** Unsupported. /// * **iOS / Android / Orbital:** Unsupported.
pub max_size: Option<Size>, #[reflect(skip)]
pub min_size: Option<Vec2>,
/// Gets/sets the maximum size of the window.
///
/// Units are in logical pixels.
///
/// Platform-specific
/// * **iOS / Android / Orbital:** Unsupported.
#[reflect(skip)]
pub max_size: Option<Vec2>,
/// Gets/sets the current window theme. /// Gets/sets the current window theme.
/// ///
@ -235,6 +260,7 @@ pub struct WindowOptions {
/// * **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used, /// * **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used,
/// it will default to [`Theme::Dark`](winit::window::Theme::Dark). /// it will default to [`Theme::Dark`](winit::window::Theme::Dark).
/// * **iOS / Android / Web / Orbital:** Unsupported. /// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
pub theme: Option<Theme>, pub theme: Option<Theme>,
/// Gets/sets the title of the window. /// Gets/sets the title of the window.
@ -274,6 +300,7 @@ pub struct WindowOptions {
/// This is just a hint to the OS, and the system could ignore it. /// This is just a hint to the OS, and the system could ignore it.
/// ///
/// See [`WindowLevel`] for details. /// See [`WindowLevel`] for details.
#[reflect(skip)]
pub window_level: WindowLevel, pub window_level: WindowLevel,
/// Show [window menu](https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu) /// Show [window menu](https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu)
@ -282,7 +309,7 @@ pub struct WindowOptions {
/// This is the context menu that is normally shown when interacting with the title bar. This is useful when implementing custom decorations. /// This is the context menu that is normally shown when interacting with the title bar. This is useful when implementing custom decorations.
/// Platform-specific /// Platform-specific
/// * **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported. /// * **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported.
pub physical_window_menu_pos: Option<Vec2>, //pub physical_window_menu_pos: Option<Vec2>,
/// Gets the window's occluded state (completely hidden from view). /// Gets the window's occluded state (completely hidden from view).
/// ///
@ -296,22 +323,42 @@ pub struct WindowOptions {
/// the application should free resources (according to the iOS application lifecycle). /// the application should free resources (according to the iOS application lifecycle).
/// * **Web:** Doesn't take into account CSS border, padding, or transform. /// * **Web:** Doesn't take into account CSS border, padding, or transform.
/// * **Android / Wayland / Windows / Orbital:** Unsupported. /// * **Android / Wayland / Windows / Orbital:** Unsupported.
pub occluded: bool, // TODO: update
pub(crate) occluded: bool,
/// Gets/sets the position of the cursor in physical coordinates. /// Gets/sets the position of the cursor in physical coordinates.
/// ///
/// Platform-specific /// Platform-specific
/// * **Wayland:** Cursor must be in [`CursorGrabMode::Locked`]. /// * **Wayland:** Cursor must be in [`CursorGrabMode::Locked`].
/// * **iOS / Android / Web / Orbital:** Unsupported. /// * **iOS / Android / Web / Orbital:** Unsupported.
#[reflect(skip)]
physical_cursor_position: Option<DVec2>, physical_cursor_position: Option<DVec2>,
} }
/* fn physical_to_vec2<P: winit::dpi::Pixel>(size: PhysicalSize<P>) -> Vec2 {
let size = size.cast::<f32>();
Vec2::new(size.width, size.height)
} */
fn logical_to_vec2(size: LogicalSize<f32>) -> Vec2 {
Vec2::new(size.width, size.height)
}
impl From<winit::window::WindowAttributes> for WindowOptions { impl From<winit::window::WindowAttributes> for WindowOptions {
fn from(value: winit::window::WindowAttributes) -> Self { fn from(value: winit::window::WindowAttributes) -> Self {
Self { Self {
enabled_buttons: value.enabled_buttons, enabled_buttons: value.enabled_buttons,
focused: false, focused: false,
fullscreen: value.fullscreen, fullscreen_mode: value.fullscreen.map(|m| match m {
winit::window::Fullscreen::Exclusive(video_mode_handle) => {
if video_mode_handle.size() == video_mode_handle.monitor().size() {
FullscreenMode::Fullscreen
} else {
FullscreenMode::SizedFullscreen
}
},
winit::window::Fullscreen::Borderless(_) => FullscreenMode::BorderlessFullscreen,
}).unwrap_or(FullscreenMode::Windowed),
position: value.position.map(|p| { position: value.position.map(|p| {
let s = p.to_physical::<i32>(1.0); let s = p.to_physical::<i32>(1.0);
IVec2::new(s.x, s.y) IVec2::new(s.x, s.y)
@ -328,7 +375,6 @@ impl From<winit::window::WindowAttributes> for WindowOptions {
resize_increments: value.resize_increments.map(|r| r.into()), resize_increments: value.resize_increments.map(|r| r.into()),
scale_factor: 1.0, scale_factor: 1.0,
blur: value.blur, blur: value.blur,
content_protected: value.content_protected,
cursor: Cursor { cursor: Cursor {
appearance: match value.cursor { appearance: match value.cursor {
winit::window::Cursor::Icon(icon) => CursorAppearance::Icon(icon), winit::window::Cursor::Icon(icon) => CursorAppearance::Icon(icon),
@ -340,14 +386,13 @@ impl From<winit::window::WindowAttributes> for WindowOptions {
}, },
ime_allowed: false, ime_allowed: false,
physical_ime_cursor_area: None, physical_ime_cursor_area: None,
min_size: value.min_inner_size.map(|m| m.into()), min_size: value.min_inner_size.map(|m| logical_to_vec2(m.to_logical(1.0))),
max_size: value.max_inner_size.map(|m| m.into()), max_size: value.max_inner_size.map(|m| logical_to_vec2(m.to_logical(1.0))),
theme: value.preferred_theme, theme: value.preferred_theme,
title: value.title, title: value.title,
transparent: value.transparent, transparent: value.transparent,
window_icon: None, window_icon: None,
window_level: value.window_level, window_level: value.window_level,
physical_window_menu_pos: None,
occluded: false, occluded: false,
physical_cursor_position: None, physical_cursor_position: None,
} }
@ -360,13 +405,35 @@ impl Default for WindowOptions {
} }
} }
fn find_closest_video_mode(monitor: MonitorHandle, physical_size: UVec2) -> Option<VideoModeHandle> {
let mut modes = monitor.video_modes();
let mut closest = modes.next()?;
let closest_size = closest.size();
let mut closest_size = UVec2::new(closest_size.width, closest_size.height);
for mode in modes {
let s = closest.size();
let s = UVec2::new(s.width, s.height);
if (physical_size - s).length_squared() < (physical_size - closest_size).length_squared() {
closest = mode;
closest_size = s;
}
}
Some(closest)
}
impl WindowOptions { impl WindowOptions {
/// Create winit [`WindowAttributes`] from self. /// Create winit [`WindowAttributes`] from self.
///
/// This will ignore [`WindowOptions::fullscreen`] mode on self, defaulting to
/// [`FullscreenMode::Windowed`]. It will be updated on first run of the sync system.
pub(crate) fn as_attributes(&self) -> winit::window::WindowAttributes { pub(crate) fn as_attributes(&self) -> winit::window::WindowAttributes {
let mut att = winit::window::Window::default_attributes(); let mut att = winit::window::Window::default_attributes();
att.enabled_buttons = self.enabled_buttons.clone(); att.enabled_buttons = self.enabled_buttons.clone();
att.fullscreen = self.fullscreen.clone(); att.fullscreen = None;
att.inner_size = Some(Size::Physical(PhysicalSize::new(self.physical_size.x, self.physical_size.y))); att.inner_size = Some(Size::Physical(PhysicalSize::new(self.physical_size.x, self.physical_size.y)));
att.decorations = self.decorated; att.decorations = self.decorated;
att.maximized = self.maximized; att.maximized = self.maximized;
@ -375,13 +442,12 @@ impl WindowOptions {
att.position = self.position.map(|p| Position::Physical(PhysicalPosition::new(p.x, p.y))); att.position = self.position.map(|p| Position::Physical(PhysicalPosition::new(p.x, p.y)));
att.resize_increments = self.resize_increments.map(|i| i.into()); att.resize_increments = self.resize_increments.map(|i| i.into());
att.blur = self.blur; att.blur = self.blur;
att.content_protected = self.content_protected;
att.cursor = match self.cursor.appearance.clone() { att.cursor = match self.cursor.appearance.clone() {
CursorAppearance::Icon(icon) => winit::window::Cursor::Icon(icon), CursorAppearance::Icon(icon) => winit::window::Cursor::Icon(icon),
CursorAppearance::Custom(custom) => winit::window::Cursor::Custom(custom), CursorAppearance::Custom(custom) => winit::window::Cursor::Custom(custom),
}; };
att.min_inner_size = self.min_size.map(|s| s.into()); att.min_inner_size = self.min_size.map(|s| Size::Logical(LogicalSize::new(s.x as _, s.y as _)));
att.max_inner_size = self.max_size.map(|s| s.into()); att.max_inner_size = self.max_size.map(|s| Size::Logical(LogicalSize::new(s.x as _, s.y as _)));
att.preferred_theme = self.theme; att.preferred_theme = self.theme;
att.title = self.title.clone(); att.title = self.title.clone();
att.transparent = self.transparent; att.transparent = self.transparent;
@ -459,6 +525,22 @@ impl WindowOptions {
pub fn set_physical_cursor_position(&mut self, pos: Option<DVec2>) { pub fn set_physical_cursor_position(&mut self, pos: Option<DVec2>) {
self.physical_cursor_position = pos; self.physical_cursor_position = pos;
} }
/// The window's occluded state (completely hidden from view).
///
/// This is different to window visibility as it depends on whether the window is
/// closed, minimised, set invisible, or fully occluded by another window.
///
/// Platform-specific
/// * **iOS:** this is set to `false` in response to an applicationWillEnterForeground
/// callback which means the application should start preparing its data.
/// Its `true` in response to an applicationDidEnterBackground callback which means
/// the application should free resources (according to the iOS application lifecycle).
/// * **Web:** Doesn't take into account CSS border, padding, or transform.
/// * **Android / Wayland / Windows / Orbital:** Unsupported.
pub fn occluded(&self) -> bool {
self.occluded
}
} }
/// The state of the window last time it was changed. /// The state of the window last time it was changed.
@ -504,8 +586,13 @@ pub fn window_sync_system(windows: Res<WinitWindows>, view: View<(Entities, &Win
window.focus_window(); window.focus_window();
} }
if opts.fullscreen != last.fullscreen { if opts.fullscreen_mode != last.fullscreen_mode {
window.set_fullscreen(opts.fullscreen.clone()); let monitor = window.primary_monitor().unwrap_or_else(|| {
let mut m = window.available_monitors();
m.next().expect("failed to find any available monitor")
});
window.set_fullscreen(opts.fullscreen_mode.as_winit_fullscreen(monitor, opts.physical_size));
} }
if opts.physical_size != last.physical_size { if opts.physical_size != last.physical_size {
@ -545,10 +632,6 @@ pub fn window_sync_system(windows: Res<WinitWindows>, view: View<(Entities, &Win
window.set_blur(opts.blur); window.set_blur(opts.blur);
} }
if opts.content_protected != last.content_protected {
window.set_content_protected(opts.content_protected);
}
if opts.cursor.appearance != last.cursor.appearance { if opts.cursor.appearance != last.cursor.appearance {
match opts.cursor.appearance.clone() { match opts.cursor.appearance.clone() {
CursorAppearance::Icon(icon) => window.set_cursor(winit::window::Cursor::Icon(icon)), CursorAppearance::Icon(icon) => window.set_cursor(winit::window::Cursor::Icon(icon)),
@ -584,11 +667,13 @@ pub fn window_sync_system(windows: Res<WinitWindows>, view: View<(Entities, &Win
} }
if opts.min_size != last.min_size { if opts.min_size != last.min_size {
window.set_min_inner_size(opts.min_size); let s = opts.min_size.map(|s| LogicalSize::new(s.x, s.y));
window.set_min_inner_size(s);
} }
if opts.max_size != last.max_size { if opts.max_size != last.max_size {
window.set_max_inner_size(opts.max_size); let s = opts.max_size.map(|s| LogicalSize::new(s.x, s.y));
window.set_max_inner_size(s);
} }
if opts.theme != last.theme { if opts.theme != last.theme {
@ -617,12 +702,6 @@ pub fn window_sync_system(windows: Res<WinitWindows>, view: View<(Entities, &Win
window.set_window_level(opts.window_level); window.set_window_level(opts.window_level);
} }
if opts.physical_window_menu_pos != last.physical_window_menu_pos && opts.physical_window_menu_pos.is_some() {
let pos = opts.physical_window_menu_pos.unwrap();
let pos = PhysicalPosition::new(pos.x, pos.y);
window.show_window_menu(pos);
}
if opts.physical_cursor_position != last.physical_cursor_position && opts.physical_cursor_position.is_some() { if opts.physical_cursor_position != last.physical_cursor_position && opts.physical_cursor_position.is_some() {
let pos = opts.physical_cursor_position.unwrap(); let pos = opts.physical_cursor_position.unwrap();
let pos = PhysicalPosition::new(pos.x, pos.y); let pos = PhysicalPosition::new(pos.x, pos.y);

View File

@ -302,7 +302,8 @@ struct WrapUsage {
meta_methods: Vec<MetaMethod>, meta_methods: Vec<MetaMethod>,
skips: Vec<SkipType>, skips: Vec<SkipType>,
extra_builds: Option<syn::Block>, extra_fields: Option<syn::Block>,
extra_methods: Option<syn::Block>,
} }
impl syn::parse::Parse for WrapUsage { impl syn::parse::Parse for WrapUsage {
@ -313,7 +314,8 @@ impl syn::parse::Parse for WrapUsage {
override_name: None, override_name: None,
auto_fields: vec![], auto_fields: vec![],
auto_derives: vec![], auto_derives: vec![],
extra_builds: None, extra_fields: None,
extra_methods: None,
auto_new: false, auto_new: false,
meta_methods: vec![], meta_methods: vec![],
skips: vec![], skips: vec![],
@ -374,15 +376,19 @@ impl syn::parse::Parse for WrapUsage {
s.meta_methods = meta_methods.into_iter().collect(); s.meta_methods = meta_methods.into_iter().collect();
} }
}, },
"extra_fields" => {
let _eq: Token![=] = input.parse()?;
s.extra_fields = Some(input.parse::<syn::Block>()?);
},
"extra_methods" => {
let _eq: Token![=] = input.parse()?;
s.extra_methods = Some(input.parse::<syn::Block>()?);
},
_ => { _ => {
return Err(syn::Error::new_spanned(ident, "unknown wrapper command")); return Err(syn::Error::new_spanned(ident, format!("unknown wrapper command: '{}'", ident_str)));
} }
} }
} }
if let Ok(block) = input.parse::<syn::Block>() {
s.extra_builds = Some(block);
}
} }
if s.auto_new && s.auto_fields.is_empty() { if s.auto_new && s.auto_fields.is_empty() {
@ -405,7 +411,8 @@ pub fn wrap_lua_struct_impl(input: proc_macro::TokenStream) -> proc_macro::Token
.unwrap_or_else(|| Ident::new(&format!("Lua{}", type_name), Span::call_site())); .unwrap_or_else(|| Ident::new(&format!("Lua{}", type_name), Span::call_site()));
let derive_idents_iter = input.auto_derives.iter(); let derive_idents_iter = input.auto_derives.iter();
let extra_builds = input.extra_builds; let extra_fields = input.extra_fields;
let extra_methods = input.extra_methods;
let field_get_set_pairs = input.auto_fields.iter().map(|i| { let field_get_set_pairs = input.auto_fields.iter().map(|i| {
let is = i.to_string(); let is = i.to_string();
@ -492,6 +499,8 @@ pub fn wrap_lua_struct_impl(input: proc_macro::TokenStream) -> proc_macro::Token
impl mlua::UserData for #wrapper_typename { impl mlua::UserData for #wrapper_typename {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
#(#field_get_set_pairs)* #(#field_get_set_pairs)*
#extra_fields
} }
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
@ -499,7 +508,7 @@ pub fn wrap_lua_struct_impl(input: proc_macro::TokenStream) -> proc_macro::Token
#new_func_tokens #new_func_tokens
#meta_methods_tokens #meta_methods_tokens
#extra_builds #extra_methods
} }
} }

View File

@ -0,0 +1,23 @@
WindowMode = {
WNDOWED = "windowed",
BORDERLESS_FULLSCREEN = "borderless_fullscreen",
SIZED_FULLSCREEN = "sized_fullscreen",
FULLSCREEN = "fullscreen",
}
CursorGrabMode = {
NONE = "none",
CONFINED = "confined",
LOCKED = "locked",
}
WindowTheme = {
LIGHT = "light",
DARK = "dark",
}
WindowLevel = {
ALWAYS_ON_BOTTOM = "always_on_bottom",
NORMAL = "normal",
ALWAYS_ON_TOP = "always_on_top",
}

View File

@ -38,7 +38,9 @@ pub enum Error {
#[error("received nil value from Lua")] #[error("received nil value from Lua")]
Nil, Nil,
#[error("{0}")] #[error("{0}")]
Mlua(#[from] mlua::Error) Mlua(#[from] mlua::Error),
#[error("unimplemented: {0}")]
Unimplemented(String)
} }
/* impl Into<mlua::Error> for Error { /* impl Into<mlua::Error> for Error {
@ -63,6 +65,10 @@ impl Error {
pub fn type_mismatch(expected: &str, got: &str) -> Self { pub fn type_mismatch(expected: &str, got: &str) -> Self {
Self::TypeMismatch { expected: expected.into(), got: got.into() } Self::TypeMismatch { expected: expected.into(), got: got.into() }
} }
pub fn unimplemented(msg: &str) -> Self {
Self::Unimplemented(msg.into())
}
} }
/// Name of a Lua function that is used to Reflect the Userdata, but without a value. /// Name of a Lua function that is used to Reflect the Userdata, but without a value.

View File

@ -4,7 +4,7 @@ use lyra_ecs::ResourceObject;
use lyra_reflect::{Reflect, TypeRegistry}; use lyra_reflect::{Reflect, TypeRegistry};
use lyra_resource::gltf::Gltf; use lyra_resource::gltf::Gltf;
use crate::{lua::{wrappers::{LuaGltfHandle, LuaActionHandler, LuaDeltaTime, LuaResHandleToComponent, LuaSceneHandle}, LuaContext, ReflectLuaProxy, RegisterLuaType, FN_NAME_INTERNAL_REFLECT_TYPE}, ScriptApiProvider, ScriptBorrow, ScriptData, ScriptDynamicBundle, ScriptWorldPtr}; use crate::{lua::{wrappers::{LuaActionHandler, LuaDeltaTime, LuaGltfHandle, LuaResHandleToComponent, LuaSceneHandle, LuaWindow}, LuaContext, ReflectLuaProxy, RegisterLuaType, FN_NAME_INTERNAL_REFLECT_TYPE}, ScriptApiProvider, ScriptBorrow, ScriptData, ScriptDynamicBundle, ScriptWorldPtr};
#[derive(Default)] #[derive(Default)]
pub struct LyraEcsApiProvider; pub struct LyraEcsApiProvider;
@ -16,6 +16,7 @@ impl ScriptApiProvider for LyraEcsApiProvider {
world.register_lua_convert::<LuaDeltaTime>(); world.register_lua_convert::<LuaDeltaTime>();
world.register_lua_wrapper::<LuaSceneHandle>(); world.register_lua_wrapper::<LuaSceneHandle>();
world.register_lua_wrapper::<LuaActionHandler>(); world.register_lua_wrapper::<LuaActionHandler>();
world.register_lua_wrapper::<LuaWindow>();
let mut registry = world.get_resource_mut::<TypeRegistry>().unwrap(); let mut registry = world.get_resource_mut::<TypeRegistry>().unwrap();
@ -39,11 +40,16 @@ impl ScriptApiProvider for LyraEcsApiProvider {
fn expose_api(&mut self, _: &ScriptData, ctx: &mut Self::ScriptContext) -> Result<(), crate::ScriptError> { fn expose_api(&mut self, _: &ScriptData, ctx: &mut Self::ScriptContext) -> Result<(), crate::ScriptError> {
let ctx = ctx.lock().unwrap(); let ctx = ctx.lock().unwrap();
// load window util
let bytes = include_str!("../../../scripts/lua/window.lua");
ctx.load(bytes).exec().unwrap();
let globals = ctx.globals(); let globals = ctx.globals();
globals.set("World", ctx.create_proxy::<ScriptWorldPtr>()?)?; globals.set("World", ctx.create_proxy::<ScriptWorldPtr>()?)?;
globals.set("DynamicBundle", ctx.create_proxy::<ScriptDynamicBundle>()?)?; globals.set("DynamicBundle", ctx.create_proxy::<ScriptDynamicBundle>()?)?;
globals.set("SceneComponent", ctx.create_proxy::<LuaSceneHandle>()?)?; globals.set("SceneComponent", ctx.create_proxy::<LuaSceneHandle>()?)?;
globals.set("ActionHandler", ctx.create_proxy::<LuaActionHandler>()?)?; globals.set("ActionHandler", ctx.create_proxy::<LuaActionHandler>()?)?;
globals.set("Window", ctx.create_proxy::<LuaWindow>()?)?;
let dt_table = create_reflect_table::<lyra_game::DeltaTime>(&ctx)?; let dt_table = create_reflect_table::<lyra_game::DeltaTime>(&ctx)?;
globals.set("DeltaTime", dt_table)?; globals.set("DeltaTime", dt_table)?;

View File

@ -1,5 +1,5 @@
use lyra_ecs::World; use lyra_ecs::World;
use crate::lua::wrappers::{LuaQuat, LuaTransform, LuaVec3}; use crate::lua::wrappers::{LuaQuat, LuaTransform, LuaVec3, LuaVec2};
use crate::ScriptData; use crate::ScriptData;
use crate::lua::RegisterLuaType; use crate::lua::RegisterLuaType;
@ -12,6 +12,7 @@ impl ScriptApiProvider for LyraMathApiProvider {
type ScriptContext = LuaContext; type ScriptContext = LuaContext;
fn prepare_world(&mut self, world: &mut World) { fn prepare_world(&mut self, world: &mut World) {
world.register_lua_wrapper::<LuaVec2>();
world.register_lua_wrapper::<LuaVec3>(); world.register_lua_wrapper::<LuaVec3>();
world.register_lua_wrapper::<LuaQuat>(); world.register_lua_wrapper::<LuaQuat>();
world.register_lua_wrapper::<LuaTransform>(); world.register_lua_wrapper::<LuaTransform>();
@ -24,6 +25,7 @@ impl ScriptApiProvider for LyraMathApiProvider {
ctx.load("lyra/math/transform.lua", bytes.as_slice())?.execute(())?; */ ctx.load("lyra/math/transform.lua", bytes.as_slice())?.execute(())?; */
let globals = ctx.globals(); let globals = ctx.globals();
globals.set("Vec2", ctx.create_proxy::<LuaVec2>()?)?;
globals.set("Vec3", ctx.create_proxy::<LuaVec3>()?)?; globals.set("Vec3", ctx.create_proxy::<LuaVec3>()?)?;
globals.set("Quat", ctx.create_proxy::<LuaQuat>()?)?; globals.set("Quat", ctx.create_proxy::<LuaQuat>()?)?;
globals.set("Transform", ctx.create_proxy::<LuaTransform>()?)?; globals.set("Transform", ctx.create_proxy::<LuaTransform>()?)?;

View File

@ -18,7 +18,7 @@ wrap_lua_struct!(
Mod(LuaVec2, f32), Mod(LuaVec2, f32),
Eq, Unm, ToString Eq, Unm, ToString
), ),
{ extra_methods = {
lua_vec_wrap_extension!(math::Vec2, LuaVec2); lua_vec_wrap_extension!(math::Vec2, LuaVec2);
methods.add_method_mut("move_by", |lua, this, vals: mlua::MultiValue| { methods.add_method_mut("move_by", |lua, this, vals: mlua::MultiValue| {
@ -50,7 +50,7 @@ wrap_lua_struct!(
Mod(LuaVec3, f32), Mod(LuaVec3, f32),
Eq, Unm, ToString Eq, Unm, ToString
), ),
{ extra_methods = {
lua_vec_wrap_extension!(math::Vec3, LuaVec3); lua_vec_wrap_extension!(math::Vec3, LuaVec3);
methods.add_method_mut("move_by", |lua, this, vals: mlua::MultiValue| { methods.add_method_mut("move_by", |lua, this, vals: mlua::MultiValue| {
@ -83,7 +83,7 @@ wrap_lua_struct!(
Mod(LuaVec4, f32), Mod(LuaVec4, f32),
Eq, Unm, ToString Eq, Unm, ToString
), ),
{ extra_methods = {
lua_vec_wrap_extension!(math::Vec4, LuaVec4); lua_vec_wrap_extension!(math::Vec4, LuaVec4);
} }
); );
@ -101,7 +101,7 @@ wrap_lua_struct!(
// __mul for LuaVec3 is manually implemented below since it doesn't return Self // __mul for LuaVec3 is manually implemented below since it doesn't return Self
//Mul(LuaQuat, f32), //Mul(LuaQuat, f32),
), ),
{ extra_methods = {
// manually implemented since Quat doesn't have a `new` function // manually implemented since Quat doesn't have a `new` function
methods.add_function("new", |_, (x, y, z, w)| { methods.add_function("new", |_, (x, y, z, w)| {
Ok(Self(math::Quat::from_xyzw(x, y, z, w))) Ok(Self(math::Quat::from_xyzw(x, y, z, w)))
@ -184,7 +184,7 @@ wrap_lua_struct!(
math::Transform, math::Transform,
derives(PartialEq, Copy), derives(PartialEq, Copy),
metamethods(ToString, Eq), metamethods(ToString, Eq),
{ extra_methods = {
methods.add_function("default", |_, ()| { methods.add_function("default", |_, ()| {
Ok(Self(math::Transform::default())) Ok(Self(math::Transform::default()))
}); });

View File

@ -8,4 +8,7 @@ mod asset;
pub use asset::*; pub use asset::*;
mod delta_time; mod delta_time;
pub use delta_time::*; pub use delta_time::*;
mod window;
pub use window::*;

View File

@ -0,0 +1,326 @@
use lyra_scripting_derive::wrap_lua_struct;
use lyra_game::winit::{WindowOptions, FullscreenMode, Theme, CursorGrabMode, WindowLevel};
use mlua::IntoLua;
use crate::lyra_engine;
use crate as lyra_scripting;
use super::super::Error;
use super::LuaVec2;
use lyra_game::math::{Vec2, IVec2, UVec2};
wrap_lua_struct!(
WindowOptions,
//derives(Clone),
name=LuaWindow,
fields(focused),
extra_fields={
fields.add_field_method_get("window_mode", |lua, this| {
let s = match &this.fullscreen_mode {
FullscreenMode::Windowed => "windowed",
FullscreenMode::BorderlessFullscreen => "borderless_fullscreen",
FullscreenMode::SizedFullscreen => "sized_fullscreen",
FullscreenMode::Fullscreen => "fullscreen"
};
s.into_lua(lua)
});
fields.add_field_method_set("window_mode", |_, this, mode: String| {
let mode = mode.as_str();
match mode {
"windowed" => {
this.fullscreen_mode = FullscreenMode::Windowed;
},
"borderless_fullscreen" => {
this.fullscreen_mode = FullscreenMode::BorderlessFullscreen;
},
"sized_fullscreen" => {
this.fullscreen_mode = FullscreenMode::SizedFullscreen;
},
"fullscreen" => {
this.fullscreen_mode = FullscreenMode::Fullscreen;
},
_ => {
return Err(mlua::Error::runtime(format!("invalid window mode {}", mode)));
}
}
Ok(())
});
// nil if its unsupported
fields.add_field_method_get("position", |lua, this| {
this.position.map(|p| {
LuaVec2(Vec2::new(p.x as f32, p.y as f32))
}).into_lua(lua)
});
fields.add_field_method_set("position", |_, this, pos: Option<LuaVec2>| {
this.position = pos.map(|p| IVec2::new(p.x as i32, p.y as i32));
Ok(())
});
fields.add_field_method_get("physical_size", |lua, this| {
let p = this.physical_size();
LuaVec2(Vec2::new(p.x as f32, p.y as f32))
.into_lua(lua)
});
fields.add_field_method_set("physical_size", |_, this, size: LuaVec2| {
this.set_physical_size(UVec2::new(size.x as _, size.y as _));
Ok(())
});
fields.add_field_method_get("size", |lua, this| {
LuaVec2(this.size())
.into_lua(lua)
});
fields.add_field_method_set("size", |_, this, size: LuaVec2| {
this.set_size(*size);
Ok(())
});
fields.add_field_method_get("decorated", |lua, this| {
this.decorated.into_lua(lua)
});
fields.add_field_method_set("decorated", |_, this, val: bool| {
this.decorated = val;
Ok(())
});
fields.add_field_method_get("maximized", |lua, this| {
this.maximized.into_lua(lua)
});
fields.add_field_method_set("maximized", |_, this, val: bool| {
this.maximized = val;
Ok(())
});
// returns `nil` if minimized state could not be determined
fields.add_field_method_get("minimized", |lua, this| {
this.minimized.into_lua(lua)
});
fields.add_field_method_set("minimized", |_, this, val: bool| {
this.minimized = Some(val);
Ok(())
});
fields.add_field_method_get("resizable", |lua, this| {
this.resizable.into_lua(lua)
});
fields.add_field_method_set("resizable", |_, this, val: bool| {
this.resizable = val;
Ok(())
});
// returns `nil` if minimized state could not be determined
fields.add_field_method_get("visible", |lua, this| {
this.visible.into_lua(lua)
});
fields.add_field_method_set("visible", |_, this, val: bool| {
this.visible = Some(val);
Ok(())
});
//todo!("resize_increments");
/* fields.add_field_method_get("resize_increments", |lua, this| {
this.visible.into_lua(lua)
});
fields.add_field_method_set("resize_increments", |_, this, val: bool| {
this.visible = Some(val);
Ok(())
}); */
fields.add_field_method_get("scale_factor", |lua, this| {
this.scale_factor.into_lua(lua)
});
fields.add_field_method_set("scale_factor", |_, this, val: f64| {
this.scale_factor = val;
Ok(())
});
fields.add_field_method_get("blur", |lua, this| {
this.blur.into_lua(lua)
});
fields.add_field_method_set("blur", |_, this, val: bool| {
this.blur = val;
Ok(())
});
// TODO: implement get/setting window icon
// must have ResHandle<Image> exposed to Lua.
fields.add_field_method_get::<_, bool>("cursor_appearance", |_, _| {
Err(mlua::Error::external(Error::unimplemented("field 'cursor_appearance' is unimplemented")))
});
fields.add_field_method_get("cursor_grab", |lua, this| {
let v = match &this.cursor.grab {
CursorGrabMode::None => "none",
CursorGrabMode::Confined => "confined",
CursorGrabMode::Locked => "locked",
};
v.into_lua(lua)
});
fields.add_field_method_set("cursor_grab", |_, this, val: String| {
let val = val.as_str();
let v = match val {
"none" => CursorGrabMode::None,
"confined" => CursorGrabMode::Confined,
"locked" => CursorGrabMode::Locked,
_ => {
return Err(mlua::Error::runtime(format!("invalid cursor grab mode {}", val)));
}
};
this.cursor.grab = v;
Ok(())
});
fields.add_field_method_get("cursor_hittest", |lua, this| {
this.cursor.hittest.into_lua(lua)
});
fields.add_field_method_set("cursor_hittest", |_, this, val: bool| {
this.cursor.hittest = val;
Ok(())
});
fields.add_field_method_get("cursor_visible", |lua, this| {
this.cursor.visible.into_lua(lua)
});
fields.add_field_method_set("cursor_visible", |_, this, val: bool| {
this.cursor.visible = val;
Ok(())
});
//todo!("window cursor");
fields.add_field_method_get("ime_allowed", |lua, this| {
this.ime_allowed.into_lua(lua)
});
fields.add_field_method_set("ime_allowed", |_, this, val: bool| {
this.ime_allowed = val;
Ok(())
});
fields.add_field_method_get("min_size", |lua, this| {
this.min_size.map(|p| LuaVec2(p))
.into_lua(lua)
});
fields.add_field_method_set("min_size", |_, this, size: Option<LuaVec2>| {
this.min_size = size.map(|p| *p);
Ok(())
});
fields.add_field_method_get("max_size", |lua, this| {
this.max_size.map(|p| LuaVec2(p))
.into_lua(lua)
});
fields.add_field_method_set("max_size", |_, this, size: Option<LuaVec2>| {
this.max_size = size.map(|p| *p);
Ok(())
});
fields.add_field_method_get("theme", |lua, this| {
this.theme.as_ref().map(|t| match t {
Theme::Light => "light",
Theme::Dark => "dark"
}).into_lua(lua)
});
fields.add_field_method_set("theme", |_, this, theme: Option<String>| {
let t = if let Some(theme) = theme.as_ref() {
let theme = theme.as_str();
match theme {
"light" => Some(Theme::Light),
"dark" => Some(Theme::Dark),
_ => {
return Err(mlua::Error::runtime(format!("invalid theme {}", theme)));
}
}
} else { None };
this.theme = t;
Ok(())
});
fields.add_field_method_get("title", |lua, this| {
this.title.clone().into_lua(lua)
});
fields.add_field_method_set("title", |_, this, title: String| {
this.title = title;
Ok(())
});
fields.add_field_method_get("transparent", |lua, this| {
this.transparent.into_lua(lua)
});
fields.add_field_method_set("transparent", |_, this, val: bool| {
this.transparent = val;
Ok(())
});
// TODO: implement get/setting window icon
// must have ResHandle<Image> exposed to Lua.
fields.add_field_method_get::<_, bool>("window_icon", |_, _| {
Err(mlua::Error::external(Error::unimplemented("field 'window_icon' is unimplemented")))
});
fields.add_field_method_get("window_level", |lua, this| {
let v = match &this.window_level {
WindowLevel::AlwaysOnBottom => "always_on_bottom",
WindowLevel::Normal => "normal",
WindowLevel::AlwaysOnTop => "always_on_top",
};
v.into_lua(lua)
});
fields.add_field_method_set("window_level", |_, this, val: String| {
let window_level = val.as_str();
let l = match window_level {
"always_on_bottom" => WindowLevel::AlwaysOnBottom,
"normal" => WindowLevel::Normal,
"always_on_top" => WindowLevel::AlwaysOnTop,
_ => {
return Err(mlua::Error::runtime(format!("invalid window level {}", window_level)));
}
};
this.window_level = l;
Ok(())
});
fields.add_field_method_get("occluded", |lua, this| {
this.occluded().into_lua(lua)
});
fields.add_field_method_get("cursor_position", |lua, this| {
this.cursor_position().map(|p| LuaVec2(p))
.into_lua(lua)
});
fields.add_field_method_set("cursor_position", |_, this, val: Option<LuaVec2>| {
this.set_cursor_position(val.map(|p| *p));
Ok(())
});
fields.add_field_method_get("physical_cursor_position", |lua, this| {
this.physical_cursor_position().map(|p| LuaVec2(p))
.into_lua(lua)
});
fields.add_field_method_set("physical_cursor_position", |_, this, val: Option<LuaVec2>| {
this.set_physical_cursor_position(val.map(|p| p.as_dvec2()));
Ok(())
});
},
extra_methods={
methods.add_method("is_mouse_inside", |lua, this, ()| {
this.is_mouse_inside().into_lua(lua)
});
}
);