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