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;
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
#[derive(Clone, Debug, Default)]
pub struct ToggleSwitchProps {
pub on: bool,
}
pub struct ToggleSwitch {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
pub props: ToggleSwitchProps,
state_cell: Option<Rc<Cell<bool>>>,
hovered: bool,
anim: crate::animation::Tween,
pressed: bool,
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(),
props: ToggleSwitchProps { on },
state_cell: None,
hovered: false,
anim: crate::animation::Tween::new(initial, ANIM_SECS),
pressed: false,
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.props.on
}
}
fn toggle(&mut self) {
let new_val = !self.is_on();
self.props.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
}
#[cfg(feature = "reflect")]
fn as_reflect(&self) -> Option<&dyn bevy_reflect::Reflect> {
Some(&self.props)
}
#[cfg(feature = "reflect")]
fn as_reflect_mut(&mut self) -> Option<&mut dyn bevy_reflect::Reflect> {
Some(&mut self.props)
}
fn is_focusable(&self) -> bool {
true
}
fn margin(&self) -> Insets {
self.base.margin
}
fn widget_base(&self) -> Option<&WidgetBase> {
Some(&self.base)
}
fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
Some(&mut self.base)
}
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 needs_draw(&self) -> bool {
if !self.is_visible() {
return false;
}
self.anim.is_animating()
|| self.press_anim.is_animating()
|| self.children().iter().any(|c| c.needs_draw())
}
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_draw();
return EventResult::Consumed;
}
EventResult::Ignored
}
Event::MouseDown {
button: MouseButton::Left,
..
} => {
self.pressed = true;
self.press_anim.set_target(1.0);
crate::animation::request_draw();
EventResult::Consumed
}
Event::MouseUp {
button: MouseButton::Left,
pos,
..
} => {
if self.hit_test(*pos) {
self.toggle();
}
self.pressed = false;
self.press_anim.set_target(0.0);
crate::animation::request_draw();
EventResult::Consumed
}
Event::KeyDown {
key: Key::Char(' '),
..
}
| Event::KeyDown {
key: Key::Enter, ..
} => {
self.toggle();
crate::animation::request_draw();
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
}
}