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
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) => Self::set_theme(theme, cx),
111 Err(error) => {
112 log::error!("error loading theme {}: {}", mat.string, error)
113 }
114 }
115 }
116 }
117
118 fn select_if_matching(&mut self, theme_name: &str) {
119 self.selected_index = self
120 .matches
121 .iter()
122 .position(|mat| mat.string == theme_name)
123 .unwrap_or(self.selected_index);
124 }
125
126 fn on_event(
127 workspace: &mut Workspace,
128 _: ViewHandle<ThemeSelector>,
129 event: &Event,
130 cx: &mut ViewContext<Workspace>,
131 ) {
132 match event {
133 Event::Dismissed => {
134 workspace.dismiss_modal(cx);
135 }
136 }
137 }
138
139 fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
140 cx.update_global::<Settings, _, _>(|settings, cx| {
141 settings.theme = theme;
142 cx.refresh_windows();
143 });
144 }
145}
146
147impl PickerDelegate for ThemeSelector {
148 fn match_count(&self) -> usize {
149 self.matches.len()
150 }
151
152 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
153 self.selection_completed = true;
154 cx.emit(Event::Dismissed);
155 }
156
157 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
158 if !self.selection_completed {
159 Self::set_theme(self.original_theme.clone(), cx);
160 self.selection_completed = true;
161 }
162 cx.emit(Event::Dismissed);
163 }
164
165 fn selected_index(&self) -> usize {
166 self.selected_index
167 }
168
169 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
170 self.selected_index = ix;
171 self.show_selected_theme(cx);
172 }
173
174 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
175 let background = cx.background().clone();
176 let candidates = self
177 .theme_data
178 .iter()
179 .enumerate()
180 .map(|(id, meta)| StringMatchCandidate {
181 id,
182 char_bag: meta.name.as_str().into(),
183 string: meta.name.clone(),
184 })
185 .collect::<Vec<_>>();
186
187 cx.spawn(|this, mut cx| async move {
188 let matches = if query.is_empty() {
189 candidates
190 .into_iter()
191 .enumerate()
192 .map(|(index, candidate)| StringMatch {
193 candidate_id: index,
194 string: candidate.string,
195 positions: Vec::new(),
196 score: 0.0,
197 })
198 .collect()
199 } else {
200 match_strings(
201 &candidates,
202 &query,
203 false,
204 100,
205 &Default::default(),
206 background,
207 )
208 .await
209 };
210
211 this.update(&mut cx, |this, cx| {
212 this.matches = matches;
213 this.selected_index = this
214 .selected_index
215 .min(this.matches.len().saturating_sub(1));
216 this.show_selected_theme(cx);
217 cx.notify();
218 });
219 })
220 }
221
222 fn render_match(
223 &self,
224 ix: usize,
225 mouse_state: MouseState,
226 selected: bool,
227 cx: &AppContext,
228 ) -> ElementBox {
229 let settings = cx.global::<Settings>();
230 let theme = &settings.theme;
231 let theme_match = &self.matches[ix];
232 let style = theme.picker.item.style_for(mouse_state, selected);
233
234 Label::new(theme_match.string.clone(), style.label.clone())
235 .with_highlights(theme_match.positions.clone())
236 .contained()
237 .with_style(style.container)
238 .boxed()
239 }
240}
241
242impl Entity for ThemeSelector {
243 type Event = Event;
244
245 fn release(&mut self, cx: &mut MutableAppContext) {
246 if !self.selection_completed {
247 Self::set_theme(self.original_theme.clone(), cx);
248 }
249 }
250}
251
252impl View for ThemeSelector {
253 fn ui_name() -> &'static str {
254 "ThemeSelector"
255 }
256
257 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
258 ChildView::new(self.picker.clone()).boxed()
259 }
260
261 fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
262 if cx.is_self_focused() {
263 cx.focus(&self.picker);
264 }
265 }
266}