embedded_ui/kit/
select.rs

1use alloc::vec::Vec;
2use embedded_graphics::{
3    geometry::Point,
4    primitives::{PrimitiveStyle, Rectangle, StyledDrawable},
5};
6
7use crate::{
8    block::{Block, Border, BorderRadius},
9    color::UiColor,
10    el::{El, ElId},
11    event::{Capture, CommonEvent, Event, Propagate},
12    icons::IconKind,
13    layout::{Layout, LayoutNode, Limits, Viewport},
14    padding::Padding,
15    render::Renderer,
16    size::{Length, Size},
17    state::{State, StateNode, StateTag},
18    style::component_style,
19    ui::UiCtx,
20    widget::Widget,
21};
22
23use super::icon::Icon;
24
25pub struct SelectState {
26    is_pressed: bool,
27    is_active: bool,
28}
29
30impl Default for SelectState {
31    fn default() -> Self {
32        Self { is_pressed: false, is_active: false }
33    }
34}
35
36#[derive(Clone, Copy)]
37pub enum SelectStatus {
38    Normal,
39    Active,
40    Pressed,
41    Focused,
42}
43
44// pub type SelectStyleFn<'a, C> = Box<dyn Fn(SelectStatus) -> SelectStyle<C> + 'a>;
45
46component_style! {
47    pub SelectStyle: SelectStyler(SelectStatus) {
48        background: background,
49        border: border,
50    }
51}
52
53pub struct Select<'a, Message, R, E, S>
54where
55    R: Renderer,
56    E: Event,
57    S: SelectStyler<R::Color>,
58{
59    id: ElId,
60    size: Size<Length>,
61    icon_left: IconKind,
62    icon_right: IconKind,
63    options: Vec<El<'a, Message, R, E, S>>,
64    chosen: usize,
65    class: S::Class<'a>,
66    cycle: bool,
67}
68
69impl<'a, Message, R, E, S> Select<'a, Message, R, E, S>
70where
71    R: Renderer,
72    E: Event,
73    S: SelectStyler<R::Color>,
74{
75    pub fn new(options: Vec<El<'a, Message, R, E, S>>) -> Self {
76        Self {
77            id: ElId::unique(),
78            size: Size::fill(),
79            icon_left: IconKind::ArrowLeft,
80            icon_right: IconKind::ArrowRight,
81            options,
82            chosen: 0,
83            class: S::default(),
84            cycle: false,
85        }
86    }
87
88    pub fn initial(mut self, index: impl Into<usize>) -> Self {
89        self.chosen = index.into();
90        self
91    }
92
93    pub fn width(mut self, width: impl Into<Length>) -> Self {
94        self.size.width = width.into();
95        self
96    }
97
98    pub fn height(mut self, height: impl Into<Length>) -> Self {
99        self.size.height = height.into();
100        self
101    }
102
103    pub fn cycle(mut self, cycle: bool) -> Self {
104        self.cycle = cycle;
105        self
106    }
107
108    pub fn icon_left(mut self, icon_left: IconKind) -> Self {
109        self.icon_left = icon_left;
110        self
111    }
112
113    pub fn icon_right(mut self, icon_right: IconKind) -> Self {
114        self.icon_right = icon_right;
115        self
116    }
117
118    // Helpers //
119    fn current(&self) -> &El<'a, Message, R, E, S> {
120        &self.options[self.chosen]
121    }
122
123    fn arrow_icon_size(&self, limits: &Limits) -> u32 {
124        // limits.max().height
125        // TODO
126        5
127    }
128
129    fn status(&self, ctx: &UiCtx<Message>, state: &StateNode) -> SelectStatus {
130        match state.get::<SelectState>() {
131            SelectState { is_active: true, .. } => return SelectStatus::Active,
132            SelectState { is_pressed: true, .. } => return SelectStatus::Pressed,
133            SelectState { is_pressed: false, is_active: false } => {},
134        }
135
136        if ctx.is_focused(self) {
137            return SelectStatus::Focused;
138        }
139
140        SelectStatus::Normal
141    }
142}
143
144impl<'a, Message, R, E, S> Widget<Message, R, E, S> for Select<'a, Message, R, E, S>
145where
146    R: Renderer,
147    E: Event,
148    S: SelectStyler<R::Color>,
149{
150    fn id(&self) -> Option<crate::el::ElId> {
151        Some(self.id)
152    }
153
154    fn tree_ids(&self) -> Vec<crate::el::ElId> {
155        vec![self.id]
156        // TODO: Maybe Select should hide ids of its children or we might fail on focusing them
157        // self.options.iter().map(|option| option.tree_ids()).flatten().collect()
158    }
159
160    fn size(&self) -> Size<Length> {
161        self.size
162    }
163
164    fn state_tag(&self) -> crate::state::StateTag {
165        StateTag::of::<SelectState>()
166    }
167
168    fn state(&self) -> State {
169        State::new(SelectState::default())
170    }
171
172    fn state_children(&self) -> Vec<StateNode> {
173        // TODO: Do we need to tell about children?
174        vec![StateNode::new(self.current())]
175    }
176
177    fn on_event(
178        &mut self,
179        ctx: &mut crate::ui::UiCtx<Message>,
180        event: E,
181        state: &mut StateNode,
182    ) -> crate::event::EventResponse<E> {
183        // TODO: Think about need of passing events to children, is it safe?
184
185        let focused = ctx.is_focused(self);
186        let current_state = state.get::<SelectState>();
187
188        if let Some(offset) = event.as_select_shift() {
189            if focused && current_state.is_active {
190                if self.cycle {
191                    let len = self.options.len() as i32;
192                    self.chosen = ((self.chosen as i32 + offset % len + len) % len) as usize;
193                } else {
194                    self.chosen = (self.chosen as i32 + offset)
195                        .clamp(0, self.options.len() as i32 - 1)
196                        as usize;
197                }
198                return Capture::Captured.into();
199            }
200        }
201
202        if let Some(common) = event.as_common() {
203            match common {
204                CommonEvent::FocusMove(_) if focused => {
205                    return Propagate::BubbleUp(self.id, event).into()
206                },
207                CommonEvent::FocusClickDown if focused => {
208                    state.get_mut::<SelectState>().is_pressed = true;
209                    return Capture::Captured.into();
210                },
211                CommonEvent::FocusClickUp if focused => {
212                    let was_pressed = current_state.is_pressed;
213
214                    state.get_mut::<SelectState>().is_pressed = false;
215
216                    if was_pressed {
217                        state.get_mut::<SelectState>().is_active =
218                            !state.get::<SelectState>().is_active;
219                        return Capture::Captured.into();
220                    }
221                },
222                CommonEvent::FocusClickDown
223                | CommonEvent::FocusClickUp
224                | CommonEvent::FocusMove(_) => {
225                    // Should we reset state on any event? Or only on common
226                    state.state.reset::<SelectState>();
227                },
228            }
229        }
230
231        Propagate::Ignored.into()
232    }
233
234    fn layout(
235        &self,
236        ctx: &mut crate::ui::UiCtx<Message>,
237        state: &mut crate::state::StateNode,
238        styler: &S,
239        limits: &crate::layout::Limits,
240        viewport: &Viewport,
241    ) -> crate::layout::LayoutNode {
242        // Layout::container(limits, self.size.width, self.size.height, |limits| {
243        //     // TODO: Use real icons layouts to be accurate?
244
245        //     // Reserve some space for arrows on the sides
246        //     let shrink_by_arrows = limits.max_square() * 2;
247        //     self.options[self.chosen].layout(ctx, state, styler, &limits.shrink(shrink_by_arrows))
248        // })
249
250        let style = styler.style(&self.class, self.status(ctx, state));
251
252        // TODO: Smarter icon size
253        let padding_for_icons = self.arrow_icon_size(limits);
254
255        Layout::container(
256            limits,
257            self.size,
258            crate::layout::Position::Relative,
259            viewport,
260            Padding::new_axis(0, padding_for_icons),
261            style.border.width,
262            crate::align::Alignment::Center,
263            crate::align::Alignment::Center,
264            |limits| {
265                self.options[self.chosen].layout(
266                    ctx,
267                    &mut state.children[0],
268                    styler,
269                    limits,
270                    viewport,
271                )
272            },
273        )
274    }
275
276    fn draw(
277        &self,
278        ctx: &mut crate::ui::UiCtx<Message>,
279        state: &mut crate::state::StateNode,
280        renderer: &mut R,
281        styler: &S,
282        layout: crate::layout::Layout,
283    ) {
284        let bounds = layout.bounds();
285        let icons_limits = Limits::new(Size::zero(), Size::new_equal(bounds.size.height));
286        let icons_node = LayoutNode::new(self.arrow_icon_size(&icons_limits).into());
287        let icons_vertical_center =
288            bounds.size.height as i32 / 2 - icons_node.size().height as i32 / 2;
289
290        let style = styler.style(&self.class, self.status(ctx, state));
291
292        renderer.block(Block {
293            border: style.border,
294            rect: bounds.into(),
295            background: style.background,
296        });
297
298        if self.cycle || self.chosen != 0 {
299            Widget::<Message, R, E, S>::draw(
300                &Icon::new(self.icon_left),
301                ctx,
302                &mut StateNode::stateless(),
303                renderer,
304                styler,
305                Layout::with_offset(
306                    bounds.position + Point::new(style.border.width as i32, icons_vertical_center),
307                    &icons_node,
308                ),
309            );
310        }
311
312        self.current().draw(
313            ctx,
314            &mut state.children[0],
315            renderer,
316            styler,
317            layout.children().next().unwrap(),
318        );
319
320        if self.cycle || self.chosen != self.options.len() - 1 {
321            Widget::<Message, R, E, S>::draw(
322                &Icon::new(self.icon_right),
323                ctx,
324                &mut StateNode::stateless(),
325                renderer,
326                styler,
327                Layout::with_offset(
328                    bounds.position
329                        + Point::new(
330                            bounds.size.width as i32
331                                - icons_node.size().width as i32
332                                - style.border.width as i32,
333                            icons_vertical_center,
334                        ),
335                    &icons_node,
336                ),
337            );
338        }
339    }
340}
341
342impl<'a, Message, R, E, S> From<Select<'a, Message, R, E, S>> for El<'a, Message, R, E, S>
343where
344    Message: Clone + 'a,
345    R: Renderer + 'a,
346    E: Event + 'a,
347    S: 'a,
348    S: SelectStyler<R::Color> + 'a,
349{
350    fn from(value: Select<'a, Message, R, E, S>) -> Self {
351        El::new(value)
352    }
353}