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