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