use druid::{
debug_state::DebugState,
kurbo::{Circle, Line, PathEl, Shape},
piet::{PietText, PietTextLayout, Text, TextLayout, TextLayoutBuilder},
theme,
theme::TEXT_COLOR,
widget::{prelude::*, Axis},
Color, Point, Rect, Vec2, WidgetPod,
};
use tracing::{instrument, trace, warn};
const TRACK_THICKNESS: f64 = 4.0;
const KNOB_STROKE_WIDTH: f64 = 2.0;
#[derive(Debug, Clone, Default)]
pub struct Slider {
mapping: SliderValueMapping,
knob: SliderKnob,
knob_style: KnobStyle,
}
#[derive(Debug, Clone, Default)]
pub struct RangeSlider {
mapping: SliderValueMapping,
left_knob: SliderKnob,
right_knob: SliderKnob,
knob_style: KnobStyle,
}
pub struct Annotated<T, W: Widget<T>> {
inner: WidgetPod<T, W>,
mapping: SliderValueMapping,
labeled_steps: f64,
unlabeled_steps: f64,
labels: Vec<PietTextLayout>,
}
#[derive(Copy, Clone, Debug)]
pub struct SliderValueMapping {
min: f64,
max: f64,
step: Option<f64>,
axis: Axis,
}
#[derive(Debug, Clone, Default)]
struct SliderKnob {
hovered: bool,
active: bool,
inner_circle_target_radius: f64,
inner_circle_current_radius: f64,
offset: f64,
}
#[derive(Debug, Copy, Clone)]
pub enum KnobStyle {
Circle,
Wedge,
}
impl Default for KnobStyle {
fn default() -> Self {
Self::Circle
}
}
impl Slider {
pub fn new() -> Slider {
Default::default()
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.mapping.min = min;
self.mapping.max = max;
self
}
pub fn with_step(mut self, step: f64) -> Self {
if step < 0.0 {
warn!("bad stepping (must be positive): {}", step);
return self;
}
self.mapping.step = if step > 0.0 {
Some(step)
} else {
None
};
self
}
pub fn knob_style(mut self, knob_style: KnobStyle) -> Self {
self.knob_style = knob_style;
self
}
pub fn axis(mut self, axis: Axis) -> Self {
self.mapping.axis = axis;
self
}
pub fn get_mapping(&self) -> SliderValueMapping {
self.mapping
}
pub fn annotated(self, named_steps: f64, unnamed_steps: f64) -> Annotated<f64, Self> {
let mapping = self.mapping;
Annotated::new(self, mapping, named_steps, unnamed_steps)
}
}
impl Widget<f64> for Slider {
#[instrument(name = "Slider", level = "trace", skip(self, ctx, event, data, env))]
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut f64, env: &Env) {
if !ctx.is_disabled() {
self.knob
.handle_input(ctx, event, data, env, self.mapping, self.knob_style);
ctx.set_active(self.knob.is_active());
if let Event::MouseDown(me) = event {
if !self.knob.active {
self.knob.activate(0.0);
let knob_size = 22.;
*data = self
.mapping
.calculate_value(me.pos, knob_size, ctx.size(), 0.0);
ctx.request_paint();
ctx.set_active(true);
}
}
}
}
#[instrument(name = "Slider", level = "trace", skip(self, ctx, event, _data, _env))]
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f64, _env: &Env) {
match event {
LifeCycle::WidgetAdded => {
self.mapping.check_range();
self.knob.inner_circle_target_radius = 6.;
self.knob.inner_circle_current_radius = 6.;
}
LifeCycle::DisabledChanged(_) => ctx.request_paint(),
_ => (),
}
}
#[instrument(
name = "Slider",
level = "trace",
skip(self, ctx, _old_data, _data, _env)
)]
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &f64, _data: &f64, _env: &Env) {
ctx.request_paint();
}
#[instrument(name = "Slider", level = "trace", skip(self, ctx, bc, _data, env))]
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &f64, env: &Env) -> Size {
bc.debug_check("Slider");
slider_layout(ctx, bc, env, self.mapping)
}
#[instrument(name = "Slider", level = "trace", skip(self, ctx, data, env))]
fn paint(&mut self, ctx: &mut PaintCtx, data: &f64, env: &Env) {
paint_slider_background(ctx, 0.0, *data, self.mapping, env);
self.knob
.paint(ctx, *data, env, self.mapping, self.knob_style);
}
fn debug_state(&self, data: &f64) -> DebugState {
DebugState {
display_name: self.short_type_name().to_string(),
main_value: data.to_string(),
..Default::default()
}
}
}
impl RangeSlider {
pub fn new() -> RangeSlider {
Default::default()
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.mapping.min = min;
self.mapping.max = max;
self
}
pub fn with_step(mut self, step: f64) -> Self {
if step < 0.0 {
warn!("bad stepping (must be positive): {}", step);
return self;
}
self.mapping.step = if step > 0.0 {
Some(step)
} else {
None
};
self
}
pub fn knob_style(mut self, knob_style: KnobStyle) -> Self {
self.knob_style = knob_style;
self
}
pub fn axis(mut self, axis: Axis) -> Self {
self.mapping.axis = axis;
self
}
pub fn get_mapping(&self) -> SliderValueMapping {
self.mapping
}
pub fn annotated(self, named_steps: f64, unnamed_steps: f64) -> Annotated<(f64, f64), Self> {
let mapping = self.mapping;
Annotated::new(self, mapping, named_steps, unnamed_steps)
}
}
impl Widget<(f64, f64)> for RangeSlider {
#[instrument(
name = "RangeSlider",
level = "trace",
skip(self, ctx, event, data, env)
)]
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut (f64, f64), env: &Env) {
if !ctx.is_disabled() {
if !self.right_knob.is_active() {
self.left_knob.handle_input(
ctx,
event,
&mut data.0,
env,
self.mapping,
self.knob_style,
);
data.0 = data.0.min(data.1);
if self.left_knob.is_active() {
self.right_knob.deactivate();
}
}
if !self.left_knob.is_active() {
self.right_knob.handle_input(
ctx,
event,
&mut data.1,
env,
self.mapping,
self.knob_style,
);
data.1 = data.1.max(data.0);
if self.right_knob.is_active() {
self.left_knob.deactivate();
}
}
ctx.set_active(self.left_knob.is_active() || self.right_knob.is_active());
if let Event::MouseDown(me) = event {
if !self.left_knob.is_active() && !self.right_knob.is_active() {
let knob_size = 22.;
let press_value =
self.mapping
.calculate_value(me.pos, knob_size, ctx.size(), 0.0);
if press_value - data.0 < data.1 - press_value {
self.left_knob.activate(0.0);
data.0 = press_value;
} else {
self.right_knob.activate(0.0);
data.1 = press_value;
}
ctx.set_active(true);
ctx.request_paint();
}
}
}
}
#[instrument(
name = "RangeSlider",
level = "trace",
skip(self, ctx, event, _data, _env)
)]
fn lifecycle(
&mut self,
ctx: &mut LifeCycleCtx,
event: &LifeCycle,
_data: &(f64, f64),
_env: &Env,
) {
match event {
LifeCycle::WidgetAdded => {
self.mapping.check_range();
self.left_knob.inner_circle_target_radius = 6.;
self.left_knob.inner_circle_current_radius = 6.;
self.right_knob.inner_circle_target_radius = 6.;
self.right_knob.inner_circle_current_radius = 6.;
}
LifeCycle::DisabledChanged(_) => ctx.request_paint(),
_ => (),
}
}
#[instrument(
name = "RangeSlider",
level = "trace",
skip(self, ctx, _old_data, _data, _env)
)]
fn update(
&mut self,
ctx: &mut UpdateCtx,
_old_data: &(f64, f64),
_data: &(f64, f64),
_env: &Env,
) {
ctx.request_paint();
}
#[instrument(name = "RangeSlider", level = "trace", skip(self, ctx, bc, _data, env))]
fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &(f64, f64),
env: &Env,
) -> Size {
bc.debug_check("Slider");
slider_layout(ctx, bc, env, self.mapping)
}
#[instrument(name = "RangeSlider", level = "trace", skip(self, ctx, data, env))]
fn paint(&mut self, ctx: &mut PaintCtx, data: &(f64, f64), env: &Env) {
paint_slider_background(ctx, data.0, data.1, self.mapping, env);
self.right_knob
.paint(ctx, data.1, env, self.mapping, self.knob_style);
self.left_knob
.paint(ctx, data.0, env, self.mapping, self.knob_style);
}
fn debug_state(&self, data: &(f64, f64)) -> DebugState {
DebugState {
display_name: self.short_type_name().to_string(),
main_value: format!("{data:?}"),
..Default::default()
}
}
}
impl<T, W: Widget<T>> Annotated<T, W> {
pub fn new(
inner: W,
mapping: SliderValueMapping,
labeled_steps: f64,
unlabeled_steps: f64,
) -> Self {
Annotated {
inner: WidgetPod::new(inner),
mapping,
labeled_steps: labeled_steps.abs(),
unlabeled_steps: unlabeled_steps.abs(),
labels: Vec::new(),
}
}
fn sanitise_values(&mut self) {
let labeled = self.mapping.range() / self.labeled_steps;
if !labeled.is_finite() || labeled > 100.0 {
warn!("Annotated: provided labeled interval \"{}\" has too many steps inside the sliders range {}..{}", self.labeled_steps, self.mapping.min, self.mapping.max);
self.labeled_steps = self.mapping.range() / 5.0;
}
let unlabeled = self.mapping.range() / self.unlabeled_steps;
if !unlabeled.is_finite() || unlabeled > 10000.0 {
warn!("Annotated: provided unlabeled interval \"{}\" has too many steps inside the sliders range {}..{}", self.unlabeled_steps, self.mapping.min, self.mapping.max);
self.unlabeled_steps = self.mapping.range() / 20.0;
}
}
fn build_labels(&mut self, text: &mut PietText, text_color: Color) {
self.labels.clear();
let mut walk = self.mapping.min;
while walk < self.mapping.max + f64::EPSILON * 10.0 {
let layout = text
.new_text_layout(format!("{walk}"))
.text_color(text_color)
.build()
.unwrap();
self.labels.push(layout);
walk += self.labeled_steps;
}
}
fn line_dir(&self) -> Vec2 {
match self.mapping.axis {
Axis::Horizontal => Vec2::new(0.0, 1.0),
Axis::Vertical => Vec2::new(-1.0, 0.0),
}
}
}
impl<T: Data, W: Widget<T>> Widget<T> for Annotated<T, W> {
#[instrument(name = "Annotated", level = "trace", skip(self, ctx, event, data, env))]
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
self.inner.event(ctx, event, data, env);
}
#[instrument(name = "Annotated", level = "trace", skip(self, ctx, event, data, env))]
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
if let LifeCycle::WidgetAdded = event {
self.sanitise_values();
self.build_labels(ctx.text(), env.get(TEXT_COLOR));
}
self.inner.lifecycle(ctx, event, data, env);
}
#[instrument(
name = "Annotated",
level = "trace",
skip(self, ctx, _old_data, data, env)
)]
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
self.inner.update(ctx, data, env);
if ctx.env_key_changed(&TEXT_COLOR) {
self.build_labels(ctx.text(), env.get(TEXT_COLOR));
ctx.request_paint();
}
}
#[instrument(name = "Annotated", level = "trace", skip(self, bc, ctx, data, env))]
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
let label_size = Size::new(40.0, 20.0);
match self.mapping.axis {
Axis::Vertical => {
let child_bc = bc.shrink((label_size.width, 0.0));
let child_size = self.inner.layout(ctx, &child_bc, data, env);
self.inner
.set_origin(ctx, Point::new(label_size.width, 0.0));
Size::new(child_size.width + label_size.width, child_size.height)
}
Axis::Horizontal => {
let child_bc = bc.shrink((0.0, label_size.height));
let child_size = self.inner.layout(ctx, &child_bc, data, env);
self.inner.set_origin(ctx, Point::ZERO);
ctx.set_baseline_offset(self.inner.baseline_offset() + label_size.height);
Size::new(child_size.width, child_size.height + label_size.height)
}
}
}
#[instrument(name = "Annotated", level = "trace", skip(self, ctx, data, env))]
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
let short_stroke = 3.0;
let long_stroke = 6.0;
let stroke_offset = 6.0;
let slider_offset = Point::new(self.inner.layout_rect().x0, self.inner.layout_rect().y0);
let knob_size = 22.;
let slider_size = self.inner.layout_rect().size();
let text_color = env.get(TEXT_COLOR);
let mut walk = self.mapping.min;
while walk < self.mapping.max + f64::EPSILON * 10.0 {
let center = self
.mapping
.get_point(walk, knob_size, slider_size)
.to_vec2()
+ slider_offset.to_vec2();
let line = Line::new(
(center + self.line_dir() * stroke_offset).to_point(),
(center + self.line_dir() * (stroke_offset + short_stroke)).to_point(),
);
ctx.stroke(line, &text_color, 1.0);
walk += self.unlabeled_steps;
}
let mut walk = self.mapping.min;
let mut labels = self.labels.iter();
while walk < self.mapping.max + f64::EPSILON * 10.0 {
let center = self
.mapping
.get_point(walk, knob_size, slider_size)
.to_vec2()
+ slider_offset.to_vec2();
let line = Line::new(
(center + self.line_dir() * stroke_offset).to_point(),
(center + self.line_dir() * (stroke_offset + long_stroke)).to_point(),
);
ctx.stroke(line, &text_color, 1.0);
let label = labels.next().unwrap();
let origin = match self.mapping.axis {
Axis::Horizontal => Vec2::new(label.size().width / 2.0, 0.0),
Axis::Vertical => Vec2::new(label.size().width, label.size().height / 2.0),
};
ctx.draw_text(
label,
(center + self.line_dir() * (stroke_offset + long_stroke) - origin).to_point(),
);
walk += self.labeled_steps;
}
self.inner.paint(ctx, data, env);
}
fn debug_state(&self, data: &T) -> DebugState {
DebugState {
display_name: "Annotated".to_string(),
children: vec![self.inner.widget().debug_state(data)],
..Default::default()
}
}
}
impl SliderValueMapping {
pub fn new() -> Self {
Self {
min: 0.0,
max: 1.0,
step: None,
axis: Axis::Horizontal,
}
}
fn calculate_value(
&self,
mouse_pos: Point,
knob_size: f64,
slider_size: Size,
offset: f64,
) -> f64 {
let mouse_pos = Point::new(mouse_pos.x, slider_size.height - mouse_pos.y);
let scalar = (self.axis.major_pos(mouse_pos) - knob_size / 2.)
/ (self.axis.major(slider_size) - knob_size);
let mut value =
(self.min + scalar * (self.max - self.min) + offset).clamp(self.min, self.max);
if let Some(step) = self.step {
let max_step_value = ((self.max - self.min) / step).floor() * step + self.min;
if value > max_step_value {
let left_dist = value - max_step_value;
let right_dist = self.max - value;
value = if left_dist < right_dist {
max_step_value
} else {
self.max
};
} else {
value = (((value - self.min) / step).round() * step + self.min).min(self.max);
}
}
value
}
fn get_point(&self, value: f64, knob_size: f64, widget_size: Size) -> Point {
let knob_major =
(self.axis.major(widget_size) - knob_size) * self.normalize(value) + knob_size / 2.;
let (w, h) = self.axis.pack(knob_major, knob_size / 2.);
Point::new(w, widget_size.height - h)
}
fn normalize(&self, data: f64) -> f64 {
(data.clamp(self.min, self.max) - self.min) / (self.max - self.min)
}
fn check_range(&mut self) {
if self.max < self.min {
warn!(
"min({}) should be less than max({}), swapping the values",
self.min, self.max
);
std::mem::swap(&mut self.max, &mut self.min);
}
}
fn range(&self) -> f64 {
self.max - self.min
}
}
impl Default for SliderValueMapping {
fn default() -> Self {
SliderValueMapping {
min: 0.0,
max: 1.0,
step: None,
axis: Axis::Horizontal,
}
}
}
impl SliderKnob {
fn handle_input(
&mut self,
ctx: &mut EventCtx,
event: &Event,
data: &mut f64,
_env: &Env,
mapping: SliderValueMapping,
knob_style: KnobStyle,
) {
let knob_size = 22.;
let slider_size = ctx.size();
let point_to_val = |point: Point, offset: f64| {
mapping.calculate_value(point, knob_size, slider_size, offset)
};
let hit_test = |val: &mut f64, mouse_pos: Point| {
let center = mapping.get_point(*val, knob_size, slider_size);
match knob_style {
KnobStyle::Circle => center.distance(mouse_pos) < knob_size,
KnobStyle::Wedge => {
(&knob_wedge(center, knob_size, mapping.axis)[..]).winding(mouse_pos) != 0
}
}
};
match event {
Event::MouseDown(mouse) => {
if !ctx.is_disabled() && hit_test(data, mouse.pos) {
self.offset = *data - point_to_val(mouse.pos, 0.0);
self.active = true;
ctx.request_paint();
if self.inner_circle_target_radius != 5. {
self.inner_circle_target_radius = 5.;
ctx.request_anim_frame();
}
}
}
Event::MouseUp(mouse) => {
if self.active && !ctx.is_disabled() {
*data = point_to_val(mouse.pos, self.offset);
ctx.request_paint();
}
let last_inner_circle_target_radius = self.inner_circle_target_radius;
if ctx.is_hot() {
self.inner_circle_target_radius = 7.;
} else {
self.inner_circle_target_radius = 6.;
}
if last_inner_circle_target_radius != self.inner_circle_target_radius {
ctx.request_anim_frame();
}
self.active = false;
}
Event::MouseMove(mouse) => {
if !ctx.is_disabled() {
if self.active {
*data = point_to_val(mouse.pos, self.offset);
ctx.request_paint();
}
let last_inner_circle_target_radius = self.inner_circle_target_radius;
if ctx.is_hot() {
let knob_hover = hit_test(data, mouse.pos);
if knob_hover != self.hovered {
self.hovered = knob_hover;
ctx.request_paint();
}
if ctx.is_active() {
self.inner_circle_target_radius = 5.;
} else {
self.inner_circle_target_radius = 7.;
}
} else {
self.inner_circle_target_radius = 6.;
}
if last_inner_circle_target_radius != self.inner_circle_target_radius {
ctx.request_anim_frame();
}
} else {
self.active = false
}
}
Event::AnimFrame(_) => {
self.inner_circle_current_radius +=
(self.inner_circle_target_radius - self.inner_circle_current_radius) / 3.;
if (self.inner_circle_target_radius - self.inner_circle_current_radius).abs()
> f64::EPSILON
{
ctx.request_anim_frame();
}
ctx.request_paint();
}
_ => (),
}
}
fn deactivate(&mut self) {
self.hovered = false;
self.active = false;
}
fn activate(&mut self, x_offset: f64) {
self.hovered = true;
self.active = true;
self.offset = x_offset;
}
fn is_active(&self) -> bool {
self.active
}
fn paint(
&self,
ctx: &mut PaintCtx,
value: f64,
env: &Env,
settings: SliderValueMapping,
knob_style: KnobStyle,
) {
let knob_size = 22.;
let is_dark = env.get(crate::theme::color::main::IS_DARK);
let knob_color = if ctx.is_disabled() {
if is_dark {
env.get(crate::theme::color::chrome::DISABLED_LOW)
} else {
env.get(crate::theme::color::chrome::WHITE)
}
} else if is_dark {
env.get(crate::theme::color::chrome::DISABLED_HIGH)
} else {
env.get(crate::theme::color::chrome::WHITE)
};
let border_color = env.get(crate::theme::color::base::LOW);
match knob_style {
KnobStyle::Circle => {
let mut knob_circle = Circle::new(
settings.get_point(value, knob_size, ctx.size()),
(knob_size - 2.) / 2.,
);
ctx.fill(knob_circle, &knob_color);
knob_circle.radius -= 0.5;
ctx.stroke(knob_circle, &border_color, 1.);
knob_circle.radius = self.inner_circle_current_radius;
ctx.fill(knob_circle, &env.get(crate::theme::color::main::PRIMARY));
}
KnobStyle::Wedge => {
let center = settings.get_point(value, knob_size, ctx.size());
let knob_wedge = knob_wedge(center, knob_size, settings.axis);
ctx.stroke(&knob_wedge[..], &border_color, KNOB_STROKE_WIDTH);
ctx.fill(&knob_wedge[..], &knob_color);
}
}
}
}
fn knob_wedge(center: Point, knob_size: f64, axis: Axis) -> [PathEl; 6] {
let (top, right, left, middle, down) = match axis {
Axis::Horizontal => (
Vec2::new(0.0, center.y - knob_size / 2.0),
Vec2::new(center.x + knob_size / 3.5, 0.0),
Vec2::new(center.x - knob_size / 3.5, 0.0),
Vec2::new(0.0, center.y + knob_size / 5.0),
Vec2::new(center.x, center.y + knob_size / 2.0),
),
Axis::Vertical => (
Vec2::new(center.x + knob_size / 2.0, 0.0),
Vec2::new(0.0, center.y + knob_size / 3.5),
Vec2::new(0.0, center.y - knob_size / 3.5),
Vec2::new(center.x - knob_size / 5.0, 0.0),
Vec2::new(center.x - knob_size / 2.0, center.y),
),
};
[
PathEl::MoveTo(down.to_point()),
PathEl::LineTo((right + middle).to_point()),
PathEl::LineTo((right + top).to_point()),
PathEl::LineTo((left + top).to_point()),
PathEl::LineTo((left + middle).to_point()),
PathEl::ClosePath,
]
}
fn paint_slider_background(
ctx: &mut PaintCtx,
lower: f64,
higher: f64,
mapping: SliderValueMapping,
env: &Env,
) {
ctx.with_save(|ctx| {
let size = ctx.size();
let knob_size = 22.;
let background_rect = Rect::from_points(
mapping.get_point(mapping.min, knob_size, size),
mapping.get_point(mapping.max, knob_size, size),
)
.inset(TRACK_THICKNESS / 2.)
.to_rounded_rect(2.);
ctx.clip(background_rect);
let background_gradient = env.get(crate::theme::color::base::MEDIUM_LOW);
ctx.fill(background_rect, &background_gradient);
let color = env.get(crate::theme::color::main::PRIMARY);
let shape = Rect::from_points(
mapping.get_point(lower, knob_size, size),
mapping.get_point(higher, knob_size, size),
)
.inset(TRACK_THICKNESS / 2.)
.to_rounded_rect(2.);
ctx.fill(shape, &color);
})
}
fn slider_layout(
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
env: &Env,
mapping: SliderValueMapping,
) -> Size {
let height = 22.;
let width = env.get(theme::WIDE_WIDGET_WIDTH);
let size = bc.constrain(mapping.axis.pack(width, height));
if mapping.axis == Axis::Horizontal {
let baseline_offset = (height / 2.0) - TRACK_THICKNESS;
ctx.set_baseline_offset(baseline_offset);
trace!(
"Computed layout: size={}, baseline_offset={:?}",
size,
baseline_offset
);
} else {
trace!("Computed layout: size={}", size,);
}
size
}