use blinc_animation::{get_scheduler, AnimatedValue, SchedulerHandle, SpringConfig};
use blinc_core::{Color, State, Transform};
use blinc_layout::div::ElementTypeId;
use blinc_layout::element::RenderProps;
use blinc_layout::motion::SharedAnimatedValue;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_theme::{ColorToken, ThemeState};
use std::sync::{Arc, Mutex};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SwitchSize {
Small,
#[default]
Medium,
Large,
}
impl SwitchSize {
fn track_width(&self) -> f32 {
match self {
SwitchSize::Small => 32.0,
SwitchSize::Medium => 44.0,
SwitchSize::Large => 52.0,
}
}
fn track_height(&self) -> f32 {
match self {
SwitchSize::Small => 18.0,
SwitchSize::Medium => 24.0,
SwitchSize::Large => 28.0,
}
}
fn thumb_size(&self) -> f32 {
match self {
SwitchSize::Small => 14.0,
SwitchSize::Medium => 20.0,
SwitchSize::Large => 24.0,
}
}
}
pub struct Switch {
inner: Div,
}
impl Switch {
pub fn new(on_state: &State<bool>) -> Self {
Self::with_config(SwitchConfig::new(on_state.clone()))
}
fn with_config(config: SwitchConfig) -> Self {
let theme = ThemeState::get();
let track_width = config.size.track_width();
let track_height = config.size.track_height();
let thumb_size = config.size.thumb_size();
let padding = 2.0; let thumb_travel = track_width - thumb_size - (padding * 2.0);
let radius = track_height / 2.0;
let on_bg = config
.on_color
.unwrap_or_else(|| theme.color(ColorToken::Primary));
let off_bg = config
.off_color
.unwrap_or_else(|| theme.color(ColorToken::Border));
let thumb_color = config
.thumb_color
.unwrap_or_else(|| theme.color(ColorToken::TextInverse));
let disabled = config.disabled;
let on_change = config.on_change.clone();
let on_state = config.on_state.clone();
let on_state_for_click = config.on_state.clone();
let thumb_anim = config.thumb_anim.clone();
let thumb_anim_for_click = config.thumb_anim.clone();
let color_anim = config.color_anim.clone();
let color_anim_for_click = config.color_anim.clone();
let off_layer = div()
.class("cn-switch-track")
.absolute()
.inset(0.0)
.rounded(radius)
.bg(off_bg);
let on_track_div = div()
.class("cn-switch-track")
.class("cn-switch-track--on")
.w(track_width)
.h(track_height)
.rounded(radius)
.bg(on_bg);
let on_layer = div()
.absolute()
.inset(0.0)
.child(motion().opacity(color_anim).child(on_track_div));
let thumb_element = div()
.class("cn-switch-thumb")
.w(thumb_size)
.h(thumb_size)
.rounded(thumb_size / 2.0)
.bg(thumb_color);
let animated_thumb = motion().translate_x(thumb_anim).child(thumb_element);
let mut switch = div()
.class("cn-switch")
.w(track_width)
.h(track_height)
.rounded(radius)
.cursor_pointer()
.relative()
.items_center()
.padding_x(blinc_layout::units::px(padding))
.child(off_layer)
.child(on_layer)
.child(animated_thumb);
if disabled {
switch = switch.class("cn-switch--disabled").opacity(0.5);
}
switch = switch.on_click(move |_| {
if disabled {
return;
}
let current = on_state_for_click.get();
let new_value = !current;
on_state_for_click.set(new_value);
let thumb_target = if new_value { thumb_travel } else { 0.0 };
let color_target = if new_value { 1.0 } else { 0.0 };
thumb_anim_for_click
.lock()
.unwrap()
.set_target(thumb_target);
color_anim_for_click
.lock()
.unwrap()
.set_target(color_target);
if let Some(ref callback) = on_change {
callback(new_value);
}
});
let inner = if let Some(ref label_text) = config.label {
let label_color = if disabled {
theme.color(ColorToken::TextTertiary)
} else {
theme.color(ColorToken::TextPrimary)
};
div()
.flex_row()
.gap(theme.spacing().space_1)
.items_center()
.cursor_pointer()
.child(switch)
.child(text(label_text).size(14.0).color(label_color))
} else {
div().child(switch)
};
Self { inner }
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for Switch {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> ElementTypeId {
self.inner.element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
#[derive(Clone)]
struct SwitchConfig {
on_state: State<bool>,
size: SwitchSize,
label: Option<String>,
disabled: bool,
on_color: Option<Color>,
off_color: Option<Color>,
thumb_color: Option<Color>,
thumb_anim: SharedAnimatedValue,
color_anim: SharedAnimatedValue,
spring_config: SpringConfig,
on_change: Option<Arc<dyn Fn(bool) + Send + Sync>>,
}
impl SwitchConfig {
fn new(on_state: State<bool>) -> Self {
let size = SwitchSize::default();
let padding = 2.0;
let thumb_travel = size.track_width() - size.thumb_size() - (padding * 2.0);
let is_on = on_state.get();
let initial_x = if is_on { thumb_travel } else { 0.0 };
let initial_color_t = if is_on { 1.0 } else { 0.0 };
let scheduler = get_scheduler();
let spring_config = SpringConfig::snappy();
let thumb_anim: SharedAnimatedValue = Arc::new(Mutex::new(AnimatedValue::new(
scheduler.clone(),
initial_x,
spring_config,
)));
let color_anim: SharedAnimatedValue = Arc::new(Mutex::new(AnimatedValue::new(
scheduler,
initial_color_t,
spring_config,
)));
Self {
on_state,
size,
label: None,
disabled: false,
on_color: None,
off_color: None,
thumb_color: None,
thumb_anim,
color_anim,
spring_config,
on_change: None,
}
}
}
pub struct SwitchBuilder {
config: SwitchConfig,
built: std::cell::OnceCell<Switch>,
}
impl SwitchBuilder {
pub fn new(on_state: &State<bool>) -> Self {
Self {
config: SwitchConfig::new(on_state.clone()),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Switch {
self.built
.get_or_init(|| Switch::with_config(self.config.clone()))
}
pub fn size(mut self, size: SwitchSize) -> Self {
self.config.size = size;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.config.label = Some(label.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.config.disabled = disabled;
self
}
pub fn on_color(mut self, color: impl Into<Color>) -> Self {
self.config.on_color = Some(color.into());
self
}
pub fn off_color(mut self, color: impl Into<Color>) -> Self {
self.config.off_color = Some(color.into());
self
}
pub fn thumb_color(mut self, color: impl Into<Color>) -> Self {
self.config.thumb_color = Some(color.into());
self
}
pub fn spring(mut self, config: SpringConfig) -> Self {
self.config.spring_config = config;
self
}
pub fn on_change<F>(mut self, callback: F) -> Self
where
F: Fn(bool) + Send + Sync + 'static,
{
self.config.on_change = Some(Arc::new(callback));
self
}
pub fn build_component(self) -> Switch {
Switch::with_config(self.config)
}
}
impl ElementBuilder for SwitchBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn element_type_id(&self) -> ElementTypeId {
self.get_or_build().element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn switch(state: &State<bool>) -> SwitchBuilder {
SwitchBuilder::new(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_switch_sizes() {
assert_eq!(SwitchSize::Small.track_width(), 32.0);
assert_eq!(SwitchSize::Medium.track_width(), 44.0);
assert_eq!(SwitchSize::Large.track_width(), 52.0);
}
#[test]
fn test_switch_track_heights() {
assert_eq!(SwitchSize::Small.track_height(), 18.0);
assert_eq!(SwitchSize::Medium.track_height(), 24.0);
assert_eq!(SwitchSize::Large.track_height(), 28.0);
}
#[test]
fn test_switch_thumb_sizes() {
assert_eq!(SwitchSize::Small.thumb_size(), 14.0);
assert_eq!(SwitchSize::Medium.thumb_size(), 20.0);
assert_eq!(SwitchSize::Large.thumb_size(), 24.0);
}
#[test]
fn test_thumb_travel() {
let size = SwitchSize::Medium;
let padding = 2.0;
let travel = size.track_width() - size.thumb_size() - (padding * 2.0);
assert_eq!(travel, 20.0);
}
}