1use gpui::{
2 anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, AppContext,
3 Bounds, ClickEvent, Context, DismissEvent, Edges, ElementId, Entity, EventEmitter, FocusHandle,
4 Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render,
5 RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
6 Task, WeakEntity, Window,
7};
8use rust_i18n::t;
9
10use crate::{
11 actions::{Cancel, Confirm, SelectDown, SelectUp},
12 h_flex,
13 input::clear_button,
14 list::{List, ListDelegate, ListState},
15 v_flex, ActiveTheme, Disableable, Icon, IconName, IndexPath, Selectable, Sizable, Size,
16 StyleSized, StyledExt,
17};
18
19const CONTEXT: &str = "Select";
20pub(crate) fn init(cx: &mut App) {
21 cx.bind_keys([
22 KeyBinding::new("up", SelectUp, Some(CONTEXT)),
23 KeyBinding::new("down", SelectDown, Some(CONTEXT)),
24 KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
25 KeyBinding::new(
26 "secondary-enter",
27 Confirm { secondary: true },
28 Some(CONTEXT),
29 ),
30 KeyBinding::new("escape", Cancel, Some(CONTEXT)),
31 ])
32}
33
34pub trait SelectItem: Clone {
36 type Value: Clone;
37 fn title(&self) -> SharedString;
38 fn display_title(&self) -> Option<AnyElement> {
42 None
43 }
44 fn render(&self, _: &mut Window, _: &mut App) -> impl IntoElement {
46 self.title().into_element()
47 }
48 fn value(&self) -> &Self::Value;
50 fn matches(&self, query: &str) -> bool {
52 self.title().to_lowercase().contains(&query.to_lowercase())
53 }
54}
55
56impl SelectItem for String {
57 type Value = Self;
58
59 fn title(&self) -> SharedString {
60 SharedString::from(self.to_string())
61 }
62
63 fn value(&self) -> &Self::Value {
64 &self
65 }
66}
67
68impl SelectItem for SharedString {
69 type Value = Self;
70
71 fn title(&self) -> SharedString {
72 SharedString::from(self.to_string())
73 }
74
75 fn value(&self) -> &Self::Value {
76 &self
77 }
78}
79
80impl SelectItem for &'static str {
81 type Value = Self;
82
83 fn title(&self) -> SharedString {
84 SharedString::from(self.to_string())
85 }
86
87 fn value(&self) -> &Self::Value {
88 self
89 }
90}
91
92pub trait SelectDelegate: Sized {
93 type Item: SelectItem;
94
95 fn sections_count(&self, _: &App) -> usize {
97 1
98 }
99
100 fn section(&self, _section: usize) -> Option<AnyElement> {
102 return None;
103 }
104
105 fn items_count(&self, section: usize) -> usize;
107
108 fn item(&self, ix: IndexPath) -> Option<&Self::Item>;
110
111 fn position<V>(&self, _value: &V) -> Option<IndexPath>
113 where
114 Self::Item: SelectItem<Value = V>,
115 V: PartialEq;
116
117 fn perform_search(
118 &mut self,
119 _query: &str,
120 _window: &mut Window,
121 _: &mut Context<SelectState<Self>>,
122 ) -> Task<()> {
123 Task::ready(())
124 }
125}
126
127impl<T: SelectItem> SelectDelegate for Vec<T> {
128 type Item = T;
129
130 fn items_count(&self, _: usize) -> usize {
131 self.len()
132 }
133
134 fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
135 self.as_slice().get(ix.row)
136 }
137
138 fn position<V>(&self, value: &V) -> Option<IndexPath>
139 where
140 Self::Item: SelectItem<Value = V>,
141 V: PartialEq,
142 {
143 self.iter()
144 .position(|v| v.value() == value)
145 .map(|ix| IndexPath::default().row(ix))
146 }
147}
148
149struct SelectListDelegate<D: SelectDelegate + 'static> {
150 delegate: D,
151 state: WeakEntity<SelectState<D>>,
152 selected_index: Option<IndexPath>,
153}
154
155impl<D> ListDelegate for SelectListDelegate<D>
156where
157 D: SelectDelegate + 'static,
158{
159 type Item = SelectListItem;
160
161 fn sections_count(&self, cx: &App) -> usize {
162 self.delegate.sections_count(cx)
163 }
164
165 fn items_count(&self, section: usize, _: &App) -> usize {
166 self.delegate.items_count(section)
167 }
168
169 fn render_section_header(
170 &self,
171 section: usize,
172 _: &mut Window,
173 cx: &mut App,
174 ) -> Option<impl IntoElement> {
175 let state = self.state.upgrade()?.read(cx);
176 let Some(item) = self.delegate.section(section) else {
177 return None;
178 };
179
180 return Some(
181 div()
182 .py_0p5()
183 .px_2()
184 .list_size(state.options.size)
185 .text_sm()
186 .text_color(cx.theme().muted_foreground)
187 .child(item),
188 );
189 }
190
191 fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut App) -> Option<Self::Item> {
192 let selected = self
193 .selected_index
194 .map_or(false, |selected_index| selected_index == ix);
195 let size = self
196 .state
197 .upgrade()
198 .map_or(Size::Medium, |state| state.read(cx).options.size);
199
200 if let Some(item) = self.delegate.item(ix) {
201 let list_item = SelectListItem::new(ix.row)
202 .selected(selected)
203 .with_size(size)
204 .child(div().whitespace_nowrap().child(item.render(window, cx)));
205 Some(list_item)
206 } else {
207 None
208 }
209 }
210
211 fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {
212 let state = self.state.clone();
213 let final_selected_index = state
214 .read_with(cx, |this, _| this.final_selected_index)
215 .ok()
216 .flatten();
217
218 let need_restore = if final_selected_index != self.selected_index {
220 self.selected_index = final_selected_index;
221 true
222 } else {
223 false
224 };
225
226 cx.defer_in(window, move |this, window, cx| {
227 if need_restore {
228 this.set_selected_index(final_selected_index, window, cx);
229 }
230
231 _ = state.update(cx, |this, cx| {
232 this.open = false;
233 this.focus(window, cx);
234 });
235 });
236 }
237
238 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
239 let selected_index = self.selected_index;
240 let selected_value = selected_index
241 .and_then(|ix| self.delegate.item(ix))
242 .map(|item| item.value().clone());
243 let state = self.state.clone();
244
245 cx.defer_in(window, move |_, window, cx| {
246 _ = state.update(cx, |this, cx| {
247 cx.emit(SelectEvent::Confirm(selected_value.clone()));
248 this.final_selected_index = selected_index;
249 this.selected_value = selected_value;
250 this.open = false;
251 this.focus(window, cx);
252 });
253 });
254 }
255
256 fn perform_search(
257 &mut self,
258 query: &str,
259 window: &mut Window,
260 cx: &mut Context<ListState<Self>>,
261 ) -> Task<()> {
262 self.state.upgrade().map_or(Task::ready(()), |state| {
263 state.update(cx, |_, cx| self.delegate.perform_search(query, window, cx))
264 })
265 }
266
267 fn set_selected_index(
268 &mut self,
269 ix: Option<IndexPath>,
270 _: &mut Window,
271 _: &mut Context<ListState<Self>>,
272 ) {
273 self.selected_index = ix;
274 }
275
276 fn render_empty(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
277 if let Some(empty) = self
278 .state
279 .upgrade()
280 .and_then(|state| state.read(cx).empty.as_ref())
281 {
282 empty(window, cx).into_any_element()
283 } else {
284 h_flex()
285 .justify_center()
286 .py_6()
287 .text_color(cx.theme().muted_foreground.opacity(0.6))
288 .child(Icon::new(IconName::Inbox).size(px(28.)))
289 .into_any_element()
290 }
291 }
292}
293
294pub enum SelectEvent<D: SelectDelegate + 'static> {
296 Confirm(Option<<D::Item as SelectItem>::Value>),
297}
298
299struct SelectOptions {
300 style: StyleRefinement,
301 size: Size,
302 icon: Option<Icon>,
303 cleanable: bool,
304 placeholder: Option<SharedString>,
305 title_prefix: Option<SharedString>,
306 search_placeholder: Option<SharedString>,
307 empty: Option<AnyElement>,
308 menu_width: Length,
309 disabled: bool,
310 appearance: bool,
311}
312
313impl Default for SelectOptions {
314 fn default() -> Self {
315 Self {
316 style: StyleRefinement::default(),
317 size: Size::default(),
318 icon: None,
319 cleanable: false,
320 placeholder: None,
321 title_prefix: None,
322 empty: None,
323 menu_width: Length::Auto,
324 disabled: false,
325 appearance: true,
326 search_placeholder: None,
327 }
328 }
329}
330
331pub struct SelectState<D: SelectDelegate + 'static> {
333 focus_handle: FocusHandle,
334 options: SelectOptions,
335 searchable: bool,
336 list: Entity<ListState<SelectListDelegate<D>>>,
337 empty: Option<Box<dyn Fn(&Window, &App) -> AnyElement>>,
338 bounds: Bounds<Pixels>,
340 open: bool,
341 selected_value: Option<<D::Item as SelectItem>::Value>,
342 final_selected_index: Option<IndexPath>,
343 _subscriptions: Vec<Subscription>,
344}
345
346#[derive(IntoElement)]
348pub struct Select<D: SelectDelegate + 'static> {
349 id: ElementId,
350 state: Entity<SelectState<D>>,
351 options: SelectOptions,
352}
353
354#[derive(Debug, Clone)]
356pub struct SearchableVec<T> {
357 items: Vec<T>,
358 matched_items: Vec<T>,
359}
360
361impl<T: Clone> SearchableVec<T> {
362 pub fn push(&mut self, item: T) {
363 self.items.push(item.clone());
364 self.matched_items.push(item);
365 }
366}
367
368impl<T: Clone> SearchableVec<T> {
369 pub fn new(items: impl Into<Vec<T>>) -> Self {
370 let items = items.into();
371 Self {
372 items: items.clone(),
373 matched_items: items,
374 }
375 }
376}
377
378impl<T: SelectItem> From<Vec<T>> for SearchableVec<T> {
379 fn from(items: Vec<T>) -> Self {
380 Self {
381 items: items.clone(),
382 matched_items: items,
383 }
384 }
385}
386
387impl<I: SelectItem> SelectDelegate for SearchableVec<I> {
388 type Item = I;
389
390 fn items_count(&self, _: usize) -> usize {
391 self.matched_items.len()
392 }
393
394 fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
395 self.matched_items.get(ix.row)
396 }
397
398 fn position<V>(&self, value: &V) -> Option<IndexPath>
399 where
400 Self::Item: SelectItem<Value = V>,
401 V: PartialEq,
402 {
403 for (ix, item) in self.matched_items.iter().enumerate() {
404 if item.value() == value {
405 return Some(IndexPath::default().row(ix));
406 }
407 }
408
409 None
410 }
411
412 fn perform_search(
413 &mut self,
414 query: &str,
415 _window: &mut Window,
416 _: &mut Context<SelectState<Self>>,
417 ) -> Task<()> {
418 self.matched_items = self
419 .items
420 .iter()
421 .filter(|item| item.title().to_lowercase().contains(&query.to_lowercase()))
422 .cloned()
423 .collect();
424
425 Task::ready(())
426 }
427}
428
429impl<I: SelectItem> SelectDelegate for SearchableVec<SelectGroup<I>> {
430 type Item = I;
431
432 fn sections_count(&self, _: &App) -> usize {
433 self.matched_items.len()
434 }
435
436 fn items_count(&self, section: usize) -> usize {
437 self.matched_items
438 .get(section)
439 .map_or(0, |group| group.items.len())
440 }
441
442 fn section(&self, section: usize) -> Option<AnyElement> {
443 Some(
444 self.matched_items
445 .get(section)?
446 .title
447 .clone()
448 .into_any_element(),
449 )
450 }
451
452 fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
453 let section = self.matched_items.get(ix.section)?;
454
455 section.items.get(ix.row)
456 }
457
458 fn position<V>(&self, value: &V) -> Option<IndexPath>
459 where
460 Self::Item: SelectItem<Value = V>,
461 V: PartialEq,
462 {
463 for (ix, group) in self.matched_items.iter().enumerate() {
464 for (row_ix, item) in group.items.iter().enumerate() {
465 if item.value() == value {
466 return Some(IndexPath::default().section(ix).row(row_ix));
467 }
468 }
469 }
470
471 None
472 }
473
474 fn perform_search(
475 &mut self,
476 query: &str,
477 _window: &mut Window,
478 _: &mut Context<SelectState<Self>>,
479 ) -> Task<()> {
480 self.matched_items = self
481 .items
482 .iter()
483 .filter(|item| item.matches(&query))
484 .cloned()
485 .map(|mut item| {
486 item.items.retain(|item| item.matches(&query));
487 item
488 })
489 .collect();
490
491 Task::ready(())
492 }
493}
494
495#[derive(Debug, Clone)]
497pub struct SelectGroup<I: SelectItem> {
498 pub title: SharedString,
499 pub items: Vec<I>,
500}
501
502impl<I> SelectGroup<I>
503where
504 I: SelectItem,
505{
506 pub fn new(title: impl Into<SharedString>) -> Self {
508 Self {
509 title: title.into(),
510 items: vec![],
511 }
512 }
513
514 pub fn item(mut self, item: I) -> Self {
516 self.items.push(item);
517 self
518 }
519
520 pub fn items(mut self, items: impl IntoIterator<Item = I>) -> Self {
522 self.items.extend(items);
523 self
524 }
525
526 fn matches(&self, query: &str) -> bool {
527 self.title.to_lowercase().contains(&query.to_lowercase())
528 || self.items.iter().any(|item| item.matches(query))
529 }
530}
531
532impl<D> SelectState<D>
533where
534 D: SelectDelegate + 'static,
535{
536 pub fn new(
538 delegate: D,
539 selected_index: Option<IndexPath>,
540 window: &mut Window,
541 cx: &mut Context<Self>,
542 ) -> Self {
543 let focus_handle = cx.focus_handle();
544 let delegate = SelectListDelegate {
545 delegate,
546 state: cx.entity().downgrade(),
547 selected_index,
548 };
549
550 let list = cx.new(|cx| ListState::new(delegate, window, cx).reset_on_cancel(false));
551 let list_focus_handle = list.read(cx).focus_handle.clone();
552 let list_search_focus_handle = list.read(cx).query_input.focus_handle(cx);
553
554 let _subscriptions = vec![
555 cx.on_blur(&list_focus_handle, window, Self::on_blur),
556 cx.on_blur(&list_search_focus_handle, window, Self::on_blur),
557 cx.on_blur(&focus_handle, window, Self::on_blur),
558 ];
559
560 let mut this = Self {
561 focus_handle,
562 options: SelectOptions::default(),
563 searchable: false,
564 list,
565 selected_value: None,
566 open: false,
567 bounds: Bounds::default(),
568 empty: None,
569 final_selected_index: None,
570 _subscriptions,
571 };
572 this.set_selected_index(selected_index, window, cx);
573 this
574 }
575
576 pub fn searchable(mut self, searchable: bool) -> Self {
580 self.searchable = searchable;
581 self
582 }
583
584 pub fn set_selected_index(
586 &mut self,
587 selected_index: Option<IndexPath>,
588 window: &mut Window,
589 cx: &mut Context<Self>,
590 ) {
591 self.list.update(cx, |list, cx| {
592 list._set_selected_index(selected_index, window, cx);
593 });
594 self.final_selected_index = selected_index;
595 self.update_selected_value(window, cx);
596 }
597
598 pub fn set_selected_value(
604 &mut self,
605 selected_value: &<D::Item as SelectItem>::Value,
606 window: &mut Window,
607 cx: &mut Context<Self>,
608 ) where
609 <<D as SelectDelegate>::Item as SelectItem>::Value: PartialEq,
610 {
611 let delegate = self.list.read(cx).delegate();
612 let selected_index = delegate.delegate.position(selected_value);
613 self.set_selected_index(selected_index, window, cx);
614 }
615
616 pub fn set_items(&mut self, items: D, _: &mut Window, cx: &mut Context<Self>)
618 where
619 D: SelectDelegate + 'static,
620 {
621 self.list.update(cx, |list, _| {
622 list.delegate_mut().delegate = items;
623 });
624 }
625
626 pub fn selected_index(&self, cx: &App) -> Option<IndexPath> {
628 self.list.read(cx).selected_index()
629 }
630
631 pub fn selected_value(&self) -> Option<&<D::Item as SelectItem>::Value> {
633 self.selected_value.as_ref()
634 }
635
636 pub fn focus(&self, window: &mut Window, _: &mut App) {
638 self.focus_handle.focus(window);
639 }
640
641 fn update_selected_value(&mut self, _: &Window, cx: &App) {
642 self.selected_value = self
643 .selected_index(cx)
644 .and_then(|ix| self.list.read(cx).delegate().delegate.item(ix))
645 .map(|item| item.value().clone());
646 }
647
648 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
649 if self.list.read(cx).is_focused(window, cx) || self.focus_handle.is_focused(window) {
651 return;
652 }
653
654 let final_selected_index = self.final_selected_index;
656 let selected_index = self.selected_index(cx);
657 if final_selected_index != selected_index {
658 self.list.update(cx, |list, cx| {
659 list.set_selected_index(self.final_selected_index, window, cx);
660 });
661 }
662
663 self.open = false;
664 cx.notify();
665 }
666
667 fn up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
668 if !self.open {
669 self.open = true;
670 }
671
672 self.list.focus_handle(cx).focus(window);
673 cx.propagate();
674 }
675
676 fn down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
677 if !self.open {
678 self.open = true;
679 }
680
681 self.list.focus_handle(cx).focus(window);
682 cx.propagate();
683 }
684
685 fn enter(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
686 cx.propagate();
688
689 if !self.open {
690 self.open = true;
691 cx.notify();
692 }
693
694 self.list.focus_handle(cx).focus(window);
695 }
696
697 fn toggle_menu(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
698 cx.stop_propagation();
699
700 self.open = !self.open;
701 if self.open {
702 self.list.focus_handle(cx).focus(window);
703 }
704 cx.notify();
705 }
706
707 fn escape(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
708 if !self.open {
709 cx.propagate();
710 }
711
712 self.open = false;
713 cx.notify();
714 }
715
716 fn clean(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
717 cx.stop_propagation();
718 self.set_selected_index(None, window, cx);
719 cx.emit(SelectEvent::Confirm(None));
720 }
721
722 fn display_title(&mut self, _: &Window, cx: &mut Context<Self>) -> impl IntoElement {
724 let default_title = div()
725 .text_color(cx.theme().accent_foreground)
726 .child(
727 self.options
728 .placeholder
729 .clone()
730 .unwrap_or_else(|| t!("Select.placeholder").into()),
731 )
732 .when(self.options.disabled, |this| {
733 this.text_color(cx.theme().muted_foreground)
734 });
735
736 let Some(selected_index) = &self.selected_index(cx) else {
737 return default_title;
738 };
739
740 let Some(title) = self
741 .list
742 .read(cx)
743 .delegate()
744 .delegate
745 .item(*selected_index)
746 .map(|item| {
747 if let Some(el) = item.display_title() {
748 el
749 } else {
750 if let Some(prefix) = self.options.title_prefix.as_ref() {
751 format!("{}{}", prefix, item.title()).into_any_element()
752 } else {
753 item.title().into_any_element()
754 }
755 }
756 })
757 else {
758 return default_title;
759 };
760
761 div()
762 .when(self.options.disabled, |this| {
763 this.text_color(cx.theme().muted_foreground)
764 })
765 .child(title)
766 }
767}
768
769impl<D> Render for SelectState<D>
770where
771 D: SelectDelegate + 'static,
772{
773 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
774 let searchable = self.searchable;
775 let is_focused = self.focus_handle.is_focused(window);
776 let show_clean = self.options.cleanable && self.selected_index(cx).is_some();
777 let bounds = self.bounds;
778 let allow_open = !(self.open || self.options.disabled);
779 let outline_visible = self.open || is_focused && !self.options.disabled;
780 let popup_radius = cx.theme().radius.min(px(8.));
781
782 self.list
783 .update(cx, |list, cx| list.set_searchable(searchable, cx));
784
785 div()
786 .size_full()
787 .relative()
788 .child(
789 div()
790 .id("input")
791 .relative()
792 .flex()
793 .items_center()
794 .justify_between()
795 .border_1()
796 .border_color(cx.theme().transparent)
797 .when(self.options.appearance, |this| {
798 this.bg(cx.theme().background)
799 .border_color(cx.theme().input)
800 .rounded(cx.theme().radius)
801 .when(cx.theme().shadow, |this| this.shadow_xs())
802 })
803 .map(|this| {
804 if self.options.disabled {
805 this.shadow_none()
806 } else {
807 this
808 }
809 })
810 .overflow_hidden()
811 .input_size(self.options.size)
812 .input_text_size(self.options.size)
813 .refine_style(&self.options.style)
814 .when(outline_visible, |this| this.focused_border(cx))
815 .when(allow_open, |this| {
816 this.on_click(cx.listener(Self::toggle_menu))
817 })
818 .child(
819 h_flex()
820 .id("inner")
821 .w_full()
822 .items_center()
823 .justify_between()
824 .gap_1()
825 .child(
826 div()
827 .id("title")
828 .w_full()
829 .overflow_hidden()
830 .whitespace_nowrap()
831 .truncate()
832 .child(self.display_title(window, cx)),
833 )
834 .when(show_clean, |this| {
835 this.child(clear_button(cx).map(|this| {
836 if self.options.disabled {
837 this.disabled(true)
838 } else {
839 this.on_click(cx.listener(Self::clean))
840 }
841 }))
842 })
843 .when(!show_clean, |this| {
844 let icon = match self.options.icon.clone() {
845 Some(icon) => icon,
846 None => Icon::new(IconName::ChevronDown),
847 };
848
849 this.child(icon.xsmall().text_color(match self.options.disabled {
850 true => cx.theme().muted_foreground.opacity(0.5),
851 false => cx.theme().muted_foreground,
852 }))
853 }),
854 )
855 .child(
856 canvas(
857 {
858 let state = cx.entity();
859 move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds)
860 },
861 |_, _, _, _| {},
862 )
863 .absolute()
864 .size_full(),
865 ),
866 )
867 .when(self.open, |this| {
868 this.child(
869 deferred(
870 anchored().snap_to_window_with_margin(px(8.)).child(
871 div()
872 .occlude()
873 .map(|this| match self.options.menu_width {
874 Length::Auto => this.w(bounds.size.width + px(2.)),
875 Length::Definite(w) => this.w(w),
876 })
877 .child(
878 v_flex()
879 .occlude()
880 .mt_1p5()
881 .bg(cx.theme().background)
882 .border_1()
883 .border_color(cx.theme().border)
884 .rounded(popup_radius)
885 .shadow_md()
886 .child(
887 List::new(&self.list)
888 .when_some(
889 self.options.search_placeholder.clone(),
890 |this, placeholder| {
891 this.search_placeholder(placeholder)
892 },
893 )
894 .with_size(self.options.size)
895 .max_h(rems(20.))
896 .paddings(Edges::all(px(4.))),
897 ),
898 )
899 .on_mouse_down_out(cx.listener(|this, _, window, cx| {
900 this.escape(&Cancel, window, cx);
901 })),
902 ),
903 )
904 .with_priority(1),
905 )
906 })
907 }
908}
909
910impl<D> Select<D>
911where
912 D: SelectDelegate + 'static,
913{
914 pub fn new(state: &Entity<SelectState<D>>) -> Self {
915 Self {
916 id: ("select", state.entity_id()).into(),
917 state: state.clone(),
918 options: SelectOptions::default(),
919 }
920 }
921
922 pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
924 self.options.menu_width = width.into();
925 self
926 }
927
928 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
930 self.options.placeholder = Some(placeholder.into());
931 self
932 }
933
934 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
936 self.options.icon = Some(icon.into());
937 self
938 }
939
940 pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
946 self.options.title_prefix = Some(prefix.into());
947 self
948 }
949
950 pub fn cleanable(mut self, cleanable: bool) -> Self {
952 self.options.cleanable = cleanable;
953 self
954 }
955
956 pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
958 self.options.search_placeholder = Some(placeholder.into());
959 self
960 }
961
962 pub fn disabled(mut self, disabled: bool) -> Self {
964 self.options.disabled = disabled;
965 self
966 }
967
968 pub fn empty(mut self, el: impl IntoElement) -> Self {
970 self.options.empty = Some(el.into_any_element());
971 self
972 }
973
974 pub fn appearance(mut self, appearance: bool) -> Self {
976 self.options.appearance = appearance;
977 self
978 }
979}
980
981impl<D> Sizable for Select<D>
982where
983 D: SelectDelegate + 'static,
984{
985 fn with_size(mut self, size: impl Into<Size>) -> Self {
986 self.options.size = size.into();
987 self
988 }
989}
990
991impl<D> EventEmitter<SelectEvent<D>> for SelectState<D> where D: SelectDelegate + 'static {}
992impl<D> EventEmitter<DismissEvent> for SelectState<D> where D: SelectDelegate + 'static {}
993impl<D> Focusable for SelectState<D>
994where
995 D: SelectDelegate,
996{
997 fn focus_handle(&self, cx: &App) -> FocusHandle {
998 if self.open {
999 self.list.focus_handle(cx)
1000 } else {
1001 self.focus_handle.clone()
1002 }
1003 }
1004}
1005
1006impl<D> Styled for Select<D>
1007where
1008 D: SelectDelegate,
1009{
1010 fn style(&mut self) -> &mut StyleRefinement {
1011 &mut self.options.style
1012 }
1013}
1014
1015impl<D> RenderOnce for Select<D>
1016where
1017 D: SelectDelegate + 'static,
1018{
1019 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1020 let disabled = self.options.disabled;
1021 let focus_handle = self.state.focus_handle(cx);
1022 self.state.update(cx, |this, _| {
1024 this.options = self.options;
1025 });
1026
1027 div()
1028 .id(self.id.clone())
1029 .key_context(CONTEXT)
1030 .when(!disabled, |this| {
1031 this.track_focus(&focus_handle.tab_stop(true))
1032 })
1033 .on_action(window.listener_for(&self.state, SelectState::up))
1034 .on_action(window.listener_for(&self.state, SelectState::down))
1035 .on_action(window.listener_for(&self.state, SelectState::enter))
1036 .on_action(window.listener_for(&self.state, SelectState::escape))
1037 .size_full()
1038 .child(self.state)
1039 }
1040}
1041
1042#[derive(IntoElement)]
1043struct SelectListItem {
1044 id: ElementId,
1045 size: Size,
1046 style: StyleRefinement,
1047 selected: bool,
1048 disabled: bool,
1049 children: Vec<AnyElement>,
1050}
1051
1052impl SelectListItem {
1053 pub fn new(ix: usize) -> Self {
1054 Self {
1055 id: ("select-item", ix).into(),
1056 size: Size::default(),
1057 style: StyleRefinement::default(),
1058 selected: false,
1059 disabled: false,
1060 children: Vec::new(),
1061 }
1062 }
1063}
1064
1065impl ParentElement for SelectListItem {
1066 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1067 self.children.extend(elements);
1068 }
1069}
1070
1071impl Disableable for SelectListItem {
1072 fn disabled(mut self, disabled: bool) -> Self {
1073 self.disabled = disabled;
1074 self
1075 }
1076}
1077
1078impl Selectable for SelectListItem {
1079 fn selected(mut self, selected: bool) -> Self {
1080 self.selected = selected;
1081 self
1082 }
1083
1084 fn is_selected(&self) -> bool {
1085 self.selected
1086 }
1087}
1088
1089impl Sizable for SelectListItem {
1090 fn with_size(mut self, size: impl Into<Size>) -> Self {
1091 self.size = size.into();
1092 self
1093 }
1094}
1095
1096impl Styled for SelectListItem {
1097 fn style(&mut self) -> &mut StyleRefinement {
1098 &mut self.style
1099 }
1100}
1101
1102impl RenderOnce for SelectListItem {
1103 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
1104 h_flex()
1105 .id(self.id)
1106 .relative()
1107 .gap_x_1()
1108 .py_1()
1109 .px_2()
1110 .rounded(cx.theme().radius)
1111 .text_base()
1112 .text_color(cx.theme().foreground)
1113 .relative()
1114 .items_center()
1115 .justify_between()
1116 .input_text_size(self.size)
1117 .list_size(self.size)
1118 .refine_style(&self.style)
1119 .when(!self.disabled, |this| {
1120 this.when(!self.selected, |this| {
1121 this.hover(|this| this.bg(cx.theme().accent.alpha(0.7)))
1122 })
1123 })
1124 .when(self.selected, |this| this.bg(cx.theme().accent))
1125 .when(self.disabled, |this| {
1126 this.text_color(cx.theme().muted_foreground)
1127 })
1128 .child(
1129 h_flex()
1130 .w_full()
1131 .items_center()
1132 .justify_between()
1133 .gap_x_1()
1134 .child(div().w_full().children(self.children)),
1135 )
1136 }
1137}