bevy_mod_config 0.2.0

A Bevy plugin for configuration management
Documentation
use std::io::Cursor;
use std::time::Duration;

use bevy::asset::Assets;
use bevy::core_pipeline::core_2d::Camera2d;
use bevy::math::primitives::Rectangle;
use bevy::math::{Quat, Vec2, Vec3};
use bevy::render::mesh::{Mesh, Mesh2d};
use bevy::transform::components::Transform;
use bevy_app::AppExit;
use bevy_color::Color;
use bevy_ecs::component::Component;
use bevy_ecs::query::With;
use bevy_ecs::resource::Resource;
use bevy_ecs::schedule::IntoScheduleConfigs;
use bevy_ecs::system::{Command, Commands, Local, Res, ResMut, Single};
use bevy_ecs::world::World;
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
use bevy_mod_config::{AppExt, Config, ReadConfig, manager};
use bevy_sprite::{ColorMaterial, MeshMaterial2d};
use bevy_time::Time;

#[derive(Config)]
#[config(expose(changed))]
struct Settings {
    #[config(default = "Rect width = length of this field")]
    text:      String,
    #[config(default = 10.)]
    thickness: f32,
    rotate:    bool,
    color:     ChooseColor,
}

#[derive(Config)]
#[config(expose(read))]
enum ChooseColor {
    White,
    Rgb(u8, u8, u8),
    BevyColor(bevy_color::Color),
}

impl ChooseColorRead<'_> {
    fn to_color(&self) -> Color {
        match self {
            Self::White => Color::WHITE,
            &Self::Rgb(r, g, b) => Color::srgb_u8(r, g, b),
            &Self::BevyColor(color) => color,
        }
    }
}

#[derive(Config)]
struct Rgba(f32, f32, f32, f32);

#[cfg(feature = "serde_json")]
type SerdeJsonManager = bevy_mod_config::manager::serde::json::Pretty;
#[cfg(not(feature = "serde_json"))]
type SerdeJsonManager = ();

type ManagerType = (SerdeJsonManager, manager::Egui);

fn main() -> AppExit {
    let mut app = bevy_app::App::new();
    app.add_plugins((bevy::DefaultPlugins, bevy_egui::EguiPlugin::default()));

    app.init_config::<ManagerType, Settings>("ui");

    #[cfg(feature = "serde_json")]
    app.init_resource::<JsonEditorText>();
    app.add_systems(bevy_app::Startup, |mut commands: Commands| {
        commands.spawn(Camera2d);
    });
    app.add_systems(EguiPrimaryContextPass, show_settings);
    app.add_systems(bevy_app::Startup, init_line);
    app.add_systems(EguiPrimaryContextPass, display_line.after(show_settings));
    #[cfg(feature = "serde_json")]
    app.add_systems(
        EguiPrimaryContextPass,
        show_json_editor.after(show_settings).before(display_line),
    );

    app.run()
}

fn show_settings(mut contexts: EguiContexts, mut display: manager::egui::Display) {
    let Ok(ctx) = contexts.ctx_mut() else { return };

    egui::SidePanel::left("settings").show(ctx, |ui| {
        ui.heading("Settings");
        display.show(ui);
    });
}

#[cfg(feature = "serde_json")]
#[derive(Resource, Default)]
struct JsonEditorText {
    text: Option<String>,
}

#[cfg(feature = "serde_json")]
struct DumpJsonCommand;

#[cfg(feature = "serde_json")]
impl Command for DumpJsonCommand {
    fn apply(self, world: &mut World) {
        let manager = world.resource::<manager::Instance<ManagerType>>();
        match manager.0.clone().to_string(world) {
            Ok(json) => {
                let mut editor = world.resource_mut::<JsonEditorText>();
                editor.text = Some(json);
            }
            Err(err) => {
                bevy_log::error!("Failed to dump JSON: {err}");
                let mut editor = world.resource_mut::<JsonEditorText>();
                editor.text = None;
            }
        }
    }
}

#[cfg(feature = "serde_json")]
struct LoadJsonCommand;

#[cfg(feature = "serde_json")]
impl Command for LoadJsonCommand {
    fn apply(self, world: &mut World) {
        let editor = world.resource_mut::<JsonEditorText>();
        let Some(text) = editor.text.clone() else {
            bevy_log::error!("No JSON text to load");
            return;
        };
        let manager = world.resource::<manager::Instance<ManagerType>>();
        if let Err(err) = manager.0.clone().from_reader(world, Cursor::new(text.into_bytes())) {
            bevy_log::error!("Failed to load JSON: {err}");
        }
    }
}

#[cfg(feature = "serde_json")]
fn show_json_editor(
    mut contexts: EguiContexts,
    mut editor_text: ResMut<JsonEditorText>,
    mut commands: Commands,
) {
    let Ok(ctx) = contexts.ctx_mut() else { return };

    egui::SidePanel::right("json_editor").show(ctx, |ui| {
        ui.heading("JSON Editor");

        if ui.button("Dump JSON").clicked() {
            commands.queue(DumpJsonCommand);
        }
        if ui.button("Reload JSON").clicked() {
            commands.queue(LoadJsonCommand);
        }

        if let Some(text) = &mut editor_text.text {
            ui.add(egui::TextEdit::multiline(text).code_editor().desired_rows(20));
        } else {
            ui.label("Press 'Dump JSON' to see the current settings as JSON.");
        }
    });
}

#[derive(Component)]
struct MainShape;

fn init_line(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let mesh = meshes.add(Mesh::from(Rectangle { half_size: Vec2::new(10., 1.) }));
    let material = materials
        .add(ColorMaterial { color: ChooseColorRead::White.to_color(), ..Default::default() });
    commands.spawn((
        Mesh2d(mesh),
        MeshMaterial2d(material),
        Transform::from_scale(Vec3::new(1., 10., 1.)),
        MainShape,
    ));
}

fn display_line(
    settings: ReadConfig<Settings>,
    mut shape: Single<(&MeshMaterial2d<ColorMaterial>, &mut Transform), With<MainShape>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    mut last_changed: Local<Option<(Duration, SettingsChanged)>>,
    time: Res<Time>,
) {
    let last_change_time = match *last_changed {
        None => {
            *last_changed = Some((time.elapsed(), settings.changed()));
            time.elapsed()
        }
        Some((last_change_time, ref last_changed_value)) => {
            if *last_changed_value != settings.changed() {
                *last_changed = Some((time.elapsed(), settings.changed()));
                time.elapsed()
            } else {
                last_change_time
            }
        }
    };
    let time_since_change = (time.elapsed() - last_change_time).as_secs_f32();

    let settings = settings.read();

    let (MeshMaterial2d(material_handle), ref mut shape_transform) = *shape;
    materials.get_mut(material_handle).unwrap().color = settings.color.to_color();
    shape_transform.scale.x = settings.text.len() as f32;
    shape_transform.scale.y = settings.thickness;
    shape_transform.rotation =
        Quat::from_rotation_z(time_since_change * if settings.rotate { 1.0 } else { 0.0 });
}