cloudiful-bevy-settings 0.1.2

Reusable Bevy settings runtime for app-defined actions, field keys, and localization context.
Documentation
# cloudiful-bevy-settings

Reusable Bevy settings framework for apps that keep their own action enums,
field keys, localization layer, and UI messages.

## What it provides

- `SettingActionHandler<Action>`: app settings resource contract for validating
  and applying a narrowed settings action
- `RequestedSettingAction<Action>`: adapter trait for app-defined UI/request
  messages
- `SettingFieldSpec<AppAction, FieldKey, TextKey>`: generic field descriptor
  with label metadata and a control definition
- `SettingControlSpec<Action, FieldKey>`: toggle, stepper, and select control
  variants
- `SettingSliderSpec<Action>`: slider metadata for stepper-style controls
- `SettingSelectOption<Action>`: select option descriptor with disabled-state
  support
- `SettingFieldSource<AppAction, Context, FieldKey, TextKey>`: trait for
  producing field specs from app context
- `SettingSystemSet`: shared `ApplyActions` and `SyncUi` schedule sets
- `apply_setting_action(...)`: immediate helper for applying one app action to
  persistent settings
- `change_setting(...)`: message-driven bridge from UI/request messages into
  persistent settings updates
- `register_setting_systems(...)`: helper that orders change systems before UI
  sync systems

## What it does not provide

- app-specific settings resources
- app-specific action enums, field keys, or text-key enums
- widget rendering, button/slider/select UI components, or layout
- persistence bootstrapping beyond using `bevy-persistent`
- localized label assembly helpers or any localization runtime

This crate stays narrow on purpose: your app owns settings data, UI messages,
field identities, and localization. This crate only coordinates schema/action
flow and the system ordering around persistent settings updates.

## Minimal end-to-end example

```rust
use bevy::prelude::*;
use bevy_persistent::{Persistent, StorageFormat};
use cloudiful_bevy_settings::{
    RequestedSettingAction, SettingActionHandler, change_setting,
    register_setting_systems,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy)]
enum AppAction {
    ToggleFullscreen,
    SetScale(f32),
    Ignore,
}

#[derive(Debug, Clone, Copy)]
enum SettingsAction {
    ToggleFullscreen,
    SetScale(f32),
}

impl TryFrom<AppAction> for SettingsAction {
    type Error = ();

    fn try_from(value: AppAction) -> Result<Self, Self::Error> {
        match value {
            AppAction::ToggleFullscreen => Ok(Self::ToggleFullscreen),
            AppAction::SetScale(scale) => Ok(Self::SetScale(scale)),
            AppAction::Ignore => Err(()),
        }
    }
}

#[derive(Resource, Debug, Default, Serialize, Deserialize)]
struct AppSettings {
    fullscreen: bool,
    ui_scale: f32,
}

impl SettingActionHandler<SettingsAction> for AppSettings {
    fn can_apply(&self, action: SettingsAction) -> bool {
        match action {
            SettingsAction::ToggleFullscreen => true,
            SettingsAction::SetScale(scale) => (0.5..=2.0).contains(&scale),
        }
    }

    fn apply(&mut self, action: SettingsAction) {
        match action {
            SettingsAction::ToggleFullscreen => {
                self.fullscreen = !self.fullscreen;
            }
            SettingsAction::SetScale(scale) => {
                self.ui_scale = scale;
            }
        }
    }
}

#[derive(Message, Debug, Clone, Copy)]
struct UiSettingRequest {
    action: AppAction,
}

impl RequestedSettingAction<AppAction> for UiSettingRequest {
    fn action(&self) -> AppAction {
        self.action
    }
}

fn sync_ui() {}

fn build_settings_resource() -> Persistent<AppSettings> {
    let path = std::env::temp_dir().join("cloudiful-bevy-settings-example.toml");

    Persistent::<AppSettings>::builder()
        .name("app settings")
        .format(StorageFormat::Toml)
        .path(&path)
        .default(AppSettings {
            fullscreen: false,
            ui_scale: 1.0,
        })
        .build()
        .unwrap()
}

fn main() {
    let mut app = App::new();
    app.add_message::<UiSettingRequest>();
    app.insert_resource(build_settings_resource());

    register_setting_systems(
        &mut app,
        change_setting::<AppSettings, SettingsAction, UiSettingRequest, AppAction>,
        sync_ui,
    );

    app.update();

    let settings = app.world().resource::<Persistent<AppSettings>>();
    assert!(!settings.get().fullscreen);
    assert_eq!(settings.get().ui_scale, 1.0);
}
```

## Schema Types

`SettingFieldSpec` is the generic description your UI layer can consume. Each
field has:

- `label`: already-resolved display text
- `label_key`: optional app-defined text key for localization-aware UIs
- `control`: one `SettingControlSpec` variant

`SettingControlSpec` models the common settings control shapes:

- `Toggle { text, action }`
- `Stepper { key, value, decrease_action, increase_action, slider }`
- `Select { key, value, options }`

Helpers on `SettingFieldSpec` keep construction compact:

- `toggle(...)`
- `stepper(...)`
- `select(...)`
- `with_label_key(...)`
- `with_slider(...)`

Use `SettingSliderSpec::new(...)` when a stepper also needs slider metadata such
as range, precision, suffix text, and the `fn(f32) -> Action` mapper.

## System Behavior

`register_setting_systems(...)` configures two ordered update sets:

1. `SettingSystemSet::ApplyActions`
2. `SettingSystemSet::SyncUi`

Put message-reading or direct action-application systems in `ApplyActions`.
Put systems that rebuild labels, refresh view state, or mirror settings back
into UI resources in `SyncUi`.

`change_setting(...)` reads Bevy messages, extracts the app action via
`RequestedSettingAction`, narrows it with `TryFrom`, checks `can_apply`, and
then updates the `Persistent<T>` resource.

`apply_setting_action(...)` is the same logic without message plumbing. Use it
when a system already has the action value in hand.