use crate::material::{Glass, GlassDynamics, GlassMorph, LiquidModifierExt, LiquidShape};
use crate::motion::LiquidMotion;
use crate::theme::liquid_colors;
use cranpose_animation::{animateColorAsState, animateFloatAsState, spring};
use cranpose_core::{mutableStateOf, remember};
use cranpose_macros::composable;
use cranpose_services::{default_haptics, HapticFeedback};
use cranpose_ui::widgets::{Box, BoxSpec};
use cranpose_ui::{Modifier, PointerEventKind, PointerInputScope, Size};
use cranpose_ui_graphics::{Brush, CornerRadii, GraphicsLayer};
use std::cell::Cell;
use std::rc::Rc;
pub(crate) const TRACK_WIDTH: f32 = 63.0;
pub(crate) const TRACK_HEIGHT: f32 = 28.0;
const THUMB_WIDTH: f32 = 37.0;
const THUMB_HEIGHT: f32 = 25.0;
const THUMB_MARGIN: f32 = 1.5;
const LENS_WIDTH: f32 = 58.0;
const LENS_HEIGHT: f32 = 39.0;
const LENS_LEAN: f32 = 12.0;
const LENS_PAD: f32 = 10.0;
const TAP_SLOP: f32 = 4.0;
#[composable]
#[allow(non_snake_case)]
pub fn LiquidToggle(modifier: Modifier, checked: bool, on_change: impl Fn(bool) + 'static) {
let colors = liquid_colors();
let drag_progress = remember(|| mutableStateOf(Option::<f32>::None)).with(|s| *s);
let pressed = remember(|| mutableStateOf(false)).with(|s| *s);
let off_track = cranpose_ui_graphics::Color::from_rgb_u8(187, 186, 188);
let drag_mix = |a: cranpose_ui_graphics::Color, b: cranpose_ui_graphics::Color, t: f32| {
cranpose_ui_graphics::Color::rgba(
a.r() + (b.r() - a.r()) * t,
a.g() + (b.g() - a.g()) * t,
a.b() + (b.b() - a.b()) * t,
1.0,
)
};
let base_track = match drag_progress.get() {
Some(progress) => drag_mix(off_track, colors.success, progress),
None => {
if checked {
colors.success
} else {
off_track
}
}
};
let lifted_track = cranpose_ui_graphics::Color::rgba(
base_track.r() + (1.0 - base_track.r()) * 0.18,
base_track.g() + (1.0 - base_track.g()) * 0.10,
base_track.b() + (1.0 - base_track.b()) * 0.18,
1.0,
);
let track_color = animateColorAsState(
if pressed.get() {
lifted_track
} else {
base_track
},
LiquidMotion::smooth(),
"toggle-track",
);
let min_x = THUMB_MARGIN;
let max_x = TRACK_WIDTH - THUMB_MARGIN - THUMB_WIDTH;
let target_x = match drag_progress.get() {
Some(progress) => min_x + (max_x - min_x) * progress,
None => {
if checked {
max_x
} else {
min_x
}
}
};
let thumb_x = animateFloatAsState(target_x, LiquidMotion::snappy(), "toggle-thumb-x");
let settling = (thumb_x.get() - target_x).abs() > 0.75;
let lens_target = if pressed.get() || (drag_progress.get().is_none() && settling) {
1.0
} else {
0.0
};
let lens_progress = animateFloatAsState(
lens_target,
if pressed.get() {
spring(0.9, 1400.0)
} else {
spring(1.0, 170.0)
},
"toggle-lens",
);
let on_change = std::rc::Rc::new(on_change);
let track = Modifier::empty()
.size(Size::new(TRACK_WIDTH, TRACK_HEIGHT))
.pointer_input((), {
let on_change = std::rc::Rc::clone(&on_change);
move |scope: PointerInputScope| {
let on_change = std::rc::Rc::clone(&on_change);
async move {
scope
.await_pointer_event_scope(|await_scope| async move {
let mut down_x = 0.0f32;
let mut dragging = false;
loop {
let event = await_scope.await_pointer_event().await;
match event.kind {
PointerEventKind::Down => {
dragging = true;
down_x = event.position.x;
pressed.set(true);
default_haptics().perform(HapticFeedback::Selection);
event.consume();
}
PointerEventKind::Move if dragging => {
let progress =
((event.position.x - THUMB_MARGIN - THUMB_WIDTH * 0.5)
/ (TRACK_WIDTH - 2.0 * THUMB_MARGIN - THUMB_WIDTH))
.clamp(0.0, 1.0);
drag_progress.set(Some(progress));
event.consume();
}
PointerEventKind::Up | PointerEventKind::Cancel if dragging => {
dragging = false;
pressed.set(false);
let travelled =
(event.position.x - down_x).abs() > TAP_SLOP;
let next = if travelled {
drag_progress
.get()
.map(|p| p >= 0.5)
.unwrap_or(!checked)
} else {
!checked
};
drag_progress.set(None);
if next != checked {
default_haptics().perform(HapticFeedback::ImpactLight);
on_change(next);
}
event.consume();
}
_ => {}
}
}
})
.await;
}
}
})
.draw_behind(move |scope| {
let size = scope.size();
scope.draw_round_rect(
Brush::solid(off_track),
CornerRadii::uniform(TRACK_HEIGHT * 0.5),
);
let split =
(thumb_x.get() + THUMB_WIDTH + THUMB_MARGIN).clamp(TRACK_HEIGHT, size.width);
let radius = TRACK_HEIGHT * 0.5;
let color = track_color.get();
scope.draw_circle(
Brush::solid(color),
cranpose_ui_graphics::Point::new(radius, radius),
radius,
);
scope.draw_rect_at(
cranpose_ui_graphics::Rect {
x: radius,
y: 0.0,
width: (split - radius).max(0.0),
height: size.height,
},
Brush::solid(color),
);
});
Box(track.then(modifier), BoxSpec::default(), move || {
let thumb_x_for_layer = thumb_x;
let lens_for_thumb = lens_progress;
let thumb = Modifier::empty()
.size(Size::new(THUMB_WIDTH, THUMB_HEIGHT))
.offset(0.0, (TRACK_HEIGHT - THUMB_HEIGHT) * 0.5)
.graphics_layer(move || {
let lens = lens_for_thumb.get();
let alpha = ((0.30 - lens) / 0.22).clamp(0.0, 1.0);
GraphicsLayer {
translation_x: thumb_x_for_layer.get(),
alpha,
..Default::default()
}
})
.drop_shadow(
cranpose_ui_graphics::LayerShape::Rounded(
cranpose_ui_graphics::RoundedCornerShape::uniform(THUMB_HEIGHT * 0.5),
),
|scope| {
scope.radius = 2.0;
scope.offset.y = 0.5;
scope.color = cranpose_ui_graphics::Color::BLACK.with_alpha(0.10);
},
)
.draw_behind(move |scope| {
scope.draw_round_rect(
Brush::solid(cranpose_ui_graphics::Color::WHITE),
CornerRadii::uniform(THUMB_HEIGHT * 0.5),
);
});
Box(thumb, BoxSpec::default(), || {});
if lens_progress.get() > 0.01 {
let node_w = LENS_WIDTH + LENS_PAD * 2.0;
let node_h = LENS_HEIGHT + LENS_PAD * 2.0;
let thumb_x_for_lens = thumb_x;
let lens_for_layer = lens_progress;
let last_x = remember(|| Rc::new(Cell::new(f32::NAN))).with(Rc::clone);
let lens = Modifier::empty()
.required_size(Size::new(node_w, node_h))
.offset(0.0, (TRACK_HEIGHT - node_h) * 0.5)
.graphics_layer(move || {
let x = thumb_x_for_lens.get();
let side = ((x - THUMB_MARGIN)
/ (TRACK_WIDTH - 2.0 * THUMB_MARGIN - THUMB_WIDTH))
.clamp(0.0, 1.0)
* 2.0
- 1.0;
GraphicsLayer {
translation_x: x + (THUMB_WIDTH - node_w) * 0.5 + side * LENS_LEAN,
alpha: (lens_for_layer.get() * 2.5).clamp(0.0, 1.0),
..Default::default()
}
})
.glass_effect_with(
Glass::lens().shape(LiquidShape::Capsule).no_clip(),
move || {
let grow = lens_for_layer.get().clamp(0.0, 1.2);
let w = THUMB_WIDTH + (LENS_WIDTH - THUMB_WIDTH) * grow;
let h = THUMB_HEIGHT + (LENS_HEIGHT - THUMB_HEIGHT) * grow;
let x = thumb_x_for_lens.get();
let prev = last_x.replace(x);
let vx = if prev.is_nan() { 0.0 } else { x - prev };
let bulge = (vx.abs() * 1.6).min(5.0);
let dir = if vx >= 0.0 { 0.0 } else { std::f32::consts::PI };
GlassDynamics {
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: bulge,
bulge_direction: dir,
}),
magnify_boost: 0.25,
..Default::default()
}
},
);
Box(lens, BoxSpec::default(), || {});
}
});
}