use core::ops::RangeInclusive;
use accesskit::{Orientation, Role};
use bevy_a11y::AccessibilityNode;
use bevy_app::{App, Plugin};
use bevy_ecs::event::EntityEvent;
use bevy_ecs::hierarchy::Children;
use bevy_ecs::lifecycle::Insert;
use bevy_ecs::query::Has;
use bevy_ecs::system::{In, Res};
use bevy_ecs::world::DeferredWorld;
use bevy_ecs::{
component::Component,
observer::On,
query::With,
reflect::ReflectComponent,
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
use bevy_input_focus::FocusedInput;
use bevy_log::warn_once;
use bevy_math::ops;
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_ui::{
ComputedNode, ComputedUiRenderTargetInfo, InteractionDisabled, UiGlobalTransform, UiScale,
};
use crate::{Callback, Notify, ValueChange};
use bevy_ecs::entity::Entity;
#[derive(Debug, Default, PartialEq, Clone, Copy, Reflect)]
#[reflect(Clone, PartialEq, Default)]
pub enum TrackClick {
#[default]
Drag,
Step,
Snap,
}
#[derive(Component, Debug, Default)]
#[require(
AccessibilityNode(accesskit::Node::new(Role::Slider)),
CoreSliderDragState,
SliderValue,
SliderRange,
SliderStep
)]
pub struct Slider {
pub on_change: Callback<In<ValueChange<f32>>>,
pub track_click: TrackClick,
}
#[derive(Component, Debug, Default)]
pub struct SliderThumb;
#[derive(Component, Debug, Default, PartialEq, Clone, Copy)]
#[component(immutable)]
pub struct SliderValue(pub f32);
#[derive(Component, Debug, PartialEq, Clone, Copy)]
#[component(immutable)]
pub struct SliderRange {
start: f32,
end: f32,
}
impl SliderRange {
pub fn new(start: f32, end: f32) -> Self {
if end < start {
warn_once!(
"Expected SliderRange::start ({}) <= SliderRange::end ({})",
start,
end
);
}
Self { start, end }
}
pub fn from_range(range: RangeInclusive<f32>) -> Self {
let (start, end) = range.into_inner();
Self { start, end }
}
pub fn start(&self) -> f32 {
self.start
}
pub fn with_start(&self, start: f32) -> Self {
Self::new(start, self.end)
}
pub fn end(&self) -> f32 {
self.end
}
pub fn with_end(&self, end: f32) -> Self {
Self::new(self.start, end)
}
pub fn span(&self) -> f32 {
self.end - self.start
}
pub fn center(&self) -> f32 {
(self.start + self.end) / 2.0
}
pub fn clamp(&self, value: f32) -> f32 {
value.clamp(self.start, self.end)
}
pub fn thumb_position(&self, value: f32) -> f32 {
if self.end > self.start {
(value - self.start) / (self.end - self.start)
} else {
0.5
}
}
}
impl Default for SliderRange {
fn default() -> Self {
Self {
start: 0.0,
end: 1.0,
}
}
}
#[derive(Component, Debug, PartialEq, Clone)]
#[component(immutable)]
#[derive(Reflect)]
#[reflect(Component)]
pub struct SliderStep(pub f32);
impl Default for SliderStep {
fn default() -> Self {
Self(1.0)
}
}
#[derive(Component, Debug, Default, Clone, Copy, Reflect)]
#[reflect(Component, Default)]
pub struct SliderPrecision(pub i32);
impl SliderPrecision {
fn round(&self, value: f32) -> f32 {
let factor = ops::powf(10.0_f32, self.0 as f32);
(value * factor).round() / factor
}
}
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct CoreSliderDragState {
pub dragging: bool,
offset: f32,
}
pub(crate) fn slider_on_pointer_down(
mut press: On<Pointer<Press>>,
q_slider: Query<(
&Slider,
&SliderValue,
&SliderRange,
&SliderStep,
Option<&SliderPrecision>,
&ComputedNode,
&ComputedUiRenderTargetInfo,
&UiGlobalTransform,
Has<InteractionDisabled>,
)>,
q_thumb: Query<&ComputedNode, With<SliderThumb>>,
q_children: Query<&Children>,
mut commands: Commands,
ui_scale: Res<UiScale>,
) {
if q_thumb.contains(press.entity) {
press.propagate(false);
} else if let Ok((
slider,
value,
range,
step,
precision,
node,
node_target,
transform,
disabled,
)) = q_slider.get(press.entity)
{
press.propagate(false);
if disabled {
return;
}
let thumb_size = q_children
.iter_descendants(press.entity)
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
.unwrap_or(0.0);
let local_pos = transform.try_inverse().unwrap().transform_point2(
press.pointer_location.position * node_target.scale_factor() / ui_scale.0,
);
let track_width = node.size().x - thumb_size;
let click_val = if track_width > 0. {
local_pos.x * range.span() / track_width + range.center()
} else {
0.
};
let new_value = range.clamp(match slider.track_click {
TrackClick::Drag => {
return;
}
TrackClick::Step => {
if click_val < value.0 {
value.0 - step.0
} else {
value.0 + step.0
}
}
TrackClick::Snap => precision
.map(|prec| prec.round(click_val))
.unwrap_or(click_val),
});
if matches!(slider.on_change, Callback::Ignore) {
commands.entity(press.entity).insert(SliderValue(new_value));
} else {
commands.notify_with(
&slider.on_change,
ValueChange {
source: press.entity,
value: new_value,
},
);
}
}
}
pub(crate) fn slider_on_drag_start(
mut drag_start: On<Pointer<DragStart>>,
mut q_slider: Query<
(
&SliderValue,
&mut CoreSliderDragState,
Has<InteractionDisabled>,
),
With<Slider>,
>,
) {
if let Ok((value, mut drag, disabled)) = q_slider.get_mut(drag_start.entity) {
drag_start.propagate(false);
if !disabled {
drag.dragging = true;
drag.offset = value.0;
}
}
}
pub(crate) fn slider_on_drag(
mut event: On<Pointer<Drag>>,
mut q_slider: Query<(
&ComputedNode,
&Slider,
&SliderRange,
Option<&SliderPrecision>,
&UiGlobalTransform,
&mut CoreSliderDragState,
Has<InteractionDisabled>,
)>,
q_thumb: Query<&ComputedNode, With<SliderThumb>>,
q_children: Query<&Children>,
mut commands: Commands,
ui_scale: Res<UiScale>,
) {
if let Ok((node, slider, range, precision, transform, drag, disabled)) =
q_slider.get_mut(event.entity)
{
event.propagate(false);
if drag.dragging && !disabled {
let mut distance = event.distance / ui_scale.0;
distance.y *= -1.;
let distance = transform.transform_vector2(distance);
let thumb_size = q_children
.iter_descendants(event.entity)
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
.unwrap_or(0.0);
let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
let span = range.span();
let new_value = if span > 0. {
drag.offset + (distance.x * span) / slider_width
} else {
range.start() + span * 0.5
};
let rounded_value = range.clamp(
precision
.map(|prec| prec.round(new_value))
.unwrap_or(new_value),
);
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(event.entity)
.insert(SliderValue(rounded_value));
} else {
commands.notify_with(
&slider.on_change,
ValueChange {
source: event.entity,
value: rounded_value,
},
);
}
}
}
}
pub(crate) fn slider_on_drag_end(
mut drag_end: On<Pointer<DragEnd>>,
mut q_slider: Query<(&Slider, &mut CoreSliderDragState)>,
) {
if let Ok((_slider, mut drag)) = q_slider.get_mut(drag_end.entity) {
drag_end.propagate(false);
if drag.dragging {
drag.dragging = false;
}
}
}
fn slider_on_key_input(
mut focused_input: On<FocusedInput<KeyboardInput>>,
q_slider: Query<(
&Slider,
&SliderValue,
&SliderRange,
&SliderStep,
Has<InteractionDisabled>,
)>,
mut commands: Commands,
) {
if let Ok((slider, value, range, step, disabled)) = q_slider.get(focused_input.focused_entity) {
let input_event = &focused_input.input;
if !disabled && input_event.state == ButtonState::Pressed {
let new_value = match input_event.key_code {
KeyCode::ArrowLeft => range.clamp(value.0 - step.0),
KeyCode::ArrowRight => range.clamp(value.0 + step.0),
KeyCode::Home => range.start(),
KeyCode::End => range.end(),
_ => {
return;
}
};
focused_input.propagate(false);
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(focused_input.focused_entity)
.insert(SliderValue(new_value));
} else {
commands.notify_with(
&slider.on_change,
ValueChange {
source: focused_input.focused_entity,
value: new_value,
},
);
}
}
}
}
pub(crate) fn slider_on_insert(insert: On<Insert, Slider>, mut world: DeferredWorld) {
let mut entity = world.entity_mut(insert.entity);
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_orientation(Orientation::Horizontal);
}
}
pub(crate) fn slider_on_insert_value(insert: On<Insert, SliderValue>, mut world: DeferredWorld) {
let mut entity = world.entity_mut(insert.entity);
let value = entity.get::<SliderValue>().unwrap().0;
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_numeric_value(value.into());
}
}
pub(crate) fn slider_on_insert_range(insert: On<Insert, SliderRange>, mut world: DeferredWorld) {
let mut entity = world.entity_mut(insert.entity);
let range = *entity.get::<SliderRange>().unwrap();
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_min_numeric_value(range.start().into());
accessibility.set_max_numeric_value(range.end().into());
}
}
pub(crate) fn slider_on_insert_step(insert: On<Insert, SliderStep>, mut world: DeferredWorld) {
let mut entity = world.entity_mut(insert.entity);
let step = entity.get::<SliderStep>().unwrap().0;
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_numeric_value_step(step.into());
}
}
#[derive(EntityEvent, Clone)]
pub struct SetSliderValue {
pub entity: Entity,
pub change: SliderValueChange,
}
#[derive(Clone)]
pub enum SliderValueChange {
Absolute(f32),
Relative(f32),
RelativeStep(f32),
}
fn slider_on_set_value(
set_slider_value: On<SetSliderValue>,
q_slider: Query<(&Slider, &SliderValue, &SliderRange, Option<&SliderStep>)>,
mut commands: Commands,
) {
if let Ok((slider, value, range, step)) = q_slider.get(set_slider_value.entity) {
let new_value = match set_slider_value.change {
SliderValueChange::Absolute(new_value) => range.clamp(new_value),
SliderValueChange::Relative(delta) => range.clamp(value.0 + delta),
SliderValueChange::RelativeStep(delta) => {
range.clamp(value.0 + delta * step.map(|s| s.0).unwrap_or_default())
}
};
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(set_slider_value.entity)
.insert(SliderValue(new_value));
} else {
commands.notify_with(
&slider.on_change,
ValueChange {
source: set_slider_value.entity,
value: new_value,
},
);
}
}
}
pub struct SliderPlugin;
impl Plugin for SliderPlugin {
fn build(&self, app: &mut App) {
app.add_observer(slider_on_pointer_down)
.add_observer(slider_on_drag_start)
.add_observer(slider_on_drag_end)
.add_observer(slider_on_drag)
.add_observer(slider_on_key_input)
.add_observer(slider_on_insert)
.add_observer(slider_on_insert_value)
.add_observer(slider_on_insert_range)
.add_observer(slider_on_insert_step)
.add_observer(slider_on_set_value);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slider_precision_rounding() {
let precision_2dp = SliderPrecision(2);
assert_eq!(precision_2dp.round(1.234567), 1.23);
assert_eq!(precision_2dp.round(1.235), 1.24);
let precision_0dp = SliderPrecision(0);
assert_eq!(precision_0dp.round(1.4), 1.0);
let precision_neg1 = SliderPrecision(-1);
assert_eq!(precision_neg1.round(14.0), 10.0);
}
}