1use fs::Fs;
2use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
3use gpui::{
4 App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity,
5 Window,
6};
7use picker::{Picker, PickerDelegate};
8use settings::{Settings as _, SettingsStore, update_settings_file};
9use std::sync::Arc;
10use theme::{Appearance, IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
11use ui::{ListItem, ListItemSpacing, prelude::*, v_flex};
12use util::ResultExt;
13use workspace::{ModalView, ui::HighlightedLabel};
14use zed_actions::{ExtensionCategoryFilter, Extensions};
15
16pub(crate) struct IconThemeSelector {
17 picker: Entity<Picker<IconThemeSelectorDelegate>>,
18}
19
20impl EventEmitter<DismissEvent> for IconThemeSelector {}
21
22impl Focusable for IconThemeSelector {
23 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
24 self.picker.focus_handle(cx)
25 }
26}
27
28impl ModalView for IconThemeSelector {}
29
30impl IconThemeSelector {
31 pub fn new(
32 delegate: IconThemeSelectorDelegate,
33 window: &mut Window,
34 cx: &mut Context<Self>,
35 ) -> Self {
36 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
37 Self { picker }
38 }
39}
40
41impl Render for IconThemeSelector {
42 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
43 v_flex().w(rems(34.)).child(self.picker.clone())
44 }
45}
46
47pub(crate) struct IconThemeSelectorDelegate {
48 fs: Arc<dyn Fs>,
49 themes: Vec<ThemeMeta>,
50 matches: Vec<StringMatch>,
51 original_theme: Arc<IconTheme>,
52 selection_completed: bool,
53 selected_theme: Option<Arc<IconTheme>>,
54 selected_index: usize,
55 selector: WeakEntity<IconThemeSelector>,
56}
57
58impl IconThemeSelectorDelegate {
59 pub fn new(
60 selector: WeakEntity<IconThemeSelector>,
61 fs: Arc<dyn Fs>,
62 themes_filter: Option<&Vec<String>>,
63 cx: &mut Context<IconThemeSelector>,
64 ) -> Self {
65 let theme_settings = ThemeSettings::get_global(cx);
66 let original_theme = theme_settings.active_icon_theme.clone();
67
68 let registry = ThemeRegistry::global(cx);
69 let mut themes = registry
70 .list_icon_themes()
71 .into_iter()
72 .filter(|meta| {
73 if let Some(theme_filter) = themes_filter {
74 theme_filter.contains(&meta.name.to_string())
75 } else {
76 true
77 }
78 })
79 .collect::<Vec<_>>();
80
81 themes.sort_unstable_by(|a, b| {
82 a.appearance
83 .is_light()
84 .cmp(&b.appearance.is_light())
85 .then(a.name.cmp(&b.name))
86 });
87 let matches = themes
88 .iter()
89 .map(|meta| StringMatch {
90 candidate_id: 0,
91 score: 0.0,
92 positions: Default::default(),
93 string: meta.name.to_string(),
94 })
95 .collect();
96 let mut this = Self {
97 fs,
98 themes,
99 matches,
100 original_theme: original_theme.clone(),
101 selected_index: 0,
102 selected_theme: None,
103 selection_completed: false,
104 selector,
105 };
106
107 this.select_if_matching(&original_theme.name);
108 this
109 }
110
111 fn show_selected_theme(
112 &mut self,
113 cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
114 ) -> Option<Arc<IconTheme>> {
115 if let Some(mat) = self.matches.get(self.selected_index) {
116 let registry = ThemeRegistry::global(cx);
117 match registry.get_icon_theme(&mat.string) {
118 Ok(theme) => {
119 Self::set_icon_theme(theme.clone(), cx);
120 Some(theme)
121 }
122 Err(err) => {
123 log::error!("error loading icon theme {}: {err}", mat.string);
124 None
125 }
126 }
127 } else {
128 None
129 }
130 }
131
132 fn select_if_matching(&mut self, theme_name: &str) {
133 self.selected_index = self
134 .matches
135 .iter()
136 .position(|mat| mat.string == theme_name)
137 .unwrap_or(self.selected_index);
138 }
139
140 fn set_icon_theme(theme: Arc<IconTheme>, cx: &mut App) {
141 SettingsStore::update_global(cx, |store, cx| {
142 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
143 theme_settings.active_icon_theme = theme;
144 store.override_global(theme_settings);
145 cx.refresh_windows();
146 });
147 }
148}
149
150impl PickerDelegate for IconThemeSelectorDelegate {
151 type ListItem = ui::ListItem;
152
153 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
154 "Select Icon Theme...".into()
155 }
156
157 fn match_count(&self) -> usize {
158 self.matches.len()
159 }
160
161 fn confirm(
162 &mut self,
163 _: bool,
164 window: &mut Window,
165 cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
166 ) {
167 self.selection_completed = true;
168
169 let theme_settings = ThemeSettings::get_global(cx);
170 let theme_name = theme_settings.active_icon_theme.name.clone();
171
172 telemetry::event!(
173 "Settings Changed",
174 setting = "icon_theme",
175 value = theme_name
176 );
177
178 let appearance = Appearance::from(window.appearance());
179
180 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
181 settings.set_icon_theme(theme_name.to_string(), appearance);
182 });
183
184 self.selector
185 .update(cx, |_, cx| {
186 cx.emit(DismissEvent);
187 })
188 .ok();
189 }
190
191 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
192 if !self.selection_completed {
193 Self::set_icon_theme(self.original_theme.clone(), cx);
194 self.selection_completed = true;
195 }
196
197 self.selector
198 .update(cx, |_, cx| cx.emit(DismissEvent))
199 .log_err();
200 }
201
202 fn selected_index(&self) -> usize {
203 self.selected_index
204 }
205
206 fn set_selected_index(
207 &mut self,
208 ix: usize,
209 _: &mut Window,
210 cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
211 ) {
212 self.selected_index = ix;
213 self.selected_theme = self.show_selected_theme(cx);
214 }
215
216 fn update_matches(
217 &mut self,
218 query: String,
219 window: &mut Window,
220 cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
221 ) -> gpui::Task<()> {
222 let background = cx.background_executor().clone();
223 let candidates = self
224 .themes
225 .iter()
226 .enumerate()
227 .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
228 .collect::<Vec<_>>();
229
230 cx.spawn_in(window, async move |this, cx| {
231 let matches = if query.is_empty() {
232 candidates
233 .into_iter()
234 .enumerate()
235 .map(|(index, candidate)| StringMatch {
236 candidate_id: index,
237 string: candidate.string,
238 positions: Vec::new(),
239 score: 0.0,
240 })
241 .collect()
242 } else {
243 match_strings(
244 &candidates,
245 &query,
246 false,
247 true,
248 100,
249 &Default::default(),
250 background,
251 )
252 .await
253 };
254
255 this.update(cx, |this, cx| {
256 this.delegate.matches = matches;
257 if query.is_empty() && this.delegate.selected_theme.is_none() {
258 this.delegate.selected_index = this
259 .delegate
260 .selected_index
261 .min(this.delegate.matches.len().saturating_sub(1));
262 } else if let Some(selected) = this.delegate.selected_theme.as_ref() {
263 this.delegate.selected_index = this
264 .delegate
265 .matches
266 .iter()
267 .enumerate()
268 .find(|(_, mtch)| mtch.string == selected.name)
269 .map(|(ix, _)| ix)
270 .unwrap_or_default();
271 } else {
272 this.delegate.selected_index = 0;
273 }
274 this.delegate.selected_theme = this.delegate.show_selected_theme(cx);
275 })
276 .log_err();
277 })
278 }
279
280 fn render_match(
281 &self,
282 ix: usize,
283 selected: bool,
284 _window: &mut Window,
285 _cx: &mut Context<Picker<Self>>,
286 ) -> Option<Self::ListItem> {
287 let theme_match = &self.matches[ix];
288
289 Some(
290 ListItem::new(ix)
291 .inset(true)
292 .spacing(ListItemSpacing::Sparse)
293 .toggle_state(selected)
294 .child(HighlightedLabel::new(
295 theme_match.string.clone(),
296 theme_match.positions.clone(),
297 )),
298 )
299 }
300
301 fn render_footer(
302 &self,
303 _window: &mut Window,
304 cx: &mut Context<Picker<Self>>,
305 ) -> Option<gpui::AnyElement> {
306 Some(
307 h_flex()
308 .p_2()
309 .w_full()
310 .justify_between()
311 .gap_2()
312 .border_t_1()
313 .border_color(cx.theme().colors().border_variant)
314 .child(
315 Button::new("docs", "View Icon Theme Docs")
316 .icon(IconName::ArrowUpRight)
317 .icon_position(IconPosition::End)
318 .icon_size(IconSize::XSmall)
319 .icon_color(Color::Muted)
320 .on_click(|_event, _window, cx| {
321 cx.open_url("https://zed.dev/docs/icon-themes");
322 }),
323 )
324 .child(
325 Button::new("more-icon-themes", "Install Icon Themes").on_click(
326 move |_event, window, cx| {
327 window.dispatch_action(
328 Box::new(Extensions {
329 category_filter: Some(ExtensionCategoryFilter::IconThemes),
330 id: None,
331 }),
332 cx,
333 );
334 },
335 ),
336 )
337 .into_any_element(),
338 )
339 }
340}