1use std::rc::Rc;
2
3use gpui::{App, ElementId, IntoElement, RenderOnce};
4use heck::ToTitleCase as _;
5use ui::{
6 ButtonSize, ContextMenu, DropdownMenu, DropdownStyle, FluentBuilder as _, IconPosition, px,
7};
8
9#[derive(IntoElement)]
10pub struct EnumVariantDropdown<T>
11where
12 T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
13{
14 id: ElementId,
15 current_value: T,
16 variants: &'static [T],
17 labels: &'static [&'static str],
18 should_do_title_case: bool,
19 tab_index: Option<isize>,
20 on_change: Rc<dyn Fn(T, &mut App) + 'static>,
21}
22
23impl<T> EnumVariantDropdown<T>
24where
25 T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
26{
27 pub fn new(
28 id: impl Into<ElementId>,
29 current_value: T,
30 variants: &'static [T],
31 labels: &'static [&'static str],
32 on_change: impl Fn(T, &mut App) + 'static,
33 ) -> Self {
34 Self {
35 id: id.into(),
36 current_value,
37 variants,
38 labels,
39 should_do_title_case: true,
40 tab_index: None,
41 on_change: Rc::new(on_change),
42 }
43 }
44
45 pub fn title_case(mut self, title_case: bool) -> Self {
46 self.should_do_title_case = title_case;
47 self
48 }
49
50 pub fn tab_index(mut self, tab_index: isize) -> Self {
51 self.tab_index = Some(tab_index);
52 self
53 }
54}
55
56impl<T> RenderOnce for EnumVariantDropdown<T>
57where
58 T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
59{
60 fn render(self, window: &mut ui::Window, cx: &mut ui::App) -> impl gpui::IntoElement {
61 let current_value_label = self.labels[self
62 .variants
63 .iter()
64 .position(|v| *v == self.current_value)
65 .unwrap()];
66
67 let context_menu = window.use_keyed_state(current_value_label, cx, |window, cx| {
68 ContextMenu::new(window, cx, move |mut menu, _, _| {
69 for (&value, &label) in std::iter::zip(self.variants, self.labels) {
70 let on_change = self.on_change.clone();
71 let current_value = self.current_value;
72 menu = menu.toggleable_entry(
73 if self.should_do_title_case {
74 label.to_title_case()
75 } else {
76 label.to_string()
77 },
78 value == current_value,
79 IconPosition::End,
80 None,
81 move |_, cx| {
82 on_change(value, cx);
83 },
84 );
85 }
86 menu
87 })
88 });
89
90 DropdownMenu::new(
91 self.id,
92 if self.should_do_title_case {
93 current_value_label.to_title_case()
94 } else {
95 current_value_label.to_string()
96 },
97 context_menu,
98 )
99 .when_some(self.tab_index, |elem, tab_index| elem.tab_index(tab_index))
100 .trigger_size(ButtonSize::Medium)
101 .style(DropdownStyle::Outlined)
102 .offset(gpui::Point {
103 x: px(0.0),
104 y: px(2.0),
105 })
106 .into_any_element()
107 }
108}