1use client::telemetry::Telemetry;
2use feature_flags::FeatureFlagAppExt;
3use fs::Fs;
4use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
5use gpui::{
6 actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, View, ViewContext,
7 VisualContext, WeakView,
8};
9use picker::{Picker, PickerDelegate};
10use settings::{update_settings_file, SettingsStore};
11use std::sync::Arc;
12use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
13use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
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
39impl ModalView for ThemeSelector {}
40
41pub struct ThemeSelector {
42 picker: View<Picker<ThemeSelectorDelegate>>,
43}
44
45impl EventEmitter<DismissEvent> for ThemeSelector {}
46
47impl FocusableView for ThemeSelector {
48 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
49 self.picker.focus_handle(cx)
50 }
51}
52
53impl Render for ThemeSelector {
54 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
55 v_flex().w(rems(34.)).child(self.picker.clone())
56 }
57}
58
59impl ThemeSelector {
60 pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
61 let picker = cx.new_view(|cx| Picker::new(delegate, cx));
62 Self { picker }
63 }
64}
65
66pub struct ThemeSelectorDelegate {
67 fs: Arc<dyn Fs>,
68 themes: Vec<ThemeMeta>,
69 matches: Vec<StringMatch>,
70 original_theme: Arc<Theme>,
71 selection_completed: bool,
72 selected_index: usize,
73 telemetry: Arc<Telemetry>,
74 view: WeakView<ThemeSelector>,
75}
76
77impl ThemeSelectorDelegate {
78 fn new(
79 weak_view: WeakView<ThemeSelector>,
80 fs: Arc<dyn Fs>,
81 telemetry: Arc<Telemetry>,
82 cx: &mut ViewContext<ThemeSelector>,
83 ) -> Self {
84 let original_theme = cx.theme().clone();
85
86 let staff_mode = cx.is_staff();
87 let registry = ThemeRegistry::global(cx);
88 let mut themes = registry.list(staff_mode);
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 selection_completed: false,
111 telemetry,
112 view: weak_view,
113 };
114 this.select_if_matching(&original_theme.name);
115 this
116 }
117
118 fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
119 if let Some(mat) = self.matches.get(self.selected_index) {
120 let registry = ThemeRegistry::global(cx);
121 match registry.get(&mat.string) {
122 Ok(theme) => {
123 Self::set_theme(theme, cx);
124 }
125 Err(error) => {
126 log::error!("error loading theme {}: {}", mat.string, error)
127 }
128 }
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_theme(theme: Arc<Theme>, cx: &mut AppContext) {
141 cx.update_global(|store: &mut SettingsStore, cx| {
142 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
143 theme_settings.active_theme = theme;
144 theme_settings.apply_theme_overrides();
145 store.override_global(theme_settings);
146 cx.refresh();
147 });
148 }
149}
150
151impl PickerDelegate for ThemeSelectorDelegate {
152 type ListItem = ui::ListItem;
153
154 fn placeholder_text(&self) -> Arc<str> {
155 "Select Theme...".into()
156 }
157
158 fn match_count(&self) -> usize {
159 self.matches.len()
160 }
161
162 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
163 self.selection_completed = true;
164
165 let theme_name = cx.theme().name.clone();
166
167 self.telemetry
168 .report_setting_event("theme", theme_name.to_string());
169
170 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
171 settings.theme = Some(theme_name.to_string());
172 });
173
174 self.view
175 .update(cx, |_, cx| {
176 cx.emit(DismissEvent);
177 })
178 .ok();
179 }
180
181 fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
182 if !self.selection_completed {
183 Self::set_theme(self.original_theme.clone(), cx);
184 self.selection_completed = true;
185 }
186
187 self.view
188 .update(cx, |_, cx| cx.emit(DismissEvent))
189 .log_err();
190 }
191
192 fn selected_index(&self) -> usize {
193 self.selected_index
194 }
195
196 fn set_selected_index(
197 &mut self,
198 ix: usize,
199 cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
200 ) {
201 self.selected_index = ix;
202 self.show_selected_theme(cx);
203 }
204
205 fn update_matches(
206 &mut self,
207 query: String,
208 cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
209 ) -> gpui::Task<()> {
210 let background = cx.background_executor().clone();
211 let candidates = self
212 .themes
213 .iter()
214 .enumerate()
215 .map(|(id, meta)| StringMatchCandidate {
216 id,
217 char_bag: meta.name.as_ref().into(),
218 string: meta.name.to_string(),
219 })
220 .collect::<Vec<_>>();
221
222 cx.spawn(|this, mut cx| async move {
223 let matches = if query.is_empty() {
224 candidates
225 .into_iter()
226 .enumerate()
227 .map(|(index, candidate)| StringMatch {
228 candidate_id: index,
229 string: candidate.string,
230 positions: Vec::new(),
231 score: 0.0,
232 })
233 .collect()
234 } else {
235 match_strings(
236 &candidates,
237 &query,
238 false,
239 100,
240 &Default::default(),
241 background,
242 )
243 .await
244 };
245
246 this.update(&mut cx, |this, cx| {
247 this.delegate.matches = matches;
248 this.delegate.selected_index = this
249 .delegate
250 .selected_index
251 .min(this.delegate.matches.len().saturating_sub(1));
252 this.delegate.show_selected_theme(cx);
253 })
254 .log_err();
255 })
256 }
257
258 fn render_match(
259 &self,
260 ix: usize,
261 selected: bool,
262 _cx: &mut ViewContext<Picker<Self>>,
263 ) -> Option<Self::ListItem> {
264 let theme_match = &self.matches[ix];
265
266 Some(
267 ListItem::new(ix)
268 .inset(true)
269 .spacing(ListItemSpacing::Sparse)
270 .selected(selected)
271 .child(HighlightedLabel::new(
272 theme_match.string.clone(),
273 theme_match.positions.clone(),
274 )),
275 )
276 }
277}