freya_components/
dropdown.rs1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6 get_theme,
7 icons::arrow::ArrowIcon,
8 theming::component_themes::{
9 DropdownItemThemePartial,
10 DropdownThemePartial,
11 },
12};
13
14#[derive(Debug, Default, PartialEq, Clone, Copy)]
15pub enum DropdownItemStatus {
16 #[default]
17 Idle,
18 Hovering,
19}
20
21#[derive(Clone, PartialEq)]
22pub struct DropdownItem {
23 pub(crate) theme: Option<DropdownItemThemePartial>,
24 pub selected: bool,
25 pub on_press: Option<EventHandler<Event<PressEventData>>>,
26 pub children: Vec<Element>,
27 pub key: DiffKey,
28}
29
30impl ChildrenExt for DropdownItem {
31 fn get_children(&mut self) -> &mut Vec<Element> {
32 &mut self.children
33 }
34}
35
36impl KeyExt for DropdownItem {
37 fn write_key(&mut self) -> &mut DiffKey {
38 &mut self.key
39 }
40}
41
42impl Default for DropdownItem {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl DropdownItem {
49 pub fn new() -> Self {
50 Self {
51 theme: None,
52 selected: false,
53 on_press: None,
54 children: Vec::new(),
55 key: DiffKey::None,
56 }
57 }
58
59 pub fn theme(mut self, theme: DropdownItemThemePartial) -> Self {
60 self.theme = Some(theme);
61 self
62 }
63
64 pub fn selected(mut self, selected: bool) -> Self {
65 self.selected = selected;
66 self
67 }
68
69 pub fn on_press(mut self, handler: impl FnMut(Event<PressEventData>) + 'static) -> Self {
70 self.on_press = Some(EventHandler::new(handler));
71 self
72 }
73}
74
75impl Render for DropdownItem {
76 fn render(&self) -> impl IntoElement {
77 let theme = get_theme!(&self.theme, dropdown_item);
78 let focus = use_focus();
79 let focus_status = use_focus_status(focus);
80 let mut status = use_state(DropdownItemStatus::default);
81 let dropdown_group = use_consume::<DropdownGroup>();
82
83 use_drop(move || {
84 if status() == DropdownItemStatus::Hovering {
85 Cursor::set(CursorIcon::default());
86 }
87 });
88
89 let background = if self.selected {
90 theme.select_background
91 } else if *status.read() == DropdownItemStatus::Hovering {
92 theme.hover_background
93 } else {
94 theme.background
95 };
96
97 let border = if focus_status() == FocusStatus::Keyboard {
98 Border::new()
99 .fill(theme.select_border_fill)
100 .width(2.)
101 .alignment(BorderAlignment::Inner)
102 } else {
103 Border::new()
104 .fill(theme.border_fill)
105 .width(1.)
106 .alignment(BorderAlignment::Inner)
107 };
108
109 rect()
110 .width(Size::fill_minimum())
111 .color(theme.color)
112 .a11y_id(focus.a11y_id())
113 .a11y_focusable(Focusable::Enabled)
114 .a11y_member_of(dropdown_group.group_id)
115 .a11y_role(AccessibilityRole::ListBoxOption)
116 .background(background)
117 .border(border)
118 .corner_radius(6.)
119 .padding((6., 10., 6., 10.))
120 .main_align(Alignment::center())
121 .on_pointer_enter(move |_| {
122 *status.write() = DropdownItemStatus::Hovering;
123 Cursor::set(CursorIcon::Pointer);
124 })
125 .on_pointer_leave(move |_| {
126 *status.write() = DropdownItemStatus::Idle;
127 Cursor::set(CursorIcon::default());
128 })
129 .map(self.on_press.clone(), |rect, on_press| {
130 rect.on_press(on_press)
131 })
132 .children(self.children.clone())
133 }
134
135 fn render_key(&self) -> DiffKey {
136 self.key.clone().or(self.default_key())
137 }
138}
139
140#[derive(Clone)]
141struct DropdownGroup {
142 group_id: AccessibilityId,
143}
144
145#[derive(Debug, Default, PartialEq, Clone, Copy)]
146pub enum DropdownStatus {
147 #[default]
148 Idle,
149 Hovering,
150}
151
152#[cfg_attr(feature = "docs",
191 doc = embed_doc_image::embed_image!("dropdown", "images/gallery_dropdown.png")
192)]
193#[derive(Clone, PartialEq)]
194pub struct Dropdown {
195 pub(crate) theme: Option<DropdownThemePartial>,
196 pub selected_item: Option<Element>,
197 pub children: Vec<Element>,
198 pub key: DiffKey,
199}
200
201impl ChildrenExt for Dropdown {
202 fn get_children(&mut self) -> &mut Vec<Element> {
203 &mut self.children
204 }
205}
206
207impl Default for Dropdown {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213impl Dropdown {
214 pub fn new() -> Self {
215 Self {
216 theme: None,
217 selected_item: None,
218 children: Vec::new(),
219 key: DiffKey::None,
220 }
221 }
222
223 pub fn theme(mut self, theme: DropdownThemePartial) -> Self {
224 self.theme = Some(theme);
225 self
226 }
227
228 pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
229 self.selected_item = Some(item.into());
230 self
231 }
232
233 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
234 self.key = key.into();
235 self
236 }
237}
238
239impl Render for Dropdown {
240 fn render(&self) -> impl IntoElement {
241 let theme = get_theme!(&self.theme, dropdown);
242 let focus = use_focus();
243 let focus_status = use_focus_status(focus);
244 let mut status = use_state(DropdownStatus::default);
245 let mut open = use_state(|| false);
246 use_provide_context(|| DropdownGroup {
247 group_id: focus.a11y_id(),
248 });
249
250 let animation = use_animation(move |conf| {
251 conf.on_change(OnChange::Rerun);
252 conf.on_creation(OnCreation::Finish);
253
254 let scale = AnimNum::new(0.8, 1.)
255 .time(350)
256 .ease(Ease::Out)
257 .function(Function::Expo);
258 let opacity = AnimNum::new(0., 1.)
259 .time(350)
260 .ease(Ease::Out)
261 .function(Function::Expo);
262
263 if open() {
264 (scale, opacity)
265 } else {
266 (scale.into_reversed(), opacity.into_reversed())
267 }
268 });
269
270 use_drop(move || {
271 if status() == DropdownStatus::Hovering {
272 Cursor::set(CursorIcon::default());
273 }
274 });
275
276 use_side_effect(move || {
278 if let Some(member_of) = Platform::get()
279 .focused_accessibility_node
280 .read()
281 .member_of()
282 {
283 if member_of != focus.a11y_id() {
284 open.set_if_modified(false);
285 }
286 } else {
287 open.set_if_modified(false);
288 }
289 });
290
291 let on_press = move |e: Event<PressEventData>| {
292 focus.request_focus();
293 open.toggle();
294 e.prevent_default();
296 e.stop_propagation();
297 };
298
299 let on_pointer_enter = move |_| {
300 *status.write() = DropdownStatus::Hovering;
301 Cursor::set(CursorIcon::Pointer);
302 };
303
304 let on_pointer_leave = move |_| {
305 *status.write() = DropdownStatus::Idle;
306 Cursor::set(CursorIcon::default());
307 };
308
309 let on_global_mouse_up = move |_| {
311 open.set_if_modified(false);
312 };
313
314 let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
315 Key::Escape => {
316 open.set_if_modified(false);
317 }
318 Key::Enter if focus.is_focused() => {
319 open.toggle();
320 }
321 _ => {}
322 };
323
324 let (scale, opacity) = animation.read().value();
325
326 let background = match *status.read() {
327 DropdownStatus::Hovering => theme.hover_background,
328 DropdownStatus::Idle => theme.background_button,
329 };
330
331 let border = if focus_status() == FocusStatus::Keyboard {
332 Border::new()
333 .fill(theme.focus_border_fill)
334 .width(2.)
335 .alignment(BorderAlignment::Inner)
336 } else {
337 Border::new()
338 .fill(theme.border_fill)
339 .width(1.)
340 .alignment(BorderAlignment::Inner)
341 };
342
343 rect()
344 .child(
345 rect()
346 .a11y_id(focus.a11y_id())
347 .a11y_member_of(focus.a11y_id())
348 .a11y_role(AccessibilityRole::ListBox)
349 .a11y_focusable(Focusable::Enabled)
350 .on_pointer_enter(on_pointer_enter)
351 .on_pointer_leave(on_pointer_leave)
352 .on_press(on_press)
353 .on_global_key_down(on_global_key_down)
354 .on_global_mouse_up(on_global_mouse_up)
355 .width(theme.width)
356 .margin(theme.margin)
357 .background(background)
358 .padding((6., 16., 6., 16.))
359 .border(border)
360 .horizontal()
361 .center()
362 .color(theme.color)
363 .corner_radius(8.)
364 .maybe_child(self.selected_item.clone())
365 .child(
366 ArrowIcon::new()
367 .margin((0., 0., 0., 8.))
368 .rotate(0.)
369 .fill(theme.arrow_fill),
370 ),
371 )
372 .maybe_child((open() || opacity > 0.).then(|| {
373 rect().height(Size::px(0.)).width(Size::px(0.)).child(
374 rect()
375 .width(Size::window_percent(100.))
376 .margin(Gaps::new(4., 0., 0., 0.))
377 .child(
378 rect()
379 .layer(Layer::Overlay)
380 .border(
381 Border::new()
382 .fill(theme.border_fill)
383 .width(1.)
384 .alignment(BorderAlignment::Inner),
385 )
386 .overflow(Overflow::Clip)
387 .corner_radius(8.)
388 .background(theme.dropdown_background)
389 .padding(6.)
391 .content(Content::Fit)
392 .opacity(opacity)
393 .scale(scale)
394 .children(self.children.clone()),
395 ),
396 )
397 }))
398 }
399
400 fn render_key(&self) -> DiffKey {
401 self.key.clone().or(self.default_key())
402 }
403}