ori_core/views/
knob.rs

1use std::f32::consts::PI;
2
3use glam::Vec2;
4use ori_graphics::{Curve, Quad, Rect};
5use ori_macro::Build;
6
7use crate::{
8    BoxConstraints, Context, DrawContext, Event, EventContext, LayoutContext, PointerEvent,
9    SharedSignal, Style, View,
10};
11
12#[derive(Clone, Debug, Build)]
13pub struct Knob {
14    #[bind]
15    value: SharedSignal<f32>,
16    #[prop]
17    max: f32,
18    #[prop]
19    min: f32,
20    #[prop]
21    label: String,
22}
23
24impl Default for Knob {
25    fn default() -> Self {
26        Self {
27            value: SharedSignal::new(0.0),
28            max: 1.0,
29            min: 0.0,
30            label: String::new(),
31        }
32    }
33}
34
35impl View for Knob {
36    type State = Option<Vec2>;
37
38    fn build(&self) -> Self::State {
39        None
40    }
41
42    fn style(&self) -> Style {
43        Style::new("knob")
44    }
45
46    fn event(&self, state: &mut Self::State, cx: &mut EventContext, event: &Event) {
47        if let Some(pointer_event) = event.get::<PointerEvent>() {
48            if pointer_event.is_press() && cx.hovered() {
49                cx.activate();
50            } else if pointer_event.is_release() && cx.active() {
51                *state = None;
52                cx.deactivate();
53            }
54
55            if cx.active() {
56                if let Some(prev_position) = *state {
57                    let delta = pointer_event.position - prev_position;
58                    let delta = delta.x - delta.y * delta.length();
59                    let range = self.max - self.min;
60                    let delta = delta / cx.rect().width() * range * 0.15;
61                    let value = self.value.get();
62                    let new_value = f32::clamp(*value + delta, self.min, self.max);
63                    self.value.set(new_value);
64                }
65
66                *state = Some(pointer_event.position);
67                cx.request_redraw();
68            }
69        }
70    }
71
72    fn layout(&self, _state: &mut Self::State, cx: &mut LayoutContext, bc: BoxConstraints) -> Vec2 {
73        let size = cx.style_range("size", bc.min.max_element()..bc.max.min_element());
74        bc.constrain(Vec2::splat(size))
75    }
76
77    fn draw(&self, _state: &mut Self::State, cx: &mut DrawContext) {
78        let size = cx.rect().size().min_element();
79
80        let diameter = size * 0.7;
81
82        let circle = Quad {
83            rect: Rect::center_size(cx.rect().center(), Vec2::splat(diameter)),
84            background: cx.style("background-color"),
85            border_radius: [diameter * 0.5; 4],
86            border_width: cx.style_range("border-width", 0.0..diameter * 0.5),
87            border_color: cx.style("border-color"),
88        };
89
90        cx.draw(circle);
91
92        let curve = Curve::arc(cx.rect().center(), diameter * 0.65, -PI * 1.25, PI * 0.25);
93        let mesh = curve.rounded_mesh(diameter * 0.075, cx.style("background-color"));
94        cx.draw(mesh);
95
96        let range = self.max - self.min;
97        let value = self.value.get();
98        let angle = (*value - self.min) / range;
99        let angle = -PI * 1.25 + angle * PI * 1.5;
100
101        let curve = Curve::arc(cx.rect().center(), diameter * 0.65, -PI * 1.25, angle);
102        let mesh = curve.rounded_mesh(diameter * 0.075, cx.style("color"));
103        cx.draw(mesh);
104
105        let mut arm = Curve::new();
106        arm.add_point(cx.rect().center());
107        arm.add_point(cx.rect().center() + Vec2::new(angle.cos(), angle.sin()) * diameter * 0.65);
108
109        let mesh = arm.rounded_mesh(diameter * 0.075, cx.style("color"));
110        cx.draw(mesh);
111    }
112}