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