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