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