use crate::element::{Element, ElementId, ElementProps, ElementWidget};
use crate::events::ElementSet;
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, FocusPolicy, Node, PositionType, UiRect, Val};
use roxmltree::Node as XmlNode;
pub(crate) fn handle_slider_input(
mut commands: Commands,
touches: Res<Touches>,
mouse_button: Res<ButtonInput<MouseButton>>,
window: Single<&Window>,
mut slider_query: Query<(
Entity,
&Element,
&mut SliderState,
&Interaction,
&ComputedNode,
&GlobalTransform,
Option<&ElementId>,
Has<SliderDragging>,
)>,
) {
let active_touch = touches.iter().next();
let cursor_pos = window
.cursor_position()
.or_else(|| active_touch.map(|t| t.position()));
let mouse_down = mouse_button.pressed(MouseButton::Left) || active_touch.is_some();
let mouse_just_released = mouse_button.just_released(MouseButton::Left)
|| touches.iter_just_released().next().is_some();
let half_window_width = window.width() / 2.0;
for (entity, element, mut state, interaction, computed_node, transform, id, is_dragging) in
&mut slider_query
{
let Some(slider_config) = element.props.widget.as_slider() else {
continue;
};
if *interaction == Interaction::Pressed && mouse_down && !is_dragging {
commands.entity(entity).insert(SliderDragging);
}
if is_dragging && (!mouse_down || mouse_just_released) {
commands.entity(entity).remove::<SliderDragging>();
continue;
}
if is_dragging {
let Some(cursor) = cursor_pos else { continue };
let node_size = computed_node.size();
if node_size.x <= 0.0 {
continue;
}
let ui_cursor_x = cursor.x - half_window_width;
let node_center_x = transform.translation().x;
let node_left = node_center_x - (node_size.x / 2.0);
let norm = ((ui_cursor_x - node_left) / node_size.x).clamp(0.0, 1.0);
let min = slider_config.min;
let max = slider_config.max;
let mut new_val = min + norm * (max - min);
if let Some(step) = slider_config.step
&& step > 0.0
{
new_val = (new_val / step).round() * step;
}
new_val = new_val.clamp(min, max);
if (state.0 - new_val).abs() > f32::EPSILON {
let delta = new_val - state.0;
state.0 = new_val;
commands.trigger(ElementSet {
entity,
id: id.cloned(),
value: new_val,
delta: Some(delta),
});
}
}
}
}
pub(crate) fn sync_visuals(
slider_query: Query<(&Element, &SliderState, &Children), Changed<SliderState>>,
mut fill_query: Query<&mut Node, (With<SliderFill>, Without<SliderThumb>)>,
mut thumb_query: Query<&mut Node, (With<SliderThumb>, Without<SliderFill>)>,
) {
for (element, state, children) in &slider_query {
let Some(slider_config) = element.props.widget.as_slider() else {
continue;
};
let norm_val = if slider_config.max > slider_config.min {
((state.0 - slider_config.min) / (slider_config.max - slider_config.min))
.clamp(0.0, 1.0)
} else {
0.0
};
let pct = norm_val * 100.0;
for &child in children {
if let Ok(mut fill_node) = fill_query.get_mut(child) {
fill_node.width = Val::Percent(pct);
}
if let Ok(mut thumb_node) = thumb_query.get_mut(child) {
thumb_node.left = Val::Percent(pct);
}
}
}
}
#[derive(Copy, Clone, Debug, Component)]
pub struct SliderState(pub f32);
#[derive(Clone, Debug, PartialEq)]
pub struct SliderWidget {
pub min: f32,
pub max: f32,
pub step: Option<f32>,
pub value: f32,
pub track_color: Option<Color>,
pub thumb_color: Option<Color>,
pub fill_color: Option<Color>,
pub track_height: Option<f32>,
pub thumb_size: Option<f32>,
}
impl Widget for SliderWidget {
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 thumb_color = self.thumb_color.unwrap_or(Color::WHITE);
let track_h = self.track_height.unwrap_or(6.0);
let thumb_s = self.thumb_size.unwrap_or(16.0);
commands
.insert((SliderState(self.value), FocusPolicy::Block))
.with_children(|parent| {
parent.spawn((
SliderTrack,
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),
FocusPolicy::Pass,
));
parent.spawn((
SliderFill,
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),
FocusPolicy::Pass,
));
parent.spawn((
SliderThumb,
Node {
width: Val::Px(thumb_s),
height: Val::Px(thumb_s),
border_radius: BorderRadius::all(Val::Px(thumb_s / 2.0)),
position_type: PositionType::Absolute,
left: Val::Percent(pct),
margin: UiRect::left(Val::Px(-thumb_s / 2.0)),
..default()
},
BackgroundColor(thumb_color),
FocusPolicy::Pass,
));
});
commands.id()
}
fn parse(
node: &XmlNode,
prefix: Option<&str>,
base: Option<&ElementWidget>,
) -> Result<Self, String>
where
Self: Sized,
{
let base_slider = match base {
Some(ElementWidget::Slider(s)) => Some(s),
_ => None,
};
let min = parse_attribute(node, "min", prefix, parse_float)?
.or_else(|| base_slider.map(|s| s.min))
.unwrap_or(0.0);
let max = parse_attribute(node, "max", prefix, parse_float)?
.or_else(|| base_slider.map(|s| s.max))
.unwrap_or(100.0);
if min >= max {
return Err(format!(
"Slider 'min' ({}) must be strictly less than 'max' ({})",
min, max
));
}
let step = parse_attribute(node, "step", prefix, parse_float)?
.or_else(|| base_slider.and_then(|s| s.step));
let mut value = parse_attribute(node, "value", prefix, parse_float)?
.or_else(|| base_slider.map(|s| s.value))
.unwrap_or(min);
if let Some(step_val) = step
&& step_val > 0.0
{
value = (value / step_val).round() * step_val;
}
value = value.clamp(min, max);
let track_color = parse_attribute(node, "track-color", prefix, parse_color)?
.or_else(|| base_slider.and_then(|s| s.track_color));
let thumb_color = parse_attribute(node, "thumb-color", prefix, parse_color)?
.or_else(|| base_slider.and_then(|s| s.thumb_color));
let fill_color = parse_attribute(node, "fill-color", prefix, parse_color)?
.or_else(|| base_slider.and_then(|s| s.fill_color));
let track_height = parse_attribute(node, "track-height", prefix, parse_float)?
.or_else(|| base_slider.and_then(|s| s.track_height));
let thumb_size = parse_attribute(node, "thumb-size", prefix, parse_float)?
.or_else(|| base_slider.and_then(|s| s.thumb_size));
Ok(SliderWidget {
min,
max,
step,
value,
track_color,
thumb_color,
fill_color,
track_height,
thumb_size,
})
}
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(24.0);
}
default.node.align_items = AlignItems::Center;
default.node.justify_content = JustifyContent::FlexStart;
default.node.position_type = PositionType::Relative;
}
}
#[derive(Component)]
pub struct SliderTrack;
#[derive(Component)]
pub struct SliderFill;
#[derive(Component)]
pub struct SliderThumb;
#[derive(Component)]
pub struct SliderDragging;