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 SelectItem(usize),
23 ConfirmItem(usize),
25 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
44pub trait DropdownItem: Clone {
46 type Value: Clone;
47 fn title(&self) -> SharedString;
48 fn display_title(&self) -> Option<AnyElement> {
52 None
53 }
54 fn value(&self) -> &Self::Value;
55 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 fn sections_count(&self, _: &App) -> usize {
90 1
91 }
92
93 fn section(&self, _section: usize) -> Option<AnyElement> {
95 return None;
96 }
97
98 fn items_count(&self, section: usize) -> usize;
100
101 fn item(&self, ix: IndexPath) -> Option<&Self::Item>;
103
104 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
277pub 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 bounds: Bounds<Pixels>,
285 open: bool,
286 selected_value: Option<<D::Item as DropdownItem>::Value>,
287 _subscriptions: Vec<Subscription>,
288}
289
290#[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#[derive(Debug, Clone)]
447pub struct DropdownItemGroup<I: DropdownItem> {
448 pub title: SharedString,
449 pub items: Vec<I>,
450}
451
452impl<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 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 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 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 pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
703 self.menu_width = width.into();
704 self
705 }
706
707 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
709 self.placeholder = Some(placeholder.into());
710 self
711 }
712
713 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
715 self.icon = Some(icon.into());
716 self
717 }
718
719 pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
725 self.title_prefix = Some(prefix.into());
726 self
727 }
728
729 pub fn cleanable(mut self) -> Self {
731 self.cleanable = true;
732 self
733 }
734
735 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 pub fn appearance(mut self, appearance: bool) -> Self {
748 self.appearance = appearance;
749 self
750 }
751
752 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 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}