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
42component_style! {
47 pub KnobStyle: KnobStyler(KnobStatus) {
48 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 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(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 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 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 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 }
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 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 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 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}