use crate::kurbo::{Circle, Point, Rect, RoundedRect, Shape, Size};
use crate::piet::{
FontBuilder, LinearGradient, RenderContext, Text, TextLayout, TextLayoutBuilder, UnitPoint,
};
use crate::theme;
use crate::{
BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, UpdateCtx,
Widget,
};
const SWITCH_PADDING: f64 = 3.;
const SWITCH_WIDTH_RATIO: f64 = 2.75;
#[derive(Debug, Clone, Default)]
pub struct Switch {
knob_pos: Point,
knob_hovered: bool,
knob_dragged: bool,
animation_in_progress: bool,
}
impl Switch {
pub fn new() -> Switch {
Self::default()
}
fn knob_hit_test(&self, knob_width: f64, mouse_pos: Point) -> bool {
let knob_circle = Circle::new(self.knob_pos, knob_width / 2.);
knob_circle.winding(mouse_pos) > 0
}
fn paint_labels(&mut self, ctx: &mut PaintCtx, env: &Env, switch_width: f64) {
let font_name = env.get(theme::FONT_NAME);
let font_size = env.get(theme::TEXT_SIZE_NORMAL);
let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT);
let knob_size = switch_height - 2. * SWITCH_PADDING;
let font = ctx
.text()
.new_font_by_name(font_name, font_size)
.build()
.unwrap();
let on_label_layout = ctx.text().new_text_layout(&font, "ON").build().unwrap();
let off_label_layout = ctx.text().new_text_layout(&font, "OFF").build().unwrap();
let mut on_label_origin = UnitPoint::LEFT.resolve(Rect::from_origin_size(
Point::ORIGIN,
Size::new(
(ctx.size().width - on_label_layout.width()).max(0.0),
switch_height + (font_size * 1.2) / 2.,
),
));
let mut off_label_origin = UnitPoint::LEFT.resolve(Rect::from_origin_size(
Point::ORIGIN,
Size::new(
(ctx.size().width - off_label_layout.width()).max(0.0),
switch_height + (font_size * 1.2) / 2.,
),
));
on_label_origin.y = on_label_origin.y.min(switch_height);
off_label_origin.y = off_label_origin.y.min(switch_height);
on_label_origin.x = self.knob_pos.x - switch_width + knob_size;
off_label_origin.x = switch_width - off_label_layout.width() - SWITCH_PADDING * 2.
+ self.knob_pos.x
- knob_size / 2.
- SWITCH_PADDING;
ctx.draw_text(
&on_label_layout,
on_label_origin,
&env.get(theme::LABEL_COLOR),
);
ctx.draw_text(
&off_label_layout,
off_label_origin,
&env.get(theme::LABEL_COLOR),
);
}
}
impl Widget<bool> for Switch {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut bool, env: &Env) {
let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT);
let switch_width = switch_height * SWITCH_WIDTH_RATIO;
let knob_size = switch_height - 2. * SWITCH_PADDING;
let on_pos = switch_width - knob_size / 2. - SWITCH_PADDING;
let off_pos = knob_size / 2. + SWITCH_PADDING;
match event {
Event::MouseDown(_) => {
ctx.set_active(true);
ctx.request_paint();
}
Event::MouseUp(_) => {
ctx.set_active(false);
if self.knob_dragged {
*data = self.knob_pos.x > switch_width / 2.;
} else {
*data = !*data;
}
ctx.request_paint();
self.knob_dragged = false;
self.animation_in_progress = true;
ctx.request_anim_frame();
}
Event::MouseMoved(mouse) => {
if ctx.is_active() {
self.knob_pos.x = mouse.pos.x.min(on_pos).max(off_pos);
self.knob_dragged = true;
}
if ctx.is_hot() {
self.knob_hovered = self.knob_hit_test(knob_size, mouse.pos)
}
ctx.request_paint();
}
_ => (),
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &bool, env: &Env) {
if let LifeCycle::AnimFrame(_) = event {
let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT);
let switch_width = switch_height * SWITCH_WIDTH_RATIO;
let knob_size = switch_height - 2. * SWITCH_PADDING;
let on_pos = switch_width - knob_size / 2. - SWITCH_PADDING;
let off_pos = knob_size / 2. + SWITCH_PADDING;
if self.animation_in_progress {
let delta = if *data { 2. } else { -2. };
self.knob_pos.x += delta;
if self.knob_pos.x > off_pos && self.knob_pos.x < on_pos {
ctx.request_anim_frame();
} else {
self.animation_in_progress = false;
}
}
}
}
fn update(&mut self, ctx: &mut UpdateCtx, old_data: &bool, data: &bool, _env: &Env) {
if old_data != data {
ctx.request_paint();
}
}
fn layout(
&mut self,
_layout_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &bool,
env: &Env,
) -> Size {
let width = env.get(theme::BORDERED_WIDGET_HEIGHT) * SWITCH_WIDTH_RATIO;
bc.constrain(Size::new(width, env.get(theme::BORDERED_WIDGET_HEIGHT)))
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &bool, env: &Env) {
let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT);
let switch_width = switch_height * SWITCH_WIDTH_RATIO;
let knob_size = switch_height - 2. * SWITCH_PADDING;
let on_pos = switch_width - knob_size / 2. - SWITCH_PADDING;
let off_pos = knob_size / 2. + SWITCH_PADDING;
let background_rect = RoundedRect::from_origin_size(
Point::ORIGIN,
Size::new(switch_width, switch_height).to_vec2(),
switch_height / 2.,
);
if !self.animation_in_progress && !self.knob_dragged {
if *data {
self.knob_pos.x = on_pos;
} else {
self.knob_pos.x = off_pos;
}
};
self.knob_pos = Point::new(self.knob_pos.x, knob_size / 2. + SWITCH_PADDING);
let knob_circle = Circle::new(self.knob_pos, knob_size / 2.);
let opacity = (self.knob_pos.x - off_pos) / (on_pos - off_pos);
let background_gradient_on_state = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::PRIMARY_LIGHT).with_alpha(opacity),
env.get(theme::PRIMARY_DARK).with_alpha(opacity),
),
);
let background_gradient_off_state = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::BACKGROUND_LIGHT).with_alpha(1. - opacity),
env.get(theme::BACKGROUND_DARK).with_alpha(1. - opacity),
),
);
ctx.stroke(background_rect, &env.get(theme::BORDER_DARK), 2.0);
ctx.fill(background_rect, &background_gradient_on_state);
ctx.fill(background_rect, &background_gradient_off_state);
ctx.clip(background_rect);
let is_active = ctx.is_active();
let is_hovered = self.knob_hovered;
let normal_knob_gradient = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::FOREGROUND_LIGHT),
env.get(theme::FOREGROUND_DARK),
),
);
let flipped_knob_gradient = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::FOREGROUND_DARK),
env.get(theme::FOREGROUND_LIGHT),
),
);
let knob_gradient = if is_active {
flipped_knob_gradient
} else {
normal_knob_gradient
};
let border_color = if is_hovered || is_active {
env.get(theme::FOREGROUND_LIGHT)
} else {
env.get(theme::FOREGROUND_DARK)
};
ctx.stroke(knob_circle, &border_color, 2.);
ctx.fill(knob_circle, &knob_gradient);
self.paint_labels(ctx, env, switch_width);
}
}