mirror of
https://github.com/bevyengine/bevy.git
synced 2026-07-06 02:23:17 -04:00
create-pull-request/patch
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a74009b22e | chore: Release | ||
|
|
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> |
||
|
|
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> |
||
|
|
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(()) ``` |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
f05ba5e794 |
Missing license files. (#23806)
# Objective Several crates that I created were missing license files. ## Solution Add the license files |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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?
|
||
|
|
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> |