Skip to main content

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