use crate::anim::{Motion, MotionComponent, Spring, run_motion};
use crate::draw::command::DrawCommand;
use crate::draw::renderer::Renderer;
use crate::ecs::{Entity, World};
use crate::event::GestureHandler;
use crate::event::gesture::GestureEvent;
use crate::types::{Color, Fixed, Rect};
use crate::widget::ComputedRect;
use crate::widget::dirty::Dirty;
use crate::widget::view::{View, ViewCtx};
pub struct Switch {
pub on: bool,
pub on_color: Color,
pub off_color: Color,
pub thumb_color: Color,
}
impl Switch {
pub fn new() -> Self {
Self {
on: false,
on_color: Color::rgb(63, 185, 80),
off_color: Color::rgb(80, 80, 100),
thumb_color: Color::rgb(255, 255, 255),
}
}
pub fn with_colors(mut self, on: Color, off: Color, thumb: Color) -> Self {
self.on_color = on;
self.off_color = off;
self.thumb_color = thumb;
self
}
pub fn toggle(&mut self) {
self.on = !self.on;
}
pub fn track_color(&self) -> Color {
if self.on {
self.on_color
} else {
self.off_color
}
}
}
impl Default for Switch {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct SwitchBgT(pub(crate) Fixed);
pub(crate) struct AnimatedThumbX(pub(crate) Fixed);
pub(crate) struct AnimateSwitchBgT(pub(crate) Motion);
impl MotionComponent for AnimateSwitchBgT {
fn motion(&self) -> &Motion {
&self.0
}
fn motion_mut(&mut self) -> &mut Motion {
&mut self.0
}
}
pub(crate) fn animate_switch_bg_t_system(world: &mut World) {
run_motion::<AnimateSwitchBgT>(world, |world, entity, value| {
if let Some(t) = world.get_mut::<SwitchBgT>(entity) {
t.0 = value;
} else {
world.insert(entity, SwitchBgT(value));
}
world.insert(entity, Dirty);
});
}
pub(crate) struct AnimateThumbX(pub(crate) Motion);
impl MotionComponent for AnimateThumbX {
fn motion(&self) -> &Motion {
&self.0
}
fn motion_mut(&mut self) -> &mut Motion {
&mut self.0
}
}
pub(crate) fn animate_thumb_x_system(world: &mut World) {
run_motion::<AnimateThumbX>(world, |world, entity, value| {
if let Some(x) = world.get_mut::<AnimatedThumbX>(entity) {
x.0 = value;
} else {
world.insert(entity, AnimatedThumbX(value));
}
world.insert(entity, Dirty);
});
}
const SWITCH_THUMB_MARGIN: Fixed = Fixed::from_int(3);
fn off_thumb_x(rect: &Rect) -> Fixed {
rect.x + SWITCH_THUMB_MARGIN
}
fn on_thumb_x(rect: &Rect) -> Fixed {
let thumb_size = (rect.h - SWITCH_THUMB_MARGIN * Fixed::from_int(2)).max(Fixed::ZERO);
rect.x + rect.w - thumb_size - SWITCH_THUMB_MARGIN
}
pub(crate) fn switch_init_system(world: &mut World) {
let entities: alloc::vec::Vec<Entity> = world.query::<Switch>().collect();
for e in entities {
if world.get::<AnimatedThumbX>(e).is_some() {
continue;
}
let Some(rect) = world.get::<ComputedRect>(e).map(|r| r.0) else {
continue;
};
let on = world.get::<Switch>(e).map(|s| s.on).unwrap_or(false);
let x = if on {
on_thumb_x(&rect)
} else {
off_thumb_x(&rect)
};
world.insert(e, AnimatedThumbX(x));
}
}
fn switch_render(
renderer: &mut dyn Renderer,
world: &World,
entity: Entity,
rect: &Rect,
ctx: &mut ViewCtx,
) {
let Some(s) = world.get::<Switch>(entity) else {
return;
};
let cap_radius = rect.h / Fixed::from_int(2);
let t = world
.get::<SwitchBgT>(entity)
.map(|x| x.0)
.unwrap_or_else(|| if s.on { Fixed::ONE } else { Fixed::ZERO });
let track_color = Color::lerp(s.off_color, s.on_color, t);
renderer.draw(
&DrawCommand::Fill {
area: *rect,
transform: ctx.transform,
quad: ctx.quad,
color: track_color,
radius: cap_radius,
opa: 255,
},
ctx.clip,
);
let thumb_size = (rect.h - SWITCH_THUMB_MARGIN * Fixed::from_int(2)).max(Fixed::ZERO);
let thumb_x = world
.get::<AnimatedThumbX>(entity)
.map(|x| x.0)
.unwrap_or_else(|| off_thumb_x(rect));
renderer.draw(
&DrawCommand::Fill {
area: Rect {
x: thumb_x,
y: rect.y + SWITCH_THUMB_MARGIN,
w: thumb_size,
h: thumb_size,
},
transform: ctx.transform,
quad: ctx.quad,
color: s.thumb_color,
radius: thumb_size / Fixed::from_int(2),
opa: 255,
},
ctx.clip,
);
}
pub(crate) fn switch_handler(world: &mut World, entity: Entity, event: &GestureEvent) -> bool {
if !matches!(event, GestureEvent::Tap { .. }) {
return false;
}
let on_now = {
let Some(s) = world.get_mut::<Switch>(entity) else {
return false;
};
s.toggle();
s.on
};
let target_t = if on_now { Fixed::ONE } else { Fixed::ZERO };
let cur_t = world
.get::<SwitchBgT>(entity)
.map(|t| t.0)
.unwrap_or_else(|| if on_now { Fixed::ZERO } else { Fixed::ONE });
world.insert(
entity,
AnimateSwitchBgT(Spring::new(cur_t, target_t, 250, Fixed::ZERO).into()),
);
if let Some(rect) = world.get::<ComputedRect>(entity).map(|r| r.0) {
let target_x = if on_now {
on_thumb_x(&rect)
} else {
off_thumb_x(&rect)
};
let cur_x = world
.get::<AnimatedThumbX>(entity)
.map(|x| x.0)
.unwrap_or_else(|| {
if on_now {
off_thumb_x(&rect)
} else {
on_thumb_x(&rect)
}
});
world.insert(
entity,
AnimateThumbX(Spring::new(cur_x, target_x, 200, Fixed::ZERO).into()),
);
}
world.insert(entity, Dirty);
true
}
fn switch_attach(world: &mut World, entity: Entity) {
if world.get::<Switch>(entity).is_none() {
return;
}
if world.get::<GestureHandler>(entity).is_some() {
return;
}
world.insert(
entity,
GestureHandler {
on_gesture: switch_handler,
},
);
let on = world.get::<Switch>(entity).map(|s| s.on).unwrap_or(false);
let initial_t = if on { Fixed::ONE } else { Fixed::ZERO };
world.insert(entity, SwitchBgT(initial_t));
}
pub fn view() -> View {
View::new("Switch", 60, switch_render)
.with_attach(switch_attach)
.with_systems(&[
switch_init_system,
animate_switch_bg_t_system,
animate_thumb_x_system,
])
}