beuvy 0.1.0

Facade crate for beuvy-runtime plus optional declarative UI authoring.
Documentation
use bevy::prelude::*;
use beuvy::{
    DeclarativeActionMessage, DeclarativeActionSpec, DeclarativeUiBuildContext,
    DeclarativeUiPlugin, DeclarativeUiRuntimeValues, FontResource, UiKitPlugin, UiValue,
    parse_declarative_ui_asset, set_action_resolver, spawn_declarative_ui_tree_collect_slots,
};

const PAGE_SOURCE: &str = include_str!("declarative_vue_page.vue");

fn main() {
    set_action_resolver(|name| match name {
        "setStatus" => Some(DeclarativeActionSpec {
            action_id: "demo.set_status",
            param_names: vec!["status"],
        }),
        _ => None,
    });

    App::new()
        .insert_resource(DeclarativeUiRuntimeValues::default())
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "beuvy declarative vue example".to_string(),
                resolution: (1280, 860).into(),
                ..default()
            }),
            ..default()
        }))
        .add_plugins(UiKitPlugin)
        .add_plugins(DeclarativeUiPlugin::default())
        .add_systems(Startup, setup)
        .add_systems(Startup, seed_runtime_values)
        .add_systems(Update, handle_actions)
        .run();
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);
    commands.insert_resource(FontResource::from_handle(
        asset_server.load("fonts/SarasaFixedSC-Regular.ttf"),
    ));

    let asset = parse_declarative_ui_asset(PAGE_SOURCE).expect("page should parse");
    let root_context = DeclarativeUiBuildContext::default().with_root(UiValue::object([
        ("title", UiValue::from("Beuvy declarative example")),
        ("subtitle", UiValue::from("Vue-style template, Rust props, and action dispatch")),
        ("items", UiValue::list([
            UiValue::object([
                ("name", UiValue::from("Engine")),
                ("count", UiValue::from(3_i32)),
            ]),
            UiValue::object([
                ("name", UiValue::from("Shield")),
                ("count", UiValue::from(8_i32)),
            ]),
            UiValue::object([
                ("name", UiValue::from("Fuel")),
                ("count", UiValue::from(42_i32)),
            ]),
        ])),
    ]));

    commands
        .spawn((
            Node {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                padding: UiRect::all(Val::Px(32.0)),
                justify_content: JustifyContent::Center,
                align_items: AlignItems::Center,
                ..default()
            },
            BackgroundColor(Color::srgb_u8(241, 245, 249)),
        ))
        .with_children(|parent| {
            parent
                .spawn((
                    Node {
                        width: Val::Px(980.0),
                        max_width: Val::Percent(100.0),
                        padding: UiRect::all(Val::Px(24.0)),
                        border_radius: BorderRadius::all(Val::Px(18.0)),
                        ..default()
                    },
                    BorderColor::all(Color::srgb_u8(203, 213, 225)),
                    BackgroundColor(Color::WHITE),
                ))
                .with_children(|panel| {
                    spawn_declarative_ui_tree_collect_slots(panel, &asset, root_context);
                });
        });
}

fn seed_runtime_values(mut values: ResMut<DeclarativeUiRuntimeValues>) {
    values.set("ui.search", "");
    values.set("ui.status", "idle");
}

fn handle_actions(
    mut actions: MessageReader<DeclarativeActionMessage>,
    mut values: ResMut<DeclarativeUiRuntimeValues>,
) {
    for action in actions.read() {
        if action.action_id != "demo.set_status" {
            continue;
        }
        if let Some(status) = action.params.get("status") {
            values.set("ui.status", status.clone());
        }
    }
}