bevy_mod_config 0.7.1

A Bevy plugin for configuration management
Documentation
#![cfg_attr(
    not(feature = "serde_json"),
    allow(unused_imports, reason = "not bothering to optimize per-import cfg")
)]

use std::io::Cursor;
use std::time::Duration;

use bevy::asset::Assets;
use bevy::camera::{Camera2d, ClearColor};
use bevy::math::primitives::Rectangle;
use bevy::math::{Quat, Vec2, Vec3};
use bevy::mesh::{Mesh, Mesh2d};
use bevy::sprite_render::{ColorMaterial, MeshMaterial2d};
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, SystemSet};
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_time::Time;

#[derive(Config)]
#[config(expose(read, changed))]
struct Settings {
    bg_color: ChooseColor,
    duration: Duration,

    line1: Line,
    #[config(rotate.default = true, fg_color.discrim.default = ChooseColorDiscrim::BevyColor, fg_color.v_BevyColor.0.default = Color::srgb(1.0, 0.0, 0.0))]
    line2: Line,
}

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

    #[config(discrim.default = ChooseColorDiscrim::Rgb, v_Rgb.2.default = 255)]
    fg_color: ChooseColor,
}

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

impl ChooseColorRead<'_> {
    fn to_bevy_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.before(DisplayLines));
    app.add_systems(bevy_app::Startup, init_line);
    app.add_systems(bevy_app::Update, set_clear_color);
    app.add_systems(bevy_app::Update, display_line::<MainShape1>.in_set(DisplayLines));
    app.add_systems(bevy_app::Update, display_line::<MainShape2>.in_set(DisplayLines));

    app.run()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)]
struct DisplayLines;

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

    let mut ui = egui::Ui::new(
        ctx.clone(),
        "background".into(),
        egui::UiBuilder::new().layer_id(egui::LayerId::background()).max_rect(ctx.viewport_rect()),
    );
    egui::Panel::left("settings").show_inside(&mut ui, |ui| {
        ui.heading("Settings");
        display.show(ui);
    });
    #[cfg(feature = "serde_json")]
    show_json_editor(&mut ui, &mut editor_text, &mut commands);
}

#[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 {
    type Out = ();

    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 {
    type Out = ();

    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(
    ui: &mut egui::Ui,
    editor_text: &mut ResMut<JsonEditorText>,
    commands: &mut Commands,
) {
    egui::Panel::right("json_editor").show_inside(ui, |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.");
        }
    });
}

trait MainShape: Component {
    fn get_settings<'a>(settings: SettingsRead<'a>) -> LineRead<'a>;
}

#[derive(Component)]
struct MainShape1;

impl MainShape for MainShape1 {
    fn get_settings<'a>(settings: SettingsRead<'a>) -> LineRead<'a> { settings.line1 }
}

impl MainShape for MainShape2 {
    fn get_settings<'a>(settings: SettingsRead<'a>) -> LineRead<'a> { settings.line2 }
}

#[derive(Component)]
struct MainShape2;

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 mut new_material = || {
        materials.add(ColorMaterial {
            color: ChooseColorRead::White.to_bevy_color(),
            ..Default::default()
        })
    };
    commands.spawn((
        Mesh2d(mesh.clone()),
        MeshMaterial2d(new_material()),
        Transform::from_scale(Vec3::new(1., 10., 1.)).with_translation(Vec3::new(0., 30., 0.)),
        MainShape1,
    ));
    commands.spawn((
        Mesh2d(mesh),
        MeshMaterial2d(new_material()),
        Transform::from_scale(Vec3::new(1., 10., 1.)).with_translation(Vec3::new(0., -30., 0.)),
        MainShape2,
    ));
}

fn set_clear_color(mut clear_color: ResMut<ClearColor>, settings: ReadConfig<Settings>) {
    let settings = settings.read();
    clear_color.0 = settings.bg_color.to_bevy_color();
}

fn display_line<WhichShape: MainShape>(
    settings: ReadConfig<Settings>,
    mut shape: Single<(&MeshMaterial2d<ColorMaterial>, &mut Transform), With<WhichShape>>,
    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 line_settings = WhichShape::get_settings(settings);

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