diff --git a/Cargo.lock b/Cargo.lock index ac3f862..d36cc29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2192,6 +2192,8 @@ dependencies = [ "glam 0.30.9", "raidillon_assets", "raidillon_core", + "serde", + "toml", "winit", ] @@ -2413,6 +2415,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2669,6 +2680,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -2699,6 +2725,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tracing" version = "0.1.41" diff --git a/app/src/prelude.rs b/app/src/prelude.rs index 1304175..1171d0f 100644 --- a/app/src/prelude.rs +++ b/app/src/prelude.rs @@ -11,6 +11,7 @@ pub use raidillon_platform::{ Camera, PlatformContext, TimeContext, + settings::{Settings, WindowMode}, }; pub use raidillon_assets::{ diff --git a/game/src/main.rs b/game/src/main.rs index 582e2d5..b49445c 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -7,7 +7,9 @@ use rapier3d::prelude::ColliderBuilder; use winit::event::{Event, WindowEvent}; use systems::debug_camera::FPSDebugCameraSystem; use crate::systems::common::should_draw_menu; -use crate::systems::{KeybindsSystem, KinematicCharacterController, MenuSystem, PhysicsSystem}; +use crate::systems::{ + DisplaySettings, KeybindsSystem, KinematicCharacterController, MenuSystem, PhysicsSystem +}; const TEST_GLTF: &str = "sphere.glb"; const PLANE_GLTF: &str = "plane.glb"; @@ -97,23 +99,21 @@ impl System for MainSystem { InputState, )>().unwrap(); - if should_draw_menu(scene) { - let mut egui_queue = pctx.egui_queue.borrow_mut(); - let time_ctx = pctx.time_ctx.clone(); - let mut character_pos = Vec3::ZERO; - for (_ent, (tr, ch_component)) in scene.world.query::<(&Transform, &CharacterBodyComponent)>().iter() { - character_pos = tr.translation; - } - egui_queue.queue(move |egui_ctx| { - egui::Window::new("Debug").show(egui_ctx, |ui| { - ui.label("Hello World!"); - ui.label(format!("Frame Delta: {}", time_ctx.frame_dt)); - ui.label(format!("Fixed Delta: {}", time_ctx.fixed_dt)); - ui.label(format!("FPS: {}", 1.0 / time_ctx.frame_dt)); - ui.label(format!("Character POS: {}", character_pos)); - }); - }); + let mut egui_queue = pctx.egui_queue.borrow_mut(); + let time_ctx = pctx.time_ctx.clone(); + let mut character_pos = Vec3::ZERO; + for (_ent, (tr, ch_component)) in scene.world.query::<(&Transform, &CharacterBodyComponent)>().iter() { + character_pos = tr.translation; } + egui_queue.queue(move |egui_ctx| { + egui::Window::new("Debug").show(egui_ctx, |ui| { + ui.label("Hello World!"); + ui.label(format!("Frame Delta: {:.3}", time_ctx.frame_dt)); + ui.label(format!("Fixed Delta: {:.3}", time_ctx.fixed_dt)); + ui.label(format!("FPS: {:.3}", 1.0 / time_ctx.frame_dt)); + ui.label(format!("Character POS: {character_pos:.3}")); + }); + }); } } @@ -124,6 +124,7 @@ fn main() { .add_system::() .add_system::() .add_system::() + .add_system::() .add_system::() .add_system::() .add_scene(MAIN_SCENE_ID, Scene::new(MAIN_SCENE_ID.to_owned(), None)) diff --git a/game/src/systems/display_settings.rs b/game/src/systems/display_settings.rs new file mode 100644 index 0000000..6384d15 --- /dev/null +++ b/game/src/systems/display_settings.rs @@ -0,0 +1,92 @@ +use std::sync::{Arc, Mutex}; +use raidillon_app::prelude::*; +use crate::systems::common::should_draw_menu; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SettingsTab { + Display, +} + +impl Default for SettingsTab { + fn default() -> Self { + SettingsTab::Display + } +} + +#[derive(Clone, Default)] +struct DisplaySettingsUiState { + selected_fullscreen_mode: WindowMode, + active_tab: SettingsTab, +} + +#[derive(Default)] +pub struct DisplaySettings { + ui_state: Arc>, +} + +impl System for DisplaySettings { + fn load_world(&mut self, res: &mut EngineResources, scene: &mut Scene) { + let pctx = res.get_mut::().unwrap(); + + // sync the settings with UI state once + if let (Ok(settings_handle), Ok(mut state)) = (pctx.settings.read(), self.ui_state.lock()) { + state.selected_fullscreen_mode = settings_handle.display_settings.fullscreen_mode; + } + } + + fn frame_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { + if should_draw_menu(scene) { + let pctx = res.get_mut::().unwrap(); + let settings = pctx.settings.clone(); + let ui_state = self.ui_state.clone(); + + pctx.egui_queue.borrow_mut().queue(move |egui_ctx| { + egui::Window::new("Settings").default_open(false).show(egui_ctx, |ui| { + let mut state = ui_state.lock().unwrap(); + + ui.horizontal(|ui| { + ui.selectable_value(&mut state.active_tab, SettingsTab::Display, "Display Settings"); + }); + ui.separator(); + + match state.active_tab { + SettingsTab::Display => { + ui.label("Window Mode"); + egui::ComboBox::from_id_salt("window_mode") + .selected_text(window_mode_label(state.selected_fullscreen_mode)) + .show_ui(ui, |ui| { + for mode in [ + WindowMode::Windowed, + WindowMode::BorderlessFullscreen, + ] { + ui.selectable_value( + &mut state.selected_fullscreen_mode, + mode, + window_mode_label(mode), + ); + } + }); + + ui.add_space(8.0); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Apply").clicked() { + if let Ok(mut settings_handle) = settings.write() { + settings_handle.display_settings.fullscreen_mode = state.selected_fullscreen_mode; + settings_handle.display_settings.dirty = true; + } + } + }); + } + } + }); + }); + } + } +} + +fn window_mode_label(mode: WindowMode) -> &'static str { + match mode { + WindowMode::Windowed => "Windowed", + WindowMode::BorderlessFullscreen => "Borderless Fullscreen", + } +} diff --git a/game/src/systems/mod.rs b/game/src/systems/mod.rs index 2f23ed5..bcf77ca 100644 --- a/game/src/systems/mod.rs +++ b/game/src/systems/mod.rs @@ -4,8 +4,10 @@ mod keybinds; mod menu; pub mod debug_camera; pub mod common; +mod display_settings; pub use physics::PhysicsSystem; pub use kinematic_character_controller::KinematicCharacterController; pub use keybinds::KeybindsSystem; pub use menu::MenuSystem; +pub use display_settings::DisplaySettings; diff --git a/glium_platform/src/platform.rs b/glium_platform/src/platform.rs index 1b1ba08..bb1e192 100644 --- a/glium_platform/src/platform.rs +++ b/glium_platform/src/platform.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use raidillon_platform::{Platform, PlatformContext, TimeContext}; use glium::backend::glutin::Display; use glium::backend::glutin::SimpleWindowBuilder; @@ -19,6 +19,7 @@ use crate::GliumAssetManager; use glam::Vec3; use winit::event::DeviceEvent::MouseMotion; use raidillon_core::EguiQueue; +use raidillon_platform::settings::{Settings, default_config_path}; pub struct GliumPlatform> { event_loop: EventLoop<()>, @@ -29,6 +30,7 @@ pub struct GliumPlatform> { engine: E, time: time::Time, egui_queue: Rc>, + settings: Arc>, } impl> Platform for GliumPlatform { @@ -57,6 +59,12 @@ impl> Platform for GliumPlatfor let egui_queue = Rc::new(RefCell::new(EguiQueue::new())); + let settings = Arc::new( + RwLock::new( + Settings::load_or_default(default_config_path()).unwrap() + ) + ); + Self { event_loop, window, @@ -66,6 +74,7 @@ impl> Platform for GliumPlatfor engine, time, egui_queue, + settings, } } @@ -82,9 +91,18 @@ impl> Platform for GliumPlatfor time_ctx: self.construct_time_ctx(), window: self.window.clone(), egui_queue: self.egui_queue.clone(), + settings: self.settings.clone(), }; self.engine.initialize(ctx.clone()); + self.settings.read().unwrap().display_settings.apply(&*self.window.lock().unwrap()); + let _ = &self.event_loop.run(move |event, el| { + let settings_handle = self.settings.read().unwrap(); + if settings_handle.display_settings.dirty { + settings_handle.display_settings.apply(&*self.window.lock().unwrap()); + } + drop(settings_handle); + self.rendering_system_manager .systems .values_mut() @@ -96,8 +114,14 @@ impl> Platform for GliumPlatfor match event { Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(size) => { + if size.width > 0 && size.height > 0 { + self.display.resize((size.width, size.height)); + } + }, WindowEvent::CloseRequested => { // TODO: Run uninitialize on renderer and engine + self.settings.read().unwrap().save_to_file(default_config_path()); el.exit(); }, WindowEvent::RedrawRequested => { diff --git a/platform/Cargo.toml b/platform/Cargo.toml index 3b42bac..1b7b5b2 100644 --- a/platform/Cargo.toml +++ b/platform/Cargo.toml @@ -8,3 +8,5 @@ winit = "0.30.12" raidillon_core = { path = "../core" } raidillon_assets = { path = "../asset" } glam = "0.30.5" +serde = "1.0.228" +toml = "0.9.8" diff --git a/platform/src/context.rs b/platform/src/context.rs index b78ea94..fdbe087 100644 --- a/platform/src/context.rs +++ b/platform/src/context.rs @@ -1,8 +1,9 @@ use std::{cell::RefCell, rc::Rc}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use winit::event::Event; use raidillon_assets::ModelManagerRef; use raidillon_core::EguiQueue; +use crate::settings::Settings; #[derive(Clone)] pub struct PlatformContext { @@ -13,6 +14,7 @@ pub struct PlatformContext { pub time_ctx: TimeContext, pub window: Arc>, pub egui_queue: Rc>, + pub settings: Arc>, } #[derive(Clone)] diff --git a/platform/src/lib.rs b/platform/src/lib.rs index 8875d35..94c467b 100644 --- a/platform/src/lib.rs +++ b/platform/src/lib.rs @@ -2,6 +2,7 @@ pub mod platform; mod camera; mod event; pub mod context; +pub mod settings; pub use platform::Platform; pub use camera::Camera; diff --git a/platform/src/settings.rs b/platform/src/settings.rs new file mode 100644 index 0000000..f6d092f --- /dev/null +++ b/platform/src/settings.rs @@ -0,0 +1,101 @@ +use winit::dpi::LogicalSize; +use winit::window::{Fullscreen, Window}; +use serde::{Serialize, Deserialize}; +use std::error::Error; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +pub fn default_config_path() -> PathBuf { + let exe_path = std::env::current_exe().unwrap(); + let exe_dir = exe_path + .parent() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "executable has no parent")).unwrap(); + + exe_dir.join("settings.toml") +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum WindowMode { + BorderlessFullscreen, + #[default] + Windowed, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Settings { + pub display_settings: DisplaySettings, +} + +impl Settings { + pub fn load_from_file(path: impl AsRef) -> Result> { + let path = path.as_ref(); + let text = fs::read_to_string(path)?; + let settings: Settings = toml::from_str(&text)?; + Ok(settings) + } + + pub fn save_to_file(&self, path: impl AsRef) -> Result<(), Box> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let toml_str = toml::to_string_pretty(self)?; + fs::write(path, toml_str)?; + Ok(()) + } + + pub fn load_or_default(path: impl AsRef) -> Result> { + let path = path.as_ref(); + + match fs::read_to_string(path) { + Ok(text) => { + let settings: Settings = toml::from_str(&text)?; + Ok(settings) + } + Err(err) if err.kind() == io::ErrorKind::NotFound => { + let settings = Settings::default(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let toml_str = toml::to_string_pretty(&settings)?; + fs::write(path, toml_str)?; + Ok(settings) + } + Err(err) => Err(Box::new(err)), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(default)] +pub struct DisplaySettings { + pub fullscreen_mode: WindowMode, + #[serde(skip)] + pub dirty: bool, +} + +impl Default for DisplaySettings { + fn default() -> Self { + Self { + fullscreen_mode: WindowMode::Windowed, + dirty: false, + } + } +} + +impl DisplaySettings { + pub fn apply(&self, window: &Window) { + // apply fullscreen mode + match self.fullscreen_mode { + WindowMode::BorderlessFullscreen => { + let monitor = window.current_monitor().or_else(|| window.primary_monitor()); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); + } + WindowMode::Windowed => { + window.set_fullscreen(None); + }, + } + } +}