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