1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
2use gpui::{
3 actions, elements::*, AnyViewHandle, AppContext, Element, ElementBox, Entity, MouseState,
4 MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
5};
6use picker::{Picker, PickerDelegate};
7use settings::{settings_file::SettingsFile, Settings};
8use std::sync::Arc;
9use theme::{Theme, ThemeMeta, ThemeRegistry};
10use workspace::{AppState, Workspace};
11
12pub struct ThemeSelector {
13 registry: Arc<ThemeRegistry>,
14 theme_data: Vec<ThemeMeta>,
15 matches: Vec<StringMatch>,
16 original_theme: Arc<Theme>,
17 picker: ViewHandle<Picker<Self>>,
18 selection_completed: bool,
19 selected_index: usize,
20}
21
22actions!(theme_selector, [Toggle, Reload]);
23
24pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
25 Picker::<ThemeSelector>::init(cx);
26 cx.add_action({
27 let theme_registry = app_state.themes.clone();
28 move |workspace, _: &Toggle, cx| {
29 ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
30 }
31 });
32}
33
34pub enum Event {
35 Dismissed,
36}
37
38impl ThemeSelector {
39 fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
40 let handle = cx.weak_handle();
41 let picker = cx.add_view(|cx| Picker::new("Select Theme...", handle, cx));
42 let settings = cx.global::<Settings>();
43
44 let original_theme = settings.theme.clone();
45
46 let mut theme_names = registry
47 .list(
48 settings.staff_mode,
49 settings.experiments.experimental_themes,
50 )
51 .collect::<Vec<_>>();
52 theme_names.sort_unstable_by(|a, b| {
53 a.is_light
54 .cmp(&b.is_light)
55 .reverse()
56 .then(a.name.cmp(&b.name))
57 });
58 let matches = theme_names
59 .iter()
60 .map(|meta| StringMatch {
61 candidate_id: 0,
62 score: 0.0,
63 positions: Default::default(),
64 string: meta.name.clone(),
65 })
66 .collect();
67 let mut this = Self {
68 registry,
69 theme_data: theme_names,
70 matches,
71 picker,
72 original_theme: original_theme.clone(),
73 selected_index: 0,
74 selection_completed: false,
75 };
76 this.select_if_matching(&original_theme.meta.name);
77 this
78 }
79
80 fn toggle(
81 workspace: &mut Workspace,
82 themes: Arc<ThemeRegistry>,
83 cx: &mut ViewContext<Workspace>,
84 ) {
85 workspace.toggle_modal(cx, |_, cx| {
86 let this = cx.add_view(|cx| Self::new(themes, cx));
87 cx.subscribe(&this, Self::on_event).detach();
88 this
89 });
90 }
91
92 #[cfg(debug_assertions)]
93 pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
94 let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
95 themes.clear();
96 match themes.get(¤t_theme_name) {
97 Ok(theme) => {
98 Self::set_theme(theme, cx);
99 log::info!("reloaded theme {}", current_theme_name);
100 }
101 Err(error) => {
102 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
103 }
104 }
105 }
106
107 fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
108 if let Some(mat) = self.matches.get(self.selected_index) {
109 match self.registry.get(&mat.string) {
110 Ok(theme) => {
111 Self::set_theme(theme, cx);
112 }
113 Err(error) => {
114 log::error!("error loading theme {}: {}", mat.string, error)
115 }
116 }
117 }
118 }
119
120 fn select_if_matching(&mut self, theme_name: &str) {
121 self.selected_index = self
122 .matches
123 .iter()
124 .position(|mat| mat.string == theme_name)
125 .unwrap_or(self.selected_index);
126 }
127
128 fn on_event(
129 workspace: &mut Workspace,
130 _: ViewHandle<ThemeSelector>,
131 event: &Event,
132 cx: &mut ViewContext<Workspace>,
133 ) {
134 match event {
135 Event::Dismissed => {
136 workspace.dismiss_modal(cx);
137 }
138 }
139 }
140
141 fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
142 cx.update_global::<Settings, _, _>(|settings, cx| {
143 settings.theme = theme;
144 cx.refresh_windows();
145 });
146 }
147}
148
149impl PickerDelegate for ThemeSelector {
150 fn match_count(&self) -> usize {
151 self.matches.len()
152 }
153
154 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
155 self.selection_completed = true;
156
157 let theme_name = cx.global::<Settings>().theme.meta.name.clone();
158 SettingsFile::update(cx, |settings_content| {
159 settings_content.theme = Some(theme_name);
160 });
161
162 cx.emit(Event::Dismissed);
163 }
164
165 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
166 if !self.selection_completed {
167 Self::set_theme(self.original_theme.clone(), cx);
168 self.selection_completed = true;
169 }
170 cx.emit(Event::Dismissed);
171 }
172
173 fn selected_index(&self) -> usize {
174 self.selected_index
175 }
176
177 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
178 self.selected_index = ix;
179 self.show_selected_theme(cx);
180 }
181
182 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
183 let background = cx.background().clone();
184 let candidates = self
185 .theme_data
186 .iter()
187 .enumerate()
188 .map(|(id, meta)| StringMatchCandidate {
189 id,
190 char_bag: meta.name.as_str().into(),
191 string: meta.name.clone(),
192 })
193 .collect::<Vec<_>>();
194
195 cx.spawn(|this, mut cx| async move {
196 let matches = if query.is_empty() {
197 candidates
198 .into_iter()
199 .enumerate()
200 .map(|(index, candidate)| StringMatch {
201 candidate_id: index,
202 string: candidate.string,
203 positions: Vec::new(),
204 score: 0.0,
205 })
206 .collect()
207 } else {
208 match_strings(
209 &candidates,
210 &query,
211 false,
212 100,
213 &Default::default(),
214 background,
215 )
216 .await
217 };
218
219 this.update(&mut cx, |this, cx| {
220 this.matches = matches;
221 this.selected_index = this
222 .selected_index
223 .min(this.matches.len().saturating_sub(1));
224 this.show_selected_theme(cx);
225 cx.notify();
226 });
227 })
228 }
229
230 fn render_match(
231 &self,
232 ix: usize,
233 mouse_state: &mut MouseState,
234 selected: bool,
235 cx: &AppContext,
236 ) -> ElementBox {
237 let settings = cx.global::<Settings>();
238 let theme = &settings.theme;
239 let theme_match = &self.matches[ix];
240 let style = theme.picker.item.style_for(mouse_state, selected);
241
242 Label::new(theme_match.string.clone(), style.label.clone())
243 .with_highlights(theme_match.positions.clone())
244 .contained()
245 .with_style(style.container)
246 .boxed()
247 }
248}
249
250impl Entity for ThemeSelector {
251 type Event = Event;
252
253 fn release(&mut self, cx: &mut MutableAppContext) {
254 if !self.selection_completed {
255 Self::set_theme(self.original_theme.clone(), cx);
256 }
257 }
258}
259
260impl View for ThemeSelector {
261 fn ui_name() -> &'static str {
262 "ThemeSelector"
263 }
264
265 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
266 ChildView::new(self.picker.clone(), cx).boxed()
267 }
268
269 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
270 if cx.is_self_focused() {
271 cx.focus(&self.picker);
272 }
273 }
274}