1use crate::debug_state::DebugState;
18use crate::kurbo::{Circle, Line};
19use crate::theme::TEXT_COLOR;
20use crate::widget::prelude::*;
21use crate::widget::Axis;
22use crate::{theme, Color, KeyOrValue, LinearGradient, Point, Rect, UnitPoint, Vec2, WidgetPod};
23use druid::kurbo::{PathEl, Shape};
24use druid::piet::{PietText, PietTextLayout, Text, TextLayout, TextLayoutBuilder};
25use tracing::{instrument, trace, warn};
26
27const TRACK_THICKNESS: f64 = 4.0;
28const BORDER_WIDTH: f64 = 2.0;
29const KNOB_STROKE_WIDTH: f64 = 2.0;
30
31#[derive(Debug, Clone, Default)]
36pub struct Slider {
37 mapping: SliderValueMapping,
38 knob: SliderKnob,
39 track_color: Option<KeyOrValue<Color>>,
40 knob_style: KnobStyle,
41}
42
43#[derive(Debug, Clone, Default)]
48pub struct RangeSlider {
49 mapping: SliderValueMapping,
50 left_knob: SliderKnob,
51 right_knob: SliderKnob,
52 track_color: Option<KeyOrValue<Color>>,
53 knob_style: KnobStyle,
54}
55
56pub struct Annotated<T, W: Widget<T>> {
58 inner: WidgetPod<T, W>,
59
60 mapping: SliderValueMapping,
61 labeled_steps: f64,
62 unlabeled_steps: f64,
63
64 labels: Vec<PietTextLayout>,
65}
66
67#[derive(Copy, Clone, Debug)]
68pub struct SliderValueMapping {
69 min: f64,
70 max: f64,
71 step: Option<f64>,
72 axis: Axis,
73}
74
75#[derive(Debug, Clone, Default)]
76struct SliderKnob {
77 hovered: bool,
78 active: bool,
79 offset: f64,
80}
81
82#[derive(Debug, Copy, Clone)]
84pub enum KnobStyle {
85 Circle,
87 Wedge,
89}
90
91impl Default for KnobStyle {
92 fn default() -> Self {
93 Self::Circle
94 }
95}
96
97impl Slider {
98 pub fn new() -> Slider {
100 Default::default()
101 }
102
103 pub fn with_range(mut self, min: f64, max: f64) -> Self {
107 self.mapping.min = min;
108 self.mapping.max = max;
109 self
110 }
111
112 pub fn with_step(mut self, step: f64) -> Self {
116 if step < 0.0 {
117 warn!("bad stepping (must be positive): {}", step);
118 return self;
119 }
120 self.mapping.step = if step > 0.0 {
121 Some(step)
122 } else {
123 None
126 };
127 self
128 }
129
130 pub fn track_color(mut self, color: impl Into<Option<KeyOrValue<Color>>>) -> Self {
134 self.track_color = color.into();
135 self
136 }
137
138 pub fn knob_style(mut self, knob_style: KnobStyle) -> Self {
142 self.knob_style = knob_style;
143 self
144 }
145
146 pub fn axis(mut self, axis: Axis) -> Self {
150 self.mapping.axis = axis;
151 self
152 }
153
154 pub fn get_mapping(&self) -> SliderValueMapping {
156 self.mapping
157 }
158
159 pub fn annotated(self, named_steps: f64, unnamed_steps: f64) -> Annotated<f64, Self> {
162 let mapping = self.mapping;
163 Annotated::new(self, mapping, named_steps, unnamed_steps)
164 }
165}
166
167impl Widget<f64> for Slider {
168 #[instrument(name = "Slider", level = "trace", skip(self, ctx, event, data, env))]
169 fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut f64, env: &Env) {
170 if !ctx.is_disabled() {
171 self.knob
172 .handle_input(ctx, event, data, env, self.mapping, self.knob_style);
173
174 ctx.set_active(self.knob.is_active());
175
176 if let Event::MouseDown(me) = event {
177 if !self.knob.active {
178 self.knob.activate(0.0);
179 let knob_size = env.get(theme::BASIC_WIDGET_HEIGHT);
180 *data = self
181 .mapping
182 .calculate_value(me.pos, knob_size, ctx.size(), 0.0);
183 ctx.request_paint();
184 ctx.set_active(true);
185 }
186 }
187 }
188 }
189
190 #[instrument(name = "Slider", level = "trace", skip(self, ctx, event, _data, _env))]
191 fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f64, _env: &Env) {
192 match event {
193 LifeCycle::WidgetAdded => self.mapping.check_range(),
195 LifeCycle::DisabledChanged(_) => ctx.request_paint(),
196 _ => (),
197 }
198 }
199
200 #[instrument(
201 name = "Slider",
202 level = "trace",
203 skip(self, ctx, _old_data, _data, _env)
204 )]
205 fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &f64, _data: &f64, _env: &Env) {
206 ctx.request_paint();
207 }
208
209 #[instrument(name = "Slider", level = "trace", skip(self, ctx, bc, _data, env))]
210 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &f64, env: &Env) -> Size {
211 bc.debug_check("Slider");
212 slider_layout(ctx, bc, env, self.mapping)
213 }
214
215 #[instrument(name = "Slider", level = "trace", skip(self, ctx, data, env))]
216 fn paint(&mut self, ctx: &mut PaintCtx, data: &f64, env: &Env) {
217 paint_slider_background(ctx, 0.0, *data, &self.track_color, self.mapping, env);
218
219 self.knob
220 .paint(ctx, *data, env, self.mapping, self.knob_style);
221 }
222
223 fn debug_state(&self, data: &f64) -> DebugState {
224 DebugState {
225 display_name: self.short_type_name().to_string(),
226 main_value: data.to_string(),
227 ..Default::default()
228 }
229 }
230}
231
232impl RangeSlider {
233 pub fn new() -> RangeSlider {
235 Default::default()
236 }
237
238 pub fn with_range(mut self, min: f64, max: f64) -> Self {
242 self.mapping.min = min;
243 self.mapping.max = max;
244 self
245 }
246
247 pub fn with_step(mut self, step: f64) -> Self {
251 if step < 0.0 {
252 warn!("bad stepping (must be positive): {}", step);
253 return self;
254 }
255 self.mapping.step = if step > 0.0 {
256 Some(step)
257 } else {
258 None
261 };
262 self
263 }
264
265 pub fn track_color(mut self, color: impl Into<Option<KeyOrValue<Color>>>) -> Self {
269 self.track_color = color.into();
270 self
271 }
272
273 pub fn knob_style(mut self, knob_style: KnobStyle) -> Self {
277 self.knob_style = knob_style;
278 self
279 }
280
281 pub fn axis(mut self, axis: Axis) -> Self {
285 self.mapping.axis = axis;
286 self
287 }
288
289 pub fn get_mapping(&self) -> SliderValueMapping {
291 self.mapping
292 }
293
294 pub fn annotated(self, named_steps: f64, unnamed_steps: f64) -> Annotated<(f64, f64), Self> {
297 let mapping = self.mapping;
298 Annotated::new(self, mapping, named_steps, unnamed_steps)
299 }
300}
301
302impl Widget<(f64, f64)> for RangeSlider {
303 #[instrument(
304 name = "RangeSlider",
305 level = "trace",
306 skip(self, ctx, event, data, env)
307 )]
308 fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut (f64, f64), env: &Env) {
309 if !ctx.is_disabled() {
310 if !self.right_knob.is_active() {
311 self.left_knob.handle_input(
312 ctx,
313 event,
314 &mut data.0,
315 env,
316 self.mapping,
317 self.knob_style,
318 );
319 data.0 = data.0.min(data.1);
320 if self.left_knob.is_active() {
323 self.right_knob.deactivate();
324 }
325 }
326 if !self.left_knob.is_active() {
327 self.right_knob.handle_input(
328 ctx,
329 event,
330 &mut data.1,
331 env,
332 self.mapping,
333 self.knob_style,
334 );
335 data.1 = data.1.max(data.0);
337
338 if self.right_knob.is_active() {
339 self.left_knob.deactivate();
340 }
341 }
342 ctx.set_active(self.left_knob.is_active() || self.right_knob.is_active());
343
344 if let Event::MouseDown(me) = event {
345 if !self.left_knob.is_active() && !self.right_knob.is_active() {
346 let knob_size = env.get(theme::BASIC_WIDGET_HEIGHT);
347 let press_value =
348 self.mapping
349 .calculate_value(me.pos, knob_size, ctx.size(), 0.0);
350
351 if press_value - data.0 < data.1 - press_value {
352 self.left_knob.activate(0.0);
353 data.0 = press_value;
354 } else {
355 self.right_knob.activate(0.0);
356 data.1 = press_value;
357 }
358 ctx.set_active(true);
359 ctx.request_paint();
360 }
361 }
362 }
363 }
364
365 #[instrument(
366 name = "RangeSlider",
367 level = "trace",
368 skip(self, ctx, event, _data, _env)
369 )]
370 fn lifecycle(
371 &mut self,
372 ctx: &mut LifeCycleCtx,
373 event: &LifeCycle,
374 _data: &(f64, f64),
375 _env: &Env,
376 ) {
377 match event {
378 LifeCycle::WidgetAdded => self.mapping.check_range(),
380 LifeCycle::DisabledChanged(_) => ctx.request_paint(),
381 _ => (),
382 }
383 }
384
385 #[instrument(
386 name = "RangeSlider",
387 level = "trace",
388 skip(self, ctx, _old_data, _data, _env)
389 )]
390 fn update(
391 &mut self,
392 ctx: &mut UpdateCtx,
393 _old_data: &(f64, f64),
394 _data: &(f64, f64),
395 _env: &Env,
396 ) {
397 ctx.request_paint();
398 }
399
400 #[instrument(name = "RangeSlider", level = "trace", skip(self, ctx, bc, _data, env))]
401 fn layout(
402 &mut self,
403 ctx: &mut LayoutCtx,
404 bc: &BoxConstraints,
405 _data: &(f64, f64),
406 env: &Env,
407 ) -> Size {
408 bc.debug_check("Slider");
409 slider_layout(ctx, bc, env, self.mapping)
410 }
411
412 #[instrument(name = "RangeSlider", level = "trace", skip(self, ctx, data, env))]
413 fn paint(&mut self, ctx: &mut PaintCtx, data: &(f64, f64), env: &Env) {
414 paint_slider_background(ctx, data.0, data.1, &self.track_color, self.mapping, env);
415
416 self.right_knob
419 .paint(ctx, data.1, env, self.mapping, self.knob_style);
420 self.left_knob
421 .paint(ctx, data.0, env, self.mapping, self.knob_style);
422 }
423
424 fn debug_state(&self, data: &(f64, f64)) -> DebugState {
425 DebugState {
426 display_name: self.short_type_name().to_string(),
427 main_value: format!("{data:?}"),
428 ..Default::default()
429 }
430 }
431}
432
433impl<T, W: Widget<T>> Annotated<T, W> {
434 pub fn new(
435 inner: W,
436 mapping: SliderValueMapping,
437 labeled_steps: f64,
438 unlabeled_steps: f64,
439 ) -> Self {
440 Annotated {
441 inner: WidgetPod::new(inner),
442
443 mapping,
444 labeled_steps: labeled_steps.abs(),
445 unlabeled_steps: unlabeled_steps.abs(),
446
447 labels: Vec::new(),
448 }
449 }
450
451 fn sanitise_values(&mut self) {
452 let labeled = self.mapping.range() / self.labeled_steps;
453 if !labeled.is_finite() || labeled > 100.0 {
454 warn!("Annotated: provided labeled interval \"{}\" has too many steps inside the sliders range {}..{}", self.labeled_steps, self.mapping.min, self.mapping.max);
455 self.labeled_steps = self.mapping.range() / 5.0;
456 }
457
458 let unlabeled = self.mapping.range() / self.unlabeled_steps;
459 if !unlabeled.is_finite() || unlabeled > 10000.0 {
460 warn!("Annotated: provided unlabeled interval \"{}\" has too many steps inside the sliders range {}..{}", self.unlabeled_steps, self.mapping.min, self.mapping.max);
461 self.unlabeled_steps = self.mapping.range() / 20.0;
462 }
463 }
464
465 fn build_labels(&mut self, text: &mut PietText, text_color: Color) {
466 self.labels.clear();
467
468 let mut walk = self.mapping.min;
469 while walk < self.mapping.max + f64::EPSILON * 10.0 {
470 let layout = text
471 .new_text_layout(format!("{walk}"))
472 .text_color(text_color)
473 .build()
474 .unwrap();
475
476 self.labels.push(layout);
477
478 walk += self.labeled_steps;
479 }
480 }
481
482 fn line_dir(&self) -> Vec2 {
483 match self.mapping.axis {
484 Axis::Horizontal => Vec2::new(0.0, 1.0),
485 Axis::Vertical => Vec2::new(-1.0, 0.0),
486 }
487 }
488}
489
490impl<T: Data, W: Widget<T>> Widget<T> for Annotated<T, W> {
491 #[instrument(name = "Annotated", level = "trace", skip(self, ctx, event, data, env))]
492 fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
493 self.inner.event(ctx, event, data, env);
494 }
495
496 #[instrument(name = "Annotated", level = "trace", skip(self, ctx, event, data, env))]
497 fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
498 if let LifeCycle::WidgetAdded = event {
499 self.sanitise_values();
500 self.build_labels(ctx.text(), env.get(TEXT_COLOR));
501 }
502 self.inner.lifecycle(ctx, event, data, env);
503 }
504
505 #[instrument(
506 name = "Annotated",
507 level = "trace",
508 skip(self, ctx, _old_data, data, env)
509 )]
510 fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
511 self.inner.update(ctx, data, env);
512 if ctx.env_key_changed(&TEXT_COLOR) {
513 self.build_labels(ctx.text(), env.get(TEXT_COLOR));
514 ctx.request_paint();
515 }
516 }
517
518 #[instrument(name = "Annotated", level = "trace", skip(self, bc, ctx, data, env))]
519 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
520 let label_size = Size::new(40.0, 20.0);
521
522 match self.mapping.axis {
523 Axis::Vertical => {
524 let child_bc = bc.shrink((label_size.width, 0.0));
525 let child_size = self.inner.layout(ctx, &child_bc, data, env);
526 self.inner
527 .set_origin(ctx, Point::new(label_size.width, 0.0));
528
529 Size::new(child_size.width + label_size.width, child_size.height)
530 }
531 Axis::Horizontal => {
532 let child_bc = bc.shrink((0.0, label_size.height));
533 let child_size = self.inner.layout(ctx, &child_bc, data, env);
534 self.inner.set_origin(ctx, Point::ZERO);
535
536 ctx.set_baseline_offset(self.inner.baseline_offset() + label_size.height);
537 Size::new(child_size.width, child_size.height + label_size.height)
538 }
539 }
540 }
541
542 #[instrument(name = "Annotated", level = "trace", skip(self, ctx, data, env))]
543 fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
544 let short_stroke = 3.0;
545 let long_stroke = 6.0;
546 let stroke_offset = 6.0;
547
548 let slider_offset = Point::new(self.inner.layout_rect().x0, self.inner.layout_rect().y0);
549
550 let knob_size = env.get(theme::BASIC_WIDGET_HEIGHT);
551 let slider_size = self.inner.layout_rect().size();
552
553 let text_color = env.get(TEXT_COLOR);
554
555 let mut walk = self.mapping.min;
556 while walk < self.mapping.max + f64::EPSILON * 10.0 {
557 let center = self
558 .mapping
559 .get_point(walk, knob_size, slider_size)
560 .to_vec2()
561 + slider_offset.to_vec2();
562
563 let line = Line::new(
564 (center + self.line_dir() * stroke_offset).to_point(),
565 (center + self.line_dir() * (stroke_offset + short_stroke)).to_point(),
566 );
567
568 ctx.stroke(line, &text_color, 1.0);
569 walk += self.unlabeled_steps;
570 }
571
572 let mut walk = self.mapping.min;
573 let mut labels = self.labels.iter();
574 while walk < self.mapping.max + f64::EPSILON * 10.0 {
575 let center = self
576 .mapping
577 .get_point(walk, knob_size, slider_size)
578 .to_vec2()
579 + slider_offset.to_vec2();
580
581 let line = Line::new(
582 (center + self.line_dir() * stroke_offset).to_point(),
583 (center + self.line_dir() * (stroke_offset + long_stroke)).to_point(),
584 );
585
586 ctx.stroke(line, &text_color, 1.0);
587
588 let label = labels.next().unwrap();
589 let origin = match self.mapping.axis {
590 Axis::Horizontal => Vec2::new(label.size().width / 2.0, 0.0),
591 Axis::Vertical => Vec2::new(label.size().width, label.size().height / 2.0),
592 };
593
594 ctx.draw_text(
595 label,
596 (center + self.line_dir() * (stroke_offset + long_stroke) - origin).to_point(),
597 );
598
599 walk += self.labeled_steps;
600 }
601
602 self.inner.paint(ctx, data, env);
603 }
604
605 fn debug_state(&self, data: &T) -> DebugState {
606 DebugState {
607 display_name: "Annotated".to_string(),
608 children: vec![self.inner.widget().debug_state(data)],
609 ..Default::default()
610 }
611 }
612}
613
614impl SliderValueMapping {
615 pub fn new() -> Self {
616 Self {
617 min: 0.0,
618 max: 1.0,
619 step: None,
620 axis: Axis::Horizontal,
621 }
622 }
623
624 fn calculate_value(
625 &self,
626 mouse_pos: Point,
627 knob_size: f64,
628 slider_size: Size,
629 offset: f64,
630 ) -> f64 {
631 let mouse_pos = Point::new(mouse_pos.x, slider_size.height - mouse_pos.y);
633
634 let scalar = (self.axis.major_pos(mouse_pos) - knob_size / 2.)
635 / (self.axis.major(slider_size) - knob_size);
636
637 let mut value =
638 (self.min + scalar * (self.max - self.min) + offset).clamp(self.min, self.max);
639
640 if let Some(step) = self.step {
641 let max_step_value = ((self.max - self.min) / step).floor() * step + self.min;
642 if value > max_step_value {
643 let left_dist = value - max_step_value;
645 let right_dist = self.max - value;
646 value = if left_dist < right_dist {
647 max_step_value
648 } else {
649 self.max
650 };
651 } else {
652 value = (((value - self.min) / step).round() * step + self.min).min(self.max);
654 }
655 }
656 value
657 }
658
659 fn get_point(&self, value: f64, knob_size: f64, widget_size: Size) -> Point {
660 let knob_major =
661 (self.axis.major(widget_size) - knob_size) * self.normalize(value) + knob_size / 2.;
662 let (w, h) = self.axis.pack(knob_major, knob_size / 2.);
663 Point::new(w, widget_size.height - h)
664 }
665
666 fn normalize(&self, data: f64) -> f64 {
667 (data.clamp(self.min, self.max) - self.min) / (self.max - self.min)
668 }
669
670 fn check_range(&mut self) {
672 if self.max < self.min {
673 warn!(
674 "min({}) should be less than max({}), swapping the values",
675 self.min, self.max
676 );
677 std::mem::swap(&mut self.max, &mut self.min);
678 }
679 }
680
681 fn range(&self) -> f64 {
683 self.max - self.min
684 }
685}
686
687impl Default for SliderValueMapping {
688 fn default() -> Self {
689 SliderValueMapping {
690 min: 0.0,
691 max: 1.0,
692 step: None,
693 axis: Axis::Horizontal,
694 }
695 }
696}
697
698impl SliderKnob {
699 fn handle_input(
700 &mut self,
701 ctx: &mut EventCtx,
702 event: &Event,
703 data: &mut f64,
704 env: &Env,
705 mapping: SliderValueMapping,
706 knob_style: KnobStyle,
707 ) {
708 let knob_size = env.get(theme::BASIC_WIDGET_HEIGHT);
709 let slider_size = ctx.size();
710
711 let point_to_val = |point: Point, offset: f64| {
712 mapping.calculate_value(point, knob_size, slider_size, offset)
713 };
714
715 let hit_test = |val: &mut f64, mouse_pos: Point| {
716 let center = mapping.get_point(*val, knob_size, slider_size);
717 match knob_style {
718 KnobStyle::Circle => center.distance(mouse_pos) < knob_size,
719 KnobStyle::Wedge => {
720 (&knob_wedge(center, knob_size, mapping.axis)[..]).winding(mouse_pos) != 0
721 }
722 }
723 };
724
725 match event {
726 Event::MouseDown(mouse) => {
727 if !ctx.is_disabled() && hit_test(data, mouse.pos) {
728 self.offset = *data - point_to_val(mouse.pos, 0.0);
729 self.active = true;
730 ctx.request_paint();
731 }
732 }
733 Event::MouseUp(mouse) => {
734 if self.active && !ctx.is_disabled() {
735 *data = point_to_val(mouse.pos, self.offset);
736 ctx.request_paint();
737 }
738 self.active = false;
739 }
740 Event::MouseMove(mouse) => {
741 if !ctx.is_disabled() {
742 if self.active {
743 *data = point_to_val(mouse.pos, self.offset);
744 ctx.request_paint();
745 }
746 if ctx.is_hot() {
747 let knob_hover = hit_test(data, mouse.pos);
748 if knob_hover != self.hovered {
749 self.hovered = knob_hover;
750 ctx.request_paint();
751 }
752 }
753 } else {
754 self.active = false
755 }
756 }
757 _ => (),
758 }
759 }
760
761 fn deactivate(&mut self) {
762 self.hovered = false;
763 self.active = false;
764 }
765
766 fn activate(&mut self, x_offset: f64) {
767 self.hovered = true;
768 self.active = true;
769 self.offset = x_offset;
770 }
771
772 fn is_active(&self) -> bool {
773 self.active
774 }
775
776 fn paint(
777 &self,
778 ctx: &mut PaintCtx,
779 value: f64,
780 env: &Env,
781 settings: SliderValueMapping,
782 knob_style: KnobStyle,
783 ) {
784 let knob_size = env.get(theme::BASIC_WIDGET_HEIGHT);
785
786 let knob_gradient = if ctx.is_disabled() {
787 LinearGradient::new(
788 UnitPoint::TOP,
789 UnitPoint::BOTTOM,
790 (
791 env.get(theme::DISABLED_FOREGROUND_LIGHT),
792 env.get(theme::DISABLED_FOREGROUND_DARK),
793 ),
794 )
795 } else if self.active {
796 LinearGradient::new(
797 UnitPoint::TOP,
798 UnitPoint::BOTTOM,
799 (
800 env.get(theme::FOREGROUND_DARK),
801 env.get(theme::FOREGROUND_LIGHT),
802 ),
803 )
804 } else {
805 LinearGradient::new(
806 UnitPoint::TOP,
807 UnitPoint::BOTTOM,
808 (
809 env.get(theme::FOREGROUND_LIGHT),
810 env.get(theme::FOREGROUND_DARK),
811 ),
812 )
813 };
814
815 let border_color = if (self.hovered || self.active) && !ctx.is_disabled() {
817 env.get(theme::FOREGROUND_LIGHT)
818 } else {
819 env.get(theme::FOREGROUND_DARK)
820 };
821
822 match knob_style {
823 KnobStyle::Circle => {
824 let knob_circle = Circle::new(
825 settings.get_point(value, knob_size, ctx.size()),
826 (knob_size - KNOB_STROKE_WIDTH) / 2.,
827 );
828
829 ctx.stroke(knob_circle, &border_color, KNOB_STROKE_WIDTH);
830
831 ctx.fill(knob_circle, &knob_gradient);
833 }
834 KnobStyle::Wedge => {
835 let center = settings.get_point(value, knob_size, ctx.size());
836
837 let knob_wedge = knob_wedge(center, knob_size, settings.axis);
838
839 ctx.stroke(&knob_wedge[..], &border_color, KNOB_STROKE_WIDTH);
840
841 ctx.fill(&knob_wedge[..], &knob_gradient);
843 }
844 }
845 }
846}
847
848fn knob_wedge(center: Point, knob_size: f64, axis: Axis) -> [PathEl; 6] {
849 let (top, right, left, middle, down) = match axis {
850 Axis::Horizontal => (
851 Vec2::new(0.0, center.y - knob_size / 2.0),
852 Vec2::new(center.x + knob_size / 3.5, 0.0),
853 Vec2::new(center.x - knob_size / 3.5, 0.0),
854 Vec2::new(0.0, center.y + knob_size / 5.0),
855 Vec2::new(center.x, center.y + knob_size / 2.0),
856 ),
857 Axis::Vertical => (
858 Vec2::new(center.x + knob_size / 2.0, 0.0),
859 Vec2::new(0.0, center.y + knob_size / 3.5),
860 Vec2::new(0.0, center.y - knob_size / 3.5),
861 Vec2::new(center.x - knob_size / 5.0, 0.0),
862 Vec2::new(center.x - knob_size / 2.0, center.y),
863 ),
864 };
865
866 [
867 PathEl::MoveTo(down.to_point()),
868 PathEl::LineTo((right + middle).to_point()),
869 PathEl::LineTo((right + top).to_point()),
870 PathEl::LineTo((left + top).to_point()),
871 PathEl::LineTo((left + middle).to_point()),
872 PathEl::ClosePath,
873 ]
874}
875
876fn paint_slider_background(
877 ctx: &mut PaintCtx,
878 lower: f64,
879 higher: f64,
880 track_color: &Option<KeyOrValue<Color>>,
881 mapping: SliderValueMapping,
882 env: &Env,
883) {
884 let size = ctx.size();
885 let knob_size = env.get(theme::BASIC_WIDGET_HEIGHT);
886
887 let background_rect = Rect::from_points(
889 mapping.get_point(mapping.min, knob_size, size),
890 mapping.get_point(mapping.max, knob_size, size),
891 )
892 .inset((TRACK_THICKNESS - BORDER_WIDTH) / 2.)
893 .to_rounded_rect(2.);
894
895 let background_gradient = LinearGradient::new(
896 UnitPoint::TOP,
897 UnitPoint::BOTTOM,
898 (
899 env.get(theme::BACKGROUND_LIGHT),
900 env.get(theme::BACKGROUND_DARK),
901 ),
902 );
903
904 ctx.stroke(background_rect, &env.get(theme::BORDER_DARK), BORDER_WIDTH);
905
906 ctx.fill(background_rect, &background_gradient);
907
908 if let Some(color) = track_color {
909 let color = color.resolve(env);
910
911 let shape = Rect::from_points(
912 mapping.get_point(lower, knob_size, size),
913 mapping.get_point(higher, knob_size, size),
914 )
915 .inset(TRACK_THICKNESS / 2.0)
916 .to_rounded_rect(2.);
917
918 ctx.fill(shape, &color);
919 }
920}
921
922fn slider_layout(
923 ctx: &mut LayoutCtx,
924 bc: &BoxConstraints,
925 env: &Env,
926 mapping: SliderValueMapping,
927) -> Size {
928 let height = env.get(theme::BASIC_WIDGET_HEIGHT);
929 let width = env.get(theme::WIDE_WIDGET_WIDTH);
930 let size = bc.constrain(mapping.axis.pack(width, height));
931
932 if mapping.axis == Axis::Horizontal {
933 let baseline_offset = (height / 2.0) - TRACK_THICKNESS;
934
935 ctx.set_baseline_offset(baseline_offset);
936 trace!(
937 "Computed layout: size={}, baseline_offset={:?}",
938 size,
939 baseline_offset
940 );
941 } else {
942 trace!("Computed layout: size={}", size,);
943 }
944
945 size
946}