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