lua: expose most fields for window component
This commit is contained in:
parent
64099f598c
commit
76b7cac699
|
@ -57,6 +57,13 @@ function on_update()
|
||||||
t:translate(0, 0.15 * dt, 0)
|
t:translate(0, 0.15 * dt, 0)
|
||||||
return t
|
return t
|
||||||
end, Transform)
|
end, Transform)
|
||||||
|
|
||||||
|
world:view(function (w)
|
||||||
|
print("cursor pos: " .. tostring(w.cursor_position))
|
||||||
|
print("mode: " .. w.window_mode)
|
||||||
|
print("pos: " .. tostring(w.position))
|
||||||
|
print("theme: " .. tostring(w.theme))
|
||||||
|
end, Window)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[ function on_post_update()
|
--[[ function on_post_update()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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");
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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. Doesn’t account for CSS `transform`.
|
/// * **Web:** The size of the canvas element. Doesn’t 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 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,
|
|
||||||
|
|
||||||
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);
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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()))
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,3 +9,6 @@ pub use asset::*;
|
||||||
|
|
||||||
mod delta_time;
|
mod delta_time;
|
||||||
pub use delta_time::*;
|
pub use delta_time::*;
|
||||||
|
|
||||||
|
mod window;
|
||||||
|
pub use window::*;
|
|
@ -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_mode", |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_mode", |_, 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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
Loading…
Reference in New Issue