embedded_ui/kit/
knob.rs

1use alloc::{boxed::Box, vec::Vec};
2use embedded_graphics::{
3    geometry::Angle,
4    primitives::{Arc, Circle, PrimitiveStyle},
5};
6
7use crate::{
8    color::UiColor,
9    el::{El, ElId},
10    event::{Capture, CommonEvent, Event, Propagate},
11    layout::{Layout, LayoutNode, Viewport},
12    padding::Padding,
13    render::Renderer,
14    size::{Length, Size},
15    state::{State, StateTag},
16    style::component_style,
17    ui::UiCtx,
18    value::Value,
19    widget::Widget,
20};
21
22#[derive(Clone, Copy)]
23struct KnobState {
24    is_active: bool,
25    is_pressed: bool,
26}
27
28impl Default for KnobState {
29    fn default() -> Self {
30        Self { is_active: false, is_pressed: false }
31    }
32}
33
34#[derive(Clone, Copy)]
35pub enum KnobStatus {
36    Normal,
37    Focused,
38    Pressed,
39    Active,
40}
41
42// TODO:
43// - Color of value (filled track)
44// - Color of track (not filled track)
45// - Center color instead of background
46component_style! {
47    pub KnobStyle: KnobStyler(KnobStatus) {
48        // background: background,
49        center_color: color,
50        color: color,
51        track_color: color,
52        track_width: width,
53    }
54}
55
56pub type KnobValue = u8;
57
58pub struct Knob<'a, Message, R, E, S>
59where
60    R: Renderer,
61    E: Event,
62    S: KnobStyler<R::Color>,
63{
64    id: ElId,
65    diameter: Length,
66    value: Value<KnobValue>,
67    step: KnobValue,
68    min: KnobValue,
69    max: KnobValue,
70    inner: Option<El<'a, Message, R, E, S>>,
71    // TODO: Can be moved to style as it doesn't affect layout
72    start: Angle,
73    on_change: Option<Box<dyn Fn(KnobValue) -> Message + 'a>>,
74    class: S::Class<'a>,
75}
76
77impl<'a, Message, R, E, S> Knob<'a, Message, R, E, S>
78where
79    R: Renderer,
80    E: Event,
81    S: KnobStyler<R::Color>,
82{
83    // pub fn new<F>(on_change: F) -> Self
84    // where
85    //     F: 'a + Fn(KnobValue) -> Message,
86    // {
87    //     Self {
88    //         id: ElId::unique(),
89    //         diameter: Length::Fill,
90    //         value: ,
91    //         step: 1,
92    //         min: 0,
93    //         max: KnobValue::MAX,
94    //         inner: None,
95    //         start: Angle::from_degrees(-90.0),
96    //         on_change: Box::new(on_change),
97    //         class: S::default(),
98    //     }
99    // }
100
101    pub fn new(value: Value<KnobValue>) -> Self {
102        Self {
103            id: ElId::unique(),
104            diameter: Length::Fill,
105            value,
106            step: 1,
107            min: 0,
108            max: KnobValue::MAX,
109            inner: None,
110            start: Angle::from_degrees(-90.0),
111            on_change: None,
112            class: S::default(),
113        }
114    }
115
116    // pub fn value(mut self, value: Value<KnobValue>) -> Self {
117    //     self.value = value;
118    //     self
119    // }
120
121    pub fn min(mut self, min: KnobValue) -> Self {
122        self.min = min;
123        self
124    }
125
126    pub fn max(mut self, max: KnobValue) -> Self {
127        self.max = max;
128        self
129    }
130
131    pub fn step(mut self, step: KnobValue) -> Self {
132        self.step = step;
133        self
134    }
135
136    pub fn diameter(mut self, diameter: impl Into<Length>) -> Self {
137        self.diameter = diameter.into();
138        self
139    }
140
141    pub fn start(mut self, start: impl Into<Angle>) -> Self {
142        self.start = start.into();
143        self
144    }
145
146    pub fn on_change<F>(mut self, on_change: F) -> Self
147    where
148        F: 'a + Fn(KnobValue) -> Message,
149    {
150        self.on_change = Some(Box::new(on_change));
151        self
152    }
153
154    pub fn inner(mut self, inner: impl Into<El<'a, Message, R, E, S>>) -> Self {
155        self.inner = Some(inner.into());
156        self
157    }
158
159    // Helpers //
160    fn status(&self, ctx: &UiCtx<Message>, state: &KnobState) -> KnobStatus {
161        let is_focused = ctx.is_focused(self);
162        match (is_focused, state) {
163            (_, KnobState { is_active: true, .. }) => KnobStatus::Active,
164            (_, KnobState { is_pressed: true, .. }) => KnobStatus::Pressed,
165            (true, KnobState { is_active: false, is_pressed: false }) => KnobStatus::Focused,
166            (false, KnobState { is_active: false, is_pressed: false }) => KnobStatus::Normal,
167        }
168    }
169}
170
171impl<'a, Message, R, E, S> Widget<Message, R, E, S> for Knob<'a, Message, R, E, S>
172where
173    R: Renderer,
174    E: Event,
175    S: KnobStyler<R::Color>,
176{
177    fn id(&self) -> Option<ElId> {
178        Some(self.id)
179    }
180
181    fn tree_ids(&self) -> Vec<ElId> {
182        vec![self.id]
183    }
184
185    fn size(&self) -> crate::size::Size<crate::size::Length> {
186        Size::new_equal(self.diameter)
187    }
188
189    fn state_tag(&self) -> crate::state::StateTag {
190        StateTag::of::<KnobState>()
191    }
192
193    fn state(&self) -> crate::state::State {
194        State::new(KnobState::default())
195    }
196
197    fn state_children(&self) -> Vec<crate::state::StateNode> {
198        vec![]
199    }
200
201    fn on_event(
202        &mut self,
203        ctx: &mut UiCtx<Message>,
204        event: E,
205        state: &mut crate::state::StateNode,
206    ) -> crate::event::EventResponse<E> {
207        let focused = ctx.is_focused::<R, E, S>(self);
208        let current_state = *state.get::<KnobState>();
209
210        if let Some(offset) = event.as_knob_rotation() {
211            if current_state.is_active {
212                let prev_value = *self.value.get();
213
214                *self.value.get_mut() = (prev_value as i32)
215                    .saturating_add(offset * self.step as i32)
216                    .clamp(self.min as i32, self.max as i32)
217                    as u8;
218
219                if let Some(on_change) = self.on_change.as_ref() {
220                    if prev_value != *self.value.get() {
221                        ctx.publish((on_change)(*self.value.get()));
222                    }
223                }
224
225                return Capture::Captured.into();
226            }
227        }
228
229        if let Some(common) = event.as_common() {
230            match common {
231                CommonEvent::FocusMove(_) if focused => {
232                    return Propagate::BubbleUp(self.id, event).into()
233                },
234                CommonEvent::FocusClickDown if focused => {
235                    state.get_mut::<KnobState>().is_pressed = true;
236                    return Capture::Captured.into();
237                },
238                CommonEvent::FocusClickUp if focused => {
239                    state.get_mut::<KnobState>().is_pressed = false;
240
241                    if current_state.is_pressed {
242                        state.get_mut::<KnobState>().is_active =
243                            !state.get::<KnobState>().is_active;
244
245                        return Capture::Captured.into();
246                    }
247                },
248                CommonEvent::FocusClickDown
249                | CommonEvent::FocusClickUp
250                | CommonEvent::FocusMove(_) => {
251                    // Should we reset state on any event? Or only on common
252                    state.reset::<KnobState>();
253                },
254            }
255        }
256
257        Propagate::Ignored.into()
258    }
259
260    fn layout(
261        &self,
262        ctx: &mut crate::ui::UiCtx<Message>,
263        state: &mut crate::state::StateNode,
264        styler: &S,
265        limits: &crate::layout::Limits,
266        viewport: &Viewport,
267    ) -> crate::layout::LayoutNode {
268        let size = Size::new_equal(self.diameter);
269        Layout::container(
270            limits,
271            size,
272            crate::layout::Position::Relative,
273            viewport,
274            Padding::zero(),
275            Padding::zero(),
276            crate::align::Alignment::Center,
277            crate::align::Alignment::Center,
278            |limits| {
279                if let Some(inner) = self.inner.as_ref() {
280                    inner.layout(ctx, &mut state.children[0], styler, limits, viewport)
281                } else {
282                    LayoutNode::new(Size::zero())
283                }
284            },
285        )
286        // Layout::sized(limits, size, |limits| {
287        //     limits.resolve_size(size.width, size.height, Size::zero())
288        // })
289    }
290
291    fn draw(
292        &self,
293        ctx: &mut crate::ui::UiCtx<Message>,
294        state_tree: &mut crate::state::StateNode,
295        renderer: &mut R,
296        styler: &S,
297        layout: crate::layout::Layout,
298    ) {
299        let state = state_tree.get::<KnobState>();
300        let status = self.status(ctx, state);
301        let style = styler.style(&self.class, status);
302        let bounds = layout.bounds();
303
304        let outer_diameter = bounds.size.max_square();
305        let track_diameter = outer_diameter - style.track_width - style.track_width / 2;
306
307        let center = bounds.center();
308
309        // TODO: Fix stroke drawing, offset by half of the stroke so it goes on outer bound of arc
310
311        // Center circle
312        renderer.circle(
313            Circle::with_center(center, outer_diameter - style.track_width - style.track_width / 2),
314            PrimitiveStyle::with_fill(style.center_color),
315        );
316
317        if let Some(inner) = self.inner.as_ref() {
318            inner.draw(
319                ctx,
320                &mut state_tree.children[0],
321                renderer,
322                styler,
323                layout.children().next().unwrap(),
324            );
325        }
326
327        // Whole track
328        renderer.arc(
329            Arc::with_center(center, track_diameter, self.start, Angle::from_degrees(360.0)),
330            PrimitiveStyle::with_stroke(style.track_color, style.track_width),
331        );
332
333        // TODO: Draw min/max serifs
334
335        let value_degree = 360.0 * (*self.value.get() as f32 / u8::MAX as f32);
336
337        renderer.arc(
338            Arc::with_center(center, track_diameter, self.start, Angle::from_degrees(value_degree)),
339            PrimitiveStyle::with_stroke(style.color, style.track_width),
340        );
341    }
342}
343
344impl<'a, Message, R, E, S> From<Knob<'a, Message, R, E, S>> for El<'a, Message, R, E, S>
345where
346    Message: Clone + 'a,
347    R: Renderer + 'a,
348    E: Event + 'a,
349    S: KnobStyler<R::Color> + 'a,
350{
351    fn from(value: Knob<'a, Message, R, E, S>) -> Self {
352        El::new(value)
353    }
354}