use std::{cell::Cell, rc::Rc};
use cranpose_core::{SideEffect, remember};
use cranpose_macros::composable;
use cranpose_ui::{
Modifier, Size,
widgets::{Box, BoxSpec, BoxWithConstraints, BoxWithConstraintsScope},
};
use cranpose_ui_graphics::{Brush, Color, CornerRadii, GraphicsLayer};
use super::slider_motion::{SliderDrag, remember_slider_fling};
use crate::{
material::{Glass, GlassDynamics, GlassMorph, LiquidModifierExt, LiquidShape},
theme::liquid_colors,
};
const TRACK_HEIGHT: f32 = 6.0;
const THUMB_WIDTH: f32 = 37.0;
const THUMB_HEIGHT: f32 = 24.0;
const SLIDER_HEIGHT: f32 = 32.0;
const LENS_WIDTH: f32 = THUMB_WIDTH * 1.55;
const LENS_HEIGHT: f32 = THUMB_HEIGHT * 1.55;
const LENS_PAD: f32 = 10.0;
#[composable]
pub fn LiquidSlider(modifier: Modifier, value: f32, on_change: impl Fn(f32) + 'static) {
let colors = liquid_colors();
let value = value.clamp(0.0, 1.0);
let on_change: Rc<dyn Fn(f32)> = Rc::new(on_change);
let active_pointer =
remember(|| Rc::new(Cell::new(Option::<SliderDrag>::None))).with(Rc::clone);
let contact = super::control_motion::remember_control_contact(
super::control_motion::ControlContactKind::Thumb,
);
let (lens_progress, material_progress) = contact.states();
Box(
modifier
.height(SLIDER_HEIGHT)
.stable_semantics(slider_semantics(value, Rc::clone(&on_change))),
BoxSpec::default(),
move || {
let on_change = Rc::clone(&on_change);
let active_pointer = Rc::clone(&active_pointer);
let contact = Rc::clone(&contact);
BoxWithConstraints(Modifier::empty(), move |scope| {
let width = scope.constraints().max_width.max(THUMB_WIDTH);
let usable = (width - THUMB_WIDTH).max(1.0);
let controlled_x = usable * value;
let fling = remember_slider_fling(value);
fling.synchronize(value);
let lens_axis = crate::motion::remember_liquid_drag_axis(controlled_x);
let shape = super::lens_motion::remember_lens_shape(
super::lens_motion::LensShapeKind::Thumb,
);
let thumb_x = if let Some(next) = fling.sample() {
let position = usable * next;
lens_axis.finish_at(position, None);
let fling = Rc::clone(&fling);
let on_change = Rc::clone(&on_change);
SideEffect(move || fling.publish(next, on_change.as_ref()));
position
} else {
lens_axis.settle_to(controlled_x, crate::motion::LiquidMotion::snappy());
lens_axis.value()
};
let gesture = super::slider_gesture::SliderGesture {
active: Rc::clone(&active_pointer),
axis: Rc::clone(&lens_axis),
contact: Rc::clone(&contact),
fling,
on_change: Rc::clone(&on_change),
value,
width,
thumb_width: THUMB_WIDTH,
};
let surface = Modifier::empty()
.size(Size::new(width, SLIDER_HEIGHT))
.pointer_input((width.to_bits(), value.to_bits()), move |scope| {
gesture.clone().run(scope)
});
Box(surface, BoxSpec::default(), move || {
let Size {
width: node_w,
height: node_h,
} = super::control_lens::node_size(
Size::new(THUMB_WIDTH, THUMB_HEIGHT),
Size::new(LENS_WIDTH, LENS_HEIGHT),
LENS_PAD,
);
let shape = Rc::clone(&shape);
let track_fill = colors.label.with_alpha(25.0 / 255.0);
let track = Modifier::empty()
.size(Size::new(width, TRACK_HEIGHT))
.offset(0.0, (SLIDER_HEIGHT - TRACK_HEIGHT) * 0.5)
.draw_behind(move |scope| {
scope.draw_round_rect(
Brush::solid(track_fill),
CornerRadii::uniform(TRACK_HEIGHT * 0.5),
);
});
Box(track, BoxSpec::default(), || {});
let accent = colors.accent;
let filled_width = (thumb_x + THUMB_WIDTH * 0.5).max(TRACK_HEIGHT);
let filled = Modifier::empty()
.size(Size::new(filled_width, TRACK_HEIGHT))
.offset(0.0, (SLIDER_HEIGHT - TRACK_HEIGHT) * 0.5)
.draw_behind(move |scope| {
scope.draw_round_rect(
Brush::solid(accent),
CornerRadii::uniform(TRACK_HEIGHT * 0.5),
);
});
Box(filled, BoxSpec::default(), || {});
let lens_for_thumb = material_progress;
let thumb_shape = Rc::clone(&shape);
let thumb_axis = Rc::clone(&lens_axis);
let thumb = Modifier::empty()
.required_size(Size::new(node_w, node_h))
.offset((THUMB_WIDTH - node_w) * 0.5, (SLIDER_HEIGHT - node_h) * 0.5)
.graphics_layer(move || {
let lens = lens_for_thumb.get();
let grow = lens_progress.get().clamp(-0.1, 1.2);
let projection = thumb_shape.projection(thumb_axis.value());
GraphicsLayer {
translation_x: thumb_x.round(),
scale_x: (THUMB_WIDTH + (LENS_WIDTH - THUMB_WIDTH) * grow)
/ THUMB_WIDTH
* projection.0,
scale_y: (THUMB_HEIGHT + (LENS_HEIGHT - THUMB_HEIGHT) * grow)
/ THUMB_HEIGHT
* projection.1,
alpha: (1.0 - lens).clamp(0.0, 1.0),
..Default::default()
}
});
super::control_lens::WhiteControlThumb(
thumb,
Size::new(THUMB_WIDTH, THUMB_HEIGHT),
);
let lens_for_layer = lens_progress;
let physics_axis = Rc::clone(&lens_axis);
let lens = Modifier::empty()
.required_size(Size::new(node_w, node_h))
.offset(0.0, (SLIDER_HEIGHT - node_h) * 0.5)
.graphics_layer(move || GraphicsLayer {
translation_x: thumb_x + (THUMB_WIDTH - node_w) * 0.5,
alpha: material_progress.get().clamp(0.0, 1.0),
..Default::default()
})
.glass_effect_with(
Glass::lens()
.shape(LiquidShape::Capsule)
.tint(Color::WHITE.with_alpha(0.07))
.face_lighting(false)
.highlight(0.15)
.no_clip(),
move || {
let grow = lens_for_layer.get().clamp(-0.1, 1.2);
let w = THUMB_WIDTH + (LENS_WIDTH - THUMB_WIDTH) * grow;
let h = THUMB_HEIGHT + (LENS_HEIGHT - THUMB_HEIGHT) * grow;
let projection = shape.projection(physics_axis.value());
GlassDynamics {
optical_projection: Some(projection),
activity: Some(material_progress.get().clamp(0.0, 1.0)),
morph: Some(GlassMorph {
node_size: (node_w, node_h),
primary: (node_w * 0.5, node_h * 0.5, w, h, -1.0),
shapes: Vec::new(),
glue: 0.0,
wobble_amplitude: 0.0,
wobble_phase: 0.0,
bulge_amplitude: 0.0,
bulge_direction: 0.0,
ellipse_blend: 0.0,
capsule_smoothing_dp: 0.0,
deformation: None,
zoom_anchor: (0.0, 0.0),
}),
..Default::default()
}
},
);
Box(lens, BoxSpec::default(), || {});
});
});
},
);
}
fn slider_semantics(
value: f32,
on_change: Rc<dyn Fn(f32)>,
) -> impl Fn(&mut cranpose_ui::SemanticsConfiguration) {
move |config| {
config.state_description = Some(format!("{}%", (value * 100.0).round() as u32));
config.progress = Some(cranpose_ui::ProgressBarRangeInfo::new(value, 0.0, 1.0, 0));
let on_change = Rc::clone(&on_change);
config.set_progress = Some(cranpose_ui::SemanticsSetProgress::new(move |next: f32| {
on_change(next.clamp(0.0, 1.0));
true
}));
}
}
#[cfg(test)]
#[path = "tests/slider_tests.rs"]
mod tests;