use bevy::prelude::*;
use bevy::ui::UiSystems;
use bevy_cobweb::prelude::*;
use smallvec::SmallVec;
use smol_str::SmolStr;
use crate::prelude::*;
use crate::sickle::*;
const SLIDER_ZOOM_PSEUDO_STATE: PseudoState = PseudoState::Custom(SmolStr::new_static("SliderZoom"));
const SLIDER_ZOOM_ATTR: &'static str = "sliderzoom";
#[derive(Reflect, PartialEq, Default, Debug, Clone)]
struct SliderZoom(SliderValue);
impl SliderZoom
{
fn apply_zoom(
In((entity, mut val)): In<(Entity, SliderValue)>,
mut c: Commands,
mut r: ReactiveMut<SliderValue>,
)
{
val.normalize();
r.set_if_neq(&mut c, entity, val);
}
}
impl Instruction for SliderZoom
{
fn apply(self, entity: Entity, world: &mut World)
{
world.syscall((entity, self.0), Self::apply_zoom);
}
fn revert(_: Entity, _: &mut World) {}
}
impl StaticAttribute for SliderZoom
{
type Value = SliderValue;
fn construct(value: Self::Value) -> Self
{
Self(value)
}
}
impl AnimatedAttribute for SliderZoom
{
fn get_value(entity: Entity, world: &World) -> Option<SliderValue>
{
let val = world.get::<React<SliderValue>>(entity)?;
Some(val.get().clone())
}
fn extract(
entity: Entity,
world: &mut World,
ref_vals: &AnimatedVals<Self::Value>,
state: &AnimationState,
) -> Self::Value
{
let val = ref_vals.to_value(state);
let Ok(mut emut) = world.get_entity_mut(entity) else { return val };
if *state.result() == AnimationResult::Hold(InteractionStyle::Idle) {
emut.remove_pseudo_state(SLIDER_ZOOM_PSEUDO_STATE.clone());
}
val
}
}
#[derive(Default)]
struct SliderDragReference
{
invalid_press: bool,
offset: Vec2,
}
#[derive(Component)]
struct ComputedSlider
{
config: Slider,
drag_reference: SliderDragReference,
press_observer: Entity,
drag_observer: Entity,
}
impl ComputedSlider
{
fn revoke(self, world: &mut World)
{
world.despawn(self.press_observer);
world.despawn(self.drag_observer);
}
}
fn get_camera_scale_factor(
ui_camera: &DefaultUiCamera,
cameras: &Query<&Camera>,
maybe_slider_camera: Option<&UiTargetCamera>,
) -> Option<f32>
{
let camera_entity = maybe_slider_camera
.map(|t| t.entity())
.or_else(|| ui_camera.get())?;
let Ok(camera) = cameras.get(camera_entity) else { return None };
Some(camera.target_scaling_factor().unwrap_or(1.))
}
fn compute_value_for_target_position(
mut target_position_physical: Vec2,
slider_transform: &UiGlobalTransform,
bar_size: Vec2,
handle_size: Vec2,
axis: SliderAxis,
) -> SliderValue
{
let mut bar_location = slider_transform.translation;
target_position_physical.y = -target_position_physical.y;
bar_location.y = -bar_location.y;
let bar_bottom = bar_location - (bar_size / 2.); let bar_action_size = (bar_size - handle_size).max(Vec2::splat(0.)); let adjusted_target = target_position_physical - (handle_size / 2.); let diff = (adjusted_target - bar_bottom).max(Vec2::splat(0.)); let mut computed_val = Vec2::default();
if bar_action_size.x > 0. {
computed_val.x = (diff.x / bar_action_size.x).min(1.);
}
if bar_action_size.y > 0. {
computed_val.y = (diff.y / bar_action_size.y).min(1.);
}
match axis {
SliderAxis::X => SliderValue::Single(computed_val.x),
SliderAxis::Y => SliderValue::Single(computed_val.y),
SliderAxis::Planar => SliderValue::Planar(computed_val),
}
}
fn slider_bar_ptr_down(
mut event: On<Pointer<Press>>,
mut iter_children: ResMut<IterChildren>,
mut c: Commands,
ps: PseudoStateParam,
cameras: Query<&Camera>,
ui_camera: DefaultUiCamera,
mut sliders: Query<(
&mut ComputedSlider,
&mut React<SliderValue>,
Option<&mut NodeAttributes>,
&ComputedNode,
&UiGlobalTransform,
&Children,
Option<&UiTargetCamera>,
)>,
children_query: Query<&Children>,
handles: Query<(Entity, &ComputedNode, &UiGlobalTransform), (With<SliderHandle>, Without<ComputedSlider>)>,
)
{
let slider_entity = event.entity;
let Ok((
mut slider,
mut slider_value,
maybe_attrs,
slider_node,
slider_transform,
slider_children,
maybe_slider_camera,
)) = sliders.get_mut(slider_entity)
else {
return;
};
event.propagate(false);
let maybe_handle =
iter_children.search_descendants(slider_children, &children_query, |child| handles.get(child).ok());
let Some((handle_entity, handle_node, handle_transform)) = maybe_handle else {
tracing::warn!("failed finding a SliderHandle on a descendant of Slider entity {:?}", slider_entity);
return;
};
let bar_size = slider_node.size();
let handle_size = handle_node.size();
let Some(camera_scale_factor) = get_camera_scale_factor(&ui_camera, &cameras, maybe_slider_camera) else {
return;
};
let pointer_position = event.event().pointer_location.position;
let pointer_position_physical = pointer_position * camera_scale_factor;
let pointer_target = event.original_event_target();
let targets_handle = iter_children
.search(handle_entity, &children_query, |entity| {
if entity == pointer_target {
Some(())
} else {
None
}
})
.is_some();
if targets_handle {
let handle_position_logical = handle_transform.translation / camera_scale_factor.max(0.0001);
let offset = handle_position_logical - pointer_position;
slider.drag_reference = SliderDragReference { invalid_press: false, offset };
return;
}
if slider.config.bar_press == SliderPress::Inert {
slider.drag_reference.invalid_press = true;
return;
}
let standard_val = compute_value_for_target_position(
pointer_position_physical,
slider_transform,
bar_size,
handle_size,
slider.config.axis,
);
let target_val = slider
.config
.direction
.flip_direction(standard_val, slider.config.axis);
slider.drag_reference = SliderDragReference { invalid_press: false, offset: Vec2::default() };
match slider.config.bar_press {
SliderPress::Jump => {
React::set_if_neq(&mut slider_value, &mut c, target_val);
}
SliderPress::Animate(_) => {
if !ps.try_insert(&mut c, slider_entity, SLIDER_ZOOM_PSEUDO_STATE) {
ps.try_remove(&mut c, slider_entity, SLIDER_ZOOM_PSEUDO_STATE);
React::set_if_neq(&mut slider_value, &mut c, target_val);
} else if let Some(zoom) = maybe_attrs.and_then(|a| {
a.into_inner()
.animated_vals_mut::<SliderZoom>(SLIDER_ZOOM_ATTR)
}) {
zoom.idle = target_val;
}
}
SliderPress::Inert => unreachable!(),
}
}
fn slider_bar_drag(
mut event: On<Pointer<Drag>>,
mut iter_children: ResMut<IterChildren>,
mut c: Commands,
ps: PseudoStateParam,
cameras: Query<&Camera>,
ui_camera: DefaultUiCamera,
mut sliders: Query<(
&ComputedSlider,
&mut React<SliderValue>,
&ComputedNode,
&UiGlobalTransform,
&Children,
Option<&UiTargetCamera>,
)>,
children_query: Query<&Children>,
handles: Query<&ComputedNode, (With<SliderHandle>, Without<ComputedSlider>)>,
)
{
let slider_entity = event.entity;
let Ok((slider, mut slider_value, slider_node, slider_transform, slider_children, maybe_slider_camera)) =
sliders.get_mut(slider_entity)
else {
return;
};
event.propagate(false);
if event.event().distance == Vec2::default() {
return;
}
if slider.drag_reference.invalid_press {
return;
}
let maybe_handle =
iter_children.search_descendants(slider_children, &children_query, |child| handles.get(child).ok());
let Some(handle_node) = maybe_handle else {
tracing::warn!("failed finding a SliderHandle on a descendant of Slider entity {:?}", slider_entity);
return;
};
let bar_size = slider_node.size();
let handle_size = handle_node.size();
let pointer_position = event.event().pointer_location.position;
let target_position_corrected = pointer_position + slider.drag_reference.offset;
let Some(camera_scale_factor) = get_camera_scale_factor(&ui_camera, &cameras, maybe_slider_camera) else {
return;
};
let target_position_physical = target_position_corrected * camera_scale_factor;
let standard_val = compute_value_for_target_position(
target_position_physical,
slider_transform,
bar_size,
handle_size,
slider.config.axis,
);
let target_val = slider
.config
.direction
.flip_direction(standard_val, slider.config.axis);
React::set_if_neq(&mut slider_value, &mut c, target_val);
if matches!(slider.config.bar_press, SliderPress::Animate(_)) {
ps.try_remove(&mut c, slider_entity, SLIDER_ZOOM_PSEUDO_STATE);
}
}
fn update_slider_handle_positions(
mut iter_children: ResMut<IterChildren>,
mut sliders: Query<(&ComputedSlider, &React<SliderValue>, &Node, &ComputedNode, &Children)>,
children_q: Query<&Children>,
handles: Query<(Entity, &ComputedNode), (With<SliderHandle>, Without<ComputedSlider>)>,
mut transforms: Query<&mut UiGlobalTransform>,
)
{
for (slider, slider_value, slider_node, slider_computed_node, children) in sliders.iter_mut() {
if slider_node.display == Display::None {
continue;
}
let Some((handle_entity, handle_node)) =
iter_children.search_descendants(children, &children_q, |c| handles.get(c).ok())
else {
continue;
};
let axis = slider.config.axis;
let bar_size = slider_computed_node.size();
let handle_size = handle_node.size();
let bar_action_size = (bar_size - handle_size).max(Vec2::splat(0.));
let mut value = slider_value.get().clone();
value.normalize();
let standard_val = slider.config.direction.flip_direction(value, axis);
let val_vec2 = standard_val.to_vec2(axis);
let mut val_pos = val_vec2 * bar_action_size;
val_pos.y = -(val_pos.y - bar_action_size.y); let transform_offset_corrected = match axis {
SliderAxis::X => {
let y_offset = (bar_size.y - handle_size.y) / 2.;
val_pos.with_y(y_offset)
}
SliderAxis::Y => {
let x_offset = (bar_size.x - handle_size.x) / 2.;
val_pos.with_x(x_offset)
}
SliderAxis::Planar => val_pos,
};
update_handle_transform_recursive(handle_entity, transform_offset_corrected, &mut transforms, &children_q);
}
}
fn update_handle_transform_recursive(
entity: Entity,
offset: Vec2,
transforms: &mut Query<&mut UiGlobalTransform>,
children_q: &Query<&Children>,
)
{
let Ok(mut transform) = transforms.get_mut(entity) else { return };
let mut temp = **transform;
temp.translation += offset;
*transform = temp.into();
let Ok(children) = children_q.get(entity) else { return };
for child in children.iter() {
update_handle_transform_recursive(child, offset, transforms, children_q);
}
}
#[derive(ReactComponent, Debug, Copy, Clone, PartialEq, Reflect)]
pub enum SliderValue
{
Single(f32),
Planar(Vec2),
}
impl SliderValue
{
pub fn single(&self) -> Option<f32>
{
match self {
Self::Single(val) => Some(*val),
Self::Planar(_) => None,
}
}
pub fn planar(&self) -> Option<Vec2>
{
match self {
Self::Single(_) => None,
Self::Planar(val) => Some(*val),
}
}
pub fn normalize(&mut self)
{
match self {
Self::Single(v) => {
*v = v.min(1.0).max(0.);
}
Self::Planar(v) => {
v.x = v.x.min(1.0).max(0.);
v.y = v.y.min(1.0).max(0.);
}
}
}
pub fn to_vec2(&self, axis: SliderAxis) -> Vec2
{
match axis {
SliderAxis::X => match *self {
Self::Single(v) => Vec2 { x: v, y: 0. },
Self::Planar(Vec2 { x, y: _ }) => Vec2 { x, y: 0. },
},
SliderAxis::Y => match *self {
Self::Single(v) => Vec2 { x: 0., y: v },
Self::Planar(Vec2 { x: _, y }) => Vec2 { x: 0., y },
},
SliderAxis::Planar => match *self {
Self::Single(v) => Vec2 { x: v, y: v },
Self::Planar(v) => v,
},
}
}
}
impl Default for SliderValue
{
fn default() -> Self
{
Self::Single(0.)
}
}
impl Lerp for SliderValue
{
fn lerp(&self, to: Self, t: f32) -> Self
{
let mut res = match (*self, to) {
(Self::Single(a), Self::Single(b)) => Self::Single(a.lerp(b, t)),
(Self::Planar(a), Self::Planar(b)) => Self::Planar(a.lerp(b, t)),
(Self::Single(a), Self::Planar(b)) => Self::Planar(Vec2::splat(a).lerp(b, t)),
(Self::Planar(a), Self::Single(b)) => Self::Planar(a.lerp(Vec2::splat(b), t)),
};
res.normalize();
res
}
}
#[derive(Reflect, Default, PartialEq, Copy, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum SliderAxis
{
#[default]
X,
Y,
Planar,
}
#[derive(Reflect, Default, PartialEq, Copy, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum SliderDirection
{
#[default]
Standard,
Reverse,
ReverseHorizontal,
ReverseVertical,
}
impl SliderDirection
{
pub fn flip_direction(&self, value: SliderValue, axis: SliderAxis) -> SliderValue
{
match self {
Self::Standard => value,
Self::Reverse => match value {
SliderValue::Single(val) => SliderValue::Single(1. - val),
SliderValue::Planar(val) => SliderValue::Planar(Vec2::splat(1.) - val),
},
Self::ReverseHorizontal => match value {
SliderValue::Single(val) => match axis {
SliderAxis::X => SliderValue::Single(1. - val),
SliderAxis::Y => SliderValue::Single(val),
SliderAxis::Planar => SliderValue::Planar(Vec2::new(1. - val, val)),
},
SliderValue::Planar(Vec2 { x, y }) => SliderValue::Planar(Vec2::new(1. - x, y)),
},
Self::ReverseVertical => match value {
SliderValue::Single(val) => match axis {
SliderAxis::X => SliderValue::Single(val),
SliderAxis::Y => SliderValue::Single(1. - val),
SliderAxis::Planar => SliderValue::Planar(Vec2::new(val, 1. - val)),
},
SliderValue::Planar(Vec2 { x, y }) => SliderValue::Planar(Vec2::new(x, 1. - y)),
},
}
}
}
#[derive(Reflect, Default, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SliderPress
{
Inert,
#[default]
Jump,
Animate(AnimationConfig),
}
#[derive(Reflect, Default, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Slider
{
#[reflect(default)]
pub axis: SliderAxis,
#[reflect(default)]
pub direction: SliderDirection,
#[reflect(default)]
pub bar_press: SliderPress,
}
impl Instruction for Slider
{
fn apply(self, entity: Entity, world: &mut World)
{
let Ok(mut emut) = world.get_entity_mut(entity) else { return };
let initial_slider_value = match self.axis {
SliderAxis::X | SliderAxis::Y => SliderValue::Single(0.),
SliderAxis::Planar => SliderValue::Planar(Vec2::default()),
};
let computed = emut.world_scope(|world| {
if let SliderPress::Animate(enter_idle_with) = self.bar_press.clone() {
let animation = Animated::<SliderZoom> {
name: Some(SmolStr::new_static(SLIDER_ZOOM_ATTR)),
state: Some(SmallVec::from_elem(SLIDER_ZOOM_PSEUDO_STATE.clone(), 1)),
enter_idle_with: Some(enter_idle_with),
idle: SliderValue::default(), delete_on_entered: true,
..default()
};
animation.apply(entity, world);
}
let press_observer = world
.spawn(Observer::new(slider_bar_ptr_down).with_entity(entity))
.id();
let drag_observer = world
.spawn(Observer::new(slider_bar_drag).with_entity(entity))
.id();
ComputedSlider {
config: self,
drag_reference: SliderDragReference::default(),
press_observer,
drag_observer,
}
});
emut.insert(computed);
world.react(|rc| rc.insert(entity, initial_slider_value));
}
fn revert(entity: Entity, world: &mut World)
{
Animated::<SliderZoom>::revert(entity, world);
let Ok(mut emut) = world.get_entity_mut(entity) else { return };
emut.remove::<React<SliderValue>>();
emut.remove_pseudo_state(SLIDER_ZOOM_PSEUDO_STATE.clone());
if let Some(computed) = emut.take::<ComputedSlider>() {
computed.revoke(world);
}
}
}
#[derive(Reflect, Component, Default, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct SliderHandle;
pub trait SliderWidgetExt
{
fn initialize_slider<M, C, R: CobwebResult>(&mut self, callback: C) -> &mut Self
where
C: IntoSystem<TargetId, R, M> + Send + Sync + 'static;
fn on_slider<M, C, R: CobwebResult>(&mut self, callback: C) -> &mut Self
where
C: IntoSystem<TargetId, R, M> + Send + Sync + 'static;
}
impl SliderWidgetExt for UiBuilder<'_, Entity>
{
fn initialize_slider<M, C, R: CobwebResult>(&mut self, callback: C) -> &mut Self
where
C: IntoSystem<TargetId, R, M> + Send + Sync + 'static,
{
self.update_on(entity_insertion::<SliderValue>(self.id()), callback)
}
fn on_slider<M, C, R: CobwebResult>(&mut self, callback: C) -> &mut Self
where
C: IntoSystem<TargetId, R, M> + Send + Sync + 'static,
{
self.update_on(entity_mutation::<SliderValue>(self.id()), callback)
}
}
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Copy, Clone)]
pub struct SliderUpdateSet;
pub(crate) struct CobwebSliderPlugin;
impl Plugin for CobwebSliderPlugin
{
fn build(&self, app: &mut App)
{
app.register_instruction_type::<Slider>()
.register_component_type::<SliderHandle>()
.configure_sets(PostUpdate, SliderUpdateSet.in_set(UiSystems::PostLayout))
.add_systems(PostUpdate, update_slider_handle_positions.in_set(SliderUpdateSet));
}
}