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