use std::{
sync::Arc,
time::{Duration, Instant},
};
use derive_setters::Setters;
use tessera_ui::{
Color, ComputedData, Constraint, DimensionValue, Dp, MeasurementError, Modifier, Px,
PxPosition, State,
accesskit::Role,
layout::{LayoutInput, LayoutOutput, LayoutSpec},
remember, tessera,
};
use crate::{
animation,
fluid_glass::{FluidGlassArgs, GlassBorder, fluid_glass},
modifier::{InteractionState, ModifierExt as _, ToggleableArgs},
shape_def::Shape,
};
const ANIMATION_DURATION: Duration = Duration::from_millis(150);
#[derive(Clone)]
pub struct GlassSwitchController {
checked: bool,
progress: f32,
last_toggle_time: Option<Instant>,
}
impl GlassSwitchController {
pub fn new(initial_state: bool) -> Self {
Self {
checked: initial_state,
progress: if initial_state { 1.0 } else { 0.0 },
last_toggle_time: None,
}
}
pub fn is_checked(&self) -> bool {
self.checked
}
pub fn set_checked(&mut self, checked: bool) {
if self.checked != checked {
self.checked = checked;
self.progress = if checked { 1.0 } else { 0.0 };
self.last_toggle_time = None;
}
}
pub fn toggle(&mut self) {
self.checked = !self.checked;
self.last_toggle_time = Some(Instant::now());
}
pub fn animation_progress(&mut self) -> f32 {
if let Some(start) = self.last_toggle_time {
let elapsed = start.elapsed();
let fraction = (elapsed.as_secs_f32() / ANIMATION_DURATION.as_secs_f32()).min(1.0);
let target = if self.checked { 1.0 } else { 0.0 };
self.progress = target * fraction + (1.0 - fraction) * (1.0 - target);
if fraction >= 1.0 {
self.last_toggle_time = None;
self.progress = target;
}
}
self.progress
}
}
impl Default for GlassSwitchController {
fn default() -> Self {
Self::new(false)
}
}
#[derive(Clone, Setters)]
pub struct GlassSwitchArgs {
pub modifier: Modifier,
#[setters(skip)]
pub on_toggle: Option<Arc<dyn Fn(bool) + Send + Sync>>,
pub checked: bool,
pub width: Dp,
pub height: Dp,
pub track_on_color: Color,
pub track_off_color: Color,
pub thumb_on_alpha: f32,
pub thumb_off_alpha: f32,
#[setters(strip_option)]
pub thumb_border: Option<GlassBorder>,
#[setters(strip_option)]
pub track_border: Option<GlassBorder>,
pub thumb_padding: Dp,
#[setters(strip_option, into)]
pub accessibility_label: Option<String>,
#[setters(strip_option, into)]
pub accessibility_description: Option<String>,
}
impl GlassSwitchArgs {
pub fn on_toggle<F>(mut self, on_toggle: F) -> Self
where
F: Fn(bool) + Send + Sync + 'static,
{
self.on_toggle = Some(Arc::new(on_toggle));
self
}
pub fn on_toggle_shared(mut self, on_toggle: Arc<dyn Fn(bool) + Send + Sync>) -> Self {
self.on_toggle = Some(on_toggle);
self
}
}
impl Default for GlassSwitchArgs {
fn default() -> Self {
Self {
modifier: Modifier::new(),
on_toggle: None,
checked: false,
width: Dp(52.0),
height: Dp(32.0),
track_on_color: Color::new(0.2, 0.7, 1.0, 0.5),
track_off_color: Color::new(0.8, 0.8, 0.8, 0.5),
thumb_on_alpha: 0.5,
thumb_off_alpha: 1.0,
thumb_border: None,
track_border: None,
thumb_padding: Dp(3.0),
accessibility_label: None,
accessibility_description: None,
}
}
}
fn interpolate_color(off: Color, on: Color, progress: f32) -> Color {
Color {
r: off.r + (on.r - off.r) * progress,
g: off.g + (on.g - off.g) * progress,
b: off.b + (on.b - off.b) * progress,
a: off.a + (on.a - off.a) * progress,
}
}
#[tessera]
pub fn glass_switch(args: impl Into<GlassSwitchArgs>) {
let args: GlassSwitchArgs = args.into();
let controller = remember(|| GlassSwitchController::new(args.checked));
glass_switch_with_controller(args, controller);
}
#[tessera]
pub fn glass_switch_with_controller(
args: impl Into<GlassSwitchArgs>,
controller: State<GlassSwitchController>,
) {
let args: GlassSwitchArgs = args.into();
let mut modifier = args.modifier;
let on_toggle = args.on_toggle.clone();
let enabled = on_toggle.is_some();
let interaction_state = enabled.then(|| remember(InteractionState::new));
let checked = controller.with(|c| c.is_checked());
if enabled {
modifier = modifier.minimum_interactive_component_size();
let on_toggle = on_toggle.clone();
let mut toggle_args = ToggleableArgs::new(
checked,
Arc::new(move |_| {
controller.with_mut(|c| c.toggle());
let checked = controller.with(|c| c.is_checked());
if let Some(on_toggle) = on_toggle.as_ref() {
on_toggle(checked);
}
}),
)
.enabled(true)
.role(Role::Switch);
if let Some(label) = args.accessibility_label.clone() {
toggle_args = toggle_args.label(label);
}
if let Some(desc) = args.accessibility_description.clone() {
toggle_args = toggle_args.description(desc);
}
if let Some(state) = interaction_state {
toggle_args = toggle_args.interaction_state(state);
}
modifier = modifier.toggleable(toggle_args);
}
let width_px = args.width.to_px();
let height_px = args.height.to_px();
let thumb_dp = Dp(args.height.0 - (args.thumb_padding.0 * 2.0));
let thumb_px = thumb_dp.to_px();
let progress = controller.with_mut(|c| c.animation_progress());
let track_color = interpolate_color(args.track_off_color, args.track_on_color, progress);
modifier.run(move || {
let mut track_args = FluidGlassArgs::default()
.modifier(Modifier::new().constrain(
Some(DimensionValue::Fixed(width_px)),
Some(DimensionValue::Fixed(height_px)),
))
.tint_color(track_color)
.shape(Shape::capsule())
.blur_radius(8.0);
if let Some(border) = args.track_border {
track_args = track_args.border(border);
}
fluid_glass(track_args, || {});
let thumb_alpha =
args.thumb_off_alpha + (args.thumb_on_alpha - args.thumb_off_alpha) * progress;
let thumb_color = Color::new(1.0, 1.0, 1.0, thumb_alpha);
let mut thumb_args = FluidGlassArgs::default()
.modifier(Modifier::new().constrain(
Some(DimensionValue::Fixed(thumb_px)),
Some(DimensionValue::Fixed(thumb_px)),
))
.tint_color(thumb_color)
.refraction_height(1.0)
.shape(Shape::Ellipse);
if let Some(border) = args.thumb_border {
thumb_args = thumb_args.border(border);
}
fluid_glass(thumb_args, || {});
layout(GlassSwitchLayout {
width: width_px,
height: height_px,
thumb_padding: args.thumb_padding.to_px(),
progress,
});
});
}
#[derive(Clone, PartialEq)]
struct GlassSwitchLayout {
width: Px,
height: Px,
thumb_padding: Px,
progress: f32,
}
impl LayoutSpec for GlassSwitchLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let track_id = input.children_ids()[0];
let thumb_id = input.children_ids()[1];
let track_constraint = Constraint::new(
DimensionValue::Fixed(self.width),
DimensionValue::Fixed(self.height),
);
let thumb_constraint = Constraint::new(
DimensionValue::Wrap {
min: None,
max: None,
},
DimensionValue::Wrap {
min: None,
max: None,
},
);
let nodes_constraints = vec![(track_id, track_constraint), (thumb_id, thumb_constraint)];
let sizes_map = input.measure_children(nodes_constraints)?;
let thumb_size = sizes_map
.get(&thumb_id)
.expect("thumb size should be measured");
let eased_progress = animation::easing(self.progress);
output.place_child(track_id, PxPosition::new(Px(0), Px(0)));
let start_x = self.thumb_padding;
let end_x = self.width - thumb_size.width - self.thumb_padding;
let thumb_x = start_x.0 as f32 + (end_x.0 - start_x.0) as f32 * eased_progress;
let thumb_y = (self.height - thumb_size.height) / 2;
output.place_child(thumb_id, PxPosition::new(Px(thumb_x as i32), thumb_y));
Ok(ComputedData {
width: self.width,
height: self.height,
})
}
}