freya_components/
select.rs1use 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#[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 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 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 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 .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}