freya_components/
select.rs

1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6    get_theme,
7    icons::arrow::ArrowIcon,
8    menu::MenuGroup,
9    theming::component_themes::SelectThemePartial,
10};
11
12#[derive(Debug, Default, PartialEq, Clone, Copy)]
13pub enum SelectStatus {
14    #[default]
15    Idle,
16    Hovering,
17}
18
19/// Select between different items component.
20///
21/// # Example
22///
23/// ```rust
24/// # use freya::prelude::*;
25/// fn app() -> impl IntoElement {
26///     let values = use_hook(|| {
27///         vec![
28///             "Rust".to_string(),
29///             "Turbofish".to_string(),
30///             "Crabs".to_string(),
31///         ]
32///     });
33///     let mut selected_select = use_state(|| 0);
34///
35///     Select::new()
36///         .selected_item(values[selected_select()].to_string())
37///         .children_iter(values.iter().enumerate().map(|(i, val)| {
38///             MenuItem::new()
39///                 .selected(selected_select() == i)
40///                 .on_press(move |_| selected_select.set(i))
41///                 .child(val.to_string())
42///                 .into()
43///         }))
44/// }
45///
46/// # use freya_testing::prelude::*;
47/// # use std::time::Duration;
48/// # launch_doc_hook(|| {
49/// #   rect().center().expanded().child(app())
50/// # }, (250., 250.).into(), "./images/gallery_select.png", |t| {
51/// #   t.move_cursor((125., 125.));
52/// #   t.click_cursor((125., 125.));
53/// #   t.poll(Duration::from_millis(1), Duration::from_millis(350));
54/// # });
55/// ```
56///
57/// # Preview
58/// ![Select Preview][select]
59#[cfg_attr(feature = "docs",
60    doc = embed_doc_image::embed_image!("select", "images/gallery_select.png")
61)]
62#[derive(Clone, PartialEq)]
63pub struct Select {
64    pub(crate) theme: Option<SelectThemePartial>,
65    pub selected_item: Option<Element>,
66    pub children: Vec<Element>,
67    pub key: DiffKey,
68}
69
70impl ChildrenExt for Select {
71    fn get_children(&mut self) -> &mut Vec<Element> {
72        &mut self.children
73    }
74}
75
76impl Default for Select {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl Select {
83    pub fn new() -> Self {
84        Self {
85            theme: None,
86            selected_item: None,
87            children: Vec::new(),
88            key: DiffKey::None,
89        }
90    }
91
92    pub fn theme(mut self, theme: SelectThemePartial) -> Self {
93        self.theme = Some(theme);
94        self
95    }
96
97    pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
98        self.selected_item = Some(item.into());
99        self
100    }
101
102    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
103        self.key = key.into();
104        self
105    }
106}
107
108impl Render for Select {
109    fn render(&self) -> impl IntoElement {
110        let theme = get_theme!(&self.theme, select);
111        let focus = use_focus();
112        let focus_status = use_focus_status(focus);
113        let mut status = use_state(SelectStatus::default);
114        let mut open = use_state(|| false);
115        use_provide_context(|| MenuGroup {
116            group_id: focus.a11y_id(),
117        });
118
119        let animation = use_animation(move |conf| {
120            conf.on_change(OnChange::Rerun);
121            conf.on_creation(OnCreation::Finish);
122
123            let scale = AnimNum::new(0.8, 1.)
124                .time(350)
125                .ease(Ease::Out)
126                .function(Function::Expo);
127            let opacity = AnimNum::new(0., 1.)
128                .time(350)
129                .ease(Ease::Out)
130                .function(Function::Expo);
131            if open() {
132                (scale, opacity)
133            } else {
134                (scale.into_reversed(), opacity.into_reversed())
135            }
136        });
137
138        use_drop(move || {
139            if status() == SelectStatus::Hovering {
140                Cursor::set(CursorIcon::default());
141            }
142        });
143
144        // Close the select when the focused accessibility node changes and its not the select or any of its children
145        use_side_effect(move || {
146            if let Some(member_of) = Platform::get()
147                .focused_accessibility_node
148                .read()
149                .member_of()
150            {
151                if member_of != focus.a11y_id() {
152                    open.set_if_modified(false);
153                }
154            } else {
155                open.set_if_modified(false);
156            }
157        });
158
159        let on_press = move |e: Event<PressEventData>| {
160            focus.request_focus();
161            open.toggle();
162            // Prevent global mouse up
163            e.prevent_default();
164            e.stop_propagation();
165        };
166
167        let on_pointer_enter = move |_| {
168            *status.write() = SelectStatus::Hovering;
169            Cursor::set(CursorIcon::Pointer);
170        };
171
172        let on_pointer_leave = move |_| {
173            *status.write() = SelectStatus::Idle;
174            Cursor::set(CursorIcon::default());
175        };
176
177        // Close the select if clicked anywhere
178        let on_global_mouse_up = move |_| {
179            open.set_if_modified(false);
180        };
181
182        let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
183            Key::Escape => {
184                open.set_if_modified(false);
185            }
186            Key::Enter if focus.is_focused() => {
187                open.toggle();
188            }
189            _ => {}
190        };
191
192        let (scale, opacity) = animation.read().value();
193
194        let background = match *status.read() {
195            SelectStatus::Hovering => theme.hover_background,
196            SelectStatus::Idle => theme.background_button,
197        };
198
199        let border = if focus_status() == FocusStatus::Keyboard {
200            Border::new()
201                .fill(theme.focus_border_fill)
202                .width(2.)
203                .alignment(BorderAlignment::Inner)
204        } else {
205            Border::new()
206                .fill(theme.border_fill)
207                .width(1.)
208                .alignment(BorderAlignment::Inner)
209        };
210
211        rect()
212            .child(
213                rect()
214                    .a11y_id(focus.a11y_id())
215                    .a11y_member_of(focus.a11y_id())
216                    .a11y_role(AccessibilityRole::ListBox)
217                    .a11y_focusable(Focusable::Enabled)
218                    .on_pointer_enter(on_pointer_enter)
219                    .on_pointer_leave(on_pointer_leave)
220                    .on_press(on_press)
221                    .on_global_key_down(on_global_key_down)
222                    .on_global_mouse_up(on_global_mouse_up)
223                    .width(theme.width)
224                    .margin(theme.margin)
225                    .background(background)
226                    .padding((6., 16., 6., 16.))
227                    .border(border)
228                    .horizontal()
229                    .center()
230                    .color(theme.color)
231                    .corner_radius(8.)
232                    .maybe_child(self.selected_item.clone())
233                    .child(
234                        ArrowIcon::new()
235                            .margin((0., 0., 0., 8.))
236                            .rotate(0.)
237                            .fill(theme.arrow_fill),
238                    ),
239            )
240            .maybe_child((open() || opacity > 0.).then(|| {
241                rect().height(Size::px(0.)).width(Size::px(0.)).child(
242                    rect()
243                        .width(Size::window_percent(100.))
244                        .margin(Gaps::new(4., 0., 0., 0.))
245                        .child(
246                            rect()
247                                .layer(Layer::Overlay)
248                                .border(
249                                    Border::new()
250                                        .fill(theme.border_fill)
251                                        .width(1.)
252                                        .alignment(BorderAlignment::Inner),
253                                )
254                                .overflow(Overflow::Clip)
255                                .corner_radius(8.)
256                                .background(theme.select_background)
257                                // TODO: Shadows
258                                .padding(6.)
259                                .content(Content::Fit)
260                                .opacity(opacity)
261                                .scale(scale)
262                                .children(self.children.clone()),
263                        ),
264                )
265            }))
266    }
267
268    fn render_key(&self) -> DiffKey {
269        self.key.clone().or(self.default_key())
270    }
271}