gpui_component/
select.rs

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
34/// A trait for items that can be displayed in a select.
35pub trait SelectItem: Clone {
36    type Value: Clone;
37    fn title(&self) -> SharedString;
38    /// Customize the display title used to selected item in Select Input.
39    ///
40    /// If return None, the title will be used.
41    fn display_title(&self) -> Option<AnyElement> {
42        None
43    }
44    /// Get the value of the item.
45    fn value(&self) -> &Self::Value;
46    /// Check if the item matches the query for search, default is to match the title.
47    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    /// Returns the number of sections in the [`Select`].
92    fn sections_count(&self, _: &App) -> usize {
93        1
94    }
95
96    /// Returns the section header element for the given section index.
97    fn section(&self, _section: usize) -> Option<AnyElement> {
98        return None;
99    }
100
101    /// Returns the number of items in the given section.
102    fn items_count(&self, section: usize) -> usize;
103
104    /// Returns the item at the given index path (Only section, row will be use).
105    fn item(&self, ix: IndexPath) -> Option<&Self::Item>;
106
107    /// Returns the index of the item with the given value, or None if not found.
108    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        // If the selected index is not the final selected index, we need to restore it.
221        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
296/// Events emitted by the [`SelectState`].
297pub 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
333/// State of the [`Select`].
334pub 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    /// Store the bounds of the input
341    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/// A Select element.
349#[derive(IntoElement)]
350pub struct Select<D: SelectDelegate + 'static> {
351    id: ElementId,
352    state: Entity<SelectState<D>>,
353    options: SelectOptions,
354}
355
356/// A built-in searchable vector for select items.
357#[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/// A group of select items with a title.
498#[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    /// Create a new SelectGroup with the given title.
509    pub fn new(title: impl Into<SharedString>) -> Self {
510        Self {
511            title: title.into(),
512            items: vec![],
513        }
514    }
515
516    /// Add an item to the group.
517    pub fn item(mut self, item: I) -> Self {
518        self.items.push(item);
519        self
520    }
521
522    /// Add multiple items to the group.
523    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    /// Create a new Select state.
539    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    /// Sets whether the dropdown menu is searchable, default is `false`.
579    ///
580    /// When `true`, there will be a search input at the top of the dropdown menu.
581    pub fn searchable(mut self, searchable: bool) -> Self {
582        self.searchable = searchable;
583        self
584    }
585
586    /// Set the selected index for the select.
587    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    /// Set selected value for the select.
601    ///
602    /// This method will to get position from delegate and set selected index.
603    ///
604    /// If the value is not found, the None will be sets.
605    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    /// Set the items for the select state.
619    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    /// Get the selected index of the select.
629    pub fn selected_index(&self, cx: &App) -> Option<IndexPath> {
630        self.list.read(cx).selected_index()
631    }
632
633    /// Get the selected value of the select.
634    pub fn selected_value(&self) -> Option<&<D::Item as SelectItem>::Value> {
635        self.selected_value.as_ref()
636    }
637
638    /// Focus the select input.
639    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        // When the select and dropdown menu are both not focused, close the dropdown menu.
652        if self.list.read(cx).is_focused(window, cx) || self.focus_handle.is_focused(window) {
653            return;
654        }
655
656        // If the selected index is not the final selected index, we need to restore it.
657        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        // Propagate the event to the parent view, for example to the Dialog to support ENTER to confirm.
689        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    /// Returns the title element for the select input.
725    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    /// Set the width of the dropdown menu, default: Length::Auto
931    pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
932        self.options.menu_width = width.into();
933        self
934    }
935
936    /// Set the placeholder for display when select value is empty.
937    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
938        self.options.placeholder = Some(placeholder.into());
939        self
940    }
941
942    /// Set the right icon for the select input, instead of the default arrow icon.
943    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
944        self.options.icon = Some(icon.into());
945        self
946    }
947
948    /// Set title prefix for the select.
949    ///
950    /// e.g.: Country: United States
951    ///
952    /// You should set the label is `Country: `
953    pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
954        self.options.title_prefix = Some(prefix.into());
955        self
956    }
957
958    /// Set whether to show the clear button when the input field is not empty, default is false.
959    pub fn cleanable(mut self, cleanable: bool) -> Self {
960        self.options.cleanable = cleanable;
961        self
962    }
963
964    /// Sets the placeholder text for the search input.
965    pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
966        self.options.search_placeholder = Some(placeholder.into());
967        self
968    }
969
970    /// Set the disable state for the select.
971    pub fn disabled(mut self, disabled: bool) -> Self {
972        self.options.disabled = disabled;
973        self
974    }
975
976    /// Set the element to display when the select list is empty.
977    pub fn empty(mut self, el: impl IntoElement) -> Self {
978        self.options.empty = Some(el.into_any_element());
979        self
980    }
981
982    /// Set the appearance of the select, if false the select input will no border, background.
983    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        // If the size has change, set size to self.list, to change the QueryInput size.
1031        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}