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