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