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 let background = if self.selected {
81 theme.select_background
82 } else if *status.read() == DropdownItemStatus::Hovering {
83 theme.hover_background
84 } else {
85 theme.background
86 };
87
88 let border = if focus_status() == FocusStatus::Keyboard {
89 Border::new()
90 .fill(theme.select_border_fill)
91 .width(2.)
92 .alignment(BorderAlignment::Inner)
93 } else {
94 Border::new()
95 .fill(theme.border_fill)
96 .width(1.)
97 .alignment(BorderAlignment::Inner)
98 };
99
100 rect()
101 .width(Size::fill_minimum())
102 .color(theme.color)
103 .a11y_id(focus.a11y_id())
104 .a11y_focusable(Focusable::Enabled)
105 .a11y_member_of(dropdown_group.group_id)
106 .a11y_role(AccessibilityRole::Button)
107 .background(background)
108 .border(border)
109 .corner_radius(6.)
110 .padding((6., 10., 6., 10.))
111 .main_align(Alignment::center())
112 .on_pointer_enter(move |_| {
113 *status.write() = DropdownItemStatus::Hovering;
114 })
115 .on_pointer_leave(move |_| {
116 *status.write() = DropdownItemStatus::Idle;
117 })
118 .map(self.on_press.clone(), |rect, on_press| {
119 rect.on_press(on_press)
120 })
121 .children(self.children.clone())
122 }
123
124 fn render_key(&self) -> DiffKey {
125 self.key.clone().or(self.default_key())
126 }
127}
128
129#[derive(Clone)]
130struct DropdownGroup {
131 group_id: AccessibilityId,
132}
133
134#[derive(Debug, Default, PartialEq, Clone, Copy)]
135pub enum DropdownStatus {
136 #[default]
137 Idle,
138 Hovering,
139}
140
141#[cfg_attr(feature = "docs",
180 doc = embed_doc_image::embed_image!("dropdown", "images/gallery_dropdown.png")
181)]
182#[derive(Clone, PartialEq)]
183pub struct Dropdown {
184 pub(crate) theme: Option<DropdownThemePartial>,
185 pub selected_item: Option<Element>,
186 pub children: Vec<Element>,
187 pub key: DiffKey,
188}
189
190impl ChildrenExt for Dropdown {
191 fn get_children(&mut self) -> &mut Vec<Element> {
192 &mut self.children
193 }
194}
195
196impl Default for Dropdown {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl Dropdown {
203 pub fn new() -> Self {
204 Self {
205 theme: None,
206 selected_item: None,
207 children: Vec::new(),
208 key: DiffKey::None,
209 }
210 }
211
212 pub fn theme(mut self, theme: DropdownThemePartial) -> Self {
213 self.theme = Some(theme);
214 self
215 }
216
217 pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
218 self.selected_item = Some(item.into());
219 self
220 }
221
222 pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
223 self.key = key.into();
224 self
225 }
226}
227
228impl Render for Dropdown {
229 fn render(&self) -> impl IntoElement {
230 let theme = get_theme!(&self.theme, dropdown);
231 let focus = use_focus();
232 let focus_status = use_focus_status(focus);
233 let mut status = use_state(DropdownStatus::default);
234 let mut open = use_state(|| false);
235 use_provide_context(|| DropdownGroup {
236 group_id: focus.a11y_id(),
237 });
238
239 let background = match *status.read() {
240 DropdownStatus::Hovering => theme.hover_background,
241 DropdownStatus::Idle => theme.background_button,
242 };
243
244 let border = if focus_status() == FocusStatus::Keyboard {
245 Border::new()
246 .fill(theme.focus_border_fill)
247 .width(2.)
248 .alignment(BorderAlignment::Inner)
249 } else {
250 Border::new()
251 .fill(theme.border_fill)
252 .width(1.)
253 .alignment(BorderAlignment::Inner)
254 };
255
256 use_side_effect(move || {
258 if let Some(member_of) = PlatformState::get()
259 .focused_accessibility_node
260 .read()
261 .member_of()
262 {
263 if member_of != focus.a11y_id() {
264 open.set(false);
265 }
266 } else {
267 open.set(false);
268 }
269 });
270
271 let on_press = move |e: Event<PressEventData>| {
272 focus.request_focus();
273 open.set(true);
274 e.prevent_default();
276 e.stop_propagation();
277 };
278
279 let on_pointer_enter = move |_| {
280 *status.write() = DropdownStatus::Hovering;
281 };
282
283 let on_pointer_leave = move |_| {
284 *status.write() = DropdownStatus::Idle;
285 };
286
287 let on_global_mouse_up = move |_| {
289 open.set(false);
290 };
291
292 let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
293 Key::Escape => {
294 open.set(false);
295 }
296 Key::Enter if focus.is_focused() => {
297 open.toggle();
298 }
299 _ => {}
300 };
301
302 rect()
303 .child(
304 rect()
305 .a11y_id(focus.a11y_id())
306 .a11y_member_of(focus.a11y_id())
307 .a11y_focusable(Focusable::Enabled)
308 .on_pointer_enter(on_pointer_enter)
309 .on_pointer_leave(on_pointer_leave)
310 .on_press(on_press)
311 .on_global_key_down(on_global_key_down)
312 .on_global_mouse_up(on_global_mouse_up)
313 .width(theme.width)
314 .margin(theme.margin)
315 .background(background)
316 .padding((6., 16., 6., 16.))
317 .border(border)
318 .horizontal()
319 .center()
320 .color(theme.color)
321 .corner_radius(8.)
322 .maybe_child(self.selected_item.clone())
323 .child(
324 ArrowIcon::new()
325 .margin((0., 0., 0., 8.))
326 .rotate(0.)
327 .fill(theme.arrow_fill),
328 ),
329 )
330 .maybe_child(open().then(|| {
331 rect().height(Size::px(0.)).width(Size::px(0.)).child(
332 rect()
333 .width(Size::window_percent(100.))
334 .margin(Gaps::new(4., 0., 0., 0.))
335 .child(
336 rect()
337 .border(
338 Border::new()
339 .fill(theme.border_fill)
340 .width(1.)
341 .alignment(BorderAlignment::Inner),
342 )
343 .overflow(Overflow::Clip)
344 .corner_radius(8.)
345 .background(theme.dropdown_background)
346 .padding(6.)
348 .content(Content::Fit)
349 .children(self.children.clone()),
350 ),
351 )
352 }))
353 }
354
355 fn render_key(&self) -> DiffKey {
356 self.key.clone().or(self.default_key())
357 }
358}