1use std::sync::Arc;
2
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
5use picker::{Picker, PickerDelegate};
6use theme::ThemeRegistry;
7use ui::{ListItem, ListItemSpacing, prelude::*};
8
9type ThemePicker = Picker<ThemePickerDelegate>;
10
11pub struct ThemePickerDelegate {
12 themes: Vec<SharedString>,
13 filtered_themes: Vec<StringMatch>,
14 selected_index: usize,
15 current_theme: SharedString,
16 on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
17}
18
19impl ThemePickerDelegate {
20 fn new(
21 current_theme: SharedString,
22 on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
23 cx: &mut Context<ThemePicker>,
24 ) -> Self {
25 let theme_registry = ThemeRegistry::global(cx);
26
27 let themes = theme_registry.list_names();
28 let selected_index = themes
29 .iter()
30 .position(|theme| *theme == current_theme)
31 .unwrap_or(0);
32
33 let filtered_themes = themes
34 .iter()
35 .enumerate()
36 .map(|(index, theme)| StringMatch {
37 candidate_id: index,
38 string: theme.to_string(),
39 positions: Vec::new(),
40 score: 0.0,
41 })
42 .collect();
43
44 Self {
45 themes,
46 filtered_themes,
47 selected_index,
48 current_theme,
49 on_theme_changed: Arc::new(on_theme_changed),
50 }
51 }
52}
53
54impl PickerDelegate for ThemePickerDelegate {
55 type ListItem = AnyElement;
56
57 fn match_count(&self) -> usize {
58 self.filtered_themes.len()
59 }
60
61 fn selected_index(&self) -> usize {
62 self.selected_index
63 }
64
65 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<ThemePicker>) {
66 self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
67 cx.notify();
68 }
69
70 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
71 "Search theme…".into()
72 }
73
74 fn update_matches(
75 &mut self,
76 query: String,
77 _window: &mut Window,
78 cx: &mut Context<ThemePicker>,
79 ) -> Task<()> {
80 let themes = self.themes.clone();
81 let current_theme = self.current_theme.clone();
82
83 let matches: Vec<StringMatch> = if query.is_empty() {
84 themes
85 .iter()
86 .enumerate()
87 .map(|(index, theme)| StringMatch {
88 candidate_id: index,
89 string: theme.to_string(),
90 positions: Vec::new(),
91 score: 0.0,
92 })
93 .collect()
94 } else {
95 let _candidates: Vec<StringMatchCandidate> = themes
96 .iter()
97 .enumerate()
98 .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
99 .collect();
100
101 themes
102 .iter()
103 .enumerate()
104 .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
105 .map(|(index, theme)| StringMatch {
106 candidate_id: index,
107 string: theme.to_string(),
108 positions: Vec::new(),
109 score: 0.0,
110 })
111 .collect()
112 };
113
114 let selected_index = if query.is_empty() {
115 themes
116 .iter()
117 .position(|theme| *theme == current_theme)
118 .unwrap_or(0)
119 } else {
120 matches
121 .iter()
122 .position(|m| themes[m.candidate_id] == current_theme)
123 .unwrap_or(0)
124 };
125
126 self.filtered_themes = matches;
127 self.selected_index = selected_index;
128 cx.notify();
129
130 Task::ready(())
131 }
132
133 fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<ThemePicker>) {
134 if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
135 let theme = theme_match.string.clone();
136 (self.on_theme_changed)(theme.into(), cx);
137 }
138 }
139
140 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<ThemePicker>) {
141 cx.defer_in(window, |picker, window, cx| {
142 picker.set_query("", window, cx);
143 });
144 cx.emit(DismissEvent);
145 }
146
147 fn render_match(
148 &self,
149 ix: usize,
150 selected: bool,
151 _window: &mut Window,
152 _cx: &mut Context<ThemePicker>,
153 ) -> Option<Self::ListItem> {
154 let theme_match = self.filtered_themes.get(ix)?;
155
156 Some(
157 ListItem::new(ix)
158 .inset(true)
159 .spacing(ListItemSpacing::Sparse)
160 .toggle_state(selected)
161 .child(Label::new(theme_match.string.clone()))
162 .into_any_element(),
163 )
164 }
165}
166
167pub fn theme_picker(
168 current_theme: SharedString,
169 on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
170 window: &mut Window,
171 cx: &mut Context<ThemePicker>,
172) -> ThemePicker {
173 let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx);
174
175 Picker::uniform_list(delegate, window, cx)
176 .show_scrollbar(true)
177 .width(rems_from_px(210.))
178 .max_height(Some(rems(18.).into()))
179}