1use feature_flags::FeatureFlagAppExt;
2use fs::Fs;
3use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
4use gpui::{
5 actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString,
6 View, ViewContext, VisualContext, WeakView,
7};
8use picker::{Picker, PickerDelegate};
9use settings::{update_settings_file, SettingsStore};
10use std::sync::Arc;
11use theme::{Theme, ThemeRegistry, ThemeSettings};
12use ui::{prelude::*, v_stack, ListItem};
13use util::ResultExt;
14use workspace::{ui::HighlightedLabel, Workspace};
15
16actions!(Toggle, Reload);
17
18pub fn init(cx: &mut AppContext) {
19 cx.observe_new_views(
20 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
21 workspace.register_action(toggle);
22 },
23 )
24 .detach();
25}
26
27pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
28 let fs = workspace.app_state().fs.clone();
29 workspace.toggle_modal(cx, |cx| {
30 ThemeSelector::new(
31 ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx),
32 cx,
33 )
34 });
35}
36
37#[cfg(debug_assertions)]
38pub fn reload(cx: &mut AppContext) {
39 let current_theme_name = cx.theme().name.clone();
40 let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
41 registry.clear();
42 registry.get(¤t_theme_name)
43 });
44 match current_theme {
45 Ok(theme) => {
46 ThemeSelectorDelegate::set_theme(theme, cx);
47 log::info!("reloaded theme {}", current_theme_name);
48 }
49 Err(error) => {
50 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
51 }
52 }
53}
54
55pub struct ThemeSelector {
56 picker: View<Picker<ThemeSelectorDelegate>>,
57}
58
59impl EventEmitter<DismissEvent> for ThemeSelector {}
60
61impl FocusableView for ThemeSelector {
62 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
63 self.picker.focus_handle(cx)
64 }
65}
66
67impl Render for ThemeSelector {
68 type Element = Div;
69
70 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
71 v_stack().min_w_96().child(self.picker.clone())
72 }
73}
74
75impl ThemeSelector {
76 pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
77 let picker = cx.build_view(|cx| Picker::new(delegate, cx));
78 Self { picker }
79 }
80}
81
82pub struct ThemeSelectorDelegate {
83 fs: Arc<dyn Fs>,
84 theme_names: Vec<SharedString>,
85 matches: Vec<StringMatch>,
86 original_theme: Arc<Theme>,
87 selection_completed: bool,
88 selected_index: usize,
89 view: WeakView<ThemeSelector>,
90}
91
92impl ThemeSelectorDelegate {
93 fn new(
94 weak_view: WeakView<ThemeSelector>,
95 fs: Arc<dyn Fs>,
96 cx: &mut ViewContext<ThemeSelector>,
97 ) -> Self {
98 let original_theme = cx.theme().clone();
99
100 let staff_mode = cx.is_staff();
101 let registry = cx.global::<ThemeRegistry>();
102 let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
103 //todo!(theme sorting)
104 // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
105 let matches = theme_names
106 .iter()
107 .map(|meta| StringMatch {
108 candidate_id: 0,
109 score: 0.0,
110 positions: Default::default(),
111 string: meta.to_string(),
112 })
113 .collect();
114 let mut this = Self {
115 fs,
116 theme_names,
117 matches,
118 original_theme: original_theme.clone(),
119 selected_index: 0,
120 selection_completed: false,
121 view: weak_view,
122 };
123 this.select_if_matching(&original_theme.name);
124 this
125 }
126
127 fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
128 if let Some(mat) = self.matches.get(self.selected_index) {
129 let registry = cx.global::<ThemeRegistry>();
130 match registry.get(&mat.string) {
131 Ok(theme) => {
132 Self::set_theme(theme, cx);
133 }
134 Err(error) => {
135 log::error!("error loading theme {}: {}", mat.string, error)
136 }
137 }
138 }
139 }
140
141 fn select_if_matching(&mut self, theme_name: &str) {
142 self.selected_index = self
143 .matches
144 .iter()
145 .position(|mat| mat.string == theme_name)
146 .unwrap_or(self.selected_index);
147 }
148
149 fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
150 cx.update_global(|store: &mut SettingsStore, cx| {
151 let mut theme_settings = store.get::<ThemeSettings>(None).clone();
152 theme_settings.active_theme = theme;
153 store.override_global(theme_settings);
154 cx.refresh();
155 });
156 }
157}
158
159impl PickerDelegate for ThemeSelectorDelegate {
160 type ListItem = ui::ListItem;
161
162 fn placeholder_text(&self) -> Arc<str> {
163 "Select Theme...".into()
164 }
165
166 fn match_count(&self) -> usize {
167 self.matches.len()
168 }
169
170 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
171 self.selection_completed = true;
172
173 let theme_name = cx.theme().name.clone();
174 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
175 settings.theme = Some(theme_name.to_string());
176 });
177
178 self.view
179 .update(cx, |_, cx| {
180 cx.emit(DismissEvent);
181 })
182 .ok();
183 }
184
185 fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
186 if !self.selection_completed {
187 Self::set_theme(self.original_theme.clone(), cx);
188 self.selection_completed = true;
189 }
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 .theme_names
213 .iter()
214 .enumerate()
215 .map(|(id, meta)| StringMatchCandidate {
216 id,
217 char_bag: meta.as_ref().into(),
218 string: meta.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 .selected(selected)
270 .child(HighlightedLabel::new(
271 theme_match.string.clone(),
272 theme_match.positions.clone(),
273 )),
274 )
275 }
276}