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 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 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 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 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 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 renderer.line(guide_start, guide_end, R::Color::default_foreground(), 1);
267
268 let knob_size = Size::new_equal(5);
270 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}