bevy_pages 0.2.0

A lightweight and elegant framework to upgrade your Bevy UI experience.
Documentation
use crate::element::{ElementProps, ElementWidget};
use crate::parser::color::parse_color;
use crate::parser::values::{parse_attribute, parse_float};
use crate::widgets::Widget;
use bevy::asset::AssetServer;
use bevy::color::Color;
use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
use bevy::ui::{BackgroundColor, BorderRadius, Node, PositionType, Val};
use roxmltree::Node as XmlNode;

pub(crate) fn sync_progress_bar_visuals(
    pb_query: Query<(&ProgressBarState, &Children), Changed<ProgressBarState>>,
    mut fill_query: Query<&mut Node, With<ProgressBarFill>>,
) {
    for (pb, children) in &pb_query {
        let pct = pb.percentage();

        for &child in children {
            if let Ok(mut fill_node) = fill_query.get_mut(child) {
                fill_node.width = Val::Percent(pct);
            }
        }
    }
}

/// The internal progress bar state.
#[derive(Component, Debug, Clone, PartialEq)]
pub struct ProgressBarState {
    /// The minimum value.
    pub min: f32,
    /// The maximum value.
    pub max: f32,
    /// The current value.
    pub value: f32,
}

impl ProgressBarState {
    /// Returns the progress normalized between `0.0` and `1.0`.
    pub fn normalized(&self) -> f32 {
        if self.max > self.min {
            ((self.value - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
        } else {
            0.0
        }
    }

    /// Returns the current progress as a percentage between `0.0` and `100.0`.
    pub fn percentage(&self) -> f32 {
        self.normalized() * 100.0
    }
}

/// A progress bar widget.
///
/// ## XML Usage
///
/// Build a new progress bar using the `<ProgressBar />` tag.
///
/// ### Attributes
/// - `min = "<float>"`: The minimum of the progress bar.
/// - `max = "<float>"`: The maximum of the progress bar.
/// - `value = "<float>"`: The **initial** value of the progress bar.
/// - `track-color = "<color>"`: The background color of the progress bar container track.
/// - `fill-color = "<color>"`: The color of the inner filled progress indicator bar.
/// - `track-height = "<float>"`: The height of the track in pixels.
#[derive(Debug, Clone, PartialEq)]
pub struct ProgressBarWidget {
    /// The minimum value of the progress bar.
    pub min: f32,
    /// The maximum value of the progress bar.
    pub max: f32,
    /// The current value of the progress bar.
    pub value: f32,
    /// Background color of the progress bar container track.
    pub track_color: Option<Color>,
    /// Color of the inner filled progress indicator bar.
    pub fill_color: Option<Color>,
    /// Height of the track in pixels.
    ///
    /// TODO: Support bevy's [Val].
    pub track_height: Option<f32>,
}

impl Widget for ProgressBarWidget {
    fn spawn(&self, commands: &mut EntityCommands, _: &AssetServer) -> Entity {
        let norm_val = if self.max > self.min {
            ((self.value - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
        } else {
            0.0
        };

        let pct = norm_val * 100.0;
        let track_color = self.track_color.unwrap_or(Color::srgb(0.16, 0.17, 0.20));
        let fill_color = self.fill_color.unwrap_or(Color::srgb(0.38, 0.69, 0.94));
        let track_h = self.track_height.unwrap_or(12.0);

        commands
            .insert(ProgressBarState {
                min: self.min,
                max: self.max,
                value: self.value,
            })
            .with_children(|parent| {
                // Background Track
                parent.spawn((
                    ProgressBarTrack,
                    Node {
                        width: Val::Percent(100.0),
                        height: Val::Px(track_h),
                        border_radius: BorderRadius::all(Val::Px(track_h / 2.0)),
                        position_type: PositionType::Absolute,
                        ..default()
                    },
                    BackgroundColor(track_color),
                ));

                // Active Fill Bar
                parent.spawn((
                    ProgressBarFill,
                    Node {
                        width: Val::Percent(pct),
                        height: Val::Px(track_h),
                        border_radius: BorderRadius::all(Val::Px(track_h / 2.0)),
                        position_type: PositionType::Absolute,
                        ..default()
                    },
                    BackgroundColor(fill_color),
                ));
            });

        commands.id()
    }

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

        let min = parse_attribute(node, "min", prefix, parse_float)?
            .or_else(|| base_pb.map(|p| p.min))
            .unwrap_or(0.0);

        let max = parse_attribute(node, "max", prefix, parse_float)?
            .or_else(|| base_pb.map(|p| p.max))
            .unwrap_or(100.0);

        if min >= max {
            return Err(format!(
                "ProgressBar 'min' ({}) must be strictly less than 'max' ({})",
                min, max
            ));
        }

        let value = parse_attribute(node, "value", prefix, parse_float)?
            .or_else(|| base_pb.map(|p| p.value))
            .unwrap_or(min)
            .clamp(min, max);

        let track_color = parse_attribute(node, "track-color", prefix, parse_color)?
            .or_else(|| base_pb.and_then(|p| p.track_color));

        let fill_color = parse_attribute(node, "fill-color", prefix, parse_color)?
            .or_else(|| base_pb.and_then(|p| p.fill_color));

        let track_height = parse_attribute(node, "track-height", prefix, parse_float)?
            .or_else(|| base_pb.and_then(|p| p.track_height));

        Ok(ProgressBarWidget {
            min,
            max,
            value,
            track_color,
            fill_color,
            track_height,
        })
    }

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

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

        default.node.align_items = AlignItems::Center;
        default.node.justify_content = JustifyContent::FlexStart;
        default.node.position_type = PositionType::Relative;
    }
}

/// Marker component for the background track node.
#[derive(Component)]
pub struct ProgressBarTrack;

/// Marker component for the active inner fill bar node.
#[derive(Component)]
pub struct ProgressBarFill;