//! The settings / preferences framework. //! //! A unified, serialized configuration store shared by the engine, the editor, //! and modules. Each contributor registers a typed **section** (a plain //! `serde`-serializable struct) under a name; the framework persists every //! section to RON and restores it, without any central code knowing the //! sections' shapes. This is what lets: //! //! - **engine** preferences (render/quality defaults), //! - **editor** preferences (theme, layout, shortcuts), and //! - **per-module** settings (each module's own options) //! //! all live in one place, while a [`Project`](crate::project::Project) persists //! the per-project subset (it stores section → RON blobs that line up exactly //! with [`Settings::export`]/[`Settings::import`]). //! //! ``` //! use oxide_engine::settings::Settings; //! use serde::{Serialize, Deserialize}; //! //! #[derive(Serialize, Deserialize, Default, PartialEq, Debug)] //! struct EditorPrefs { theme: String, grid: bool } //! //! let mut settings = Settings::new(); //! settings.register::("editor"); //! settings.get_mut::("editor").unwrap().theme = "dark".into(); //! //! // Persist every section to RON, and restore it later. //! let saved = settings.export(); //! let mut restored = Settings::new(); //! restored.register::("editor"); //! restored.import(&saved); //! assert_eq!(restored.get::("editor").unwrap().theme, "dark"); //! ``` use std::any::Any; use std::collections::BTreeMap; use serde::de::DeserializeOwned; use serde::Serialize; /// The monomorphized operations for one registered section, as plain function /// pointers (the closures capture nothing). struct SectionOps { value: Box, to_ron: fn(&dyn Any) -> Option, from_ron: fn(&str) -> Option>, default: fn() -> Box, } /// A registry of typed, serializable settings sections keyed by name. #[derive(Default)] pub struct Settings { sections: BTreeMap<&'static str, SectionOps>, } impl Settings { /// An empty settings store. pub fn new() -> Self { Self::default() } /// Registers section type `T` under `name`, initialized to `T::default()`. /// Re-registering the same name resets it to default. pub fn register(&mut self, name: &'static str) where T: Serialize + DeserializeOwned + Default + 'static, { self.sections.insert( name, SectionOps { value: Box::new(T::default()), to_ron: |any| any.downcast_ref::().and_then(|v| ron::to_string(v).ok()), from_ron: |text| { ron::from_str::(text) .ok() .map(|v| Box::new(v) as Box) }, default: || Box::new(T::default()) as Box, }, ); } /// Whether a section is registered under `name`. pub fn is_registered(&self, name: &str) -> bool { self.sections.contains_key(name) } /// The registered section names, sorted. pub fn names(&self) -> impl Iterator + '_ { self.sections.keys().copied() } /// Borrows section `name` as `T`, or `None` if absent or the type mismatches. pub fn get(&self, name: &str) -> Option<&T> { self.sections.get(name)?.value.downcast_ref::() } /// Mutably borrows section `name` as `T`. pub fn get_mut(&mut self, name: &str) -> Option<&mut T> { self.sections.get_mut(name)?.value.downcast_mut::() } /// Replaces the value of section `name`. Returns whether it was registered /// (with a matching type). pub fn set(&mut self, name: &str, value: T) -> bool { match self.sections.get_mut(name) { // Only overwrite if the registered type matches. Some(section) if section.value.is::() => { section.value = Box::new(value); true } _ => false, } } /// Resets section `name` to its default. Returns whether it was registered. pub fn reset(&mut self, name: &str) -> bool { match self.sections.get_mut(name) { Some(section) => { section.value = (section.default)(); true } None => false, } } /// Serializes section `name` to RON, or `None` if it is not registered. pub fn section_ron(&self, name: &str) -> Option { let section = self.sections.get(name)?; (section.to_ron)(section.value.as_ref()) } /// Loads section `name` from a RON blob, replacing its value. Returns `false` /// if the section is not registered or the text fails to parse. pub fn load_section(&mut self, name: &str, ron: &str) -> bool { match self.sections.get_mut(name) { Some(section) => match (section.from_ron)(ron) { Some(value) => { section.value = value; true } None => false, }, None => false, } } /// Serializes every section to a `name → RON` map (the format a /// [`Project`](crate::project::Project) stores). pub fn export(&self) -> BTreeMap { self.sections .iter() .filter_map(|(name, section)| { (section.to_ron)(section.value.as_ref()).map(|ron| (name.to_string(), ron)) }) .collect() } /// Loads every matching, registered section from a `name → RON` map. /// Unknown sections are ignored (a module may be disabled); malformed /// sections are skipped, leaving their current value. pub fn import(&mut self, map: &BTreeMap) { for (name, ron) in map { self.load_section(name, ron); } } } #[cfg(test)] mod tests { use super::*; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default, PartialEq, Debug)] struct EditorPrefs { theme: String, grid: bool, } #[derive(Serialize, Deserialize, PartialEq, Debug)] struct Render { shadows: bool, msaa: u32, } impl Default for Render { fn default() -> Self { Self { shadows: true, msaa: 4, } } } fn settings() -> Settings { let mut s = Settings::new(); s.register::("editor"); s.register::("render"); s } #[test] fn defaults_and_typed_access() { let mut s = settings(); assert_eq!(s.names().collect::>(), vec!["editor", "render"]); assert!(s.get::("render").unwrap().shadows); s.get_mut::("editor").unwrap().theme = "dark".into(); assert_eq!(s.get::("editor").unwrap().theme, "dark"); // Wrong type → None. assert!(s.get::("editor").is_none()); } #[test] fn set_and_reset() { let mut s = settings(); assert!(s.set( "render", Render { shadows: false, msaa: 8 } )); assert_eq!(s.get::("render").unwrap().msaa, 8); // Setting an unregistered section fails. assert!(!s.set("missing", 5u32)); // Reset returns to default. assert!(s.reset("render")); assert_eq!(s.get::("render").unwrap(), &Render::default()); } #[test] fn export_import_round_trips() { let mut s = settings(); s.get_mut::("editor").unwrap().theme = "light".into(); s.get_mut::("editor").unwrap().grid = true; s.set( "render", Render { shadows: false, msaa: 2, }, ); let saved = s.export(); // A fresh store with the same sections restores the saved values. let mut restored = settings(); restored.import(&saved); assert_eq!( restored.get::("editor").unwrap(), &EditorPrefs { theme: "light".into(), grid: true } ); assert_eq!(restored.get::("render").unwrap().msaa, 2); } #[test] fn import_ignores_unknown_and_malformed() { let mut s = settings(); let mut map = BTreeMap::new(); map.insert("editor".to_string(), "(theme:\"x\",grid:true)".to_string()); map.insert("disabled_module".to_string(), "(whatever:1)".to_string()); map.insert("render".to_string(), "not valid ron".to_string()); s.import(&map); // Known + valid applied. assert_eq!(s.get::("editor").unwrap().theme, "x"); // Malformed left the section at its default (unchanged). assert_eq!(s.get::("render").unwrap(), &Render::default()); // Unknown silently ignored. assert!(!s.is_registered("disabled_module")); } }