bevy_pages 0.2.0

A lightweight and elegant framework to upgrade your Bevy UI experience.
Documentation
use crate::element::{ElementId, ElementProps, ElementWidget};
use crate::events::{ElementClick, ElementToggle};
use crate::parser::color::{darken_color, lighten_color, parse_color};
use crate::parser::values::{parse_attribute, parse_bool};
use crate::widgets::Widget;
use bevy::asset::AssetServer;
use bevy::color::Color;
use bevy::prelude::{
    AlignItems, AlignSelf, BorderColor, Changed, Children, Commands, Component, Entity,
    EntityCommands, FontSize, JustifyContent, JustifySelf, Node, On, Query, Text, TextColor,
    TextFont, UiRect, Val, With,
};
use bevy::ui::Display;
use roxmltree::Node as XmlNode;

pub(crate) fn toggle_checkbox(
    trigger: On<ElementClick>,
    mut commands: Commands,
    mut query: Query<(&mut CheckboxState, Option<&ElementId>), With<CheckboxWidget>>,
) {
    if let Ok((mut state, id)) = query.get_mut(trigger.entity) {
        state.0 = !state.0;

        commands.trigger(ElementToggle {
            entity: trigger.entity,
            id: id.cloned(),
            state: state.0,
        });
    }
}

pub(crate) fn sync_visuals(
    query: Query<(&CheckboxState, &Children), Changed<CheckboxState>>,
    mut checkmarks: Query<&mut Node, With<CheckboxCheckmark>>,
) {
    for (state, children) in &query {
        for child in children.iter() {
            if let Ok(mut node) = checkmarks.get_mut(*child) {
                node.display = if state.0 {
                    Display::Flex
                } else {
                    Display::None
                };
            }
        }
    }
}

/// The runtime state of a checkbox widget.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Component)]
pub struct CheckboxState(pub bool);

/// Marker component for the inner checkmark visual entity.
#[derive(Component, Debug, Clone, Copy)]
pub struct CheckboxCheckmark;

/// A checkbox widget.
///
/// ## XML Usage
///
/// Build a checkbox widget with the `<Checkbox />` tag.
///
/// ### Attributes
/// - `checked = "<bool>"`: Sets the initial state of the checkbox.
/// - `check-color = "<color>"`: Sets the checkmark color.
/// - `check-symbol = "<string>"`: Sets the symbol used for the check indicator (defaults to "x").
///
/// ## Logic
///
/// Use the internal [CheckboxState] to query the state of the checkbox.
/// You may also use the [ElementToggle] event or other element events.
#[derive(Clone, Debug, Component)]
pub struct CheckboxWidget {
    /// The initial state of the checkbox.
    pub state: bool,
    /// Color of the checkmark.
    pub check_color: Option<Color>,
    /// Symbol used for the check indicator (defaults to "x").
    pub symbol: String,
}

impl Widget for CheckboxWidget {
    fn spawn(&self, commands: &mut EntityCommands, _: &AssetServer) -> Entity {
        let is_checked = self.state;
        let check_color = self.check_color.unwrap_or(Color::WHITE);
        let symbol = self.symbol.clone();

        commands.insert((self.clone(), CheckboxState(is_checked)));

        commands.with_children(|parent| {
            parent.spawn((
                CheckboxCheckmark,
                Text::new(symbol),
                TextFont {
                    font_size: FontSize::Px(14.0),
                    ..Default::default()
                },
                TextColor(check_color),
                Node {
                    display: if is_checked {
                        Display::Flex
                    } else {
                        Display::None
                    },
                    align_self: AlignSelf::Center,
                    justify_self: JustifySelf::Center,
                    ..Default::default()
                },
            ));
        });

        commands.id()
    }

    fn parse(
        node: &XmlNode,
        prefix: Option<&str>,
        base: Option<&ElementWidget>,
    ) -> Result<Self, String>
    where
        Self: Sized,
    {
        let base_cb = match base {
            Some(ElementWidget::Checkbox(cb)) => Some(cb),
            _ => None,
        };

        let state = parse_attribute(node, "checked", prefix, parse_bool)?
            .or_else(|| base_cb.map(|b| b.state))
            .unwrap_or(false);

        let check_color = parse_attribute(node, "check-color", prefix, parse_color)?
            .or_else(|| base_cb.and_then(|b| b.check_color));

        let symbol = parse_attribute(node, "check-symbol", prefix, |s| Ok(s.to_string()))?
            .or_else(|| base_cb.map(|b| b.symbol.clone()))
            .unwrap_or_else(|| "x".to_string());

        Ok(Self {
            state,
            check_color,
            symbol,
        })
    }

    fn apply_defaults(
        node: &XmlNode,
        default: &mut ElementProps,
        hover: &mut ElementProps,
        click: &mut ElementProps,
    ) {
        if !node.has_attribute("width") {
            default.node.width = Val::Px(20.0);
        }

        if !node.has_attribute("height") {
            hover.node.height = Val::Px(20.0);
        }

        default.node.align_items = AlignItems::Center;
        default.node.justify_content = JustifyContent::Center;
        hover.node.align_items = AlignItems::Center;
        hover.node.justify_content = JustifyContent::Center;
        click.node.align_items = AlignItems::Center;
        click.node.justify_content = JustifyContent::Center;

        let base_bg = default
            .bg_color
            .unwrap_or_else(|| Color::srgb(0.18, 0.18, 0.22));

        if default.bg_color.is_none() {
            default.bg_color = Some(base_bg);
        }

        if !node.has_attribute("hover.bg-color") {
            hover.bg_color = Some(lighten_color(base_bg, 0.12));
        }
        if !node.has_attribute("click.bg-color") {
            click.bg_color = Some(darken_color(base_bg, 0.08));
        }

        if default.border_color.is_none() {
            let border = BorderColor::all(Color::srgb(0.4, 0.4, 0.45));
            default.border_color = Some(border);
            if node.attribute("hover:border-color").is_none() {
                hover.border_color = Some(BorderColor::all(Color::srgb(0.6, 0.6, 0.7)));
            }
        }

        if default.node.border == UiRect::ZERO {
            let border_rect = UiRect::all(Val::Px(1.5));
            default.node.border = border_rect;
            hover.node.border = border_rect;
            click.node.border = border_rect;
        }
    }
}