Skip to main content

freya_components/
select.rs

1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6    define_theme,
7    get_theme,
8    icons::arrow::ArrowIcon,
9    menu::MenuGroup,
10};
11
12define_theme! {
13    %[component]
14    pub Select {
15        %[fields]
16        width: Size,
17        margin: Gaps,
18        select_background: Color,
19        background_button: Color,
20        hover_background: Color,
21        border_fill: Color,
22        focus_border_fill: Color,
23        arrow_fill: Color,
24        color: Color,
25    }
26}
27
28#[derive(Debug, Default, PartialEq, Clone, Copy)]
29pub enum SelectStatus {
30    #[default]
31    Idle,
32    Hovering,
33}
34
35/// Select between different items component.
36///
37/// # Example
38///
39/// ```rust
40/// # use freya::prelude::*;
41/// fn app() -> impl IntoElement {
42///     let values = use_hook(|| {
43///         vec![
44///             "Rust".to_string(),
45///             "Turbofish".to_string(),
46///             "Crabs".to_string(),
47///         ]
48///     });
49///     let mut selected_select = use_state(|| 0);
50///
51///     Select::new()
52///         .selected_item(values[selected_select()].to_string())
53///         .children(values.iter().enumerate().map(|(i, val)| {
54///             MenuItem::new()
55///                 .selected(selected_select() == i)
56///                 .on_press(move |_| selected_select.set(i))
57///                 .child(val.to_string())
58///         }))
59/// }
60///
61/// # use freya_testing::prelude::*;
62/// # use std::time::Duration;
63/// # launch_doc(|| {
64/// #   rect().center().expanded().child(app())
65/// # }, "./images/gallery_select.png").with_hook(|t| { t.move_cursor((125., 125.)); t.click_cursor((125., 125.)); t.poll(Duration::from_millis(1), Duration::from_millis(350)); }).with_scale_factor(1.).render();
66/// ```
67///
68/// # Preview
69/// ![Select Preview][select]
70#[cfg_attr(feature = "docs",
71    doc = embed_doc_image::embed_image!("select", "images/gallery_select.png")
72)]
73#[derive(Clone, PartialEq)]
74pub struct Select {
75    pub(crate) theme: Option<SelectThemePartial>,
76    selected_item: Option<Element>,
77    children: Vec<Element>,
78    cursor_icon: CursorIcon,
79    key: DiffKey,
80}
81
82impl ChildrenExt for Select {
83    fn get_children(&mut self) -> &mut Vec<Element> {
84        &mut self.children
85    }
86}
87
88impl KeyExt for Select {
89    fn write_key(&mut self) -> &mut DiffKey {
90        &mut self.key
91    }
92}
93
94impl Default for Select {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl Select {
101    pub fn new() -> Self {
102        Self {
103            theme: None,
104            selected_item: None,
105            children: Vec::new(),
106            cursor_icon: CursorIcon::default(),
107            key: DiffKey::None,
108        }
109    }
110
111    pub fn theme(mut self, theme: SelectThemePartial) -> Self {
112        self.theme = Some(theme);
113        self
114    }
115
116    pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
117        self.selected_item = Some(item.into());
118        self
119    }
120
121    /// Override the cursor icon shown when hovering over this component.
122    pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
123        self.cursor_icon = cursor_icon.into();
124        self
125    }
126}
127
128impl Component for Select {
129    fn render(&self) -> impl IntoElement {
130        let theme = get_theme!(&self.theme, SelectThemePreference, "select");
131        let a11y_id = use_a11y();
132        let focus = use_focus(a11y_id);
133        let mut status = use_state(SelectStatus::default);
134        let mut open = use_state(|| false);
135        let mut button_area = use_state(|| None::<Area>);
136        let mut list_size = use_state(|| None::<Size2D>);
137        use_provide_context(|| MenuGroup { group_id: a11y_id });
138
139        let animation = use_animation(move |conf| {
140            conf.on_change(OnChange::Rerun);
141            conf.on_creation(OnCreation::Finish);
142
143            let scale = AnimNum::new(0.9, 1.)
144                .time(125)
145                .ease(Ease::Out)
146                .function(Function::Quart);
147            let opacity = AnimNum::new(0., 1.)
148                .time(125)
149                .ease(Ease::Out)
150                .function(Function::Quart);
151            let offset_y = AnimNum::new(-8., 1.)
152                .time(125)
153                .ease(Ease::Out)
154                .function(Function::Quart);
155            if open() {
156                (scale, opacity, offset_y)
157            } else {
158                (
159                    scale.into_reversed(),
160                    opacity.into_reversed(),
161                    offset_y.into_reversed(),
162                )
163            }
164        });
165
166        let (scale, opacity, slide) = animation.read().value();
167
168        // Clear the list size when the select dropdown is not rendered
169        if !open() && opacity == 0. && list_size().is_some() {
170            let _ = list_size.take();
171        }
172
173        let cursor_icon = self.cursor_icon;
174        use_drop(move || {
175            if status() == SelectStatus::Hovering {
176                Cursor::set(CursorIcon::default());
177            }
178        });
179
180        // Close the select when the focus leaves it.
181        use_side_effect(move || {
182            let platform = Platform::get();
183            let focus_within =
184                platform.focused_accessibility_node.read().member_of() == Some(a11y_id);
185            if !focus_within && list_size.peek().is_some() {
186                open.set_if_modified(false);
187            }
188        });
189
190        let on_press = move |e: Event<PressEventData>| {
191            a11y_id.request_focus();
192            open.toggle();
193            // Prevent global mouse up
194            e.prevent_default();
195            e.stop_propagation();
196        };
197
198        let on_pointer_enter = move |_| {
199            *status.write() = SelectStatus::Hovering;
200            Cursor::set(cursor_icon);
201        };
202
203        let on_pointer_leave = move |_| {
204            *status.write() = SelectStatus::Idle;
205            Cursor::set(CursorIcon::default());
206        };
207
208        // Close the select if clicked anywhere
209        let on_global_pointer_press = move |_: Event<PointerEventData>| {
210            open.set_if_modified(false);
211        };
212
213        let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
214            Key::Named(NamedKey::Escape) => {
215                open.set_if_modified(false);
216            }
217            Key::Named(NamedKey::Enter) if a11y_id.is_focused() => {
218                open.toggle();
219            }
220            _ => {}
221        };
222
223        let offset_y = match (button_area(), list_size()) {
224            (Some(button), Some(list)) => {
225                let root_height = Platform::get().root_size.peek().height;
226                let space_below = root_height - button.max_y();
227                let space_above = button.min_y();
228                let flips = list.height > space_below && list.height <= space_above;
229                if flips {
230                    -(button.height() + list.height) - slide
231                } else {
232                    slide
233                }
234            }
235            _ => slide,
236        };
237
238        let opacity = if list_size().is_some() { opacity } else { 0. };
239
240        let background = match *status.read() {
241            SelectStatus::Hovering => theme.hover_background,
242            SelectStatus::Idle => theme.background_button,
243        };
244
245        let border = if focus() == Focus::Keyboard {
246            Border::new()
247                .fill(theme.focus_border_fill)
248                .width(2.)
249                .alignment(BorderAlignment::Inner)
250        } else {
251            Border::new()
252                .fill(theme.border_fill)
253                .width(1.)
254                .alignment(BorderAlignment::Inner)
255        };
256
257        rect()
258            .child(
259                rect()
260                    .a11y_id(a11y_id)
261                    .a11y_member_of(a11y_id)
262                    .a11y_role(AccessibilityRole::ListBox)
263                    .a11y_focusable(Focusable::Enabled)
264                    .on_pointer_enter(on_pointer_enter)
265                    .on_pointer_leave(on_pointer_leave)
266                    .on_press(on_press)
267                    .on_global_key_down(on_global_key_down)
268                    .on_global_pointer_press(on_global_pointer_press)
269                    .on_sized(move |e: Event<SizedEventData>| {
270                        button_area.set_if_modified(Some(e.area));
271                    })
272                    .width(theme.width)
273                    .margin(theme.margin)
274                    .background(background)
275                    .padding((8., 18., 8., 18.))
276                    .border(border)
277                    .horizontal()
278                    .center()
279                    .color(theme.color)
280                    .corner_radius(8.)
281                    .maybe_child(self.selected_item.clone())
282                    .child(
283                        ArrowIcon::new()
284                            .margin((0., 0., 0., 8.))
285                            .rotate(0.)
286                            .fill(theme.arrow_fill),
287                    ),
288            )
289            .maybe_child((open() || opacity > 0.).then(|| {
290                rect().height(Size::px(0.)).width(Size::px(0.)).child(
291                    rect()
292                        .width(Size::window_percent(100.))
293                        .margin(Gaps::new(4., 0., 4., 0.))
294                        .offset_y(offset_y)
295                        .on_sized(move |e: Event<SizedEventData>| {
296                            list_size.set_if_modified(Some(e.area.size));
297                        })
298                        .child(
299                            rect()
300                                .layer(Layer::Overlay)
301                                .border(
302                                    Border::new()
303                                        .fill(theme.border_fill)
304                                        .width(1.)
305                                        .alignment(BorderAlignment::Inner),
306                                )
307                                .overflow(Overflow::Clip)
308                                .corner_radius(8.)
309                                .background(theme.select_background)
310                                .padding(4.)
311                                .content(Content::Fit)
312                                .opacity(opacity)
313                                .scale(scale)
314                                .children(self.children.clone()),
315                        ),
316                )
317            }))
318    }
319
320    fn render_key(&self) -> DiffKey {
321        self.key.clone().or(self.default_key())
322    }
323}