use std::f64::EPSILON;
use std::time::Duration;
use tracing::{instrument, trace};
use crate::debug_state::DebugState;
use crate::kurbo::BezPath;
use crate::piet::{LinearGradient, RenderContext, UnitPoint};
use crate::widget::prelude::*;
use crate::{theme, Point, Rect, TimerToken};
const STEPPER_REPEAT_DELAY: Duration = Duration::from_millis(500);
const STEPPER_REPEAT: Duration = Duration::from_millis(200);
pub struct Stepper {
max: f64,
min: f64,
step: f64,
wrap: bool,
increase_active: bool,
decrease_active: bool,
timer_id: TimerToken,
}
impl Stepper {
pub fn new() -> Self {
Stepper {
max: std::f64::MAX,
min: std::f64::MIN,
step: 1.0,
wrap: false,
increase_active: false,
decrease_active: false,
timer_id: TimerToken::INVALID,
}
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.min = min;
self.max = max;
self
}
pub fn with_step(mut self, step: f64) -> Self {
self.step = step;
self
}
pub fn with_wraparound(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
fn increment(&mut self, data: &mut f64) {
let next = *data + self.step;
let was_greater = *data + EPSILON >= self.max;
let is_greater = next + EPSILON > self.max;
*data = match (self.wrap, was_greater, is_greater) {
(true, true, true) => self.min,
(true, false, true) => self.max,
(false, _, true) => self.max,
_ => next,
}
}
fn decrement(&mut self, data: &mut f64) {
let next = *data - self.step;
let was_less = *data - EPSILON <= self.min;
let is_less = next - EPSILON < self.min;
*data = match (self.wrap, was_less, is_less) {
(true, true, true) => self.max,
(true, false, true) => self.min,
(false, _, true) => self.min,
_ => next,
}
}
}
impl Default for Stepper {
fn default() -> Self {
Self::new()
}
}
impl Widget<f64> for Stepper {
#[instrument(name = "Stepper", level = "trace", skip(self, ctx, _data, env))]
fn paint(&mut self, ctx: &mut PaintCtx, _data: &f64, env: &Env) {
let stroke_width = 2.0;
let rounded_rect = ctx
.size()
.to_rect()
.inset(-stroke_width / 2.0)
.to_rounded_rect(4.0);
let height = ctx.size().height;
let width = env.get(theme::BASIC_WIDGET_HEIGHT);
let button_size = Size::new(width, height / 2.);
ctx.stroke(rounded_rect, &env.get(theme::BORDER_DARK), stroke_width);
ctx.clip(rounded_rect);
let increase_button_origin = Point::ORIGIN;
let decrease_button_origin = Point::new(0., height / 2.0);
let increase_button_rect = Rect::from_origin_size(increase_button_origin, button_size);
let decrease_button_rect = Rect::from_origin_size(decrease_button_origin, button_size);
let disabled_gradient = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::DISABLED_BUTTON_LIGHT),
env.get(theme::DISABLED_BUTTON_DARK),
),
);
let active_gradient = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(env.get(theme::PRIMARY_LIGHT), env.get(theme::PRIMARY_DARK)),
);
let inactive_gradient = LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(env.get(theme::BUTTON_DARK), env.get(theme::BUTTON_LIGHT)),
);
if ctx.is_disabled() {
ctx.fill(increase_button_rect, &disabled_gradient);
} else if self.increase_active {
ctx.fill(increase_button_rect, &active_gradient);
} else {
ctx.fill(increase_button_rect, &inactive_gradient);
};
if ctx.is_disabled() {
ctx.fill(decrease_button_rect, &disabled_gradient);
} else if self.decrease_active {
ctx.fill(decrease_button_rect, &active_gradient);
} else {
ctx.fill(decrease_button_rect, &inactive_gradient);
};
let mut arrows = BezPath::new();
arrows.move_to(Point::new(4., height / 2. - 4.));
arrows.line_to(Point::new(width - 4., height / 2. - 4.));
arrows.line_to(Point::new(width / 2., 4.));
arrows.close_path();
arrows.move_to(Point::new(4., height / 2. + 4.));
arrows.line_to(Point::new(width - 4., height / 2. + 4.));
arrows.line_to(Point::new(width / 2., height - 4.));
arrows.close_path();
let color = if ctx.is_disabled() {
env.get(theme::DISABLED_TEXT_COLOR)
} else {
env.get(theme::TEXT_COLOR)
};
ctx.fill(arrows, &color);
}
#[instrument(
name = "Stepper",
level = "trace",
skip(self, _layout_ctx, bc, _data, env)
)]
fn layout(
&mut self,
_layout_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &f64,
env: &Env,
) -> Size {
let size = bc.constrain(Size::new(
env.get(theme::BASIC_WIDGET_HEIGHT),
env.get(theme::BORDERED_WIDGET_HEIGHT),
));
trace!("Computed size: {}", size);
size
}
#[instrument(name = "Stepper", level = "trace", skip(self, ctx, event, data, env))]
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut f64, env: &Env) {
let height = env.get(theme::BORDERED_WIDGET_HEIGHT);
match event {
Event::MouseDown(mouse) => {
if !ctx.is_disabled() {
ctx.set_active(true);
if mouse.pos.y > height / 2. {
self.decrease_active = true;
self.decrement(data);
} else {
self.increase_active = true;
self.increment(data);
}
self.timer_id = ctx.request_timer(STEPPER_REPEAT_DELAY);
ctx.request_paint();
}
}
Event::MouseUp(_) => {
ctx.set_active(false);
self.decrease_active = false;
self.increase_active = false;
self.timer_id = TimerToken::INVALID;
ctx.request_paint();
}
Event::Timer(id) if *id == self.timer_id => {
if !ctx.is_disabled() {
if self.increase_active {
self.increment(data);
}
if self.decrease_active {
self.decrement(data);
}
self.timer_id = ctx.request_timer(STEPPER_REPEAT);
} else {
ctx.set_active(false);
}
}
_ => (),
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f64, _env: &Env) {
if let LifeCycle::DisabledChanged(_) = event {
ctx.request_paint();
}
}
#[instrument(
name = "Stepper",
level = "trace",
skip(self, ctx, old_data, data, _env)
)]
fn update(&mut self, ctx: &mut UpdateCtx, old_data: &f64, data: &f64, _env: &Env) {
if (*data - old_data).abs() > EPSILON {
ctx.request_paint();
}
}
fn debug_state(&self, data: &f64) -> DebugState {
DebugState {
display_name: self.short_type_name().to_string(),
main_value: data.to_string(),
..Default::default()
}
}
}