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        // Draw the track arc
322        let path = vg::Path::new();
323        // path.arc(centerx, centery, radius - span / 2.0, end, start, Solidity::Solid);
324        let oval = vg::Rect::new(bounds.left(), bounds.top(), bounds.right(), bounds.bottom());
325
326        let mut paint = vg::Paint::default();
327        paint.set_color(background_color);
328        paint.set_stroke_width(span);
329        paint.set_stroke_cap(vg::PaintCap::Round);
330        paint.set_style(vg::PaintStyle::Stroke);
331        // canvas.draw_path(&path, &paint);
332        canvas.draw_arc(oval, start, end - start, true, &paint);
333
334        // Draw the active arc
335        let mut path = vg::PathBuilder::new();
336
337        let value = match self.mode {
338            KnobMode::Continuous => self.normalized_value,
339            // snapping
340            KnobMode::Discrete(steps) => {
341                (self.normalized_value * (steps - 1) as f32).floor() / (steps - 1) as f32
342            }
343        };
344
345        if self.center {
346            let center = -90.0;
347
348            if value <= 0.5 {
349                let current = value * 2.0 * (center - start) + start;
350                path.arc_to(oval.with_inset((span / 2.0, span / 2.0)), start, current, false);
351            } else {
352                let current = (value * 2.0 - 1.0) * (end - center);
353                path.arc_to(oval.with_inset((span / 2.0, span / 2.0)), center, current, false);
354            }
355        } else {
356            let current = value * (end - start) + start;
357            path.arc_to(oval.with_inset((span / 2.0, span / 2.0)), start, current - start, false);
358        }
359
360        let mut paint = vg::Paint::default();
361        paint.set_color(foreground_color);
362        paint.set_stroke_width(span);
363        paint.set_stroke_cap(vg::PaintCap::Round);
364        paint.set_style(vg::PaintStyle::Stroke);
365        paint.set_anti_alias(true);
366        let path = path.detach();
367        canvas.draw_path(&path, &paint);
368    }
369}
370
371impl Handle<'_, ArcTrack> {
372    pub fn value<R: Res<f32>>(self, value: R) -> Self {
373        let entity = self.entity;
374        value.set_or_bind(self.cx, move |cx, value| {
375            let value = Res::get_value(&value, cx);
376            if let Some(view) = cx.views.get_mut(&entity) {
377                if let Some(knob) = view.downcast_mut::<ArcTrack>() {
378                    knob.normalized_value = value;
379                    cx.needs_redraw(entity);
380                }
381            }
382        });
383
384        self
385    }
386}
387
388#[derive(Debug, Default, Copy, Clone, PartialEq)]
389pub enum KnobMode {
390    Discrete(usize),
391    #[default]
392    Continuous,
393}
394
395/// Adds tickmarks to a knob to show the steps that a knob can be set to.
396/// When added to a knob, the knob should be made smaller (depending on span),
397/// so the knob doesn't overlap with the tick marks
398pub struct Ticks {
399    angle_start: f32,
400    angle_end: f32,
401    radius: Units,
402    // TODO: should this be renamed to inner_radius?
403    tick_len: Units,
404    tick_width: Units,
405    // steps: u32,
406    mode: KnobMode,
407}
408impl Ticks {
409    /// Creates a new [Ticks] view.
410    pub fn new(
411        cx: &mut Context,
412        radius: Units,
413        tick_len: Units,
414        tick_width: Units,
415        arc_len: f32,
416        mode: KnobMode,
417    ) -> Handle<Self> {
418        Self {
419            // angle_start: -150.0,
420            // angle_end: 150.0,
421            angle_start: -arc_len / 2.0,
422            angle_end: arc_len / 2.0,
423            radius,
424            tick_len,
425            tick_width,
426            mode,
427        }
428        .build(cx, |_| {})
429    }
430}
431
432impl View for Ticks {
433    fn element(&self) -> Option<&'static str> {
434        Some("ticks")
435    }
436    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
437        let opacity = cx.opacity();
438        //let mut background_color: femtovg::Color = cx.current.get_background_color(cx).into();
439        // background_color.set_alphaf(background_color.a * opacity);
440        let foreground_color = cx.background_color();
441        // let background_color = femtovg::Color::rgb(54, 54, 54);
442        //et mut foreground_color = femtovg::Color::rgb(50, 50, 200);
443        let bounds = cx.bounds();
444        // Clalculate arc center
445        let centerx = bounds.x + 0.5 * bounds.w;
446        let centery = bounds.y + 0.5 * bounds.h;
447        // Convert start and end angles to radians and rotate origin direction to be upwards instead of to the right
448        let start = self.angle_start.to_radians() - PI / 2.0;
449        let end = self.angle_end.to_radians() - PI / 2.0;
450        let parent = cx.tree.get_parent(cx.current).unwrap();
451        let parent_width = cx.cache.get_width(parent);
452        // Convert radius and span into screen coordinates
453        let radius = self.radius.to_px(parent_width / 2.0, 0.0);
454        // default value of span is 15 % of radius. Original span value was 16.667%
455        let tick_len = self.tick_len.to_px(radius, 0.0);
456        let line_width = self.tick_width.to_px(radius, 0.0);
457        // Draw ticks
458        let mut path = vg::PathBuilder::new();
459        match self.mode {
460            // can't really make ticks for a continuous knob
461            KnobMode::Continuous => return,
462            KnobMode::Discrete(steps) => {
463                for n in 0..steps {
464                    let a = n as f32 / (steps - 1) as f32;
465                    let angle = start + (end - start) * a;
466                    path.move_to((
467                        centerx + angle.cos() * (radius - tick_len),
468                        centery + angle.sin() * (radius - tick_len),
469                    ));
470                    path.line_to((
471                        centerx + angle.cos() * (radius - line_width / 2.0),
472                        centery + angle.sin() * (radius - line_width / 2.0),
473                    ));
474                }
475            }
476        }
477        let mut paint = vg::Paint::default();
478        paint.set_color(foreground_color);
479        paint.set_stroke_width(line_width);
480        paint.set_stroke_cap(vg::PaintCap::Round);
481        paint.set_style(vg::PaintStyle::Stroke);
482        let path = path.detach();
483        canvas.draw_path(&path, &paint);
484    }
485}
486
487/// Makes a round knob with a tick to show the current value
488pub struct TickKnob {
489    angle_start: f32,
490    angle_end: f32,
491    radius: Units,
492    tick_width: Units,
493    tick_len: Units,
494    normalized_value: f32,
495    mode: KnobMode,
496}
497impl TickKnob {
498    /// Creates a new [TickKnob] view.
499    pub fn new(
500        cx: &mut Context,
501        radius: Units,
502        tick_width: Units,
503        tick_len: Units,
504        arc_len: f32,
505        // steps: u32,
506        mode: KnobMode,
507    ) -> Handle<Self> {
508        Self {
509            // angle_start: -150.0,
510            // angle_end: 150.0,
511            angle_start: -arc_len / 2.0,
512            angle_end: arc_len / 2.0,
513            radius,
514            tick_width,
515            tick_len,
516            normalized_value: 0.5,
517            mode,
518        }
519        .build(cx, |_| {})
520    }
521}
522
523impl View for TickKnob {
524    fn element(&self) -> Option<&'static str> {
525        Some("tickknob")
526    }
527    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
528        let opacity = cx.opacity();
529        //let mut background_color: femtovg::Color = cx.current.get_background_color(cx).into();
530        // background_color.set_alphaf(background_color.a * opacity);
531        let foreground_color = cx.font_color();
532        let background_color = cx.background_color();
533        //et mut foreground_color = femtovg::Color::rgb(50, 50, 200);
534        let bounds = cx.bounds();
535        // Calculate arc center
536        let centerx = bounds.x + 0.5 * bounds.w;
537        let centery = bounds.y + 0.5 * bounds.h;
538        // Convert start and end angles to radians and rotate origin direction to be upwards instead of to the right
539        let start = self.angle_start.to_radians() - PI / 2.0;
540        let end = self.angle_end.to_radians() - PI / 2.0;
541        let parent = cx.tree.get_parent(cx.current).unwrap();
542        let parent_width = cx.cache.get_width(parent);
543        // Convert radius and span into screen coordinates
544        let radius = self.radius.to_px(parent_width / 2.0, 0.0);
545        let tick_width = self.tick_width.to_px(radius, 0.0);
546        let tick_len = self.tick_len.to_px(radius, 0.0);
547        // Draw the circle
548        let mut path = vg::PathBuilder::new();
549        path.add_circle((centerx, centery), radius, None);
550        // path.arc(centerx, centery, radius - span / 2.0, end, start, Solidity::Solid);
551        let mut paint = vg::Paint::default();
552        paint.set_color(background_color);
553        paint.set_stroke_width(tick_width);
554        paint.set_stroke_cap(vg::PaintCap::Round);
555        paint.set_style(vg::PaintStyle::Stroke);
556        let path = path.detach();
557        canvas.draw_path(&path, &paint);
558        // Draw the tick
559        let mut path = vg::PathBuilder::new();
560        let angle = match self.mode {
561            KnobMode::Continuous => start + (end - start) * self.normalized_value,
562            // snapping
563            KnobMode::Discrete(steps) => {
564                start
565                    + (end - start) * (self.normalized_value * (steps - 1) as f32).floor()
566                        / (steps - 1) as f32
567            }
568        };
569        path.move_to(
570            // centerx + angle.cos() * (radius * 0.70),
571            (
572                centerx + angle.cos() * (radius - tick_len),
573                centery + angle.sin() * (radius - tick_len),
574            ),
575        );
576        path.line_to((
577            centerx + angle.cos() * (radius - tick_width / 2.0),
578            centery + angle.sin() * (radius - tick_width / 2.0),
579        ));
580        let mut paint = vg::Paint::default();
581        paint.set_color(foreground_color);
582        paint.set_stroke_width(tick_width);
583        paint.set_stroke_cap(vg::PaintCap::Round);
584        paint.set_style(vg::PaintStyle::Stroke);
585        let path = path.detach();
586        canvas.draw_path(&path, &paint);
587    }
588}
589
590impl Handle<'_, TickKnob> {
591    pub fn value<R: Res<f32>>(self, value: R) -> Self {
592        let entity = self.entity;
593        value.set_or_bind(self.cx, move |cx, value| {
594            let value = Res::get_value(&value, cx);
595            if let Some(view) = cx.views.get_mut(&entity) {
596                if let Some(knob) = view.downcast_mut::<TickKnob>() {
597                    knob.normalized_value = value;
598                    cx.needs_redraw(entity);
599                }
600            }
601        });
602        self
603    }
604}