embedded_ui/kit/
slider.rs

1use alloc::{boxed::Box, vec::Vec};
2use embedded_graphics::{geometry::Point, primitives::Rectangle};
3
4use crate::{
5    align::Axis,
6    block::{Block, Border},
7    color::UiColor,
8    el::{El, ElId},
9    event::{Capture, CommonEvent, Event, Propagate},
10    icons::IconKind,
11    layout::{Layout, Viewport},
12    render::Renderer,
13    size::{Length, Size},
14    state::{State, StateNode, StateTag},
15    style::component_style,
16    ui::UiCtx,
17    widget::Widget,
18};
19
20#[derive(Clone, Copy)]
21struct SliderState {
22    is_active: bool,
23    is_pressed: bool,
24}
25
26impl Default for SliderState {
27    fn default() -> Self {
28        Self { is_active: false, is_pressed: false }
29    }
30}
31
32#[derive(Clone, Copy)]
33pub enum SliderStatus {
34    Normal,
35    Focused,
36    Pressed,
37    Active,
38}
39
40component_style! {
41    pub SliderStyle: SliderStyler(SliderStatus) {
42        background: background,
43        border: border,
44    }
45}
46
47pub type SliderPosition = u8;
48
49pub struct Slider<'a, Message, R, S>
50where
51    R: Renderer,
52    S: SliderStyler<R::Color>,
53{
54    axis: Axis,
55    id: ElId,
56    size: Size<Length>,
57    value: u8,
58    step: u8,
59    // knob_icon: IconKind,
60    on_change: Box<dyn Fn(SliderPosition) -> Message + 'a>,
61    class: S::Class<'a>,
62}
63
64impl<'a, Message, R, S> Slider<'a, Message, R, S>
65where
66    R: Renderer,
67    S: SliderStyler<R::Color>,
68{
69    pub fn new<F>(axis: Axis, on_change: F) -> Self
70    where
71        F: 'a + Fn(SliderPosition) -> Message,
72    {
73        Self {
74            axis,
75            id: ElId::unique(),
76            size: Size::fill(),
77            value: 0,
78            step: 1,
79            on_change: Box::new(on_change),
80            class: S::default(),
81        }
82    }
83
84    pub fn vertical<F>(on_change: F) -> Self
85    where
86        F: 'a + Fn(SliderPosition) -> Message,
87    {
88        Self::new(Axis::Y, on_change)
89    }
90
91    pub fn horizontal<F>(on_change: F) -> Self
92    where
93        F: 'a + Fn(SliderPosition) -> Message,
94    {
95        Self::new(Axis::X, on_change)
96    }
97
98    pub fn width(mut self, width: impl Into<Length>) -> Self {
99        self.size.width = width.into();
100        self
101    }
102
103    pub fn height(mut self, height: impl Into<Length>) -> Self {
104        self.size.height = height.into();
105        self
106    }
107
108    pub fn step(mut self, step: u8) -> Self {
109        self.step = step;
110        self
111    }
112
113    // Helpers //
114    fn status<E: Event>(&self, ctx: &UiCtx<Message>, state: &StateNode) -> SliderStatus {
115        match state.get::<SliderState>() {
116            SliderState { is_active: true, .. } => return SliderStatus::Active,
117            SliderState { is_pressed: true, .. } => return SliderStatus::Pressed,
118            SliderState { is_active: false, is_pressed: false } => {},
119        }
120
121        if UiCtx::is_focused::<R, E, S>(&ctx, self) {
122            return SliderStatus::Focused;
123        }
124
125        SliderStatus::Normal
126    }
127}
128
129impl<'a, Message, R, E, S> Widget<Message, R, E, S> for Slider<'a, Message, R, S>
130where
131    R: Renderer,
132    E: Event,
133    S: SliderStyler<R::Color>,
134{
135    fn id(&self) -> Option<ElId> {
136        Some(self.id)
137    }
138
139    fn tree_ids(&self) -> Vec<ElId> {
140        vec![self.id]
141    }
142
143    fn size(&self) -> Size<Length> {
144        self.size
145    }
146
147    fn state_tag(&self) -> crate::state::StateTag {
148        StateTag::of::<SliderState>()
149    }
150
151    fn state(&self) -> State {
152        State::new(SliderState::default())
153    }
154
155    fn state_children(&self) -> Vec<crate::state::StateNode> {
156        vec![]
157    }
158
159    fn on_event(
160        &mut self,
161        ctx: &mut crate::ui::UiCtx<Message>,
162        event: E,
163        state: &mut crate::state::StateNode,
164    ) -> crate::event::EventResponse<E> {
165        let focused = ctx.is_focused::<R, E, S>(self);
166        let current_state = *state.get::<SliderState>();
167
168        if let Some(offset) = event.as_slider_shift() {
169            if current_state.is_active {
170                let prev_value = self.value;
171
172                self.value = (self.value as i32)
173                    .saturating_add(offset * self.step as i32)
174                    .clamp(0, u8::MAX as i32) as u8;
175
176                if prev_value != self.value {
177                    ctx.publish((self.on_change)(self.value));
178                }
179
180                return Capture::Captured.into();
181            }
182        }
183
184        // TODO: Generalize this focus logic for button, select and slider, etc.
185        if let Some(common) = event.as_common() {
186            match common {
187                CommonEvent::FocusMove(_) if focused => {
188                    return Propagate::BubbleUp(self.id, event).into()
189                },
190                CommonEvent::FocusClickDown if focused => {
191                    state.get_mut::<SliderState>().is_pressed = true;
192                    return Capture::Captured.into();
193                },
194                CommonEvent::FocusClickUp if focused => {
195                    state.get_mut::<SliderState>().is_pressed = false;
196
197                    if current_state.is_pressed {
198                        state.get_mut::<SliderState>().is_active =
199                            !state.get::<SliderState>().is_active;
200                        return Capture::Captured.into();
201                    }
202                },
203                CommonEvent::FocusClickDown
204                | CommonEvent::FocusClickUp
205                | CommonEvent::FocusMove(_) => {
206                    // Should we reset state on any event? Or only on common
207                    state.reset::<SliderState>();
208                },
209            }
210        }
211
212        Propagate::Ignored.into()
213    }
214
215    fn layout(
216        &self,
217        ctx: &mut crate::ui::UiCtx<Message>,
218        state: &mut crate::state::StateNode,
219        styler: &S,
220        limits: &crate::layout::Limits,
221        viewport: &Viewport,
222    ) -> crate::layout::LayoutNode {
223        Layout::sized(limits, self.size, crate::layout::Position::Relative, viewport, |limits| {
224            limits.resolve_size(self.size.width, self.size.height, Size::zero())
225        })
226    }
227
228    fn draw(
229        &self,
230        ctx: &mut crate::ui::UiCtx<Message>,
231        state: &mut crate::state::StateNode,
232        renderer: &mut R,
233        styler: &S,
234        layout: crate::layout::Layout,
235    ) {
236        let style = styler.style(&self.class, self.status::<E>(ctx, state));
237
238        let state = state.get::<SliderState>();
239
240        let bounds = layout.bounds();
241
242        if bounds.size.width == 0 || bounds.size.height == 0 {
243            return;
244        }
245
246        renderer.block(Block {
247            border: style.border,
248            rect: bounds.into(),
249            background: style.background,
250        });
251
252        // TODO: WTF. Just finally add compound type `AxisData` for such calculations, looks buggy asf
253        let (main_axis_pos, anti_axis_pos) = self.axis.canon(bounds.position.x, bounds.position.y);
254        let (main_length, anti_length) = self.axis.canon(bounds.size.width, bounds.size.height);
255
256        let guide_anti_axis_pos = anti_axis_pos + anti_length as i32 / 2;
257
258        let guide_start_pos = self.axis.canon(main_axis_pos, guide_anti_axis_pos);
259        let guide_start = Point::new(guide_start_pos.0, guide_start_pos.1);
260
261        let guide_end_pos =
262            self.axis.canon(main_axis_pos + main_length as i32, guide_anti_axis_pos);
263        let guide_end = Point::new(guide_end_pos.0, guide_end_pos.1);
264
265        // TODO: Style for guide
266        renderer.line(guide_start, guide_end, R::Color::default_foreground(), 1);
267
268        // let knob_size = Size::new_equal(bounds.size.width.min(bounds.size.height));
269        let knob_size = Size::new_equal(5);
270        // let (knob_center_main, knob_center_anti) = self.axis.canon(
271        //     main_axis_pos + knob_size.width as i32 / 2,
272        //     anti_axis_pos + knob_size.height as i32 / 2,
273        // );
274
275        let knob_shift_offset = self.value as u32 * main_length / u8::MAX as u32;
276        let (knob_main_axis_pos, knob_anti_axis_pos) =
277            self.axis.canon(main_axis_pos + knob_shift_offset as i32, guide_anti_axis_pos);
278
279        let knob_background = if state.is_active {
280            R::Color::default_foreground()
281        } else {
282            R::Color::default_background()
283        };
284
285        let knob = Block {
286            border: Border { color: R::Color::default_foreground(), width: 1, radius: 0.into() },
287            rect: Rectangle::with_center(
288                Point::new(knob_main_axis_pos, knob_anti_axis_pos),
289                knob_size.into(),
290            ),
291            background: knob_background,
292        };
293
294        renderer.block(knob);
295    }
296}
297
298impl<'a, Message, R, E, S> From<Slider<'a, Message, R, S>> for El<'a, Message, R, E, S>
299where
300    Message: Clone + 'a,
301    R: Renderer + 'a,
302    E: Event + 'a,
303    S: SliderStyler<R::Color> + 'a,
304{
305    fn from(value: Slider<'a, Message, R, S>) -> Self {
306        El::new(value)
307    }
308}