beuvy 0.1.0

Facade crate for beuvy-runtime plus optional declarative UI authoring.
Documentation
use super::context::DeclarativeUiBuildContext;
use super::state::{
    DeclarativeCheckedBinding, DeclarativeDisabledExpr, DeclarativeEventBindings,
    DeclarativeModelBinding, DeclarativeNodeStyleBindingComponent, DeclarativeRefBinding,
    DeclarativeShowExpr, DeclarativeValueBinding, ResolvedDeclarativeEventBinding,
};
use super::style::DeclarativeEntityInsert;
use crate::ast::*;
use crate::value::UiValue;
use bevy::prelude::*;
use std::collections::BTreeMap;

pub(crate) fn resolve_declarative_button_event_bindings(
    node: &DeclarativeUiNode,
    context: &DeclarativeUiBuildContext,
) -> Option<Vec<ResolvedDeclarativeEventBinding>> {
    let DeclarativeUiNode::Button { event_bindings, .. } = node else {
        unreachable!();
    };

    event_bindings
        .iter()
        .map(|binding| resolve_event_binding(binding, context))
        .collect()
}

pub(crate) fn resolve_event_binding(
    binding: &DeclarativeEventBinding,
    context: &DeclarativeUiBuildContext,
) -> Option<ResolvedDeclarativeEventBinding> {
    let mut params = BTreeMap::new();
    for (name, value) in &binding.params {
        let value = match value {
            DeclarativeValueExpr::Literal(value) => literal_value_string(value),
            DeclarativeValueExpr::Binding(path) => context.string(path)?,
        };
        params.insert(name.clone(), value);
    }
    Some(ResolvedDeclarativeEventBinding {
        kind: binding.kind,
        action_id: binding.action_id.clone(),
        params,
    })
}

fn literal_value_string(value: &DeclarativeLiteral) -> String {
    match value {
        DeclarativeLiteral::String(value) => value.clone(),
        DeclarativeLiteral::Bool(value) => value.to_string(),
        DeclarativeLiteral::Number(value) => match value {
            DeclarativeNumber::I32(value) => value.to_string(),
            DeclarativeNumber::I64(value) => value.to_string(),
            DeclarativeNumber::F32(value) => value.to_string(),
            DeclarativeNumber::F64(value) => value.to_string(),
        },
    }
}

pub(crate) fn conditional_matches(
    conditional: &DeclarativeConditional,
    context: &DeclarativeUiBuildContext,
) -> bool {
    match conditional {
        DeclarativeConditional::Always | DeclarativeConditional::Else => true,
        DeclarativeConditional::If(expr) | DeclarativeConditional::ElseIf(expr) => {
            condition_expr_matches(expr, context)
        }
    }
}

pub(crate) fn conditional_chain_matches(
    conditional: &DeclarativeConditional,
    context: &DeclarativeUiBuildContext,
    previous_branch_matched: &mut Option<bool>,
) -> bool {
    match conditional {
        DeclarativeConditional::Always => {
            *previous_branch_matched = None;
            true
        }
        DeclarativeConditional::If(expr) => {
            let matched = condition_expr_matches(expr, context);
            *previous_branch_matched = Some(matched);
            matched
        }
        DeclarativeConditional::ElseIf(expr) => {
            let previous_matched = previous_branch_matched.unwrap_or(false);
            let matched = !previous_matched && condition_expr_matches(expr, context);
            *previous_branch_matched = Some(previous_matched || matched);
            matched
        }
        DeclarativeConditional::Else => {
            let previous_matched = previous_branch_matched.unwrap_or(false);
            let matched = !previous_matched;
            *previous_branch_matched = Some(true);
            matched
        }
    }
}

pub(crate) fn condition_expr_matches(
    expr: &DeclarativeConditionExpr,
    context: &DeclarativeUiBuildContext,
) -> bool {
    match expr {
        DeclarativeConditionExpr::Binding(path) => context.bool(path).unwrap_or(false),
        DeclarativeConditionExpr::Equals { name, value } => context
            .resolve(name)
            .is_some_and(|candidate| candidate == &UiValue::from_literal(value)),
    }
}

pub(crate) fn visibility_for_show_binding(
    show_expr: Option<&DeclarativeConditionExpr>,
    context: &DeclarativeUiBuildContext,
) -> Visibility {
    if show_expr
        .map(|expr| condition_expr_matches(expr, context))
        .unwrap_or(true)
    {
        Visibility::Visible
    } else {
        Visibility::Hidden
    }
}

pub(crate) fn apply_common_bindings_to_entity(
    entity: &mut impl DeclarativeEntityInsert,
    show_expr: Option<&DeclarativeConditionExpr>,
    disabled_expr: Option<&DeclarativeConditionExpr>,
    value_binding: Option<&str>,
    model_binding: Option<&str>,
    checked_binding: Option<&str>,
    ref_binding: Option<&DeclarativeRefSource>,
    style_binding: Option<&DeclarativeNodeStyleBinding>,
    event_bindings: &[DeclarativeEventBinding],
    context: &DeclarativeUiBuildContext,
) {
    if let Some(expr) = show_expr {
        entity.insert_component(DeclarativeShowExpr(expr.clone()));
        entity.insert_component(visibility_for_show_binding(show_expr, context));
    }
    if let Some(expr) = disabled_expr {
        entity.insert_component(DeclarativeDisabledExpr(expr.clone()));
    }
    if let Some(path) = model_binding.or(value_binding) {
        entity.insert_component(DeclarativeValueBinding(path.to_string()));
    }
    if model_binding.is_some() {
        entity.insert_component(DeclarativeModelBinding);
    }
    if let Some(path) = checked_binding {
        entity.insert_component(DeclarativeCheckedBinding(path.to_string()));
    }
    if let Some(ref_binding) = ref_binding {
        entity.insert_component(DeclarativeRefBinding(ref_binding.clone()));
    }
    if let Some(binding) = style_binding {
        entity.insert_component(DeclarativeNodeStyleBindingComponent(binding.clone()));
    }
    if !event_bindings.is_empty() {
        let resolved = event_bindings
            .iter()
            .filter_map(|binding| resolve_event_binding(binding, context))
            .collect::<Vec<_>>();
        if !resolved.is_empty() {
            entity.insert_component(DeclarativeEventBindings(resolved));
        }
    }
}