freya_components/
dropdown.rs

1use freya_core::prelude::*;
2use torin::prelude::*;
3
4use crate::{
5    get_theme,
6    icons::arrow::ArrowIcon,
7    theming::component_themes::{
8        DropdownItemThemePartial,
9        DropdownThemePartial,
10    },
11};
12
13#[derive(Debug, Default, PartialEq, Clone, Copy)]
14pub enum DropdownItemStatus {
15    #[default]
16    Idle,
17    Hovering,
18}
19
20#[derive(Clone, PartialEq)]
21pub struct DropdownItem {
22    pub(crate) theme: Option<DropdownItemThemePartial>,
23    pub selected: bool,
24    pub on_press: Option<EventHandler<Event<PressEventData>>>,
25    pub children: Vec<Element>,
26    pub key: DiffKey,
27}
28
29impl Default for DropdownItem {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl DropdownItem {
36    pub fn new() -> Self {
37        Self {
38            theme: None,
39            selected: false,
40            on_press: None,
41            children: Vec::new(),
42            key: DiffKey::None,
43        }
44    }
45
46    pub fn theme(mut self, theme: DropdownItemThemePartial) -> Self {
47        self.theme = Some(theme);
48        self
49    }
50
51    pub fn selected(mut self, selected: bool) -> Self {
52        self.selected = selected;
53        self
54    }
55
56    pub fn on_press(mut self, handler: impl FnMut(Event<PressEventData>) + 'static) -> Self {
57        self.on_press = Some(EventHandler::new(handler));
58        self
59    }
60
61    pub fn child(mut self, child: impl Into<Element>) -> Self {
62        self.children.push(child.into());
63        self
64    }
65
66    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
67        self.key = key.into();
68        self
69    }
70}
71
72impl Render for DropdownItem {
73    fn render(&self) -> impl IntoElement {
74        let theme = get_theme!(&self.theme, dropdown_item);
75        let focus = use_focus();
76        let focus_status = use_focus_status(focus);
77        let mut status = use_state(DropdownItemStatus::default);
78        let dropdown_group = use_consume::<DropdownGroup>();
79
80        use_drop(move || {
81            if status() == DropdownItemStatus::Hovering {
82                Cursor::set(CursorIcon::default());
83            }
84        });
85
86        let background = if self.selected {
87            theme.select_background
88        } else if *status.read() == DropdownItemStatus::Hovering {
89            theme.hover_background
90        } else {
91            theme.background
92        };
93
94        let border = if focus_status() == FocusStatus::Keyboard {
95            Border::new()
96                .fill(theme.select_border_fill)
97                .width(2.)
98                .alignment(BorderAlignment::Inner)
99        } else {
100            Border::new()
101                .fill(theme.border_fill)
102                .width(1.)
103                .alignment(BorderAlignment::Inner)
104        };
105
106        rect()
107            .width(Size::fill_minimum())
108            .color(theme.color)
109            .a11y_id(focus.a11y_id())
110            .a11y_focusable(Focusable::Enabled)
111            .a11y_member_of(dropdown_group.group_id)
112            .a11y_role(AccessibilityRole::Button)
113            .background(background)
114            .border(border)
115            .corner_radius(6.)
116            .padding((6., 10., 6., 10.))
117            .main_align(Alignment::center())
118            .on_pointer_enter(move |_| {
119                *status.write() = DropdownItemStatus::Hovering;
120                Cursor::set(CursorIcon::Pointer);
121            })
122            .on_pointer_leave(move |_| {
123                *status.write() = DropdownItemStatus::Idle;
124                Cursor::set(CursorIcon::default());
125            })
126            .map(self.on_press.clone(), |rect, on_press| {
127                rect.on_press(on_press)
128            })
129            .children(self.children.clone())
130    }
131
132    fn render_key(&self) -> DiffKey {
133        self.key.clone().or(self.default_key())
134    }
135}
136
137#[derive(Clone)]
138struct DropdownGroup {
139    group_id: AccessibilityId,
140}
141
142#[derive(Debug, Default, PartialEq, Clone, Copy)]
143pub enum DropdownStatus {
144    #[default]
145    Idle,
146    Hovering,
147}
148
149/// Select between different items component.
150///
151/// # Example
152///
153/// ```rust
154/// # use freya::prelude::*;
155/// fn app() -> impl IntoElement {
156///     let values = use_hook(|| {
157///         vec![
158///             "Rust".to_string(),
159///             "Turbofish".to_string(),
160///             "Crabs".to_string(),
161///         ]
162///     });
163///     let mut selected_dropdown = use_state(|| 0);
164///
165///     Dropdown::new()
166///         .selected_item(values[selected_dropdown()].to_string())
167///         .children_iter(values.iter().enumerate().map(|(i, val)| {
168///             DropdownItem::new()
169///                 .on_press(move |_| selected_dropdown.set(i))
170///                 .child(val.to_string())
171///                 .into()
172///         }))
173/// }
174///
175/// # use freya_testing::prelude::*;
176/// # launch_doc_hook(|| {
177/// #   rect().center().expanded().child(app())
178/// # }, (250., 250.).into(), "./images/gallery_dropdown.png", |t| {
179/// #   t.move_cursor((125., 125.));
180/// #   t.click_cursor((125., 125.));
181/// #   t.sync_and_update();
182/// # });
183/// ```
184///
185/// # Preview
186/// ![Dropdown Preview][dropdown]
187#[cfg_attr(feature = "docs",
188    doc = embed_doc_image::embed_image!("dropdown", "images/gallery_dropdown.png")
189)]
190#[derive(Clone, PartialEq)]
191pub struct Dropdown {
192    pub(crate) theme: Option<DropdownThemePartial>,
193    pub selected_item: Option<Element>,
194    pub children: Vec<Element>,
195    pub key: DiffKey,
196}
197
198impl ChildrenExt for Dropdown {
199    fn get_children(&mut self) -> &mut Vec<Element> {
200        &mut self.children
201    }
202}
203
204impl Default for Dropdown {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210impl Dropdown {
211    pub fn new() -> Self {
212        Self {
213            theme: None,
214            selected_item: None,
215            children: Vec::new(),
216            key: DiffKey::None,
217        }
218    }
219
220    pub fn theme(mut self, theme: DropdownThemePartial) -> Self {
221        self.theme = Some(theme);
222        self
223    }
224
225    pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
226        self.selected_item = Some(item.into());
227        self
228    }
229
230    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
231        self.key = key.into();
232        self
233    }
234}
235
236impl Render for Dropdown {
237    fn render(&self) -> impl IntoElement {
238        let theme = get_theme!(&self.theme, dropdown);
239        let focus = use_focus();
240        let focus_status = use_focus_status(focus);
241        let mut status = use_state(DropdownStatus::default);
242        let mut open = use_state(|| false);
243        use_provide_context(|| DropdownGroup {
244            group_id: focus.a11y_id(),
245        });
246
247        use_drop(move || {
248            if status() == DropdownStatus::Hovering {
249                Cursor::set(CursorIcon::default());
250            }
251        });
252
253        // Close the dropdown when the focused accessibility node changes and its not the dropdown or any of its childrens
254        use_side_effect(move || {
255            if let Some(member_of) = PlatformState::get()
256                .focused_accessibility_node
257                .read()
258                .member_of()
259            {
260                if member_of != focus.a11y_id() {
261                    open.set(false);
262                }
263            } else {
264                open.set(false);
265            }
266        });
267
268        let on_press = move |e: Event<PressEventData>| {
269            focus.request_focus();
270            open.toggle();
271            // Prevent global mouse up
272            e.prevent_default();
273            e.stop_propagation();
274        };
275
276        let on_pointer_enter = move |_| {
277            *status.write() = DropdownStatus::Hovering;
278            Cursor::set(CursorIcon::Pointer);
279        };
280
281        let on_pointer_leave = move |_| {
282            *status.write() = DropdownStatus::Idle;
283            Cursor::set(CursorIcon::default());
284        };
285
286        // Close the dropdown if clicked anywhere
287        let on_global_mouse_up = move |_| {
288            open.set(false);
289        };
290
291        let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
292            Key::Escape => {
293                open.set(false);
294            }
295            Key::Enter if focus.is_focused() => {
296                open.toggle();
297            }
298            _ => {}
299        };
300
301        let background = match *status.read() {
302            DropdownStatus::Hovering => theme.hover_background,
303            DropdownStatus::Idle => theme.background_button,
304        };
305
306        let border = if focus_status() == FocusStatus::Keyboard {
307            Border::new()
308                .fill(theme.focus_border_fill)
309                .width(2.)
310                .alignment(BorderAlignment::Inner)
311        } else {
312            Border::new()
313                .fill(theme.border_fill)
314                .width(1.)
315                .alignment(BorderAlignment::Inner)
316        };
317
318        rect()
319            .child(
320                rect()
321                    .a11y_id(focus.a11y_id())
322                    .a11y_member_of(focus.a11y_id())
323                    .a11y_focusable(Focusable::Enabled)
324                    .on_pointer_enter(on_pointer_enter)
325                    .on_pointer_leave(on_pointer_leave)
326                    .on_press(on_press)
327                    .on_global_key_down(on_global_key_down)
328                    .on_global_mouse_up(on_global_mouse_up)
329                    .width(theme.width)
330                    .margin(theme.margin)
331                    .background(background)
332                    .padding((6., 16., 6., 16.))
333                    .border(border)
334                    .horizontal()
335                    .center()
336                    .color(theme.color)
337                    .corner_radius(8.)
338                    .maybe_child(self.selected_item.clone())
339                    .child(
340                        ArrowIcon::new()
341                            .margin((0., 0., 0., 8.))
342                            .rotate(0.)
343                            .fill(theme.arrow_fill),
344                    ),
345            )
346            .maybe_child(open().then(|| {
347                rect().height(Size::px(0.)).width(Size::px(0.)).child(
348                    rect()
349                        .width(Size::window_percent(100.))
350                        .margin(Gaps::new(4., 0., 0., 0.))
351                        .child(
352                            rect()
353                                .border(
354                                    Border::new()
355                                        .fill(theme.border_fill)
356                                        .width(1.)
357                                        .alignment(BorderAlignment::Inner),
358                                )
359                                .overflow(Overflow::Clip)
360                                .corner_radius(8.)
361                                .background(theme.dropdown_background)
362                                // TODO: Shadows
363                                .padding(6.)
364                                .content(Content::Fit)
365                                .children(self.children.clone()),
366                        ),
367                )
368            }))
369    }
370
371    fn render_key(&self) -> DiffKey {
372        self.key.clone().or(self.default_key())
373    }
374}