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