vizia_core/views/
knob.rs

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