Skip to main content

vizia_core/views/
knob.rs

1#![allow(dead_code)]
2#![allow(unused_imports)]
3#![allow(unused_variables)]
4use crate::vg;
5use accesskit::ActionData;
6use morphorm::Units;
7
8use crate::prelude::*;
9
10static DEFAULT_DRAG_SCALAR: f32 = 0.0042;
11static DEFAULT_WHEEL_SCALAR: f32 = 0.005;
12static DEFAULT_ARROW_SCALAR: f32 = 0.1;
13static DEFAULT_MODIFIER_SCALAR: f32 = 0.04;
14
15use std::{default, f32::consts::PI};
16
17/// A circular view which represents a value.
18pub struct Knob<T> {
19    value: T,
20    default_normal: f32,
21
22    is_dragging: bool,
23    prev_drag_y: f32,
24    continuous_normal: f32,
25
26    drag_scalar: f32,
27    wheel_scalar: f32,
28    arrow_scalar: f32,
29    modifier_scalar: f32,
30
31    on_changing: Option<Box<dyn Fn(&mut EventContext, f32)>>,
32}
33
34impl<R: Res<f32> + Clone + 'static> Knob<R> {
35    /// Create a new [Knob] view.
36    pub fn new(
37        cx: &mut Context,
38        normalized_default: impl Res<f32>,
39        value: R,
40        centered: bool,
41    ) -> Handle<Self> {
42        let value_for_track = value.clone().to_signal(cx);
43        let value_for_head = value.clone().to_signal(cx);
44
45        Self {
46            value: value.clone(),
47            default_normal: normalized_default.get_value(cx),
48
49            is_dragging: false,
50            prev_drag_y: 0.0,
51            continuous_normal: value.get_value(cx),
52
53            drag_scalar: DEFAULT_DRAG_SCALAR,
54            wheel_scalar: DEFAULT_WHEEL_SCALAR,
55            arrow_scalar: DEFAULT_ARROW_SCALAR,
56            modifier_scalar: DEFAULT_MODIFIER_SCALAR,
57
58            on_changing: None,
59        }
60        .build(cx, move |cx| {
61            ZStack::new(cx, move |cx| {
62                ArcTrack::new(
63                    cx,
64                    centered,
65                    Percentage(100.0),
66                    Percentage(15.0),
67                    -240.,
68                    60.,
69                    KnobMode::Continuous,
70                )
71                .value(value_for_track)
72                .class("knob-track");
73
74                HStack::new(cx, |cx| {
75                    Element::new(cx).class("knob-tick");
76                })
77                .bind(value_for_head, move |handle| {
78                    let value = value_for_head.get();
79                    handle.rotate(Angle::Deg(value * 300.0 - 150.0));
80                })
81                .class("knob-head");
82            });
83        })
84        .navigable(true)
85        .role(Role::Slider)
86        .numeric_value(value_for_track.map(|val| (*val as f64 * 100.0).round()))
87    }
88}
89
90impl<R: Res<f32> + Clone + 'static> Knob<R> {
91    /// Create a custom [Knob] view.
92    pub fn custom<F, V: View>(
93        cx: &mut Context,
94        default_normal: f32,
95        value: R,
96        content: F,
97    ) -> Handle<'_, Self>
98    where
99        F: 'static + Fn(&mut Context, R) -> Handle<V>,
100    {
101        let value_for_content = value.clone();
102
103        Self {
104            value: value.clone(),
105            default_normal,
106
107            is_dragging: false,
108            prev_drag_y: 0.0,
109            continuous_normal: value.get_value(cx),
110
111            drag_scalar: DEFAULT_DRAG_SCALAR,
112            wheel_scalar: DEFAULT_WHEEL_SCALAR,
113            arrow_scalar: DEFAULT_ARROW_SCALAR,
114            modifier_scalar: DEFAULT_MODIFIER_SCALAR,
115
116            on_changing: None,
117        }
118        .build(cx, move |cx| {
119            ZStack::new(cx, move |cx| {
120                (content)(cx, value_for_content.clone())
121                    .width(Percentage(100.0))
122                    .height(Percentage(100.0));
123            });
124        })
125    }
126}
127
128impl<T: Res<f32> + 'static> Handle<'_, Knob<T>> {
129    /// Sets the callback triggered when the knob value is changed.
130    pub fn on_change<F>(self, callback: F) -> Self
131    where
132        F: 'static + Fn(&mut EventContext, f32),
133    {
134        if let Some(view) = self.cx.views.get_mut(&self.entity) {
135            if let Some(knob) = view.downcast_mut::<Knob<T>>() {
136                knob.on_changing = Some(Box::new(callback));
137            }
138        }
139
140        self
141    }
142}
143
144impl<T: Res<f32> + 'static> View for Knob<T> {
145    fn element(&self) -> Option<&'static str> {
146        Some("knob")
147    }
148
149    fn accessibility(&self, _cx: &mut AccessContext, node: &mut AccessNode) {
150        node.set_min_numeric_value(0.0);
151        node.set_max_numeric_value(100.0);
152    }
153
154    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
155        let move_virtual_slider = |self_ref: &mut Self, cx: &mut EventContext, new_normal: f32| {
156            self_ref.continuous_normal = new_normal;
157
158            if let Some(callback) = &self_ref.on_changing {
159                (callback)(cx, self_ref.continuous_normal.clamp(0.0, 1.0));
160            }
161        };
162
163        event.map(|window_event, _| match window_event {
164            WindowEvent::MouseDown(button) if *button == MouseButton::Left => {
165                self.is_dragging = true;
166                self.prev_drag_y = cx.mouse.left.pos_down.1;
167
168                cx.capture();
169                cx.focus_with_visibility(false);
170
171                self.continuous_normal = self.value.get_value(cx);
172            }
173
174            WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
175                self.is_dragging = false;
176
177                self.continuous_normal = self.value.get_value(cx);
178
179                cx.release();
180            }
181
182            WindowEvent::MouseMove(_, y) => {
183                if self.is_dragging && !cx.is_disabled() {
184                    let mut delta_normal = (*y - self.prev_drag_y) * self.drag_scalar;
185
186                    self.prev_drag_y = *y;
187
188                    if cx.modifiers.shift() {
189                        delta_normal *= self.modifier_scalar;
190                    }
191
192                    let new_normal = self.continuous_normal - delta_normal;
193
194                    move_virtual_slider(self, cx, new_normal);
195                }
196            }
197
198            WindowEvent::MouseScroll(_, y) => {
199                if *y != 0.0 {
200                    let delta_normal = -*y * self.wheel_scalar;
201
202                    let new_normal = self.continuous_normal - delta_normal;
203
204                    move_virtual_slider(self, cx, new_normal);
205                }
206            }
207
208            WindowEvent::MouseDoubleClick(button) if *button == MouseButton::Left => {
209                self.is_dragging = false;
210
211                move_virtual_slider(self, cx, self.default_normal);
212            }
213
214            WindowEvent::KeyDown(Code::ArrowUp | Code::ArrowRight, _) => {
215                self.continuous_normal = self.value.get_value(cx);
216                move_virtual_slider(self, cx, self.continuous_normal + self.arrow_scalar);
217            }
218
219            WindowEvent::KeyDown(Code::ArrowDown | Code::ArrowLeft, _) => {
220                self.continuous_normal = self.value.get_value(cx);
221                move_virtual_slider(self, cx, self.continuous_normal - self.arrow_scalar);
222            }
223
224            WindowEvent::ActionRequest(action) => match action.action {
225                Action::Increment => {
226                    self.continuous_normal = self.value.get_value(cx);
227                    move_virtual_slider(self, cx, self.continuous_normal + self.arrow_scalar);
228                }
229
230                Action::Decrement => {
231                    self.continuous_normal = self.value.get_value(cx);
232                    move_virtual_slider(self, cx, self.continuous_normal - self.arrow_scalar);
233                }
234
235                Action::SetValue => {
236                    if let Some(ActionData::NumericValue(val)) = action.data {
237                        let val = (val as f32).clamp(0.0, 1.0);
238                        move_virtual_slider(self, cx, val);
239                    }
240                }
241
242                _ => {}
243            },
244
245            _ => {}
246        });
247    }
248}
249
250/// Makes a knob that represents the current value with an arc
251pub struct ArcTrack {
252    angle_start: f32,
253    angle_end: f32,
254    radius: Units,
255    span: Units,
256    normalized_value: f32,
257
258    center: bool,
259    mode: KnobMode,
260}
261
262impl ArcTrack {
263    /// Creates a new [ArcTrack] view.
264    pub fn new(
265        cx: &mut Context,
266        center: bool,
267        radius: Units,
268        span: Units,
269        angle_start: f32,
270        angle_end: f32,
271        mode: KnobMode,
272    ) -> Handle<Self> {
273        Self {
274            // angle_start: -150.0,
275            // angle_end: 150.0,
276            angle_start,
277            angle_end,
278            radius,
279            span,
280
281            normalized_value: 0.5,
282
283            center,
284            mode,
285        }
286        .build(cx, |_| {})
287    }
288}
289
290impl View for ArcTrack {
291    fn element(&self) -> Option<&'static str> {
292        Some("arctrack")
293    }
294
295    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
296        let opacity = cx.opacity();
297
298        let foreground_color = cx.font_color();
299
300        let background_color = cx.background_color();
301
302        let bounds = cx.bounds();
303
304        // Calculate arc center
305        let centerx = bounds.x + 0.5 * bounds.w;
306        let centery = bounds.y + 0.5 * bounds.h;
307
308        // Convert start and end angles to radians and rotate origin direction to be upwards instead of to the right
309        let start = self.angle_start;
310        let end = self.angle_end;
311
312        let parent = cx.tree.get_parent(cx.current).unwrap();
313
314        let parent_width = cx.cache.get_width(parent);
315
316        // Convert radius and span into screen coordinates
317        let radius = self.radius.to_px(parent_width / 2.0, 0.0);
318        // default value of span is 15 % of radius. Original span value was 16.667%
319        let span = self.span.to_px(radius, 0.0);
320
321        let oval = vg::Rect::new(bounds.left(), bounds.top(), bounds.right(), bounds.bottom());
322
323        let mut paint = vg::Paint::default();
324        paint.set_color(background_color);
325        paint.set_stroke_width(span);
326        paint.set_stroke_cap(vg::PaintCap::Round);
327        paint.set_style(vg::PaintStyle::Stroke);
328        canvas.draw_arc(oval, start, end - start, true, &paint);
329
330        let value = match self.mode {
331            KnobMode::Continuous => self.normalized_value,
332            // snapping
333            KnobMode::Discrete(steps) => {
334                (self.normalized_value * (steps - 1) as f32).floor() / (steps - 1) as f32
335            }
336        };
337
338        let (active_start, active_sweep) = if self.center {
339            let center = -90.0;
340
341            if value <= 0.5 {
342                let current = value * 2.0 * (center - start) + start;
343                (start, current)
344            } else {
345                let current = (value * 2.0 - 1.0) * (end - center);
346                (center, current)
347            }
348        } else {
349            let current = value * (end - start) + start;
350            (start, current - start)
351        };
352
353        let mut paint = vg::Paint::default();
354        paint.set_color(foreground_color);
355        paint.set_stroke_width(span);
356        paint.set_stroke_cap(vg::PaintCap::Round);
357        paint.set_style(vg::PaintStyle::Stroke);
358        paint.set_anti_alias(true);
359        canvas.draw_arc(
360            oval.with_inset((span / 2.0, span / 2.0)),
361            active_start,
362            active_sweep,
363            false,
364            &paint,
365        );
366    }
367}
368
369impl Handle<'_, ArcTrack> {
370    pub fn value<R: Res<f32>>(self, value: R) -> Self {
371        let entity = self.entity;
372        value.set_or_bind(self.cx, move |cx, value| {
373            let value = Res::get_value(&value, cx);
374            if let Some(view) = cx.views.get_mut(&entity) {
375                if let Some(knob) = view.downcast_mut::<ArcTrack>() {
376                    knob.normalized_value = value;
377                    cx.needs_redraw(entity);
378                }
379            }
380        });
381
382        self
383    }
384}
385
386#[derive(Debug, Default, Copy, Clone, PartialEq)]
387pub enum KnobMode {
388    Discrete(usize),
389    #[default]
390    Continuous,
391}
392
393/// Adds tickmarks to a knob to show the steps that a knob can be set to.
394/// When added to a knob, the knob should be made smaller (depending on span),
395/// so the knob doesn't overlap with the tick marks
396pub struct Ticks {
397    angle_start: f32,
398    angle_end: f32,
399    radius: Units,
400    // TODO: should this be renamed to inner_radius?
401    tick_len: Units,
402    tick_width: Units,
403    // steps: u32,
404    mode: KnobMode,
405}
406impl Ticks {
407    /// Creates a new [Ticks] view.
408    pub fn new(
409        cx: &mut Context,
410        radius: Units,
411        tick_len: Units,
412        tick_width: Units,
413        arc_len: f32,
414        mode: KnobMode,
415    ) -> Handle<Self> {
416        Self {
417            // angle_start: -150.0,
418            // angle_end: 150.0,
419            angle_start: -arc_len / 2.0,
420            angle_end: arc_len / 2.0,
421            radius,
422            tick_len,
423            tick_width,
424            mode,
425        }
426        .build(cx, |_| {})
427    }
428}
429
430impl View for Ticks {
431    fn element(&self) -> Option<&'static str> {
432        Some("ticks")
433    }
434    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
435        let opacity = cx.opacity();
436        //let mut background_color: femtovg::Color = cx.current.get_background_color(cx).into();
437        // background_color.set_alphaf(background_color.a * opacity);
438        let foreground_color = cx.background_color();
439        // let background_color = femtovg::Color::rgb(54, 54, 54);
440        //et mut foreground_color = femtovg::Color::rgb(50, 50, 200);
441        let bounds = cx.bounds();
442        // Clalculate arc center
443        let centerx = bounds.x + 0.5 * bounds.w;
444        let centery = bounds.y + 0.5 * bounds.h;
445        // Convert start and end angles to radians and rotate origin direction to be upwards instead of to the right
446        let start = self.angle_start.to_radians() - PI / 2.0;
447        let end = self.angle_end.to_radians() - PI / 2.0;
448        let parent = cx.tree.get_parent(cx.current).unwrap();
449        let parent_width = cx.cache.get_width(parent);
450        // Convert radius and span into screen coordinates
451        let radius = self.radius.to_px(parent_width / 2.0, 0.0);
452        // default value of span is 15 % of radius. Original span value was 16.667%
453        let tick_len = self.tick_len.to_px(radius, 0.0);
454        let line_width = self.tick_width.to_px(radius, 0.0);
455        let mut paint = vg::Paint::default();
456        paint.set_color(foreground_color);
457        paint.set_stroke_width(line_width);
458        paint.set_stroke_cap(vg::PaintCap::Round);
459        paint.set_style(vg::PaintStyle::Stroke);
460
461        match self.mode {
462            // can't really make ticks for a continuous knob
463            KnobMode::Continuous => (),
464            KnobMode::Discrete(steps) => {
465                for n in 0..steps {
466                    let a = n as f32 / (steps - 1) as f32;
467                    let angle = start + (end - start) * a;
468                    let start = vg::Point::new(
469                        centerx + angle.cos() * (radius - tick_len),
470                        centery + angle.sin() * (radius - tick_len),
471                    );
472                    let end = vg::Point::new(
473                        centerx + angle.cos() * (radius - line_width / 2.0),
474                        centery + angle.sin() * (radius - line_width / 2.0),
475                    );
476                    canvas.draw_line(start, end, &paint);
477                }
478            }
479        }
480    }
481}
482
483/// Makes a round knob with a tick to show the current value
484pub struct TickKnob {
485    angle_start: f32,
486    angle_end: f32,
487    radius: Units,
488    tick_width: Units,
489    tick_len: Units,
490    normalized_value: f32,
491    mode: KnobMode,
492}
493impl TickKnob {
494    /// Creates a new [TickKnob] view.
495    pub fn new(
496        cx: &mut Context,
497        radius: Units,
498        tick_width: Units,
499        tick_len: Units,
500        arc_len: f32,
501        // steps: u32,
502        mode: KnobMode,
503    ) -> Handle<Self> {
504        Self {
505            // angle_start: -150.0,
506            // angle_end: 150.0,
507            angle_start: -arc_len / 2.0,
508            angle_end: arc_len / 2.0,
509            radius,
510            tick_width,
511            tick_len,
512            normalized_value: 0.5,
513            mode,
514        }
515        .build(cx, |_| {})
516    }
517}
518
519impl View for TickKnob {
520    fn element(&self) -> Option<&'static str> {
521        Some("tickknob")
522    }
523    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
524        let opacity = cx.opacity();
525        //let mut background_color: femtovg::Color = cx.current.get_background_color(cx).into();
526        // background_color.set_alphaf(background_color.a * opacity);
527        let foreground_color = cx.font_color();
528        let background_color = cx.background_color();
529        //et mut foreground_color = femtovg::Color::rgb(50, 50, 200);
530        let bounds = cx.bounds();
531        // Calculate arc center
532        let centerx = bounds.x + 0.5 * bounds.w;
533        let centery = bounds.y + 0.5 * bounds.h;
534        // Convert start and end angles to radians and rotate origin direction to be upwards instead of to the right
535        let start = self.angle_start.to_radians() - PI / 2.0;
536        let end = self.angle_end.to_radians() - PI / 2.0;
537        let parent = cx.tree.get_parent(cx.current).unwrap();
538        let parent_width = cx.cache.get_width(parent);
539        // Convert radius and span into screen coordinates
540        let radius = self.radius.to_px(parent_width / 2.0, 0.0);
541        let tick_width = self.tick_width.to_px(radius, 0.0);
542        let tick_len = self.tick_len.to_px(radius, 0.0);
543        let mut paint = vg::Paint::default();
544        paint.set_color(background_color);
545        paint.set_stroke_width(tick_width);
546        paint.set_stroke_cap(vg::PaintCap::Round);
547        paint.set_style(vg::PaintStyle::Stroke);
548        canvas.draw_circle((centerx, centery), radius, &paint);
549        // Draw the tick
550        let angle = match self.mode {
551            KnobMode::Continuous => start + (end - start) * self.normalized_value,
552            // snapping
553            KnobMode::Discrete(steps) => {
554                start
555                    + (end - start) * (self.normalized_value * (steps - 1) as f32).floor()
556                        / (steps - 1) as f32
557            }
558        };
559        let start = vg::Point::new(
560            centerx + angle.cos() * (radius - tick_len),
561            centery + angle.sin() * (radius - tick_len),
562        );
563        let end = vg::Point::new(
564            centerx + angle.cos() * (radius - tick_width / 2.0),
565            centery + angle.sin() * (radius - tick_width / 2.0),
566        );
567        let mut paint = vg::Paint::default();
568        paint.set_color(foreground_color);
569        paint.set_stroke_width(tick_width);
570        paint.set_stroke_cap(vg::PaintCap::Round);
571        paint.set_style(vg::PaintStyle::Stroke);
572        canvas.draw_line(start, end, &paint);
573    }
574}
575
576impl Handle<'_, TickKnob> {
577    pub fn value<R: Res<f32>>(self, value: R) -> Self {
578        let entity = self.entity;
579        value.set_or_bind(self.cx, move |cx, value| {
580            let value = Res::get_value(&value, cx);
581            if let Some(view) = cx.views.get_mut(&entity) {
582                if let Some(knob) = view.downcast_mut::<TickKnob>() {
583                    knob.normalized_value = value;
584                    cx.needs_redraw(entity);
585                }
586            }
587        });
588        self
589    }
590}