Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ bevy_input_focus = ["bevy_internal/bevy_input_focus"]
# Headless widget collection for Bevy UI.
bevy_ui_widgets = ["bevy_internal/bevy_ui_widgets"]

# Load and save user preferences
bevy_settings = ["bevy_internal/bevy_settings"]

# Feathers widget collection.
experimental_bevy_feathers = ["bevy_internal/bevy_feathers", "bevy_ui_widgets"]

Expand Down Expand Up @@ -5147,6 +5150,30 @@ description = "Demonstrates use of core scrollbar in Bevy UI"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "prefs_counter"
path = "examples/app/prefs_counter.rs"
doc-scrape-examples = true
required-features = ["bevy_settings"]

[package.metadata.example.prefs_counter]
name = "User Preferences"
description = "Demonstrates persistence of user preferences"
category = "Application"
wasm = true

[[example]]
name = "prefs_window"
path = "examples/app/prefs_window.rs"
doc-scrape-examples = true
required-features = ["bevy_settings"]

[package.metadata.example.prefs_window]
name = "Save Window Position"
description = "Demonstrates saving window position in preferences"
category = "Application"
wasm = false

[[example]]
name = "system_fonts"
path = "examples/ui/text/system_fonts.rs"
Expand Down
60 changes: 59 additions & 1 deletion crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ use crate::{
component::map_entities, query_data::derive_query_data_impl,
query_filter::derive_query_filter_impl,
};
use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest};
use bevy_macro_utils::{
derive_label, ensure_no_collision, get_struct_fields, pascal_to_snake_case, BevyManifest,
};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{format_ident, quote, ToTokens};
Expand Down Expand Up @@ -567,6 +569,62 @@ pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
}

/// Implement the `SettingsGroup` trait.
#[proc_macro_derive(SettingsGroup, attributes(settings_group))]
pub fn derive_settings_group(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

let name = &input.ident;

let (override_name, override_file) = {
let mut override_name: Option<String> = None;
let mut override_file: Option<String> = None;

input
.attrs
.iter()
.find(|attr| attr.path().is_ident("settings_group"))
.and_then(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("group") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
override_name = Some(s.value());
Ok(())
} else if meta.path.is_ident("file") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
override_file = Some(s.value());
Ok(())
} else {
Err(meta.error("unsupported attribute"))
}
})
.ok()
});

(override_name, override_file)
};

let group_name = override_name.unwrap_or(pascal_to_snake_case(&name.to_string()));
let file_name = override_file
.map(|f| quote! { Some(#f) })
.unwrap_or(quote! { None });

let expanded = quote! {
impl SettingsGroup for #name {
fn settings_group_name() -> &'static str {
#group_name
}
fn settings_source() -> Option<&'static str> {
#file_name
}
}
};

TokenStream::from(expanded)
}

/// Cheat sheet for derive syntax,
/// see full explanation and examples on the `Component` trait doc.
///
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0
] }
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.19.0-dev" }
bevy_picking = { path = "../bevy_picking", optional = true, version = "0.19.0-dev" }
bevy_settings = { path = "../bevy_settings", optional = true, version = "0.19.0-dev" }
bevy_remote = { path = "../bevy_remote", optional = true, version = "0.19.0-dev" }
bevy_render = { path = "../bevy_render", optional = true, version = "0.19.0-dev" }
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.19.0-dev" }
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_internal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub use bevy_remote as remote;
pub use bevy_render as render;
#[cfg(feature = "bevy_scene")]
pub use bevy_scene as scene;
#[cfg(feature = "bevy_settings")]
pub use bevy_settings as settings;
#[cfg(feature = "bevy_shader")]
pub use bevy_shader as shader;
#[cfg(feature = "bevy_solari")]
Expand Down
33 changes: 33 additions & 0 deletions crates/bevy_macro_utils/src/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,36 @@ pub fn derive_label(
}
.into()
}

/// Convert a string from ``PascalCase`` to ``snake_case``.
pub fn pascal_to_snake_case(s: &str) -> String {
let mut out = String::new();
let chars: Vec<char> = s.chars().collect();

for (i, &ch) in chars.iter().enumerate() {
if ch.is_uppercase() {
let prev_is_lower = i > 0 && chars[i - 1].is_lowercase();
let next_is_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());

if i > 0 && (prev_is_lower || next_is_lower) {
out.push('_');
}
out.push(ch.to_lowercase().next().unwrap());
} else {
out.push(ch);
}
}

out
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_pascal_to_snake_case() {
assert_eq!(pascal_to_snake_case("PascalCase"), "pascal_case");
assert_eq!(pascal_to_snake_case("lowercase"), "lowercase");
assert_eq!(pascal_to_snake_case("HTTPServer"), "http_server");
}
}
40 changes: 40 additions & 0 deletions crates/bevy_settings/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "bevy_settings"
version = "0.19.0-dev"
edition = "2024"
description = "User settings framework for Bevy Engine"
homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.19.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.19.0-dev" }
bevy_ecs_macros = { path = "../bevy_ecs/macros", version = "0.19.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.19.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.19.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.19.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.19.0-dev" }

serde = "1.0.217"
thiserror = "2.0.18"
toml = { version = "0.8.19" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", default-features = false, features = [
"Window",
"Storage",
] }

[features]
default = []

[lints]
workspace = true

[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
all-features = true
Loading