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 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", 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
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 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
283pub 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 bounds: Bounds<Pixels>,
291 open: bool,
292 selected_value: Option<<D::Item as DropdownItem>::Value>,
293 _subscriptions: Vec<Subscription>,
294}
295
296#[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#[derive(Debug, Clone)]
453pub struct DropdownItemGroup<I: DropdownItem> {
454 pub title: SharedString,
455 pub items: Vec<I>,
456}
457
458impl<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 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 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 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 pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
709 self.menu_width = width.into();
710 self
711 }
712
713 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
715 self.placeholder = Some(placeholder.into());
716 self
717 }
718
719 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
721 self.icon = Some(icon.into());
722 self
723 }
724
725 pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
731 self.title_prefix = Some(prefix.into());
732 self
733 }
734
735 pub fn cleanable(mut self) -> Self {
737 self.cleanable = true;
738 self
739 }
740
741 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 pub fn appearance(mut self, appearance: bool) -> Self {
754 self.appearance = appearance;
755 self
756 }
757
758 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 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}