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///                 .into()
59///         }))
60/// }
61///
62/// # use freya_testing::prelude::*;
63/// # use std::time::Duration;
64/// # launch_doc(|| {
65/// #   rect().center().expanded().child(app())
66/// # }, "./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();
67/// ```
68///
69/// # Preview
70/// ![Select Preview][select]
71#[cfg_attr(feature = "docs",
72    doc = embed_doc_image::embed_image!("select", "images/gallery_select.png")
73)]
74#[derive(Clone, PartialEq)]
75pub struct Select {
76    pub(crate) theme: Option<SelectThemePartial>,
77    selected_item: Option<Element>,
78    children: Vec<Element>,
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            key: DiffKey::None,
107        }
108    }
109
110    pub fn theme(mut self, theme: SelectThemePartial) -> Self {
111        self.theme = Some(theme);
112        self
113    }
114
115    pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
116        self.selected_item = Some(item.into());
117        self
118    }
119}
120
121impl Component for Select {
122    fn render(&self) -> impl IntoElement {
123        let theme = get_theme!(&self.theme, SelectThemePreference, "select");
124        let focus = use_focus();
125        let focus_status = use_focus_status(focus);
126        let mut status = use_state(SelectStatus::default);
127        let mut open = use_state(|| false);
128        use_provide_context(|| MenuGroup {
129            group_id: focus.a11y_id(),
130        });
131
132        let animation = use_animation(move |conf| {
133            conf.on_change(OnChange::Rerun);
134            conf.on_creation(OnCreation::Finish);
135
136            let scale = AnimNum::new(0.9, 1.)
137                .time(125)
138                .ease(Ease::Out)
139                .function(Function::Quart);
140            let opacity = AnimNum::new(0., 1.)
141                .time(125)
142                .ease(Ease::Out)
143                .function(Function::Quart);
144            let offset_y = AnimNum::new(-8., 1.)
145                .time(125)
146                .ease(Ease::Out)
147                .function(Function::Quart);
148            if open() {
149                (scale, opacity, offset_y)
150            } else {
151                (
152                    scale.into_reversed(),
153                    opacity.into_reversed(),
154                    offset_y.into_reversed(),
155                )
156            }
157        });
158
159        use_drop(move || {
160            if status() == SelectStatus::Hovering {
161                Cursor::set(CursorIcon::default());
162            }
163        });
164
165        // Close the select when the focused accessibility node changes and its not the select or any of its children
166        use_side_effect(move || {
167            let platform = Platform::get();
168            if *platform.navigation_mode.read() == NavigationMode::Keyboard {
169                let should_close = platform
170                    .focused_accessibility_node
171                    .read()
172                    .member_of()
173                    .is_none_or(|member_of| member_of != focus.a11y_id());
174                if should_close {
175                    open.set_if_modified(false);
176                }
177            }
178        });
179
180        let on_press = move |e: Event<PressEventData>| {
181            focus.request_focus();
182            open.toggle();
183            // Prevent global mouse up
184            e.prevent_default();
185            e.stop_propagation();
186        };
187
188        let on_pointer_enter = move |_| {
189            *status.write() = SelectStatus::Hovering;
190            Cursor::set(CursorIcon::Pointer);
191        };
192
193        let on_pointer_leave = move |_| {
194            *status.write() = SelectStatus::Idle;
195            Cursor::set(CursorIcon::default());
196        };
197
198        // Close the select if clicked anywhere
199        let on_global_pointer_press = move |_: Event<PointerEventData>| {
200            open.set_if_modified(false);
201        };
202
203        let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
204            Key::Named(NamedKey::Escape) => {
205                open.set_if_modified(false);
206            }
207            Key::Named(NamedKey::Enter) if focus.is_focused() => {
208                open.toggle();
209            }
210            _ => {}
211        };
212
213        let (scale, opacity, offset_y) = animation.read().value();
214
215        let background = match *status.read() {
216            SelectStatus::Hovering => theme.hover_background,
217            SelectStatus::Idle => theme.background_button,
218        };
219
220        let border = if focus_status() == FocusStatus::Keyboard {
221            Border::new()
222                .fill(theme.focus_border_fill)
223                .width(2.)
224                .alignment(BorderAlignment::Inner)
225        } else {
226            Border::new()
227                .fill(theme.border_fill)
228                .width(1.)
229                .alignment(BorderAlignment::Inner)
230        };
231
232        rect()
233            .child(
234                rect()
235                    .a11y_id(focus.a11y_id())
236                    .a11y_member_of(focus.a11y_id())
237                    .a11y_role(AccessibilityRole::ListBox)
238                    .a11y_focusable(Focusable::Enabled)
239                    .on_pointer_enter(on_pointer_enter)
240                    .on_pointer_leave(on_pointer_leave)
241                    .on_press(on_press)
242                    .on_global_key_down(on_global_key_down)
243                    .on_global_pointer_press(on_global_pointer_press)
244                    .width(theme.width)
245                    .margin(theme.margin)
246                    .background(background)
247                    .padding((8., 18., 8., 18.))
248                    .border(border)
249                    .horizontal()
250                    .center()
251                    .color(theme.color)
252                    .corner_radius(8.)
253                    .maybe_child(self.selected_item.clone())
254                    .child(
255                        ArrowIcon::new()
256                            .margin((0., 0., 0., 8.))
257                            .rotate(0.)
258                            .fill(theme.arrow_fill),
259                    ),
260            )
261            .maybe_child((open() || opacity > 0.).then(|| {
262                rect().height(Size::px(0.)).width(Size::px(0.)).child(
263                    rect()
264                        .width(Size::window_percent(100.))
265                        .margin(Gaps::new(4., 0., 0., 0.))
266                        .offset_y(offset_y)
267                        .child(
268                            rect()
269                                .layer(Layer::Overlay)
270                                .border(
271                                    Border::new()
272                                        .fill(theme.border_fill)
273                                        .width(1.)
274                                        .alignment(BorderAlignment::Inner),
275                                )
276                                .overflow(Overflow::Clip)
277                                .corner_radius(8.)
278                                .background(theme.select_background)
279                                .padding(4.)
280                                .content(Content::Fit)
281                                .opacity(opacity)
282                                .scale(scale)
283                                .children(self.children.clone()),
284                        ),
285                )
286            }))
287    }
288
289    fn render_key(&self) -> DiffKey {
290        self.key.clone().or(self.default_key())
291    }
292}