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