freya_components/
dropdown.rs

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