use std::time::Duration;
use tracing::{instrument, trace};
use crate::debug_state::DebugState;
use crate::kurbo::{Circle, Shape};
use crate::piet::{LinearGradient, RenderContext, UnitPoint};
use crate::widget::prelude::*;
use crate::{theme, ArcStr, Point, TextLayout};
const SWITCH_CHANGE_TIME: f64 = 0.2;
const SWITCH_PADDING: f64 = 3.;
const SWITCH_WIDTH_RATIO: f64 = 2.75;
#[derive(Debug, Clone)]
pub struct Switch {
knob_pos: Point,
knob_hovered: bool,
knob_dragged: bool,
animation_in_progress: bool,
on_text: TextLayout<ArcStr>,
off_text: TextLayout<ArcStr>,
}
impl Default for Switch {
fn default() -> Self {
Switch {
knob_pos: Point::ZERO,
knob_hovered: false,
knob_dragged: false,
animation_in_progress: false,
on_text: TextLayout::from_text("ON"),
off_text: TextLayout::from_text("OFF"),
}
}
}
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) {
self.on_text.rebuild_if_needed(ctx.text(), env);
self.off_text.rebuild_if_needed(ctx.text(), env);
let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT);
let knob_size = switch_height - 2. * SWITCH_PADDING;
let on_size = self.on_text.size();
let off_size = self.off_text.size();
let label_y = (switch_height - on_size.height).max(0.0) / 2.0;
let label_x_space = switch_width - knob_size - SWITCH_PADDING * 2.0;
let off_pos = knob_size / 2. + SWITCH_PADDING;
let knob_delta = self.knob_pos.x - off_pos;
let on_label_width = on_size.width;
let on_base_x_pos =
-on_label_width - (label_x_space - on_label_width) / 2.0 + SWITCH_PADDING;
let on_label_origin = Point::new(on_base_x_pos + knob_delta, label_y);
let off_base_x_pos = knob_size + (label_x_space - off_size.width) / 2.0 + SWITCH_PADDING;
let off_label_origin = Point::new(off_base_x_pos + knob_delta, label_y);
self.on_text.draw(ctx, on_label_origin);
self.off_text.draw(ctx, off_label_origin);
}
}
impl Widget<bool> for Switch {
#[instrument(name = "Switch", level = "trace", skip(self, ctx, event, data, env))]
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(_) => {
if !ctx.is_disabled() {
ctx.set_active(true);
ctx.request_paint();
}
}
Event::MouseUp(_) => {
if !ctx.is_disabled() {
if self.knob_dragged {
*data = self.knob_pos.x > switch_width / 2.;
} else if ctx.is_active() {
*data = !*data;
}
}
ctx.set_active(false);
self.knob_dragged = false;
self.animation_in_progress = true;
ctx.request_anim_frame();
}
Event::MouseMove(mouse) => {
if !ctx.is_disabled() {
if ctx.is_active() {
self.knob_pos.x = mouse.pos.x.clamp(off_pos, on_pos);
self.knob_dragged = true;
}
if ctx.is_hot() {
self.knob_hovered = self.knob_hit_test(knob_size, mouse.pos)
}
ctx.request_paint();
} else {
ctx.set_active(false);
}
}
Event::AnimFrame(interval) => {
let delta = Duration::from_nanos(*interval).as_secs_f64();
if self.animation_in_progress {
let change_time = if *data {
SWITCH_CHANGE_TIME
} else {
-SWITCH_CHANGE_TIME
};
let change = (switch_width / change_time) * delta;
self.knob_pos.x = (self.knob_pos.x + change).clamp(off_pos, on_pos);
if (self.knob_pos.x > off_pos && !*data) || (self.knob_pos.x < on_pos && *data)
{
ctx.request_anim_frame();
} else {
self.animation_in_progress = false;
}
ctx.request_paint();
}
}
_ => (),
}
}
#[instrument(name = "Switch", level = "trace", skip(self, ctx, event, _data, env))]
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &bool, env: &Env) {
match event {
LifeCycle::WidgetAdded => {
self.on_text.rebuild_if_needed(ctx.text(), env);
self.off_text.rebuild_if_needed(ctx.text(), env);
}
LifeCycle::DisabledChanged(true) if self.knob_dragged => {
self.knob_dragged = false;
self.animation_in_progress = true;
ctx.request_anim_frame();
}
LifeCycle::DisabledChanged(disabled) => {
ctx.request_paint();
let color = if *disabled {
theme::DISABLED_TEXT_COLOR
} else {
theme::TEXT_COLOR
};
self.off_text.set_text_color(color.clone());
self.on_text.set_text_color(color);
ctx.request_paint();
}
_ => {}
}
}
#[instrument(
name = "Switch",
level = "trace",
skip(self, ctx, old_data, data, _env)
)]
fn update(&mut self, ctx: &mut UpdateCtx, old_data: &bool, data: &bool, _env: &Env) {
if old_data != data {
self.animation_in_progress = true;
ctx.request_anim_frame();
}
}
#[instrument(name = "Switch", level = "trace", skip(self, ctx, bc, _data, env))]
fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &bool,
env: &Env,
) -> Size {
self.on_text.rebuild_if_needed(ctx.text(), env);
let text_metrics = self.on_text.layout_metrics();
let height = env.get(theme::BORDERED_WIDGET_HEIGHT);
let width = height * SWITCH_WIDTH_RATIO;
let label_y = (height - text_metrics.size.height).max(0.0) / 2.0;
let text_bottom_padding = height - (text_metrics.size.height + label_y);
let text_baseline_offset = text_metrics.size.height - text_metrics.first_baseline;
ctx.set_baseline_offset(text_bottom_padding + text_baseline_offset);
let size = bc.constrain(Size::new(width, height));
trace!("Computed size: {}", size);
size
}
#[instrument(name = "Switch", level = "trace", skip(self, ctx, data, env))]
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 stroke_width = 2.0;
let background_rect = Size::new(switch_width, switch_height)
.to_rect()
.inset(-stroke_width / 2.0)
.to_rounded_rect(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 = if ctx.is_disabled() {
0.0
} else {
(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), stroke_width);
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 knob_gradient = if ctx.is_disabled() {
LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::DISABLED_FOREGROUND_LIGHT),
env.get(theme::DISABLED_FOREGROUND_DARK),
),
)
} else if is_active {
LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::FOREGROUND_DARK),
env.get(theme::FOREGROUND_LIGHT),
),
)
} else {
LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::FOREGROUND_LIGHT),
env.get(theme::FOREGROUND_DARK),
),
)
};
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);
}
fn debug_state(&self, data: &bool) -> DebugState {
DebugState {
display_name: self.short_type_name().to_string(),
main_value: data.to_string(),
..Default::default()
}
}
}