Commit Graph

14 Commits

Author SHA1 Message Date
Bevy Auto Releaser a74009b22e chore: Release 2026-06-21 16:55:28 +00:00
Carter Anderson a3fa0c2c8f Rename Preferences to Settings in the appropriate places (#24613)
# Objective

`bevy_settings` currently conflates "settings" and "preferences" in a
number of places. The name of the framework itself is Bevy Settings, so
anything that drives general "settings" behaviors should use "settings"
terminology. For example, `PreferencesPlugin` should be `SettingsPlugin`
because it handles _all_ `SettingsGroup` types (regardless of their
"scope" such as "preferences.toml").

## Solution

Use "settings" instead of "preferences" in the appropriate places. I've
also removed the custom `ExitAfterSave` commands in the examples as they
are unnecessary.

This should land in 0.19 because we haven't published this API yet and
getting naming right is important.

---------

Co-authored-by: Dave Waggoner <waggoner.dave@gmail.com>
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
2026-06-17 04:28:56 +00:00
laund e30b5013b4 fix all clippy lints (#24475)
# Objective

Resolve all clippy lints currently warning in one way or another. They
were annoying me while contributing, especially having to find the lints
i caused in the ~50 of them which this PR resolves.

## Solution

Most of them were fixes that were reasonable to apply.

`std_instead_of_core` currently has false hits on `std::io`: because
`core::io` is unstable we don't want to apply those. So ive decided to
disable this lint for now. This was already "allow" in `[lints.clippy]`,
i made it "allow" in `[workspace.lints.clippy]` as well.

## Testing

- [x] `cargo test --all` passes, except for
`error::bevy_error::tests::filtered_backtrace_test` which fails on
`main` in the same way for me
- [x] `bevy_city` example works

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Mike <mike.hsu@gmail.com>
Co-authored-by: Mira <specificprotagonist@posteo.org>
2026-06-02 01:35:04 +00:00
Gino Valente d38a00ff8c bevy_reflect: Allow parameters to be passed to type data (#13723)
# Objective

Addresses
[comments](https://github.com/bevyengine/bevy/pull/7317#issuecomment-2143075852)
regarding #7317 (note that this doesn't replace #7317, there are still
some great improvements there besides this syntactical problem).

There currently exist some "special" type data registrations that can be
registered like other type data (e.g. `#[reflect(Hash)]`) or can use a
"special" syntax to allow specifying custom implementations (e.g.
`#[reflect(Hash(custom_hash_fn))]`). And there may be more to follow
(#13432).

What's interesting is that most of these special cased registrations
don't actually come with any type data type. Instead, they simply modify
methods on `Reflect` (e.g. `Reflect::reflect_hash`).

#7317 sought to distinguish between these "special" registrations by
making them lowercase and use a more conventional attribute style:
`#[reflect(hash = "custom_hash_fn")]`.

However, while this did help distinguish these registrations and make
them a bit prettier, they now require the user to actually know which
traits are "special" and which are not (as pointed out
[here](https://github.com/bevyengine/bevy/pull/7317#issuecomment-2143075852)).

Ideally, users shouldn't have to know which traits are "special" until
they need to. For most users, they should just know that they need to
register their trait in order for certain things to work. And the
special-casing may be easier to follow if we open up the configuration
abilities to _all_ type data.

## Solution

This PR introduces `CreateTypeData` which replaces `FromType`. This was
done for two reasons.

Firstly, `FromType` isn't very descriptive as to what it should be used
for. We are creating type data from a type, but it's not immediately
clear this is even for type data. Renaming to `CreateTypeData` should
hopefully make this much clearer.

Secondly, in order to support type data with parameters like the
`custom_hash_fn` in `reflect(Hash(custom_hash_fn))`, an additional
`Input` type parameter had to be added. This makes the new signature
`CreateTypeData<T, Input = ()>`.

We can now create type data that accepts input!

```rust
trait Combine {
  fn combine(a: f32, b: f32) -> f32;
}

#[derive(Clone)]
struct ReflectCombine {
  multiplier: f32,
  additional: f32,
  combine: fn(f32, f32) -> f32,
}

impl ReflectCombine {
  pub fn combine(&self, a: f32, b: f32) -> f32 {
    let combined = (self.combine)(a, b);
    let multiplied = self.multiplier * combined;
    multiplied + self.additional
  }
}

impl<T: Combine + Reflect> CreateTypeData<T, (f32, f32)> for ReflectCombine {
  fn create_type_data(input: (f32, f32)) -> Self {
    Self {
      multiplier: input.0,
      additional: input.1,
      combine: T::combine,
    }
  }
}
```

And then register them with the special function-like syntax:

```rust
#[derive(Reflect)]
#[reflect(Combine(2.0, 4.0))]
struct Foo;
```

The above code will compile into the following registration:

```rust
registration.insert(<ReflectCombine as CreateTypeData<Self, _>>::create_type_data((2.0, 4.0)))
```

Notice how the macro automatically generates the tuple for us, so we
don't have to add an additional layer of parentheses.

### Multiple Input Types

You might be wondering why we're using a type parameter instead of an
associated type to specify the input type.

An associated type would limit us to a single implementation. This means
that if we want to support the type data with optional parameters (e.g.
support both `Hash` and `Hash(custom_hash_fn)`), then all type data must
take in `Option<Self::Input>`, regardless of whether or not a `None`
case is supported.

This is important because the macro has to be pass in _something_,
whether that be `()` or `None`.

By using a type parameter we open the door to type data with required
input:

```rust
// `ReflectMyTrait` must be registered with input
impl<T> CreateTypeData<T, u32> for ReflectMyTrait {
  fn create_type_data(input: u32) -> Self {
    Self {
      value: input,
    }
  }
}

// And we can support all different input types
impl<T> CreateTypeData<T, i32> for ReflectMyTrait {
  fn create_type_data(input: i32) -> Self {
    Self {
      value: input.abs() as u32,
    }
  }
}
```

However, this may be something we don't necessarily care about since
users could also get away with this using custom input enums. And the
required-input case could be deferred until runtime (i.e. maybe a panic
in the `None` case).

### Adding `ReflectPartialEq` and `ReflectHash`

I had originally considered adding `ReflectPartialEq` and `ReflectHash`
type data to further decrease the differences between the "special"
registrations and the regular ones. However, I chose not to do that to
(1) reduce the complexity of this PR and (2) we may end up removing
these entirely due to #8695.

### What else is this good for?

Another question you might have is what else this is good for beyond
just making things a bit more consistent.

I'm not sure exactly how the community will use it, but I can see it
being used for things like feature gating certain functionality:

```rust
#[derive(Reflect)]
#[cfg_attr(feature = "debug", reflect(MyTrait(true)))]
#[cfg_attr(not(feature = "debug"), reflect(MyTrait(false)))]
struct Foo;
```

Or to emulate specialization via reflection:

```rust
impl<T> DoSomething for T {
  fn do_something(&self) {
    println!("Doing the same old stuff.");
  }
}

#[derive(Reflect)]
#[reflect(ReflectDoSomething(|_| {
  println!("Doing something special!");
}))]
struct Foo;
```

Note that all of the above could always be done with manual
registration. However, due to them requiring input, some cases could
_only_ be done with manual registration.

This PR mainly opens the door to doing more of this interesting stuff
with type data via the macro registration. It not only unifies "special"
and regular registrations, but also manual and automatic registrations.

## Testing

The tests for this feature are split into doctests (for the docs on
`CreateTypeData`) and in the compile-fail tests.

These will both be verified automatically by CI.

---

## Changelog

- Replaced `FromType<T>` with `CreateTypeData<T, Input = ()>`
- Type data may now opt-in to accepting input during creation using the
`#[reflect(MyTrait(...))]` syntax
- Added `TypeRegistry::register_type_data_with` method

## Migration Guide

`FromType<T>` has been replaced by `CreateTypeData<T, Input = ()>`.
Implementors of `FromType<T>` will need to update their implementation:

```rust
// BEFORE
impl<T> FromType<T> for ReflectMyTrait {
  fn from_type() -> Self {
    // ...
  }
}

// AFTER
impl<T> CreateTypeData<T> for ReflectMyTrait {
  fn create_type_data(input: ()) -> Self {
    // ...
  }
}
```

Additionally, any calls made to `FromType::from_type` will need to be
updated as well:

```rust
// BEFORE
<ReflectMyTrait as FromType<Foo>>::from_type()

// AFTER
<ReflectMyTrait as CreateTypeData<Foo>>::create_type_data(())
```
2026-05-21 09:49:51 +00:00
François Mockers 549f387ee3 rename bevy_settings to bevy-settings (#24317)
# Objective

- Fix #24282
- Fix #24279

## Solution

- Rename bevy_settings to bevy-settings, but keep the feature exposed as
bevy_settings

## Testing

- CI
2026-05-20 19:42:17 +00:00
François Mockers d38c6afbd3 bevy_settings: wasm clippy (#24195)
# Objective

- `cargo clippy --target wasm32-unknown-unknown -p bevy_settings
--no-deps -- -D warnings`
- Complains about `clippy::needless_borrow`

## Solution

- Fix it

## Testing

- CI
2026-05-08 13:25:07 +00:00
Mat 695c8e0481 Bevy settings support tuple structs and non-unit like enums (#23812)
# Objective

Implements part of https://github.com/bevyengine/bevy/issues/23302

For background on bevy settings see the initial PR:
https://github.com/bevyengine/bevy/pull/23034

## Solution
As we invoke the serde toml serializer:
Single field tuple structs are serialized as single values:
```rust
#[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
#[reflect(Resource, SettingsGroup, Default)]
struct SingleFieldTupleStruct(u8);
```
Results in:
```toml
[single_field_tuple_struct]
single_field_tuple_struct = 0
```

Multi-field tuple structs are serialized as arrays of values, with
nested structs serialized as inline tables:
```rust
#[derive(Reflect, PartialEq, Debug, Default)]
#[reflect(Default)]
struct NestedStruct {
    a: u8,
    b: u16,
}

#[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
#[reflect(Resource, SettingsGroup, Default)]
struct MultiFieldTupleStruct(u8, NestedStruct);
```
Results in:
```toml
[multi_field_tuple_struct]
multi_field_tuple_struct = [0, { a = 0, b = 0 }]
```

Non-unit enums retain the "key" field with it defaulting the resources
name, with it being serialized into the group table:
```rust
#[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
#[reflect(Resource, SettingsGroup, Default)]
enum EnumSingleTupleVariant {
    A(u8),
}

impl Default for EnumSingleTupleVariant {
    fn default() -> Self {
        EnumSingleTupleVariant::A(0)
    }
}
```
Results in:
```toml
[enum_single_tuple_variant.enum_single_tuple_variant]
A = 0
```

The same multi-field tuple serialization occurs in enums:
```rust
#[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
#[reflect(Resource, SettingsGroup, Default)]
enum EnumMultiNewTypeVariant {
    A(SingleFieldTupleStruct, MultiFieldTupleStruct),
}

impl Default for EnumMultiNewTypeVariant {
    fn default() -> Self {
        EnumMultiNewTypeVariant::A(
            SingleFieldTupleStruct(0),
            MultiFieldTupleStruct(0, NestedStruct { a: 0, b: 0 }),
        )
    }
}
```
Results in:
```toml
[enum_multi_new_type_variant.enum_multi_new_type_variant]
A = [0, [0, { a = 0, b = 0 }]]
```

Enums variants with fields are serialised with field entries:
```rust
#[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
#[reflect(Resource, SettingsGroup, Default)]
enum EnumStructVariant {
    A { x: u8, y: u16 },
}

impl Default for EnumStructVariant {
    fn default() -> Self {
        EnumStructVariant::A { x: 0, y: 0 }
    }
}
```
Results in:
```toml
[enum_struct_variant.enum_struct_variant.A]
x = 0
y = 0
```

## Notes
- I'm not 100% sold on the `[group_name.key_name]` for non-unit like
enums, its easy enough to not include the key for these types but i'll
keep in for the sake of discussion.
- Still not supporting Unions.
https://github.com/bevyengine/bevy/issues/23302 did not mention them but
I can add support for these in this PR or we can just not support them
at all. Open to discussion.

## Testing

- I've added to the above examples to the bevy settings test suite.
- I've ran the `persisting_preferences` example and cross referenced the
toml output in the file.
2026-04-15 16:23:47 +00:00
Talin f05ba5e794 Missing license files. (#23806)
# Objective

Several crates that I created were missing license files.

## Solution

Add the license files
2026-04-14 20:00:15 +00:00
Mat c040d76030 Enum support in SettingsGroup derive macro, implemented key attribute (#23719)
# Objective

Implements part of https://github.com/bevyengine/bevy/issues/23302

Fixes: https://github.com/bevyengine/bevy/issues/23722

For background on bevy settings see the initial PR:
https://github.com/bevyengine/bevy/pull/23034

## Solution

Supports `unit-like` Enum resources:
```rust
#[derive(Resource, SettingsGroup, Reflect, Debug, Default)]
#[reflect(Resource, SettingsGroup, Default)]
enum CounterRefreshRateSettings {
    #[default]
    Slow,
    Fast,
}
```

Assuming the above is initialised as `CounterRefreshRateSettings::Slow`,
results in:
```toml
[counter_refresh_rate_settings]
counter_refresh_rate_settings = "Slow"
```

Setting the `group` attribute works the same as structs:
```rust
#[settings_group(group = "counter_settings")]
```
Results in:
```toml
[counter_settings]
counter_refresh_rate_settings = "Slow"
```

This PR adds a `key` attribute which can only be used on enums (for
now), otherwise a compile-time error is thrown:
```rust
#[settings_group(key = "refresh_rate")]
```
Results in:
```toml
[counter_refresh_rate_settings]
refresh_rate = "Slow"
```

## Testing
- Added tests that checks merging enums into groups and round trip
serialisation.
- Added a "cheat sheet" for the derive syntax, immitating other derive
macros in the file.

---------

Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2026-04-13 01:19:31 +00:00
Kevin Chen d15ada7211 fix(settings wasm): fix compilation errors / match store_fs fn signatures (#23779)
# Objective

- Unblock #23719 - The PR revealed that wasm builds are currently broken
for `bevy_settings` when trying to merge to main.
- On main, if you try to build an example that requires `bevy_settings`
for wasm targets, it fails with the same compilation error (i.e. `cargo
build --release --example persisting_preferences --target
wasm32-unknown-unknown --features=“bevy_settings”`)

## Solution

- Fix up `store_wasm.rs`. The function signatures should match those in
`store_fs.rs`, and remove any references that do not exist.

## Testing

- With the `Cargo.toml` change from #23719, was able to build the wasm
target described in Objective successfully. Then, prepared the wasm
target and served it locally following the examples/README.md
directions. The example works as expected for web interactions.
2026-04-13 00:29:14 +00:00
dependabot[bot] dd90d23b47 Update toml requirement from 0.8.19 to 1.1.0 (#23634)
Updates the requirements on [toml](https://github.com/toml-rs/toml) to
permit the latest version.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/toml-rs/toml/commit/d66e46e2c3f91e1d6d6479c5decea0993c2c76ba"><code>d66e46e</code></a>
chore: Release</li>
<li><a
href="https://github.com/toml-rs/toml/commit/8a05aef303b194e0b6fc07ecddd2968243e9f9ef"><code>8a05aef</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/toml-rs/toml/commit/dae17528484ebfd8c223bff6e34e2fb2df84d0bf"><code>dae1752</code></a>
chore: Bump to Edition 2024 (<a
href="https://redirect.github.com/toml-rs/toml/issues/1124">#1124</a>)</li>
<li><a
href="https://github.com/toml-rs/toml/commit/88aaa9ceec8d3dd71333f2a54b0c10ed175c2ecc"><code>88aaa9c</code></a>
chore: Bump to Edition 2024</li>
<li><a
href="https://github.com/toml-rs/toml/commit/35ae47fb75ed61950370353c2782474b6ea78ba3"><code>35ae47f</code></a>
refactor(bench): Rename away from 'gen'</li>
<li><a
href="https://github.com/toml-rs/toml/commit/7f439365135f9c833c145b1c64fd6718844af7ac"><code>7f43936</code></a>
style: Remove redundant ref</li>
<li><a
href="https://github.com/toml-rs/toml/commit/24a472a8b1494970a66f085509a2844d5236a5bb"><code>24a472a</code></a>
refactor: Use core::error::Error with MSRV 1.85</li>
<li><a
href="https://github.com/toml-rs/toml/commit/b4c084065e88190b83b9efc60e75da924e7f84f1"><code>b4c0840</code></a>
chore: Bump MSRV to 1.85</li>
<li><a
href="https://github.com/toml-rs/toml/commit/90790723370aa4981bafe054633c928eb78bcf94"><code>9079072</code></a>
chore: Release</li>
<li><a
href="https://github.com/toml-rs/toml/commit/06f2ba38f2377ab01b46c8acc1c4536254c24a50"><code>06f2ba3</code></a>
docs: Update changelog</li>
<li>Additional commits viewable in <a
href="https://github.com/toml-rs/toml/compare/toml-v0.8.19...toml-v1.1.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 21:16:35 +00:00
Chris Russell 6a5ef7388f Change ResourceEntities from SparseSet to SparseArray to speed up resource lookups (#23616)
# Objective

Reduce memory usage for resources, and maybe improve performance of
resource lookups.

Related to #23039, but not a solution.

## Solution

Change `ResourceEntities` from a `SparseSet` to a `SparseArray`.  

`SparseArray` is `pub (crate)`, so simply exposing it through `Deref`
would cause privacy errors. Instead, remove the `Deref` impl and add
wrapper methods for `iter`, `get`, and `remove`. Change the return types
from `&Entity` to `Entity` now that they aren't generic.

As background: A `SparseArray` is a simple `Vec<Option<V>>`, while a
`SparseSet` is a `SparseArray` that maps keys to dense indexes, combined
with dense arrays of keys and values. That requires a second array
operation to find the actual value, but can be much better for memory
usage when the values are large, since missing items only take up space
for a single index instead of an entire value.

But the values in `ResourceEntities` are `Entity`, which are already
small! A `SparseArray` will always be smaller on 64-bit systems, since
an `Entity` is the same size as a `usize`, and we don't need to store
the additional `dense` and `indices` arrays. So switching to
`SparseArray` will save a lookup *and* save memory.

One drawback is that we can no longer use the dense lists to iterate all
resources, so methods like `iter_resources` now need to scan all
component ids. I don't expect this to be a problem in practice, though.
`iter_resources` is rarely used, and O(components) isn't all that much
worse than O(resources). If it turns out to be an issue, it's also
possible to recover this data by querying the `IsResource` component.

## Testing

Inconclusive.  

I attempted to run benchmarks, both `bevymark` as in the linked issue
and `cargo bench -p benches --bench ecs`, but the results were too noisy
on my machine to reach any conclusions. And now that I look more
closely, we don't have many benches that even use resources!

---------

Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
2026-04-08 15:40:20 +00:00
Christian Hughes 4b09a461e9 Simplify Command error handling traits (#23432)
# Objective

I have working `Command` reflection for my game:

```rust
#[derive(Clone)]
pub struct ReflectCommand {
    pub apply: fn(&mut World, &dyn PartialReflect, &TypeRegistry),
}

impl ReflectCommand {
    pub fn apply(&self, world: &mut World, command: &dyn PartialReflect, registry: &TypeRegistry) {
        (self.apply)(world, command, registry);
    }
}

impl<C: Command<Result> + Reflect + TypePath> FromType<C> for ReflectCommand {
    fn from_type() -> Self {
        ReflectCommand {
            apply: |world, command, registry| {
                let command = from_reflect_with_fallback::<C>(command, world, registry);
                command.apply(world);
            },
        }
    }
}
```

However, I am currently only allowed to support a single output type
across *all* command types (which I've chosen to be `Result` for the
time being). This is because, by virtue of `Out` being a generic
parameter, `Command` *can* be implemented multiple times for the same
type, but with different output types. In order for my command
reflection logic to support command types with *any* output type, I need
the ability to guarantee that `Command` will only be implemented once
for some `FooCommand`.

That's why `Out` should be changed into an associated type.

## Solution

- Turned the `Out` generic parameter into an associated type on both
`Command` and `EntityCommand`.
- Bounded `Command::Out` associated type with a new `CommandOutput`
trait.
- This replaces the functionality of the now removed `HandleError`
trait, and allows us to add its functions directly on the `Command`
trait.
- Also bounded `EntityCommand::Out` associated type with the new
`CommandOutput` trait.
- This replaces the functionality of the now removed `CommandWithEntity`
trait, and allows us to add its functions directly on the
`EntityCommand` trait.

Additionally, the new `CommandOutput` trait gives a place for bevy users
to hook into error handling logic with their own types! It also comes
with `diagnostic::on_unimplemented` diagnostics!

## Testing

Maybe TODO: Current tests appear green but should we add tests for the
new `CommandOutput` trait?
2026-03-22 20:09:31 +00:00
Talin 53050f90e2 New bevy_settings crate (#23034)
Yet another attempt at implementing bevy preferences. This version uses
bevy_reflect serialization to convert resources from toml values into
Rust types and vice versa. This is based on the feedback that I got from
the earlier attempt in #22770

To indicate that a resource type should be loaded as preferences, you'll
need to add the `SettingsGroup` annotation:

```rust
#[derive(Resource, SettingsGroup, Reflect, Default)]
#[reflect(Resource, SettingsGroup, Default)]
struct Counter {
    count: i32,
}
```

This will produce a TOML file that looks like this:

```toml
[counter]
count = 3
```

## Theory of Operation

The `PreferencesPlugin` scans the type registry for all resource types
that impl `SettingsGroup` and `Default`. Derive attributes can be used
to write the resource to a different file (or different key in browser
local storage).

`PreferencesPlugin` should be added before other plugins. This ensures
that any other plugins can have access to the settings data during
initialization.

The loader checks to see if the resource already exists; if so, it uses
that resource instance and patches the toml values into it, preserving
any defaults that have been set. If the resource does not exist, it
constructs a new one via `ReflectDefault` before applying the toml
properties.

(There was a suggestion of using `FromWorld` instead of `Default`. This
is worth considering, although there may be issues with calling
`FromWorld` so early in the app initialization lifecycle, before most
resources have been created.)

On `wasm` platforms, this uses browser local storage rather than the
filesystem to store preferences. On platforms which have neither,
preferences are not supported (although it's possible that some
platform-specific settings storage could be implemented).

## Note on terminology

I've tried to consistently use the term "preferences" rather than
"settings" or "config" because those are broader terms. For example, the
`xorg.conf` file, commonly used to configure an XWindows display, is
technically a "settings" file, but it is not "preferences". However, for
end users it's perfectly permissible to use the word "Settings" in menus
and navigation elements since that is the term most commonly used in
software today.

## Open Issues

### Syncing with non-resources

Some important settings are not stored in resources: one of the most
common things that users will want to preserve is the window position
and size, which exist on the window entity. It's not possible, under my
design, to store arbitrary entities as preferences, so in order for the
window properties to be saved they will have to be copied to a resource
before being serialized. We probably don't want to be continually
copying the window size every time the window is dragged or moved, so
we'll need some way to know when serialization is about to happen. I'm
thinking that possibly some global event could be triggered just before
serialization, and the handlers could use this event to make last-minute
patches to resources.

## Saving If Changed

Because saving involves i/o, we want to only save when preferences have
actually changed. This involves two discrete checks:

* Whether a save operation needs to be done at all
* Which files need to be saved

The reason for these two steps is that even checking which files need to
be saved is non-trivial and probably should not be done every frame.

Rather than check the `is_changed()` field of every preference resource
every frame, the code currently relies on the user to issue an explicit
`Command` whenever they change a preferences property. This gets
especially tricky if the settings to be saved aren't actually in a
resource, like the aforementioned window position.

There are two forms of the command: `SavePreferences` and
`SavePreferencesSync`. The former, which uses an i/o task, is the
preferred approach, unless the app is about to exit, in which case the
sync version is preferred.

Once we know that a save will take place, a second pass can be used to
check the timestamp on every resource: if any resource has a tick value
later than the last time the file was either loaded or saved, then we
know that file is out of date.

Also some properties can change at high frequency - for example,
dragging the master volume slider changes the volume every frame. For
this reason, we will generally want to put in a delay / debounce logic
to batch updated together (not present in this PR). However, this delay
means that if the user adjusts the setting and then immediately
terminates the app, the setting won't be recorded. (There is no chance
of the file being corrupted, as it uses standard practices for ensuring
file integrity.)

Unfortunately, on some platforms, depending on how the user chooses to
quit (Command-Q on Mac) there's no opportunity to listen for the
`AppExit` event. For this reason, it's best to use a "belt and
suspenders" approach which listens for both `AppExit` and autosave timer
events.

Fixes #23172
Fixes #13311

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
2026-03-10 20:29:09 +00:00