beuvy-runtime 0.1.0

A low-level Bevy UI kit with reusable controls and utility-class styling.
Documentation
use crate::button::{
    ActiveButton, Button, ButtonInner, ButtonLabel, DisabledButton, sync_button_active_state,
};
use crate::interaction_style::UiDisabled;
use crate::select::model::{
    Select, SelectChevron, SelectOptionButton, SelectOptionIndicator, SelectPanel, SelectTrigger,
    selected_option, sync_select_label, trigger_label_entity,
};
use bevy::prelude::*;
use bevy::ui::Val::{Percent, Px};
use bevy_localization::Localization;
use std::collections::HashSet;

pub(crate) fn sync_select_semantics(
    mut commands: Commands,
    localization: Option<Res<Localization>>,
    mut select_queries: ParamSet<(
        Query<Entity, With<Select>>,
        Query<Entity, Changed<Select>>,
        Query<&mut Select>,
    )>,
    trigger_labels: Query<&ButtonLabel>,
    disabled_buttons: Query<Has<DisabledButton>, With<Button>>,
    mut panel_nodes: Query<&mut Node, With<SelectPanel>>,
) {
    let localization_changed = localization
        .as_ref()
        .is_some_and(|value| value.is_changed());
    let mut dirty = if localization_changed {
        select_queries.p0().iter().collect::<Vec<_>>()
    } else {
        select_queries.p1().iter().collect::<Vec<_>>()
    };
    dirty.sort_unstable();
    dirty.dedup();

    for entity in dirty {
        let mut select_states = select_queries.p2();
        let Ok(mut select) = select_states.get_mut(entity) else {
            continue;
        };

        sync_trigger_disabled(
            &mut commands,
            &disabled_buttons,
            &mut panel_nodes,
            &mut select,
        );

        if let Some(label_entity) = trigger_label_entity(&trigger_labels, &select) {
            sync_select_label(
                &mut commands,
                localization.as_deref(),
                label_entity,
                selected_option(&select),
                &select.value,
            );
        }

        for option in &select.options {
            sync_button_active_state(&mut commands, option.entity, option.value == select.value);
            sync_option_disabled(
                &mut commands,
                &disabled_buttons,
                option.entity,
                option.disabled,
            );
            if let Ok(label) = trigger_labels.get(option.entity) {
                sync_select_label(
                    &mut commands,
                    localization.as_deref(),
                    label.entity,
                    Some(option),
                    &option.value,
                );
            }
        }
    }
}

fn sync_trigger_disabled(
    commands: &mut Commands,
    disabled_buttons: &Query<Has<DisabledButton>, With<Button>>,
    panel_nodes: &mut Query<&mut Node, With<SelectPanel>>,
    select: &mut Select,
) {
    sync_option_disabled(commands, disabled_buttons, select.trigger, select.disabled);
    if select.disabled && select.open {
        select.open = false;
        if let Ok(mut panel) = panel_nodes.get_mut(select.panel) {
            panel.display = Display::None;
        }
    }
}

fn sync_option_disabled(
    commands: &mut Commands,
    disabled_buttons: &Query<Has<DisabledButton>, With<Button>>,
    entity: Entity,
    disabled: bool,
) {
    let Ok(has_disabled) = disabled_buttons.get(entity) else {
        return;
    };
    let Ok(mut entity_commands) = commands.get_entity(entity) else {
        return;
    };

    if disabled {
        if !has_disabled {
            entity_commands.try_insert((DisabledButton, UiDisabled));
        }
    } else if has_disabled {
        entity_commands
            .try_remove::<DisabledButton>()
            .try_remove::<UiDisabled>();
    }
}

pub(crate) fn sync_select_button_layouts(
    mut buttons: Query<
        (
            &mut Node,
            Option<&SelectTrigger>,
            Option<&SelectOptionButton>,
        ),
        Added<Button>,
    >,
) {
    for (mut node, trigger, option) in &mut buttons {
        if trigger.is_none() && option.is_none() {
            continue;
        }

        node.min_width = Px(0.0);
        node.max_width = Percent(100.0);
        node.flex_grow = 1.0;
        node.justify_content = JustifyContent::FlexStart;
        node.align_items = AlignItems::Center;
        node.position_type = PositionType::Relative;
        node.overflow = Overflow::clip_x();
        node.column_gap = Px(10.0);
        node.padding.right = Px(12.0);
    }
}

pub(crate) fn sync_select_option_indicators(
    added_option_buttons: Query<Entity, Added<SelectOptionButton>>,
    activated_option_buttons: Query<Entity, (With<SelectOptionButton>, Added<ActiveButton>)>,
    mut removed_active_buttons: RemovedComponents<ActiveButton>,
    option_buttons: Query<(&Children, Has<ActiveButton>), With<SelectOptionButton>>,
    mut indicators: Query<&mut Visibility, With<SelectOptionIndicator>>,
) {
    let mut dirty_buttons = HashSet::new();
    for entity in &added_option_buttons {
        dirty_buttons.insert(entity);
    }
    for entity in &activated_option_buttons {
        dirty_buttons.insert(entity);
    }
    for entity in removed_active_buttons.read() {
        if option_buttons.contains(entity) {
            dirty_buttons.insert(entity);
        }
    }
    if dirty_buttons.is_empty() {
        return;
    }

    for entity in dirty_buttons {
        let Ok((children, is_active)) = option_buttons.get(entity) else {
            continue;
        };

        for child in children {
            let Ok(mut visibility) = indicators.get_mut(*child) else {
                continue;
            };
            *visibility = if is_active {
                Visibility::Visible
            } else {
                Visibility::Hidden
            };
        }
    }
}

pub(crate) fn sync_select_accessory_layout(
    select_buttons: Query<
        (
            &ButtonLabel,
            Option<&SelectTrigger>,
            Option<&SelectOptionButton>,
        ),
        With<Button>,
    >,
    mut nodes: ParamSet<(
        Query<&mut Node, With<ButtonInner>>,
        Query<&mut Node, Or<(With<SelectChevron>, With<SelectOptionIndicator>)>>,
    )>,
) {
    for (label, trigger, option) in &select_buttons {
        if trigger.is_none() && option.is_none() {
            continue;
        }

        if let Ok(mut label_node) = nodes.p0().get_mut(label.entity) {
            label_node.flex_grow = 1.0;
            label_node.flex_shrink = 1.0;
            label_node.min_width = Px(0.0);
        }
    }

    for mut node in &mut nodes.p1() {
        node.position_type = PositionType::Relative;
        node.left = Val::Auto;
        node.right = Val::Auto;
        node.top = Val::Auto;
        node.bottom = Val::Auto;
        node.margin.left = Val::Auto;
    }
}