use crate::element::ElementProps;
use crate::parser::color::parse_color;
use crate::parser::values::{parse_attribute, parse_float};
use crate::props::Properties;
use crate::systems::PageSystemSet;
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;
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);
}
}
}
}
fn update_props(
mut root_query: Query<
(
Ref<Properties<ProgressBarProps>>,
&Interaction,
&Children,
&mut ProgressBarState,
),
Or<(Changed<Properties<ProgressBarProps>>, Changed<Interaction>)>,
>,
mut track_query: Query<
(&mut Node, &mut BackgroundColor),
(With<ProgressBarTrack>, Without<ProgressBarFill>),
>,
mut fill_query: Query<
(&mut Node, &mut BackgroundColor),
(With<ProgressBarFill>, Without<ProgressBarTrack>),
>,
) {
for (props, interaction, children, mut state) in &mut root_query {
let active_props = match interaction {
Interaction::Pressed => &props.click,
Interaction::Hovered => &props.hover,
Interaction::None => &props.default,
};
if props.is_changed() {
crate::set_if_changed!(
state.min, active_props.min;
state.max, active_props.max;
state.value, active_props.value => active_props.value.clamp(active_props.min, active_props.max);
);
}
for &child in children {
if let Ok((mut track_node, mut track_bg)) = track_query.get_mut(child) {
crate::set_if_changed!(
track_bg.0, active_props.track_color;
track_node.height, Val::Px(active_props.track_height);
track_node.border_radius, BorderRadius::all(Val::Px(active_props.track_height / 2.0));
);
}
if let Ok((mut fill_node, mut fill_bg)) = fill_query.get_mut(child) {
crate::set_if_changed!(
fill_bg.0, active_props.fill_color;
fill_node.height, Val::Px(active_props.track_height);
fill_node.border_radius, BorderRadius::all(Val::Px(active_props.track_height / 2.0));
);
}
}
}
}
#[derive(Component, Debug, Clone, PartialEq)]
pub struct ProgressBarState {
pub min: f32,
pub max: f32,
pub value: f32,
}
impl ProgressBarState {
#[inline(always)]
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
}
}
#[inline(always)]
pub fn percentage(&self) -> f32 {
self.normalized() * 100.0
}
}
#[derive(Component)]
pub struct ProgressBarTrack;
#[derive(Component)]
pub struct ProgressBarFill;
#[derive(Debug, Clone, Default)]
pub struct ProgressBarWidget {
props: Properties<ProgressBarProps>,
}
impl Widget for ProgressBarWidget {
fn name() -> &'static str
where
Self: Sized,
{
"ProgressBar"
}
fn setup(&self, app: &mut App) {
app.add_systems(
Update,
(update_props, sync_progress_bar_visuals).in_set(PageSystemSet),
);
}
fn parse(&mut self, node: &XmlNode) -> Result<(), String>
where
Self: Sized,
{
let default = ProgressBarProps::parse(node, None, &ProgressBarProps::default())?;
let hover = ProgressBarProps::parse(node, Some("hover"), &default)?;
let click = ProgressBarProps::parse(node, Some("click"), &default)?;
self.props = Properties {
default,
hover,
click,
};
Ok(())
}
fn spawn(&self, commands: &mut EntityCommands, _: &AssetServer) -> Entity {
let props = &self.props.default;
let norm_val = if props.max > props.min {
((props.value - props.min) / (props.max - props.min)).clamp(0.0, 1.0)
} else {
0.0
};
let pct = norm_val * 100.0;
commands
.insert((
self.props.clone(),
ProgressBarState {
min: props.min,
max: props.max,
value: props.value,
},
))
.with_children(|parent| {
parent.spawn((
ProgressBarTrack,
Node {
width: Val::Percent(100.0),
height: Val::Px(props.track_height),
border_radius: BorderRadius::all(Val::Px(props.track_height / 2.0)),
position_type: PositionType::Absolute,
..default()
},
BackgroundColor(props.track_color),
));
parent.spawn((
ProgressBarFill,
Node {
width: Val::Percent(pct),
height: Val::Px(props.track_height),
border_radius: BorderRadius::all(Val::Px(props.track_height / 2.0)),
position_type: PositionType::Absolute,
..default()
},
BackgroundColor(props.fill_color),
));
});
commands.id()
}
fn apply_defaults(
&self,
node: &XmlNode,
default: &mut ElementProps,
hover: &mut ElementProps,
click: &mut ElementProps,
) {
crate::set_missing_attrs!(
node,
"width" => default.node.width = Val::Px(200.0),
"hover.width" => hover.node.width = default.node.width,
"click.width" => click.node.width = default.node.width,
"height" => default.node.height = Val::Px(12.0),
"hover.height" => hover.node.height = default.node.height,
"click.height" => click.node.height = default.node.height,
"align-items" => default.node.align_items = AlignItems::Center,
"hover.align-items" => hover.node.align_items = default.node.align_items,
"click.align-items" => click.node.align_items = default.node.align_items,
"justify-content" => default.node.justify_content = JustifyContent::FlexStart,
"hover.justify-content" => hover.node.justify_content = default.node.justify_content,
"click.justify-content" => click.node.justify_content = default.node.justify_content,
"position" => default.node.position_type = PositionType::Relative,
"hover.position" => hover.node.position_type = default.node.position_type,
"click.position" => click.node.position_type = default.node.position_type,
);
}
fn dyn_clone(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
#[derive(Clone, Debug)]
pub struct ProgressBarProps {
pub min: f32,
pub max: f32,
pub value: f32,
pub track_color: Color,
pub fill_color: Color,
pub track_height: f32,
}
impl ProgressBarProps {
fn parse(
node: &XmlNode,
prefix: Option<&str>,
base: &Self,
) -> std::result::Result<Self, String> {
let min = parse_attribute(node, "min", prefix, parse_float)?.unwrap_or(base.min);
let max = parse_attribute(node, "max", prefix, parse_float)?.unwrap_or(base.max);
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)?
.unwrap_or(base.min)
.clamp(min, max);
let track_color =
parse_attribute(node, "track-color", prefix, parse_color)?.unwrap_or(base.track_color);
let fill_color =
parse_attribute(node, "fill-color", prefix, parse_color)?.unwrap_or(base.fill_color);
let track_height = parse_attribute(node, "track-height", prefix, parse_float)?
.unwrap_or(base.track_height);
Ok(Self {
min,
max,
value,
track_color,
fill_color,
track_height,
})
}
}
impl Default for ProgressBarProps {
#[inline(always)]
fn default() -> Self {
Self {
min: 0.0,
max: 1.0,
value: 0.0,
track_color: Color::srgb(0.16, 0.17, 0.20),
fill_color: Color::srgb(0.38, 0.69, 0.94),
track_height: 12.0,
}
}
}