gpui_component/
dropdown.rs

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