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