1use std::sync::Arc;
4
5use crate::callback::{Callback, KeyHandler};
6use crate::core::element::Element;
7use crate::core::event::{KeyCode, KeyEvent, MouseEvent};
8use crate::style::{BorderStyle, Length, Padding, ScrollbarConfig, Style, StyleSlot};
9use crate::widgets::button::ButtonVariant;
10use crate::widgets::internal::scroll_action_from_key;
11use crate::widgets::{
12 Button, List, ListConfig, ListItem, Popover, PopoverPlacement, ScrollKeymap, ZStack,
13};
14
15#[derive(Clone)]
17pub struct Select {
18 pub(crate) options: Vec<Arc<str>>,
19 pub(crate) selected: Option<usize>,
20 pub(crate) placeholder: Arc<str>,
21 pub(crate) expanded: bool,
22 pub(crate) on_toggle: Option<Callback<bool>>,
23 pub(crate) on_select: Option<Callback<usize>>,
24 pub(crate) on_change: Option<Callback<usize>>,
25 pub(crate) width: Length,
26 pub(crate) disabled: bool,
27 pub(crate) button_variant: ButtonVariant,
28 pub(crate) button_style: Style,
29 pub(crate) button_hover_style: StyleSlot,
30 pub(crate) button_focus_style: StyleSlot,
31 pub(crate) button_disabled_style: Style,
32 pub(crate) button_border_style: BorderStyle,
33 pub(crate) button_hover_border_style: Option<BorderStyle>,
34 pub(crate) button_focus_border_style: Option<BorderStyle>,
35 pub(crate) button_open_suffix: Option<Arc<str>>,
36 pub(crate) button_closed_suffix: Option<Arc<str>>,
37 pub(crate) button_suffix_style: Style,
38 pub(crate) list_title: Option<Arc<str>>,
39 pub(crate) list_title_style: Style,
40 pub(crate) list_config: ListConfig,
41
42 pub(crate) list_width: Option<Length>,
43 pub(crate) list_height: Length,
44 pub(crate) match_button_width: bool,
45 pub(crate) list_empty_text: Option<Arc<str>>,
46 pub(crate) list_disabled_style: Style,
47 pub(crate) focusable: bool,
48 pub(crate) tab_stop: bool,
49 pub(crate) on_focus: Option<Callback<()>>,
50 pub(crate) on_blur: Option<Callback<()>>,
51 pub(crate) on_key: Option<KeyHandler>,
52}
53
54impl Default for Select {
55 fn default() -> Self {
56 Self {
57 options: Vec::new(),
58 selected: None,
59 placeholder: "Select...".into(),
60 expanded: false,
61 on_toggle: None,
62 on_select: None,
63 on_change: None,
64 width: Length::Auto,
65 disabled: false,
66 button_variant: ButtonVariant::Outlined,
67 button_style: Style::default(),
68 button_hover_style: StyleSlot::Inherit,
69 button_focus_style: StyleSlot::Inherit,
70 button_disabled_style: Style::default(),
71 button_border_style: BorderStyle::Plain,
72 button_hover_border_style: None,
73 button_focus_border_style: None,
74 button_open_suffix: None,
75 button_closed_suffix: None,
76 button_suffix_style: Style::default(),
77 list_title: None,
78 list_title_style: Style::default(),
79 list_config: ListConfig {
80 border: true,
81 border_style: BorderStyle::Plain,
82 padding: Padding::default(),
83 style: Style::default(),
84 selection_style: StyleSlot::Inherit,
85 unfocused_selection_style: StyleSlot::Inherit,
86 selection_full_width: false,
87 selection_symbol: Some("> ".into()),
88 selection_symbol_right: None,
89 selection_symbol_style: None,
90 unfocused_selection_symbol_style: None,
91 symbol_column: true,
92 gutter_gap: 0,
93 gutter_for_non_selectable: false,
94 item_horizontal_padding: Padding::default(),
95 header_horizontal_padding: Padding::default(),
96 empty_text_style: Style::default(),
97 item_hover_style: None,
98 scrollbar: false,
99 scrollbar_config: ScrollbarConfig::default(),
100 },
101 list_width: None,
102 list_height: Length::Px(6),
103 match_button_width: false,
104 list_empty_text: None,
105 list_disabled_style: Style::default(),
106 focusable: true,
107 tab_stop: true,
108 on_focus: None,
109 on_blur: None,
110 on_key: None,
111 }
112 }
113}
114
115impl Select {
116 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn options(mut self, options: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
123 self.options = options.into_iter().map(Into::into).collect();
124 self
125 }
126
127 pub fn selected(mut self, selected: Option<usize>) -> Self {
129 self.selected = selected;
130 self
131 }
132
133 pub fn placeholder(mut self, placeholder: impl Into<Arc<str>>) -> Self {
135 self.placeholder = placeholder.into();
136 self
137 }
138
139 pub fn expanded(mut self, expanded: bool) -> Self {
141 self.expanded = expanded;
142 self
143 }
144
145 pub fn on_toggle(mut self, cb: Callback<bool>) -> Self {
147 self.on_toggle = Some(cb);
148 self
149 }
150
151 pub fn on_select(mut self, cb: Callback<usize>) -> Self {
153 self.on_select = Some(cb);
154 self
155 }
156
157 pub fn on_change(mut self, cb: Callback<usize>) -> Self {
159 self.on_change = Some(cb);
160 self
161 }
162
163 pub fn width(mut self, width: Length) -> Self {
165 self.width = width;
166 self
167 }
168
169 pub fn disabled(mut self, disabled: bool) -> Self {
171 self.disabled = disabled;
172 self
173 }
174
175 pub fn focusable(mut self, focusable: bool) -> Self {
177 self.focusable = focusable;
178 self
179 }
180
181 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
183 self.tab_stop = tab_stop;
184 self
185 }
186
187 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
189 self.on_focus = Some(cb);
190 self
191 }
192
193 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
195 self.on_blur = Some(cb);
196 self
197 }
198
199 pub fn on_key(mut self, handler: KeyHandler) -> Self {
201 self.on_key = Some(handler);
202 self
203 }
204
205 pub fn button_variant(mut self, variant: ButtonVariant) -> Self {
207 self.button_variant = variant;
208 self
209 }
210
211 pub fn button_style(mut self, style: Style) -> Self {
213 self.button_style = style;
214 self
215 }
216
217 pub fn button_hover_style(mut self, style: Style) -> Self {
219 self.button_hover_style = StyleSlot::Replace(style);
220 self
221 }
222
223 pub fn extend_button_hover_style(mut self, style: Style) -> Self {
225 self.button_hover_style = StyleSlot::Extend(style);
226 self
227 }
228
229 pub fn inherit_button_hover_style(mut self) -> Self {
231 self.button_hover_style = StyleSlot::Inherit;
232 self
233 }
234
235 pub fn button_hover_style_slot(mut self, slot: StyleSlot) -> Self {
237 self.button_hover_style = slot;
238 self
239 }
240
241 pub fn button_focus_style(mut self, style: Style) -> Self {
243 self.button_focus_style = StyleSlot::Replace(style);
244 self
245 }
246
247 pub fn extend_button_focus_style(mut self, style: Style) -> Self {
249 self.button_focus_style = StyleSlot::Extend(style);
250 self
251 }
252
253 pub fn inherit_button_focus_style(mut self) -> Self {
255 self.button_focus_style = StyleSlot::Inherit;
256 self
257 }
258
259 pub fn button_focus_style_slot(mut self, slot: StyleSlot) -> Self {
261 self.button_focus_style = slot;
262 self
263 }
264
265 pub fn button_disabled_style(mut self, style: Style) -> Self {
267 self.button_disabled_style = style;
268 self
269 }
270
271 pub fn button_border_style(mut self, style: BorderStyle) -> Self {
273 self.button_border_style = style;
274 self
275 }
276
277 pub fn button_hover_border_style(mut self, style: BorderStyle) -> Self {
279 self.button_hover_border_style = Some(style);
280 self
281 }
282
283 pub fn button_focus_border_style(mut self, style: BorderStyle) -> Self {
285 self.button_focus_border_style = Some(style);
286 self
287 }
288
289 pub fn button_open_suffix(mut self, suffix: impl Into<Arc<str>>) -> Self {
291 self.button_open_suffix = Some(suffix.into());
292 self
293 }
294
295 pub fn button_closed_suffix(mut self, suffix: impl Into<Arc<str>>) -> Self {
297 self.button_closed_suffix = Some(suffix.into());
298 self
299 }
300
301 pub fn button_suffix_style(mut self, style: Style) -> Self {
303 self.button_suffix_style = style;
304 self
305 }
306
307 pub fn list_title(mut self, title: impl Into<Arc<str>>) -> Self {
309 self.list_title = Some(title.into());
310 self
311 }
312
313 pub fn list_title_style(mut self, style: Style) -> Self {
315 self.list_title_style = style;
316 self
317 }
318
319 pub fn list_config(mut self, config: ListConfig) -> Self {
321 self.list_config = config;
322 self
323 }
324
325 pub fn list_border(mut self, border: bool) -> Self {
327 self.list_config.border = border;
328 self
329 }
330
331 pub fn list_border_style(mut self, style: BorderStyle) -> Self {
333 self.list_config.border_style = style;
334 self
335 }
336
337 pub fn list_padding(mut self, padding: impl Into<Padding>) -> Self {
339 self.list_config.padding = padding.into();
340 self
341 }
342
343 pub fn list_style(mut self, style: Style) -> Self {
345 self.list_config.style = style;
346 self
347 }
348
349 pub fn list_selection_style(mut self, style: Style) -> Self {
351 self.list_config.selection_style = StyleSlot::Replace(style);
352 self
353 }
354
355 pub fn extend_list_selection_style(mut self, style: Style) -> Self {
357 self.list_config.selection_style = StyleSlot::Extend(style);
358 self
359 }
360
361 pub fn inherit_list_selection_style(mut self) -> Self {
363 self.list_config.selection_style = StyleSlot::Inherit;
364 self
365 }
366
367 pub fn list_selection_style_slot(mut self, slot: StyleSlot) -> Self {
369 self.list_config.selection_style = slot;
370 self
371 }
372
373 pub fn list_unfocused_selection_style(mut self, style: Style) -> Self {
375 self.list_config.unfocused_selection_style = StyleSlot::Replace(style);
376 self
377 }
378
379 pub fn extend_list_unfocused_selection_style(mut self, style: Style) -> Self {
381 self.list_config.unfocused_selection_style = StyleSlot::Extend(style);
382 self
383 }
384
385 pub fn inherit_list_unfocused_selection_style(mut self) -> Self {
387 self.list_config.unfocused_selection_style = StyleSlot::Inherit;
388 self
389 }
390
391 pub fn list_unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
393 self.list_config.unfocused_selection_style = slot;
394 self
395 }
396
397 pub fn list_selection_full_width(mut self, full_width: bool) -> Self {
399 self.list_config.selection_full_width = full_width;
400 self
401 }
402
403 pub fn list_selection_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
405 self.list_config.selection_symbol = symbol.map(Into::into);
406 self
407 }
408
409 pub fn list_selection_symbol_right(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
412 self.list_config.selection_symbol_right = symbol.map(Into::into);
413 self
414 }
415
416 pub fn list_selection_symbol_style(mut self, style: Style) -> Self {
418 self.list_config.selection_symbol_style = Some(style);
419 self
420 }
421
422 pub fn list_unfocused_selection_symbol_style(mut self, style: Style) -> Self {
424 self.list_config.unfocused_selection_symbol_style = Some(style);
425 self
426 }
427
428 pub fn list_hover_style(mut self, style: Style) -> Self {
430 self.list_config.item_hover_style = Some(StyleSlot::Replace(style));
431 self
432 }
433
434 pub fn extend_list_hover_style(mut self, style: Style) -> Self {
436 self.list_config.item_hover_style = Some(StyleSlot::Extend(style));
437 self
438 }
439
440 pub fn inherit_list_hover_style(mut self) -> Self {
442 self.list_config.item_hover_style = Some(StyleSlot::Inherit);
443 self
444 }
445
446 pub fn list_hover_style_slot(mut self, slot: StyleSlot) -> Self {
448 self.list_config.item_hover_style = Some(slot);
449 self
450 }
451
452 pub fn list_width(mut self, width: Length) -> Self {
454 self.list_width = Some(width);
455 self
456 }
457
458 pub fn list_height(mut self, height: Length) -> Self {
460 self.list_height = height;
461 self
462 }
463
464 pub fn match_button_width(mut self, match_button_width: bool) -> Self {
466 self.match_button_width = match_button_width;
467 self
468 }
469
470 pub fn list_scrollbar(mut self, scrollbar: bool) -> Self {
472 self.list_config.scrollbar = scrollbar;
473 self
474 }
475
476 pub fn list_scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
478 self.list_config.scrollbar_config = config;
479 self
480 }
481
482 pub fn list_empty_text(mut self, text: impl Into<Arc<str>>) -> Self {
484 self.list_empty_text = Some(text.into());
485 self
486 }
487
488 pub fn list_empty_text_style(mut self, style: Style) -> Self {
490 self.list_config.empty_text_style = style;
491 self
492 }
493
494 pub fn list_disabled_style(mut self, style: Style) -> Self {
496 self.list_disabled_style = style;
497 self
498 }
499}
500
501impl From<Select> for Element {
502 fn from(select: Select) -> Self {
503 let label = if let Some(idx) = select.selected {
504 select
505 .options
506 .get(idx)
507 .cloned()
508 .unwrap_or(select.placeholder.clone())
509 } else {
510 select.placeholder.clone()
511 };
512
513 let mut button = Button::new(label)
514 .variant(select.button_variant)
515 .width(select.width)
516 .style(select.button_style)
517 .hover_style_slot(select.button_hover_style)
518 .focus_style_slot(select.button_focus_style)
519 .disabled_style(select.button_disabled_style)
520 .disabled(select.disabled)
521 .focusable(select.focusable)
522 .tab_stop(select.tab_stop);
523
524 if let Some(cb) = select.on_focus.clone() {
525 button = button.on_focus(cb);
526 }
527 if let Some(cb) = select.on_blur.clone() {
528 button = button.on_blur(cb);
529 }
530
531 if matches!(select.button_variant, ButtonVariant::Outlined) {
532 button = button.border_style(select.button_border_style);
533 }
534 if let Some(style) = select.button_hover_border_style {
535 button = button.hover_border_style(Some(style));
536 }
537 if let Some(style) = select.button_focus_border_style {
538 button = button.focus_border_style(Some(style));
539 }
540
541 let suffix = if select.expanded {
542 select.button_open_suffix.clone()
543 } else {
544 select.button_closed_suffix.clone()
545 };
546 if let Some(suffix) = suffix {
547 button = button
548 .shortcut(suffix)
549 .shortcut_style(select.button_suffix_style);
550 }
551
552 if let Some(cb) = select.on_toggle.clone()
553 && !select.disabled
554 {
555 let expanded = select.expanded;
556 button = button.on_click(Callback::new(move |_: MouseEvent| cb.emit(!expanded)));
557 }
558
559 if select.expanded && !select.disabled {
560 let options_len = select.options.len();
561 let selected = select
562 .selected
563 .unwrap_or(0)
564 .min(options_len.saturating_sub(1));
565 let change_cb = select.on_change.clone().or(select.on_select.clone());
566 let on_select = select.on_select.clone();
567 let on_toggle = select.on_toggle.clone();
568 let caller_on_key = select.on_key.clone();
569 button = button.on_key(KeyHandler::new(move |key: KeyEvent| {
570 if caller_on_key
571 .as_ref()
572 .is_some_and(|handler| handler.handle(key))
573 {
574 return true;
575 }
576
577 if key.code == KeyCode::Esc
578 && let Some(toggle) = &on_toggle
579 {
580 toggle.emit(false);
581 return true;
582 }
583
584 if key.code == KeyCode::Enter {
585 let mut handled = false;
586 if let Some(cb) = &on_select {
587 cb.emit(selected);
588 handled = true;
589 }
590 if let Some(toggle) = &on_toggle {
591 toggle.emit(false);
592 handled = true;
593 }
594 return handled;
595 }
596
597 let Some(action) = scroll_action_from_key(&key, ScrollKeymap::default()) else {
598 return false;
599 };
600
601 if options_len == 0 {
602 return true;
603 }
604
605 if let Some(next) = List::selection_for_action_in_len(selected, options_len, action)
606 && next != selected
607 && let Some(cb) = &change_cb
608 {
609 cb.emit(next);
610 }
611
612 true
613 }));
614 } else if let Some(handler) = select.on_key {
615 button = button.on_key(handler);
616 }
617
618 let list_hover_slot = select
619 .list_config
620 .item_hover_style
621 .unwrap_or(select.list_config.selection_style);
622
623 let mut list = List::new()
624 .items(select.options.iter().map(|s| ListItem::new(s.clone())))
625 .selected(select.selected.unwrap_or(0))
626 .title_style(select.list_title_style)
627 .border(select.list_config.border)
628 .border_style(select.list_config.border_style)
629 .padding(select.list_config.padding)
630 .style(select.list_config.style)
631 .selection_full_width(select.list_config.selection_full_width)
632 .selection_symbol_style(
633 select.list_config.selection_symbol_style.unwrap_or(
634 select
635 .list_config
636 .selection_style
637 .explicit_style()
638 .unwrap_or_default(),
639 ),
640 )
641 .unfocused_selection_symbol_style(
642 select
643 .list_config
644 .unfocused_selection_symbol_style
645 .or_else(|| {
646 select
647 .list_config
648 .unfocused_selection_style
649 .explicit_style()
650 })
651 .unwrap_or(
652 select
653 .list_config
654 .selection_style
655 .explicit_style()
656 .unwrap_or_default(),
657 ),
658 )
659 .selection_symbol(select.list_config.selection_symbol)
660 .selection_symbol_right(select.list_config.selection_symbol_right)
661 .symbol_column(select.list_config.symbol_column)
662 .gutter_gap(select.list_config.gutter_gap)
663 .gutter_for_non_selectable(select.list_config.gutter_for_non_selectable)
664 .scrollbar(select.list_config.scrollbar)
665 .scrollbar_config(select.list_config.scrollbar_config)
666 .width(select.list_width.unwrap_or(select.width))
667 .height(select.list_height)
668 .item_horizontal_padding(select.list_config.item_horizontal_padding)
669 .header_horizontal_padding(select.list_config.header_horizontal_padding)
670 .empty_text_style(select.list_config.empty_text_style)
671 .disabled_style(select.list_disabled_style)
672 .disabled(select.disabled);
673 list = list
674 .selection_style_slot(select.list_config.selection_style)
675 .unfocused_selection_style_slot(select.list_config.unfocused_selection_style)
676 .item_hover_style_slot(list_hover_slot);
677
678 if let Some(title) = select.list_title {
679 list = list.title(title);
680 }
681 if let Some(empty_text) = select.list_empty_text {
682 list = list.empty_text(empty_text);
683 }
684
685 let change_cb = select.on_change.clone().or(select.on_select.clone());
686 if let Some(cb) = change_cb {
687 list = list.on_select(Callback::new(move |ev: crate::widgets::ListEvent| {
688 cb.emit(ev.index);
689 }));
690 }
691
692 let emit_on_activate = select.on_change.is_some();
693 if (emit_on_activate && select.on_select.is_some()) || select.on_toggle.is_some() {
694 let on_select = select.on_select.clone();
695 let on_toggle = select.on_toggle.clone();
696 list = list.on_activate(Callback::new(move |ev: crate::widgets::ListEvent| {
697 if emit_on_activate && let Some(cb) = &on_select {
698 cb.emit(ev.index);
699 }
700 if let Some(toggle) = &on_toggle {
701 toggle.emit(false);
702 }
703 }));
704 }
705
706 let overlay = ZStack::new().child(list);
707 let mut popover = Popover::new()
708 .trigger(button)
709 .content(overlay)
710 .open(select.expanded && !select.disabled)
711 .fit_trigger_width(select.match_button_width && select.list_width.is_none())
712 .placement(PopoverPlacement::BelowStart);
713
714 if let Some(cb) = select.on_toggle.clone() {
715 popover = popover.on_close(Callback::new(move |_| cb.emit(false)));
716 }
717
718 popover.into()
719 }
720}