beuvy 0.1.0

Facade crate for beuvy-runtime plus optional declarative UI authoring.
Documentation
use super::{nearest_ancestor, walk_descendants};
use crate::ast::DeclarativeContainerKind;
use crate::runtime::state::{
    DeclarativeContainerSemantic, DeclarativeFormResetMessage, DeclarativeFormSubmitMessage,
};
use crate::value::UiValue;
use beuvy_runtime::button::{Button, ButtonClickMessage, ButtonType};
use beuvy_runtime::input::{InputField, InputSubmitMessage};
use beuvy_runtime::select::Select;
use bevy::prelude::*;

pub(crate) fn handle_form_button_clicks(
    mut button_clicks: MessageReader<ButtonClickMessage>,
    parents: Query<&ChildOf>,
    semantics: Query<&DeclarativeContainerSemantic>,
    children: Query<&Children>,
    buttons: Query<&Button>,
    mut inputs_mut: Query<&mut InputField, Without<Select>>,
    mut selects_mut: Query<&mut Select, Without<InputField>>,
    mut submit_messages: MessageWriter<DeclarativeFormSubmitMessage>,
    mut reset_messages: MessageWriter<DeclarativeFormResetMessage>,
) {
    for click in button_clicks.read() {
        let Ok(button) = buttons.get(click.entity) else {
            continue;
        };
        let Some(form) = nearest_form(click.entity, &parents, &semantics) else {
            continue;
        };
        match button.button_type {
            ButtonType::Submit => {
                submit_messages.write(DeclarativeFormSubmitMessage {
                    entity: form,
                    values: collect_form_values_mut(
                        form,
                        &children,
                        &mut inputs_mut,
                        &mut selects_mut,
                    ),
                });
            }
            ButtonType::Reset => {
                reset_form_values(form, &children, &mut inputs_mut, &mut selects_mut);
                reset_messages.write(DeclarativeFormResetMessage { entity: form });
            }
            ButtonType::Button => {}
        }
    }
}

pub(crate) fn handle_form_input_submits(
    mut input_submits: MessageReader<InputSubmitMessage>,
    parents: Query<&ChildOf>,
    semantics: Query<&DeclarativeContainerSemantic>,
    children: Query<&Children>,
    inputs: Query<&InputField, Without<Select>>,
    selects: Query<&Select, Without<InputField>>,
    mut submit_messages: MessageWriter<DeclarativeFormSubmitMessage>,
) {
    for submit in input_submits.read() {
        let Some(form) = nearest_form(submit.entity, &parents, &semantics) else {
            continue;
        };
        submit_messages.write(DeclarativeFormSubmitMessage {
            entity: form,
            values: collect_form_values(form, &children, &inputs, &selects),
        });
    }
}

fn nearest_form(
    entity: Entity,
    parents: &Query<&ChildOf>,
    semantics: &Query<&DeclarativeContainerSemantic>,
) -> Option<Entity> {
    nearest_ancestor(entity, parents, |candidate| {
        semantics
            .get(candidate)
            .is_ok_and(|semantic| semantic.kind == DeclarativeContainerKind::Form)
    })
}

fn collect_form_values(
    form: Entity,
    children: &Query<&Children>,
    inputs: &Query<&InputField, Without<Select>>,
    selects: &Query<&Select, Without<InputField>>,
) -> std::collections::HashMap<String, UiValue> {
    let mut values = std::collections::HashMap::new();
    walk_descendants(form, children, &mut |entity| {
        if let Ok(input) = inputs.get(entity) {
            let value = match input.input_type {
                beuvy_runtime::input::InputType::Checkbox => UiValue::from(input.checked),
                beuvy_runtime::input::InputType::Radio => {
                    if input.checked {
                        UiValue::from(input.value().to_string())
                    } else {
                        return;
                    }
                }
                beuvy_runtime::input::InputType::Number
                | beuvy_runtime::input::InputType::Range => input
                    .numeric_value()
                    .map(UiValue::from)
                    .unwrap_or_else(|| UiValue::from(input.value().to_string())),
                beuvy_runtime::input::InputType::Text
                | beuvy_runtime::input::InputType::Textarea
                | beuvy_runtime::input::InputType::Password => {
                    UiValue::from(input.value().to_string())
                }
            };
            values.insert(input.name.clone(), value);
            return;
        }
        if let Ok(select) = selects.get(entity) {
            values.insert(select.name.clone(), UiValue::from(select.value.clone()));
        }
    });
    values
}

fn collect_form_values_mut(
    form: Entity,
    children: &Query<&Children>,
    inputs: &mut Query<&mut InputField, Without<Select>>,
    selects: &mut Query<&mut Select, Without<InputField>>,
) -> std::collections::HashMap<String, UiValue> {
    let mut values = std::collections::HashMap::new();
    walk_descendants(form, children, &mut |entity| {
        if let Ok(input) = inputs.get_mut(entity) {
            let value = match input.input_type {
                beuvy_runtime::input::InputType::Checkbox => UiValue::from(input.checked),
                beuvy_runtime::input::InputType::Radio => {
                    if input.checked {
                        UiValue::from(input.value().to_string())
                    } else {
                        return;
                    }
                }
                beuvy_runtime::input::InputType::Number
                | beuvy_runtime::input::InputType::Range => input
                    .numeric_value()
                    .map(UiValue::from)
                    .unwrap_or_else(|| UiValue::from(input.value().to_string())),
                beuvy_runtime::input::InputType::Text
                | beuvy_runtime::input::InputType::Textarea
                | beuvy_runtime::input::InputType::Password => {
                    UiValue::from(input.value().to_string())
                }
            };
            values.insert(input.name.clone(), value);
            return;
        }
        if let Ok(select) = selects.get_mut(entity) {
            values.insert(select.name.clone(), UiValue::from(select.value.clone()));
        }
    });
    values
}

fn reset_form_values(
    form: Entity,
    children: &Query<&Children>,
    inputs: &mut Query<&mut InputField, Without<Select>>,
    selects: &mut Query<&mut Select, Without<InputField>>,
) {
    walk_descendants(form, children, &mut |entity| {
        if let Ok(mut input) = inputs.get_mut(entity) {
            input.reset();
            return;
        }
        if let Ok(mut select) = selects.get_mut(entity) {
            select.value = select.initial_value.clone();
        }
    });
}