diff --git a/Cargo.toml b/Cargo.toml index 55061642..ec49f284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,8 @@ bevy = { git = "https://github.com/bevyengine/bevy.git", rev = "89d094e50f10fc56 bevy_derive = { git = "https://github.com/bevyengine/bevy.git", rev = "89d094e50f10fc56ec3c4b046c830c650f9f09d5" } thiserror = "1" serde = { version = "1", features = ["derive"] } +tracing-test = "0.2.5" +tracing = "0.1.40" atomicow = "1.0.0" rfd = "0.15" ron = "0.8.1" @@ -71,3 +73,4 @@ bevy_transform_gizmos = { path = "crates/bevy_transform_gizmos" } bevy_undo = { path = "crates/bevy_undo" } bevy_infinite_grid = { path = "crates/bevy_infinite_grid" } bevy_editor_cam = { path = "crates/bevy_editor_cam" } + diff --git a/crates/bevy_editor_settings/Bevy.toml b/crates/bevy_editor_settings/Bevy.toml new file mode 100644 index 00000000..8653be26 --- /dev/null +++ b/crates/bevy_editor_settings/Bevy.toml @@ -0,0 +1,40 @@ + +# basic struct +[basic_settings] +name = "bevy_editor_settings" + + +# list with replace +[list_testing] +list = ["three", "four"] + +# list with append +[list_testing_append] +list = [3, 4] + +# top level enum +[enum_testing] +variant = "Two" + +# enum in a struct +# and enum tuple +[enum_settings] +test1 = {Tuple = ["hello", 42]} + +# a struct enum on a struct +[enum_settings.test2.Struct] +name = "four" +age = 4 + +# a struct with a list of enums +[enum_settings_list] +settings = ["Three"] + +# top level tuple struct +# adds the fields field to the tuple struct. this is ONLY done for a top level tuple struct +[tuple_struct] +fields = [2, "two"] + +# tuple struct in a struct +[struct_with_tuple] +tuple = [3, "three"] \ No newline at end of file diff --git a/crates/bevy_editor_settings/Cargo.toml b/crates/bevy_editor_settings/Cargo.toml index 0badc32c..78a0230f 100644 --- a/crates/bevy_editor_settings/Cargo.toml +++ b/crates/bevy_editor_settings/Cargo.toml @@ -3,19 +3,17 @@ name = "bevy_editor_settings" version = "0.1.0" edition = "2021" -[features] -default = [] -schema = ["schemars"] [dependencies] bevy.workspace = true -serde.workspace = true thiserror.workspace = true toml = "0.8.19" directories = "5.0.1" +heck = "0.5.0" -# used for generating a json schema which can be used with toml -schemars = { version = "0.8.21", features = ["semver"], optional = true} +[dev-dependencies] +tracing.workspace = true +tracing-test.workspace = true [lints] workspace = true diff --git a/crates/bevy_editor_settings/src/file_system/de/array.rs b/crates/bevy_editor_settings/src/file_system/de/array.rs new file mode 100644 index 00000000..9610a5e3 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/array.rs @@ -0,0 +1,63 @@ +use bevy::{ + prelude::warn, + reflect::{Array, ArrayInfo}, +}; + +use super::LoadStructure; + +pub struct LoadArray<'a> { + pub array_info: &'a ArrayInfo, + pub array: &'a mut dyn Array, + pub toml_array: &'a toml::value::Array, +} + +impl LoadArray<'_> { + pub fn load_array(self) { + if self.toml_array.len() != self.array_info.capacity() { + warn!( + "Preferences: Expected Array length {}, got {}", + self.array_info.capacity(), + self.toml_array.len() + ); + return; + } + + for i in 0..self.array_info.capacity() { + let Some(toml_value) = self.toml_array.get(i) else { + continue; + }; + + let field_mut = self.array.get_mut(i).unwrap(); + + LoadStructure { + type_info: field_mut.get_represented_type_info().unwrap(), + table: toml_value, + structure: field_mut, + custom_attributes: None, + } + .load(); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::DynamicTyped as _; + + use super::*; + + #[tracing_test::traced_test] + #[test] + fn load_array() { + let mut array = [0, 0]; + + let toml_value = toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]); + LoadArray { + array_info: array.reflect_type_info().as_array().unwrap(), + toml_array: toml_value.as_array().unwrap(), + array: &mut array, + } + .load_array(); + assert_eq!(array, [1, 2]); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/default.rs b/crates/bevy_editor_settings/src/file_system/de/default.rs new file mode 100644 index 00000000..588e5e58 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/default.rs @@ -0,0 +1,118 @@ +use bevy::reflect::{ + ArrayInfo, DynamicEnum, DynamicList, DynamicMap, DynamicSet, DynamicStruct, DynamicTuple, + EnumInfo, ListInfo, MapInfo, PartialReflect, SetInfo, Type, TypeInfo, +}; + +use super::{struct_utils::StructLikeInfo, tuple_utils::TupleLikeInfo}; + +pub fn default_data_type(type_info: &TypeInfo) -> Option> { + match type_info { + TypeInfo::Opaque(opaque_info) => default_value(opaque_info.ty()), + TypeInfo::Struct(struct_info) => { + default_struct(struct_info).map(|s| Box::new(s) as Box) + } + TypeInfo::TupleStruct(tuple_struct_info) => { + default_tuple(tuple_struct_info).map(|t| Box::new(t) as Box) + } + TypeInfo::Tuple(tuple_info) => { + default_tuple(tuple_info).map(|t| Box::new(t) as Box) + } + TypeInfo::Array(type_info) => { + default_array(type_info).map(|a| Box::new(a) as Box) + } + TypeInfo::List(type_info) => { + default_list(type_info).map(|l| Box::new(l) as Box) + } + TypeInfo::Map(type_info) => { + default_map(type_info).map(|m| Box::new(m) as Box) + } + TypeInfo::Set(type_info) => { + default_set(type_info).map(|s| Box::new(s) as Box) + } + TypeInfo::Enum(type_info) => { + default_enum(type_info).map(|e| Box::new(e) as Box) + } + } +} + +pub fn default_enum(_type_info: &EnumInfo) -> Option { + Some(DynamicEnum::default()) +} + +pub fn default_set(_type_info: &SetInfo) -> Option { + let output = DynamicSet::default(); + Some(output) +} + +pub fn default_map(_type_info: &MapInfo) -> Option { + let output = DynamicMap::default(); + Some(output) +} + +pub fn default_list(_type_info: &ListInfo) -> Option { + let output = DynamicList::default(); + Some(output) +} + +pub fn default_array(_type_info: &ArrayInfo) -> Option { + let output = DynamicList::default(); + Some(output) +} + +pub fn default_value(type_info: &Type) -> Option> { + if type_info.is::() { + Some(Box::new(String::default())) + } else if type_info.is::() { + Some(Box::new(f64::default())) + } else if type_info.is::() { + Some(Box::new(f32::default())) + } else if type_info.is::() { + Some(Box::new(i64::default())) + } else if type_info.is::() { + Some(Box::new(i32::default())) + } else if type_info.is::() { + Some(Box::new(i16::default())) + } else if type_info.is::() { + Some(Box::new(i8::default())) + } else if type_info.is::() { + Some(Box::new(u64::default())) + } else if type_info.is::() { + Some(Box::new(u32::default())) + } else if type_info.is::() { + Some(Box::new(u16::default())) + } else if type_info.is::() { + Some(Box::new(u8::default())) + } else if type_info.is::() { + Some(Box::new(bool::default())) + } else { + None + } +} + +pub fn default_struct(type_info: &S) -> Option { + let mut dyn_struct = DynamicStruct::default(); + // dyn_struct.set_represented_type(type_info); + + for i in 0..type_info.field_len() { + let field_at = type_info.field_at(i).unwrap(); + + let value = default_data_type(field_at.type_info().unwrap())?; + + dyn_struct.insert_boxed(field_at.name(), value); + } + + Some(dyn_struct) +} + +pub fn default_tuple(type_info: &S) -> Option { + let mut tuple = DynamicTuple::default(); + + for i in 0..type_info.field_len() { + let field_at = type_info.field_at(i).unwrap(); + + let value = default_data_type(field_at.type_info().unwrap())?; + tuple.insert_boxed(value); + } + + Some(tuple) +} diff --git a/crates/bevy_editor_settings/src/file_system/de/enums.rs b/crates/bevy_editor_settings/src/file_system/de/enums.rs new file mode 100644 index 00000000..4f11979c --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/enums.rs @@ -0,0 +1,194 @@ +use bevy::{ + prelude::warn, + reflect::{DynamicEnum, DynamicVariant, Enum, EnumInfo, VariantInfo}, +}; + +use super::{structs::LoadStruct, tuple::LoadTuple}; + +pub struct LoadEnum<'a> { + pub enum_info: &'a EnumInfo, + pub toml_value: &'a toml::Value, + pub enm: &'a mut dyn Enum, +} + +impl LoadEnum<'_> { + pub fn load_enum(self) { + match self.toml_value { + toml::Value::String(str_val) => { + if let Some(VariantInfo::Unit(variant)) = self.enum_info.variant(str_val) { + let dyn_enum = DynamicEnum::new(variant.name(), DynamicVariant::Unit); + self.enm.apply(&dyn_enum); + } else { + warn!("Preferences: Unknown variant: {}", str_val); + } + } + toml::Value::Table(table) => { + if let Some(value) = self + .enum_info + .variant_names() + .iter() + .find(|name| table.contains_key(**name)) + { + let variant_info = self.enum_info.variant(value).unwrap(); + let value = table.get(*value).unwrap(); + + match variant_info { + VariantInfo::Unit(variant) => { + let dyn_enum = DynamicEnum::new(variant.name(), DynamicVariant::Unit); + self.enm.apply(&dyn_enum); + } + VariantInfo::Struct(struct_info) => { + let Some(map) = value.as_table() else { + warn!("Preferences: Table"); + return; + }; + + let Some(mut dyn_struct) = super::default::default_struct(struct_info) + else { + warn!("Preferences: Expected Struct"); + return; + }; + + LoadStruct { + struct_info, + table: map, + strct: &mut dyn_struct, + } + .load_struct(); + + let dyn_enum = DynamicEnum::new( + variant_info.name(), + DynamicVariant::Struct(dyn_struct), + ); + self.enm.apply(&dyn_enum); + } + // TODO: handle single field tuple structs differently this could just be a raw value instead of an array + // VariantInfo::Tuple(tuple_variant_info) + // if tuple_variant_info.field_len() == 1 && !value.is_array() => { + // // TODO: This is a hack to support single field tuple structs + // } + VariantInfo::Tuple(tuple_variant_info) => { + let Some(array) = value.as_array() else { + warn!("Preferences: Expected Array"); + return; + }; + + let Some(mut dyn_tuple) = + super::default::default_tuple(tuple_variant_info) + else { + warn!("Preferences: Expected TupleStruct"); + return; + }; + + LoadTuple { + tuple_info: tuple_variant_info, + table: array, + tuple: &mut dyn_tuple, + } + .load_tuple(); + + let dyn_enum = DynamicEnum::new( + variant_info.name(), + DynamicVariant::Tuple(dyn_tuple), + ); + self.enm.apply(&dyn_enum); + } + } + } + } + _ => { + warn!("Preferences: Unsupported type: {:?}", self.toml_value); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy::reflect::{DynamicTyped as _, Reflect}; + + #[derive(Debug, Clone, PartialEq, Reflect, Default)] + enum TestEnum { + #[default] + Variant1, + Variant2(u32), + Variant3 { + name: String, + age: u32, + }, + Variant4(u32, u32), + } + + #[tracing_test::traced_test] + #[test] + fn load_enum_unit() { + let mut enum_test = TestEnum::Variant2(0); + + let toml_value = toml::Value::String("Variant1".to_string()); + LoadEnum { + enum_info: enum_test.reflect_type_info().as_enum().unwrap(), + toml_value: &toml_value, + enm: &mut enum_test, + } + .load_enum(); + + assert_eq!(enum_test, TestEnum::Variant1); + } + + fn enum_test_toml() -> toml::Value { + let mut table = toml::value::Table::new(); + let mut var3 = toml::value::Table::new(); + var3.insert("name".to_string(), toml::Value::String("John".to_string())); + var3.insert("age".to_string(), toml::Value::Integer(10)); + table.insert("Variant3".to_string(), toml::Value::Table(var3)); + toml::Value::Table(table) + } + + #[tracing_test::traced_test] + #[test] + fn load_enum_struct() { + let mut enum_test = TestEnum::default(); + + let toml_value = enum_test_toml(); + LoadEnum { + enum_info: enum_test.reflect_type_info().as_enum().unwrap(), + toml_value: &toml_value, + enm: &mut enum_test, + } + .load_enum(); + + assert_eq!( + enum_test, + TestEnum::Variant3 { + name: "John".to_string(), + age: 10, + } + ); + } + + fn enum_test_tuple_toml() -> toml::Value { + let mut table = toml::value::Table::new(); + table.insert( + "Variant4".to_string(), + toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]), + ); + toml::Value::Table(table) + } + + #[tracing_test::traced_test] + #[test] + fn load_enum_tuple() { + let mut enum_test = TestEnum::default(); + + let toml_value = enum_test_tuple_toml(); + LoadEnum { + enum_info: enum_test.reflect_type_info().as_enum().unwrap(), + toml_value: &toml_value, + enm: &mut enum_test, + } + .load_enum(); + + assert_eq!(enum_test, TestEnum::Variant4(1, 2)); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/list.rs b/crates/bevy_editor_settings/src/file_system/de/list.rs new file mode 100644 index 00000000..8b852bb6 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/list.rs @@ -0,0 +1,110 @@ +use bevy::{ + prelude::warn, + reflect::{attributes::CustomAttributes, List, ListInfo}, +}; + +use crate::MergeStrategy; + +use super::LoadStructure; + +pub struct LoadList<'a> { + pub list_info: &'a ListInfo, + pub list: &'a mut dyn List, + pub toml_array: &'a toml::value::Array, + pub custom_attributes: Option<&'a CustomAttributes>, +} + +impl LoadList<'_> { + pub fn load_list(self) { + let merge_strategy = self + .custom_attributes + .and_then(|attrs| attrs.get::()) + .cloned() + .unwrap_or_default(); + + let Some(item_info) = self.list_info.item_info() else { + warn!("Preferences: Expected List item info"); + return; + }; + + if let MergeStrategy::Replace = merge_strategy { + self.list.drain(); + } + + for toml_value in self.toml_array.iter() { + let Some(mut value) = super::default::default_data_type(item_info) else { + warn!("Unable to create default value for list item"); + return; + }; + + LoadStructure { + type_info: item_info, + table: toml_value, + structure: value.as_mut(), + custom_attributes: None, + } + .load(); + + self.list.push(value); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::{DynamicTyped as _, Reflect}; + + use super::*; + + #[tracing_test::traced_test] + #[test] + fn load_list() { + let mut list: Vec = Vec::new(); + + let toml_value = toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]); + LoadList { + list_info: list.reflect_type_info().as_list().unwrap(), + list: &mut list, + toml_array: toml_value.as_array().unwrap(), + custom_attributes: None, + } + .load_list(); + assert_eq!(list, vec![1, 2]); + } + + #[derive(Debug, Clone, PartialEq, Reflect, Default)] + struct TestMergeStrategy { + #[reflect(@MergeStrategy::Append)] + pub list: Vec, + } + + fn list_test_toml() -> toml::Value { + toml::Value::Array(vec![toml::Value::Integer(3), toml::Value::Integer(4)]) + } + + #[tracing_test::traced_test] + #[test] + fn load_list_with_merge_strategy() { + let mut list = TestMergeStrategy::default(); + list.list.push(1); + list.list.push(2); + + let attrs = list + .reflect_type_info() + .as_struct() + .unwrap() + .field_at(0) + .unwrap() + .custom_attributes(); + + let toml_value = list_test_toml(); + LoadList { + list_info: list.list.reflect_type_info().as_list().unwrap(), + list: &mut list.list, + toml_array: toml_value.as_array().unwrap(), + custom_attributes: Some(attrs), + } + .load_list(); + assert_eq!(list.list, vec![1, 2, 3, 4]); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/map.rs b/crates/bevy_editor_settings/src/file_system/de/map.rs new file mode 100644 index 00000000..6754d685 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/map.rs @@ -0,0 +1,73 @@ +use bevy::{ + prelude::warn, + reflect::{Map, MapInfo}, +}; + +use super::LoadStructure; + +pub struct LoadMap<'a> { + pub map: &'a mut dyn Map, + pub map_info: &'a MapInfo, + pub table: &'a toml::value::Table, +} + +impl LoadMap<'_> { + pub fn load_map(self) { + if !self + .map_info + .key_info() + .map(bevy::reflect::TypeInfo::is::) + .unwrap_or(false) + { + warn!("Preferences: Map key must be a String"); + return; + } + + for (key, toml_value) in self.table.iter() { + let Some(value_info) = self.map_info.value_info() else { + warn!("Preferences: Expected Map value info"); + return; + }; + + let Some(mut value) = super::default::default_data_type(value_info) else { + warn!("Unable to create default value for map item"); + return; + }; + + LoadStructure { + type_info: value_info, + table: toml_value, + structure: value.as_mut(), + custom_attributes: None, + } + .load(); + + self.map.insert_boxed(Box::new(key.clone()), value); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::DynamicTyped as _; + + use super::*; + + #[tracing_test::traced_test] + #[test] + fn load_map() { + let mut map: std::collections::HashMap = std::collections::HashMap::new(); + + let mut table = toml::value::Table::default(); + table.insert("key".to_string(), toml::Value::Integer(1)); + + LoadMap { + map_info: map.reflect_type_info().as_map().unwrap(), + map: &mut map, + table: &table, + } + .load_map(); + + assert_eq!(map.get("key"), Some(&1)); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/mod.rs b/crates/bevy_editor_settings/src/file_system/de/mod.rs new file mode 100644 index 00000000..84c27e45 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/mod.rs @@ -0,0 +1,299 @@ +mod array; +mod default; +mod enums; +mod list; +mod map; +mod set; +mod struct_utils; +mod structs; +mod tuple; +mod tuple_struct; +mod tuple_utils; +mod value; + +use array::LoadArray; +use bevy::{ + prelude::*, + reflect::{attributes::CustomAttributes, ReflectFromPtr, ReflectMut, TypeInfo}, +}; +use enums::LoadEnum; +use heck::ToSnakeCase; +use list::LoadList; +use map::LoadMap; +use set::LoadSet; +use structs::LoadStruct; +use tuple::LoadTuple; +use tuple_struct::LoadTupleStruct; +use value::LoadValue; + +use crate::{SettingKey, SettingsType}; + +/// Errors that can occur when loading a TOML file. +#[derive(Debug, thiserror::Error)] +pub enum LoadError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("TOML deserialization error: {0}")] + TomlDe(#[from] toml::de::Error), +} + +/// Load a toml file from the given path +pub fn load_toml_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + let file = std::fs::read_to_string(path)?; + Ok(toml::from_str(&file)?) +} + +pub struct LoadStructure<'a> { + pub type_info: &'static TypeInfo, + pub table: &'a toml::Value, + pub structure: &'a mut dyn PartialReflect, + pub custom_attributes: Option<&'a CustomAttributes>, +} + +impl LoadStructure<'_> { + pub fn load(self) { + match self.type_info { + TypeInfo::Opaque(opaque_info) => { + LoadValue { + value_info: opaque_info.ty(), + toml_value: self.table, + value: self.structure, + } + .load_value(); + } + TypeInfo::Struct(struct_info) => { + if let Some(table) = self.table.as_table() { + let ReflectMut::Struct(strct) = self.structure.reflect_mut() else { + warn!("Preferences: Expected Struct"); + return; + }; + LoadStruct { + struct_info, + table, + strct, + } + .load_struct(); + } + } + TypeInfo::TupleStruct(tuple_struct_info) => { + if let Some(array_value) = self.table.as_array() { + let ReflectMut::TupleStruct(tuple_struct) = self.structure.reflect_mut() else { + warn!("Preferences: Expected TupleStruct"); + return; + }; + LoadTupleStruct { + tuple_struct_info, + table: array_value, + tuple_struct, + } + .load_tuple_struct(); + } + } + TypeInfo::Tuple(tuple_info) => { + if let Some(array_value) = self.table.as_array() { + let ReflectMut::Tuple(tuple) = self.structure.reflect_mut() else { + warn!("Preferences: Expected Tuple"); + return; + }; + LoadTuple { + tuple_info, + table: array_value, + tuple, + } + .load_tuple(); + } + } + TypeInfo::List(list_info) => { + if let Some(array_value) = self.table.as_array() { + let ReflectMut::List(list) = self.structure.reflect_mut() else { + warn!("Preferences: Expected List"); + return; + }; + LoadList { + list_info, + list, + toml_array: array_value, + custom_attributes: self.custom_attributes, + } + .load_list(); + } + } + TypeInfo::Array(array_info) => { + if let Some(array_value) = self.table.as_array() { + let ReflectMut::Array(array) = self.structure.reflect_mut() else { + warn!("Preferences: Expected Array"); + return; + }; + LoadArray { + array_info, + array, + toml_array: array_value, + } + .load_array(); + } + } + TypeInfo::Map(map_info) => { + if let Some(toml_map) = self.table.as_table() { + let ReflectMut::Map(map) = self.structure.reflect_mut() else { + warn!("Preferences: Expected Map"); + return; + }; + LoadMap { + map_info, + map, + table: toml_map, + } + .load_map(); + } + } + TypeInfo::Set(set_info) => { + if let Some(toml_array) = self.table.as_array() { + let ReflectMut::Set(set) = self.structure.reflect_mut() else { + warn!("Preferences: Expected Set"); + return; + }; + LoadSet { + set_info, + set, + toml_array, + } + .load_set(); + } + } + TypeInfo::Enum(enum_info) => { + let ReflectMut::Enum(enm) = self.structure.reflect_mut() else { + warn!("Preferences: Expected Enum"); + return; + }; + + LoadEnum { + enum_info, + enm, + toml_value: self.table, + } + .load_enum(); + } + } + } +} + +pub fn load_preferences(world: &mut World, table: toml::Table, settings_type: SettingsType) { + let registry = world.get_resource::().unwrap().clone(); + // get all resources that + let resources = world + .iter_resources() + .filter_map(|(res, _)| res.type_id().map(|type_id| (type_id, res.id()))) + .collect::>(); + + for (type_id, res_id) in resources { + if let Some(type_reg) = registry.read().get(type_id) { + match type_reg.type_info() { + TypeInfo::Struct(struct_info) => { + let s_type = struct_info.custom_attributes().get::(); + let toml_key = struct_info.custom_attributes().get::(); + if let Some(s_type) = s_type { + if settings_type != *s_type { + continue; + } + let mut ptr = world.get_resource_mut_by_id(res_id).unwrap(); + let reflect_from_ptr = type_reg.data::().unwrap(); + #[allow(unsafe_code)] + let ReflectMut::Struct(strct) = + // SAFETY: `value` is of type `Reflected`, which the `ReflectFromPtr` was created for + unsafe { reflect_from_ptr.as_reflect_mut(ptr.as_mut()) }.reflect_mut() + else { + panic!("Expected Struct"); + }; + + let name = toml_key + .map(|key| key.0.to_string()) + .unwrap_or_else(|| strct.reflect_type_ident().unwrap().to_snake_case()); + + if let Some(table) = table.get(&name).and_then(|v| v.as_table()) { + LoadStruct { + struct_info, + table, + strct, + } + .load_struct(); + } + } + } + TypeInfo::Enum(enum_info) => { + let s_type = enum_info.custom_attributes().get::(); + let toml_key = enum_info.custom_attributes().get::(); + if let Some(s_type) = s_type { + if settings_type != *s_type { + continue; + } + let mut ptr = world.get_resource_mut_by_id(res_id).unwrap(); + let reflect_from_ptr = type_reg.data::().unwrap(); + #[allow(unsafe_code)] + let ReflectMut::Enum(enm) = + // SAFETY: `value` is of type `Reflected`, which the `ReflectFromPtr` was created for + unsafe { reflect_from_ptr.as_reflect_mut(ptr.as_mut()) }.reflect_mut() + else { + panic!("Expected Struct"); + }; + + let name = toml_key + .map(|key| key.0.to_string()) + .unwrap_or_else(|| enm.reflect_type_ident().unwrap().to_snake_case()); + + if let Some(table) = table.get(&name).and_then(|v| v.as_table()) { + if let Some(value) = table.get("variant") { + LoadEnum { + enum_info, + enm, + toml_value: value, + } + .load_enum(); + } + } + } + } + TypeInfo::TupleStruct(tuple_struct_info) => { + let s_type = tuple_struct_info.custom_attributes().get::(); + let toml_key = tuple_struct_info.custom_attributes().get::(); + if let Some(s_type) = s_type { + if settings_type != *s_type { + continue; + } + let mut ptr = world.get_resource_mut_by_id(res_id).unwrap(); + let reflect_from_ptr = type_reg.data::().unwrap(); + #[allow(unsafe_code)] + let ReflectMut::TupleStruct(tuple_struct) = + // SAFETY: `value` is of type `Reflected`, which the `ReflectFromPtr` was created for + unsafe { reflect_from_ptr.as_reflect_mut(ptr.as_mut()) }.reflect_mut() + else { + panic!("Expected TupleStruct"); + }; + + let name = toml_key.map(|key| key.0.to_string()).unwrap_or_else(|| { + tuple_struct.reflect_type_ident().unwrap().to_snake_case() + }); + + if let Some(table) = table.get(&name).and_then(|v| v.as_table()) { + if let Some(array_value) = + table.get("fields").and_then(|v| v.as_array()) + { + LoadTupleStruct { + tuple_struct_info, + table: array_value, + tuple_struct, + } + .load_tuple_struct(); + } + } + } + } + + _ => { + warn!("Preferences: Unsupported type: {:?}", type_reg.type_info()); + } + } + } + // println!("Saving preferences for {:?}", res.name()); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/set.rs b/crates/bevy_editor_settings/src/file_system/de/set.rs new file mode 100644 index 00000000..2881062d --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/set.rs @@ -0,0 +1,56 @@ +use bevy::reflect::{Set, SetInfo}; + +use super::value::LoadValue; + +pub struct LoadSet<'a> { + pub set: &'a mut dyn Set, + pub set_info: &'a SetInfo, + pub toml_array: &'a toml::value::Array, +} + +impl LoadSet<'_> { + pub fn load_set(self) { + for toml_value in self.toml_array.iter() { + let mut value = super::default::default_value(&self.set_info.value_ty()).unwrap(); + + LoadValue { + value_info: &self.set_info.value_ty(), + toml_value, + value: value.as_mut(), + } + .load_value(); + + self.set.insert_boxed(value); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::{DynamicTyped as _, TypeInfo}; + + use super::*; + + #[tracing_test::traced_test] + #[test] + fn load_set() { + let mut set: std::collections::HashSet = std::collections::HashSet::new(); + + let toml_value = toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]); + + let TypeInfo::Set(set_info) = set.reflect_type_info() else { + panic!("Expected Set TypeInfo"); + }; + + LoadSet { + set_info, + toml_array: toml_value.as_array().unwrap(), + set: &mut set, + } + .load_set(); + + assert_eq!(set.len(), 2); + assert!(set.contains(&1)); + assert!(set.contains(&2)); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/struct_utils.rs b/crates/bevy_editor_settings/src/file_system/de/struct_utils.rs new file mode 100644 index 00000000..00e35f0e --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/struct_utils.rs @@ -0,0 +1,48 @@ +use bevy::reflect::{NamedField, StructInfo, StructVariantInfo}; +use core::slice::Iter; + +/// A helper trait for accessing type information from struct-like types. +pub(super) trait StructLikeInfo { + #[allow(dead_code)] + fn field(&self, name: &str) -> Option<&NamedField>; + fn field_at(&self, index: usize) -> Option<&NamedField>; + fn field_len(&self) -> usize; + #[allow(dead_code)] + fn iter_fields(&self) -> Iter<'_, NamedField>; +} + +impl StructLikeInfo for StructInfo { + fn field(&self, name: &str) -> Option<&NamedField> { + Self::field(self, name) + } + + fn field_at(&self, index: usize) -> Option<&NamedField> { + Self::field_at(self, index) + } + + fn field_len(&self) -> usize { + Self::field_len(self) + } + + fn iter_fields(&self) -> Iter<'_, NamedField> { + self.iter() + } +} + +impl StructLikeInfo for StructVariantInfo { + fn field(&self, name: &str) -> Option<&NamedField> { + Self::field(self, name) + } + + fn field_at(&self, index: usize) -> Option<&NamedField> { + Self::field_at(self, index) + } + + fn field_len(&self) -> usize { + Self::field_len(self) + } + + fn iter_fields(&self) -> Iter<'_, NamedField> { + self.iter() + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/structs.rs b/crates/bevy_editor_settings/src/file_system/de/structs.rs new file mode 100644 index 00000000..0b7295db --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/structs.rs @@ -0,0 +1,126 @@ +use bevy::reflect::Struct; + +use super::{struct_utils::StructLikeInfo, LoadStructure}; + +pub struct LoadStruct<'a> { + pub struct_info: &'a dyn StructLikeInfo, + pub table: &'a toml::Table, + pub strct: &'a mut dyn Struct, +} + +impl LoadStruct<'_> { + pub fn load_struct(self) { + let struct_info = self.struct_info; + let table = self.table; + let strct = self.strct; + for i in 0..struct_info.field_len() { + let field = struct_info.field_at(i).unwrap(); + let key = field.name(); + + let Some(toml_value) = table.get(key) else { + continue; + }; + + let field_mut = strct.field_at_mut(i).unwrap(); + let field_attrs = field.custom_attributes(); + LoadStructure { + type_info: field_mut.get_represented_type_info().unwrap(), + table: toml_value, + structure: field_mut, + custom_attributes: Some(field_attrs), + } + .load(); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::{DynamicTyped, Reflect}; + + use super::*; + + #[derive(Debug, Clone, Reflect, Default, PartialEq)] + struct Values { + pub string: String, + pub float: f64, + pub float32: f32, + } + + fn values_toml() -> toml::value::Table { + let mut table = toml::value::Table::default(); + table.insert( + "string".to_string(), + toml::Value::String("Hello".to_string()), + ); + table.insert( + "float".to_string(), + toml::Value::Float(std::f64::consts::PI), + ); + table.insert( + "float32".to_string(), + toml::Value::Float(std::f64::consts::PI), + ); + table + } + + #[tracing_test::traced_test] + #[test] + fn load_struct_basic_values() { + let mut struct_info = Values::default(); + let table = values_toml(); + + LoadStruct { + struct_info: struct_info.reflect_type_info().as_struct().unwrap(), + table: &table, + strct: &mut struct_info, + } + .load_struct(); + + assert_eq!( + struct_info, + Values { + string: "Hello".to_string(), + float: std::f64::consts::PI, + float32: std::f32::consts::PI, + } + ); + } + + #[derive(Debug, Clone, Reflect, Default, PartialEq)] + struct StructWithStruct { + values: Values, + } + + fn load_struct_with_struct_toml() -> toml::value::Table { + let mut table = toml::value::Table::default(); + table.insert("values".to_string(), toml::Value::Table(values_toml())); + table + } + + #[tracing_test::traced_test] + #[test] + fn load_struct_with_struct() { + let mut struct_info = StructWithStruct::default(); + + let table = load_struct_with_struct_toml(); + + LoadStruct { + struct_info: struct_info.reflect_type_info().as_struct().unwrap(), + table: &table, + strct: &mut struct_info, + } + .load_struct(); + + assert_eq!( + struct_info, + StructWithStruct { + values: Values { + string: "Hello".to_string(), + float: std::f64::consts::PI, + float32: std::f32::consts::PI, + }, + } + ); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/tuple.rs b/crates/bevy_editor_settings/src/file_system/de/tuple.rs new file mode 100644 index 00000000..13b0ca32 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/tuple.rs @@ -0,0 +1,76 @@ +use bevy::reflect::Tuple; + +use super::{tuple_utils::TupleLikeInfo, LoadStructure}; + +pub struct LoadTuple<'a> { + pub tuple_info: &'a dyn TupleLikeInfo, + pub table: &'a toml::value::Array, + pub tuple: &'a mut dyn Tuple, +} + +impl LoadTuple<'_> { + pub fn load_tuple(self) { + for i in 0..self.tuple_info.field_len() { + let Some(toml_value) = self.table.get(i) else { + continue; + }; + + let field_mut = self.tuple.field_mut(i).unwrap(); + let field_attrs = self.tuple_info.field_at(i).unwrap().custom_attributes(); + + LoadStructure { + type_info: field_mut.get_represented_type_info().unwrap(), + table: toml_value, + structure: field_mut, + custom_attributes: Some(field_attrs), + } + .load(); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::DynamicTyped as _; + + use super::*; + + fn tuple_test_toml() -> toml::Value { + toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]) + } + + #[tracing_test::traced_test] + #[test] + fn load_tuple() { + let mut tuple = (0, 0); + + let toml_value = tuple_test_toml(); + LoadTuple { + tuple_info: tuple.reflect_type_info().as_tuple().unwrap(), + table: toml_value.as_array().unwrap(), + tuple: &mut tuple, + } + .load_tuple(); + assert_eq!(tuple, (1, 2)); + } + + fn tuple_struct_struct_toml() -> toml::Value { + toml::Value::Array(vec![tuple_test_toml(), tuple_test_toml()]) + } + + #[tracing_test::traced_test] + #[test] + fn load_tuple_struct_struct() { + let mut tuple = ((0, 0), (0, 0)); + + let toml_value = tuple_struct_struct_toml(); + LoadTuple { + tuple_info: tuple.reflect_type_info().as_tuple().unwrap(), + table: toml_value.as_array().unwrap(), + tuple: &mut tuple, + } + .load_tuple(); + + assert_eq!(tuple, ((1, 2), (1, 2))); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/tuple_struct.rs b/crates/bevy_editor_settings/src/file_system/de/tuple_struct.rs new file mode 100644 index 00000000..17200f90 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/tuple_struct.rs @@ -0,0 +1,86 @@ +use bevy::reflect::TupleStruct; + +use super::{tuple_utils::TupleLikeInfo, LoadStructure}; + +pub struct LoadTupleStruct<'a> { + pub tuple_struct_info: &'a dyn TupleLikeInfo, + pub table: &'a toml::value::Array, + pub tuple_struct: &'a mut dyn TupleStruct, +} + +impl LoadTupleStruct<'_> { + pub fn load_tuple_struct(self) { + for i in 0..self.tuple_struct_info.field_len() { + let Some(toml_value) = self.table.get(i) else { + continue; + }; + + let field_mut = self.tuple_struct.field_mut(i).unwrap(); + let field_attrs = self + .tuple_struct_info + .field_at(i) + .unwrap() + .custom_attributes(); + + LoadStructure { + type_info: field_mut.get_represented_type_info().unwrap(), + table: toml_value, + structure: field_mut, + custom_attributes: Some(field_attrs), + } + .load(); + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::{DynamicTyped as _, Reflect}; + + use super::*; + + #[derive(Debug, Clone, PartialEq, Reflect, Default)] + struct TupleStructTest(u32, u32); + + fn tuple_struct_test_toml() -> toml::Value { + toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]) + } + + #[tracing_test::traced_test] + #[test] + fn load_tuple_struct() { + let mut tuple_struct = TupleStructTest::default(); + + let toml_value = tuple_struct_test_toml(); + LoadTupleStruct { + tuple_struct_info: tuple_struct.reflect_type_info().as_tuple_struct().unwrap(), + table: toml_value.as_array().unwrap(), + tuple_struct: &mut tuple_struct, + } + .load_tuple_struct(); + assert_eq!(tuple_struct, TupleStructTest(1, 2)); + } + + #[derive(Debug, Clone, PartialEq, Reflect, Default)] + struct TupleStructStruct(TupleStructTest); + + fn tuple_struct_struct_toml() -> toml::Value { + toml::Value::Array(vec![tuple_struct_test_toml()]) + } + + #[tracing_test::traced_test] + #[test] + fn load_tuple_struct_struct() { + let mut tuple_struct = TupleStructStruct::default(); + + let toml_value = tuple_struct_struct_toml(); + LoadTupleStruct { + tuple_struct_info: tuple_struct.reflect_type_info().as_tuple_struct().unwrap(), + table: toml_value.as_array().unwrap(), + tuple_struct: &mut tuple_struct, + } + .load_tuple_struct(); + + assert_eq!(tuple_struct, TupleStructStruct(TupleStructTest(1, 2))); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/tuple_utils.rs b/crates/bevy_editor_settings/src/file_system/de/tuple_utils.rs new file mode 100644 index 00000000..f3b1aa99 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/tuple_utils.rs @@ -0,0 +1,36 @@ +use bevy::reflect::{TupleInfo, TupleStructInfo, TupleVariantInfo, UnnamedField}; + +pub(super) trait TupleLikeInfo { + fn field_at(&self, index: usize) -> Option<&UnnamedField>; + fn field_len(&self) -> usize; +} + +impl TupleLikeInfo for TupleInfo { + fn field_len(&self) -> usize { + Self::field_len(self) + } + + fn field_at(&self, index: usize) -> Option<&UnnamedField> { + Self::field_at(self, index) + } +} + +impl TupleLikeInfo for TupleStructInfo { + fn field_len(&self) -> usize { + Self::field_len(self) + } + + fn field_at(&self, index: usize) -> Option<&UnnamedField> { + Self::field_at(self, index) + } +} + +impl TupleLikeInfo for TupleVariantInfo { + fn field_len(&self) -> usize { + Self::field_len(self) + } + + fn field_at(&self, index: usize) -> Option<&UnnamedField> { + Self::field_at(self, index) + } +} diff --git a/crates/bevy_editor_settings/src/file_system/de/value.rs b/crates/bevy_editor_settings/src/file_system/de/value.rs new file mode 100644 index 00000000..3d6e0979 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/de/value.rs @@ -0,0 +1,286 @@ +use bevy::prelude::warn; +use bevy::reflect::{PartialReflect, Type}; + +pub struct LoadValue<'a> { + pub value_info: &'a Type, + pub toml_value: &'a toml::Value, + pub value: &'a mut dyn PartialReflect, +} + +impl LoadValue<'_> { + pub fn load_value(self) { + let value_info = self.value_info; + match self.toml_value { + toml::Value::String(str_val) => { + if value_info.is::() { + self.value.apply(str_val); + } else { + warn!("Preferences: Expected {:?}, got String", value_info); + } + } + toml::Value::Integer(int_val) => { + if value_info.is::() { + self.value.apply(&(*int_val as f64)); + } else if value_info.is::() { + self.value + .apply(&((*int_val as f64).clamp(f32::MIN as f64, f32::MAX as f64) as f32)); + } else if value_info.is::() { + self.value.apply(int_val); + } else if value_info.is::() { + self.value.apply(&(*int_val as i32)); + } else if value_info.is::() { + self.value.apply(&(*int_val as i16)); + } else if value_info.is::() { + self.value.apply(&(*int_val as i8)); + } else if value_info.is::() { + self.value.apply(&((*int_val).max(0) as u64)); + } else if value_info.is::() { + self.value.apply(&((*int_val).max(0) as u32)); + } else if value_info.is::() { + self.value.apply(&((*int_val).max(0) as u16)); + } else if value_info.is::() { + self.value.apply(&((*int_val).max(0) as u8)); + } else { + warn!("Preferences: Expected {:?}, got Integer", value_info); + } + } + toml::Value::Float(float_val) => { + if value_info.is::() { + self.value.apply(float_val); + } else if value_info.is::() { + self.value + .apply(&(float_val.clamp(f32::MIN as f64, f32::MAX as f64) as f32)); + } else { + warn!("Preferences: Expected {:?}, got Float", value_info); + } + } + toml::Value::Boolean(bool_val) => { + if value_info.is::() { + self.value.apply(bool_val); + } else { + warn!("Preferences: Expected {:?}, got Bool", value_info); + } + } + value => { + warn!("Preferences: Unsupported type: {:?}", value); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy::reflect::DynamicTyped as _; + + use super::*; + + #[tracing_test::traced_test] + #[test] + fn load_str() { + let mut value = "".to_string(); + let toml_value = &toml::Value::String("Hello".to_string()); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, "Hello"); + } + + #[tracing_test::traced_test] + #[test] + fn load_float_f64() { + let mut value = 0.0; + let toml_value = &toml::Value::Float(std::f64::consts::PI); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, std::f64::consts::PI); + } + + #[tracing_test::traced_test] + #[test] + fn load_float_f32() { + let mut value = 0.0_f32; + let toml_value = &toml::Value::Float(std::f64::consts::PI); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, std::f32::consts::PI); + } + + #[tracing_test::traced_test] + #[test] + fn load_bool() { + let mut value = false; + let toml_value = &toml::Value::Boolean(true); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert!(value); + } + + #[tracing_test::traced_test] + #[test] + fn load_float_from_int_f64() { + let mut value = 0.0; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42.0); + } + + #[tracing_test::traced_test] + #[test] + fn load_float_from_int_f32() { + let mut value = 0.0_f32; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42.0); + } + + #[tracing_test::traced_test] + #[test] + fn load_int() { + let mut value = 0; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_u8() { + let mut value = 0_u8; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_u16() { + let mut value = 0_u16; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_u32() { + let mut value = 0_u32; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_u64() { + let mut value = 0_u64; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_i8() { + let mut value = 0_i8; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_i16() { + let mut value = 0_i16; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_i32() { + let mut value = 0_i32; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } + + #[tracing_test::traced_test] + #[test] + fn load_i64() { + let mut value = 0_i64; + let toml_value = &toml::Value::Integer(42); + LoadValue { + value_info: value.reflect_type_info().as_opaque().unwrap().ty(), + toml_value, + value: &mut value, + } + .load_value(); + assert_eq!(value, 42); + } +} diff --git a/crates/bevy_editor_settings/src/file_system/mod.rs b/crates/bevy_editor_settings/src/file_system/mod.rs new file mode 100644 index 00000000..560f7cd8 --- /dev/null +++ b/crates/bevy_editor_settings/src/file_system/mod.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use bevy::log::{error, warn}; +use de::{load_preferences, load_toml_file}; + +pub mod de; + +use crate::{GlobalSettingsPath, SettingsType}; + +const SETTINGS_BASE_DIR: &str = "bevy_editor"; + +pub fn global_settings_path() -> Option { + let path = directories::BaseDirs::new()?; + let config_dir = path.config_dir(); + let path = config_dir.join(SETTINGS_BASE_DIR); + + if !path.exists() { + if let Err(e) = std::fs::create_dir_all(&path) { + error!("Failed to create global settings directory: {}", e); + return None; + } + } + Some(path) +} + +pub fn load_settings(app: &mut bevy::app::App) { + if app.world().get_resource::().is_some() { + load_global_settings(app.world_mut()); + } + load_project_settings(app.world_mut()); +} + +pub fn load_project_settings(world: &mut bevy::prelude::World) { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let Ok(file) = load_toml_file(path.join("Bevy.toml")) else { + warn!("Failed to load project settings"); + return; + }; + + load_preferences(world, file, SettingsType::Project); +} + +pub fn load_global_settings(world: &mut bevy::prelude::World) { + let path = &world.get_resource::().unwrap().0; + let Ok(file) = load_toml_file(path.join("global.toml")) else { + warn!("Failed to load global settings"); + return; + }; + + load_preferences(world, file, SettingsType::Global); +} diff --git a/crates/bevy_editor_settings/src/lib.rs b/crates/bevy_editor_settings/src/lib.rs index f395c495..0fdf291e 100644 --- a/crates/bevy_editor_settings/src/lib.rs +++ b/crates/bevy_editor_settings/src/lib.rs @@ -2,76 +2,255 @@ use bevy::prelude::*; -pub mod modals; -mod persistent; +mod file_system; + +/// Annotation for a type to show which type of settings it belongs to. +#[derive(Debug, Clone, PartialEq, Eq, Reflect)] +pub enum SettingsType { + /// These are settings that are saved in the os user's configuration directory. \ + /// These settings are global to the user and are not tied to a specific project. \ + /// Settings are along the lines of hotkeys etc. + Global, + /// Workspace preferences use the global preferences by default. End users can modify them, customizing their layout, theming and hotkeys. \ + /// The file is created when the user applies changes to their workspace preferences within the editor. \ + /// Workspace preferences can be shared between multiple projects and are not isolated to project folders.* + Workspace, + /// Project preference overrides are empty and stored within the project settings. \ + /// When a project overrides a global/workspace preference, it is no longer possible to change them. \ + /// In order to modify the preference, users must modify the project settings instead. + /// There are two states that overrides can be in: + /// - Inheriting - No override is set. Users can freely change the preference. Users can use what they have set within the global/workspace preferences. + /// - Modified - When an override has been set, users can no longer change the preference without modifying the project settings. You can switch between inheriting and modified at any time without consequence. + Project, +} + +#[derive(Debug, Clone, Reflect, Default)] +/// Annotation for a type to show how to merge lists when loading settings. +/// if not set, the default is to replace the existing list. +pub enum MergeStrategy { + #[default] + /// When Mergeing the list, the new list will replace the existing list. + Replace, + /// When Mergeing the list, the new list will be appended to the existing list. + Append, +} + +#[derive(Debug, Clone, Reflect)] +/// Annotation for a type to add tags to the settings. these tags can be used to filter settings in the editor. +pub struct SettingsTags(pub Vec<&'static str>); + +#[derive(Debug, Clone, Reflect)] +/// Annotation for a type to add what key the setting should be stored under. if not set the snake case of the type name will be used. +pub struct SettingKey(pub &'static str); + +#[derive(Resource)] +/// Store the path for the global preferences directory. +pub struct GlobalSettingsPath(pub std::path::PathBuf); /// A Bevy plugin for editor settings. /// This plugin loads the workspace settings, user settings, and project settings. pub struct EditorSettingsPlugin; -#[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect)] -/// Represents the settings for the editor. -/// This includes workspace settings, user settings, and project settings. -pub struct Settings { - /// Settings for the workspace - pub workspace_settings: Option, - /// Settings for the user - pub user_settings: Option, - /// default project settings used when no workspace or user settings are present for a given setting - pub project_settings: modals::project::ProjectSettings, +impl Plugin for EditorSettingsPlugin { + fn build(&self, app: &mut App) { + match file_system::global_settings_path() { + Some(path) => { + debug!("Global settings path: {:?}", path); + app.insert_resource(GlobalSettingsPath(path)); + } + None => { + warn!("Failed to load global settings"); + } + }; + } + + fn finish(&self, app: &mut App) { + file_system::load_settings(app); + } } -impl Settings { - /// Get the project settings. - /// - /// TODO this needs to do some kind of merging of settings - /// the order of precedence should be from highest to lowest: - /// 1. user settings - /// 2. workspace settings - /// 3. default project settings - pub fn project_settings(&self) -> &modals::project::ProjectSettings { - self.user_settings - .as_ref() - .map(|settings| &settings.project_settings) - .or_else(|| { - self.workspace_settings - .as_ref() - .map(|settings| &settings.editor_settings) - }) - .unwrap_or(&self.project_settings) +#[cfg(test)] +mod tests { + + use super::*; + use tracing_test::traced_test; + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct BasicSettings { + pub name: String, + pub age: u32, } - /// Save the user settings. - pub fn save_user_settings(&self) -> Result<(), persistent::PersistentError> { - if let Some(user_settings) = &self.user_settings { - persistent::save_user_settings(user_settings)?; - Ok(()) - } else { - warn!("No user settings to save."); - Ok(()) - } + #[traced_test] + #[test] + fn basic_test() { + let mut app = App::new(); + + app.register_type::(); + + app.insert_resource(BasicSettings { + name: "John".to_string(), + age: 25, + }); + + file_system::load_project_settings(app.world_mut()); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!(settings.name, "bevy_editor_settings"); + assert_eq!(settings.age, 25); } -} -impl Plugin for EditorSettingsPlugin { - fn build(&self, app: &mut App) { - let workspace_settings = persistent::load_workspace_settings() - .inspect_err(|error| { - error!("Error loading workspace settings: {:?}", error); - }) - .ok(); - let user_settings = persistent::load_user_settings() - .inspect_err(|error| { - error!("Error loading user settings: {:?}", error); - }) - .ok(); - - let project_settings = modals::project::ProjectSettings::default(); - - app.insert_resource(Settings { - workspace_settings, - user_settings, - project_settings, + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct ListTesting { + pub list: Vec, + } + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct ListTestingAppend { + #[reflect(@MergeStrategy::Append)] + pub list: Vec, + } + + #[traced_test] + #[test] + fn test_lists() { + let mut app = App::new(); + + app.register_type::(); + app.register_type::(); + + app.insert_resource(ListTesting { + list: vec!["one".to_string(), "two".to_string()], }); + + app.insert_resource(ListTestingAppend { list: vec![1, 2] }); + + file_system::load_project_settings(app.world_mut()); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!(settings.list, vec!["three".to_string(), "four".to_string()]); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!(settings.list, vec![1, 2, 3, 4]); + } + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + enum EnumTesting { + One, + Two, + Three, + } + + #[derive(Debug, Clone, PartialEq, Eq, Reflect)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + enum EnumTestingField { + Unit, + Tuple(String, i32), + Struct { name: String, age: i32 }, + } + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct EnumSettings { + pub test1: EnumTestingField, + pub test2: EnumTestingField, + } + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct EnumSettingsList { + #[reflect(@MergeStrategy::Append)] + pub settings: Vec, + } + + #[traced_test] + #[test] + fn test_enum() { + let mut app = App::new(); + + app.register_type::(); + app.register_type::(); + app.register_type::(); + + app.insert_resource(EnumTesting::One); + app.insert_resource(EnumSettings { + test1: EnumTestingField::Unit, + test2: EnumTestingField::Unit, + }); + app.insert_resource(EnumSettingsList { + settings: vec![EnumTesting::One, EnumTesting::Two], + }); + + file_system::load_project_settings(app.world_mut()); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!(*settings, EnumTesting::Two); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!( + *settings, + EnumSettings { + test1: EnumTestingField::Tuple("hello".to_string(), 42), + test2: EnumTestingField::Struct { + name: "four".to_string(), + age: 4, + }, + } + ); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!( + settings.settings, + vec![EnumTesting::One, EnumTesting::Two, EnumTesting::Three] + ); + } + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct TupleStruct(i32, String); + + #[derive(Debug, Clone, PartialEq, Eq, Reflect, Resource)] + #[reflect(@SettingsType::Project, @SettingsTags(vec!["basic", "settings", "testing"]))] + struct StructWithTuple { + pub tuple: TupleStruct, + } + + #[traced_test] + #[test] + fn test_tuple_struct() { + let mut app = App::new(); + + app.register_type::(); + app.register_type::(); + + app.insert_resource(TupleStruct(1, "one".to_string())); + app.insert_resource(StructWithTuple { + tuple: TupleStruct(2, "two".to_string()), + }); + + file_system::load_project_settings(app.world_mut()); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!(*settings, TupleStruct(2, "two".to_string())); + + let settings = app.world().get_resource::().unwrap(); + + assert_eq!( + *settings, + StructWithTuple { + tuple: TupleStruct(3, "three".to_string()), + } + ); } } diff --git a/crates/bevy_editor_settings/src/modals/editor.rs b/crates/bevy_editor_settings/src/modals/editor.rs deleted file mode 100644 index 380c886a..00000000 --- a/crates/bevy_editor_settings/src/modals/editor.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Editor settings - -use bevy::reflect::Reflect; -use serde::{Deserialize, Serialize}; - -use super::project::ProjectSettings; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Reflect)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -/// Settings for the editor -pub struct EditorSettings { - /// current project settings - pub project_settings: ProjectSettings, -} diff --git a/crates/bevy_editor_settings/src/modals/mod.rs b/crates/bevy_editor_settings/src/modals/mod.rs deleted file mode 100644 index 63e05ebe..00000000 --- a/crates/bevy_editor_settings/src/modals/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Modals for the editor settings. - -pub mod editor; -pub mod project; -pub mod user; -pub mod workspace; diff --git a/crates/bevy_editor_settings/src/modals/project.rs b/crates/bevy_editor_settings/src/modals/project.rs deleted file mode 100644 index 435be7bd..00000000 --- a/crates/bevy_editor_settings/src/modals/project.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Project settings for the editor - -use bevy::reflect::Reflect; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Reflect)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -/// Settings for the editor -#[serde(default)] -pub struct ProjectSettings { - /// The name of the project - name: String, -} - -impl Default for ProjectSettings { - fn default() -> Self { - Self { - name: "My Project".to_string(), - } - } -} diff --git a/crates/bevy_editor_settings/src/modals/user.rs b/crates/bevy_editor_settings/src/modals/user.rs deleted file mode 100644 index bf290523..00000000 --- a/crates/bevy_editor_settings/src/modals/user.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! this is for the user to override workspace settings - -use bevy::reflect::Reflect; -use serde::{Deserialize, Serialize}; - -use super::project::ProjectSettings; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Reflect)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -/// Settings for the user -pub struct UserSettings { - /// project settings for the user - pub project_settings: ProjectSettings, -} diff --git a/crates/bevy_editor_settings/src/modals/workspace.rs b/crates/bevy_editor_settings/src/modals/workspace.rs deleted file mode 100644 index 397c409a..00000000 --- a/crates/bevy_editor_settings/src/modals/workspace.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Workspace settings - -use bevy::reflect::Reflect; -use serde::{Deserialize, Serialize}; - -use super::project::ProjectSettings; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Reflect)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -/// Settings for the entire workspace -/// This should be in the root of your project -pub struct WorkspaceSettings { - /// Settings for the editor per workspace - pub editor_settings: ProjectSettings, - /// Settings for building the project - pub build: Build, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Reflect)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[serde(default)] -/// Settings for building the project -pub struct Build { - /// The Command for building the project in debug mode - debug: String, - /// The Command for building the project in release mode - release: String, - /// The Command for running the project in debug mode - run_debug: String, - /// The Command for running the project in release mode - run_release: String, -} - -impl Default for Build { - fn default() -> Self { - Self { - debug: "cargo build".to_string(), - release: "cargo build --release".to_string(), - run_debug: "cargo run".to_string(), - run_release: "cargo run --release".to_string(), - } - } -} diff --git a/crates/bevy_editor_settings/src/persistent.rs b/crates/bevy_editor_settings/src/persistent.rs deleted file mode 100644 index 60894f32..00000000 --- a/crates/bevy_editor_settings/src/persistent.rs +++ /dev/null @@ -1,60 +0,0 @@ -/// Load a type implementing `serde::Deserialize` from a TOML file. -pub fn load(path: impl AsRef) -> Result -where - T: serde::de::DeserializeOwned, -{ - let path = path.as_ref(); - let file = std::fs::read_to_string(path).unwrap(); - Ok(toml::from_str(&file)?) -} - -#[inline] -/// TODO: when the editor is an external application this should be moved to the user's configuration directory -fn user_settings_path() -> Result { - Ok(std::env::var("CARGO_MANIFEST_DIR") - .map(std::path::PathBuf::from) - .map_err(|_| PersistentError::WorkspaceConfigDirs)? - .join("user.toml")) -} - -/// Save the user settings to the default location. -pub fn save_user_settings( - settings: &crate::modals::user::UserSettings, -) -> Result<(), PersistentError> { - let path = user_settings_path()?; - let toml_string = toml::to_string(settings)?; - - std::fs::write(path, toml_string)?; - - Ok(()) -} - -/// Load the user settings from the default location. -pub fn load_user_settings() -> Result { - let path = user_settings_path()?; - - load(path) -} - -/// Load the workspace settings from the default location. -pub fn load_workspace_settings( -) -> Result { - let path = std::env::var("CARGO_MANIFEST_DIR") - .map(std::path::PathBuf::from) - .map_err(|_| PersistentError::WorkspaceConfigDirs)?; - - load(path.join("Bevy.toml")) -} - -/// Errors that can occur when loading a TOML file. -#[derive(Debug, thiserror::Error)] -pub enum PersistentError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("TOML deserialization error: {0}")] - TomlDe(#[from] toml::de::Error), - #[error("TOML serialization error: {0}")] - TomlSer(#[from] toml::ser::Error), - #[error("Error reading CARGO_MANIFEST_DIR required for workspace settings")] - WorkspaceConfigDirs, -}