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