use std::cell::Cell;
use std::rc::Rc;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, Key, MouseButton};
use crate::geometry::{Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::widget::Widget;
const PILL_W: f64 = 32.0;
const PILL_H: f64 = 18.0;
const PILL_R: f64 = PILL_H / 2.0;
const CIRCLE_MARGIN: f64 = 2.5;
const CIRCLE_R: f64 = PILL_H / 2.0 - CIRCLE_MARGIN;
const ANIM_SECS: f64 = 0.14;
const PILL_HALO: f64 = 1.0;
const RING_MAX_R: f64 = CIRCLE_R * 2.4;
const RING_PEAK_ALPHA: f32 = 0.20;
const RING_ANIM_SECS: f64 = 0.22;
pub struct ToggleSwitch {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
on: bool,
state_cell: Option<Rc<Cell<bool>>>,
hovered: bool,
anim: crate::animation::Tween,
press_anim: crate::animation::Tween,
on_change: Option<Box<dyn FnMut(bool)>>,
}
impl ToggleSwitch {
pub fn new(on: bool) -> Self {
let initial = if on { 1.0 } else { 0.0 };
Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
on,
state_cell: None,
hovered: false,
anim: crate::animation::Tween::new(initial, ANIM_SECS),
press_anim: crate::animation::Tween::new(0.0, RING_ANIM_SECS),
on_change: None,
}
}
pub fn with_state_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
self.state_cell = Some(cell);
self
}
pub fn on_change(mut self, cb: impl FnMut(bool) + 'static) -> Self {
self.on_change = Some(Box::new(cb));
self
}
pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
pub fn is_on(&self) -> bool {
if let Some(ref cell) = self.state_cell { cell.get() } else { self.on }
}
fn toggle(&mut self) {
let new_val = !self.is_on();
self.on = new_val;
if let Some(ref cell) = self.state_cell { cell.set(new_val); }
if let Some(cb) = self.on_change.as_mut() { cb(new_val); }
}
fn circle_cx_at(t: f64) -> f64 {
let x_off = PILL_HALO + CIRCLE_MARGIN + CIRCLE_R;
let x_on = PILL_HALO + PILL_W - CIRCLE_MARGIN - CIRCLE_R;
x_off + (x_on - x_off) * t.clamp(0.0, 1.0)
}
}
fn lerp_color(a: Color, b: Color, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
Color::rgba(
a.r + (b.r - a.r) * t,
a.g + (b.g - a.g) * t,
a.b + (b.b - a.b) * t,
a.a + (b.a - a.a) * t,
)
}
impl Widget for ToggleSwitch {
fn type_name(&self) -> &'static str { "ToggleSwitch" }
fn bounds(&self) -> Rect { self.bounds }
fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
fn children(&self) -> &[Box<dyn Widget>] { &self.children }
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
fn is_focusable(&self) -> bool { true }
fn margin(&self) -> Insets { self.base.margin }
fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
fn min_size(&self) -> Size { self.base.min_size }
fn max_size(&self) -> Size { self.base.max_size }
fn layout(&mut self, _available: Size) -> Size {
Size::new(PILL_W + 2.0 * PILL_HALO, PILL_H + 2.0 * PILL_HALO)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let v = ctx.visuals();
self.anim.set_target(if self.is_on() { 1.0 } else { 0.0 });
let t = self.anim.tick();
let pill_x = PILL_HALO;
let pill_y = PILL_HALO;
let off_color = v.widget_stroke;
let on_color = v.accent;
let mut bg = lerp_color(off_color, on_color, t as f32);
if self.hovered {
let hover_off = v.widget_bg_hovered;
let hover_on = v.accent_hovered;
bg = lerp_color(hover_off, hover_on, t as f32);
}
ctx.set_fill_color(bg);
ctx.begin_path();
ctx.rounded_rect(pill_x, pill_y, PILL_W, PILL_H, PILL_R);
ctx.fill();
let cx = Self::circle_cx_at(t);
let cy = PILL_HALO + PILL_H * 0.5;
ctx.set_fill_color(Color::white());
ctx.begin_path();
ctx.circle(cx, cy, CIRCLE_R);
ctx.fill();
}
fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
let ring_t = self.press_anim.tick();
if ring_t <= 0.001 { return; }
let v = ctx.visuals();
let cx = Self::circle_cx_at(self.anim.value());
let cy = PILL_HALO + PILL_H * 0.5;
let toggle_color = if self.is_on() { v.accent } else { v.widget_stroke };
let alpha = RING_PEAK_ALPHA * (ring_t as f32);
ctx.save();
ctx.reset_clip();
ctx.set_fill_color(Color::rgba(
toggle_color.r, toggle_color.g, toggle_color.b, alpha));
ctx.begin_path();
ctx.circle(cx, cy, RING_MAX_R * ring_t);
ctx.fill();
ctx.restore();
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
if was != self.hovered { crate::animation::request_tick(); }
EventResult::Ignored
}
Event::MouseDown { button: MouseButton::Left, .. } => {
self.press_anim.set_target(1.0);
crate::animation::request_tick();
EventResult::Consumed
}
Event::MouseUp { button: MouseButton::Left, pos, .. } => {
if self.hit_test(*pos) { self.toggle(); }
self.press_anim.set_target(0.0);
crate::animation::request_tick();
EventResult::Consumed
}
Event::KeyDown { key: Key::Char(' '), .. }
| Event::KeyDown { key: Key::Enter, .. } => {
self.toggle();
crate::animation::request_tick();
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}
fn hit_test(&self, local_pos: crate::geometry::Point) -> bool {
local_pos.x >= PILL_HALO && local_pos.x <= PILL_HALO + PILL_W
&& local_pos.y >= PILL_HALO && local_pos.y <= PILL_HALO + PILL_H
}
}