1use editor::Editor;
2use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
3use gpui::{
4 action,
5 elements::*,
6 keymap::{self, Binding},
7 AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
8 ViewContext, ViewHandle,
9};
10use parking_lot::Mutex;
11use postage::watch;
12use std::{cmp, sync::Arc};
13use theme::{Theme, ThemeRegistry};
14use workspace::{
15 menu::{Confirm, SelectNext, SelectPrev},
16 AppState, Settings, Workspace,
17};
18
19#[derive(Clone)]
20pub struct ThemeSelectorParams {
21 pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
22 pub settings: watch::Receiver<Settings>,
23 pub themes: Arc<ThemeRegistry>,
24}
25
26pub struct ThemeSelector {
27 settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
28 settings: watch::Receiver<Settings>,
29 themes: Arc<ThemeRegistry>,
30 matches: Vec<StringMatch>,
31 query_editor: ViewHandle<Editor>,
32 list_state: UniformListState,
33 selected_index: usize,
34 original_theme: Arc<Theme>,
35 selection_completed: bool,
36}
37
38action!(Toggle, ThemeSelectorParams);
39action!(Reload, ThemeSelectorParams);
40
41pub fn init(params: ThemeSelectorParams, cx: &mut MutableAppContext) {
42 cx.add_action(ThemeSelector::confirm);
43 cx.add_action(ThemeSelector::select_prev);
44 cx.add_action(ThemeSelector::select_next);
45 cx.add_action(ThemeSelector::toggle);
46 cx.add_action(ThemeSelector::reload);
47
48 cx.add_bindings(vec![
49 Binding::new("cmd-k cmd-t", Toggle(params.clone()), None),
50 Binding::new("cmd-k t", Reload(params.clone()), None),
51 Binding::new("escape", Toggle(params.clone()), Some("ThemeSelector")),
52 ]);
53}
54
55pub enum Event {
56 Dismissed,
57}
58
59impl ThemeSelector {
60 fn new(
61 settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
62 settings: watch::Receiver<Settings>,
63 registry: Arc<ThemeRegistry>,
64 cx: &mut ViewContext<Self>,
65 ) -> Self {
66 let query_editor = cx.add_view(|cx| {
67 Editor::single_line(
68 settings.clone(),
69 Some(|theme| theme.selector.input_editor.clone()),
70 cx,
71 )
72 });
73
74 cx.subscribe(&query_editor, Self::on_query_editor_event)
75 .detach();
76
77 let original_theme = settings.borrow().theme.clone();
78
79 let mut this = Self {
80 settings,
81 settings_tx,
82 themes: registry,
83 query_editor,
84 matches: Vec::new(),
85 list_state: Default::default(),
86 selected_index: 0, // Default index for now
87 original_theme: original_theme.clone(),
88 selection_completed: false,
89 };
90 this.update_matches(cx);
91
92 // Set selected index to current theme
93 this.select_if_matching(&original_theme.name);
94
95 this
96 }
97
98 fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
99 workspace.toggle_modal(cx, |cx, _| {
100 let selector = cx.add_view(|cx| {
101 Self::new(
102 action.0.settings_tx.clone(),
103 action.0.settings.clone(),
104 action.0.themes.clone(),
105 cx,
106 )
107 });
108 cx.subscribe(&selector, Self::on_event).detach();
109 selector
110 });
111 }
112
113 fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
114 let current_theme_name = action.0.settings.borrow().theme.name.clone();
115 action.0.themes.clear();
116 match action.0.themes.get(¤t_theme_name) {
117 Ok(theme) => {
118 action.0.settings_tx.lock().borrow_mut().theme = theme;
119 cx.refresh_windows();
120 log::info!("reloaded theme {}", current_theme_name);
121 }
122 Err(error) => {
123 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
124 }
125 }
126 }
127
128 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
129 self.selection_completed = true;
130 cx.emit(Event::Dismissed);
131 }
132
133 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
134 if self.selected_index > 0 {
135 self.selected_index -= 1;
136 }
137 self.list_state
138 .scroll_to(ScrollTarget::Show(self.selected_index));
139
140 self.show_selected_theme(cx);
141 cx.notify();
142 }
143
144 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
145 if self.selected_index + 1 < self.matches.len() {
146 self.selected_index += 1;
147 }
148 self.list_state
149 .scroll_to(ScrollTarget::Show(self.selected_index));
150
151 self.show_selected_theme(cx);
152 cx.notify();
153 }
154
155 fn show_selected_theme(&mut self, cx: &mut MutableAppContext) {
156 if let Some(mat) = self.matches.get(self.selected_index) {
157 match self.themes.get(&mat.string) {
158 Ok(theme) => {
159 self.set_theme(theme, cx);
160 }
161 Err(error) => {
162 log::error!("error loading theme {}: {}", mat.string, error)
163 }
164 }
165 }
166 }
167
168 fn select_if_matching(&mut self, theme_name: &str) {
169 self.selected_index = self
170 .matches
171 .iter()
172 .position(|mat| mat.string == theme_name)
173 .unwrap_or(self.selected_index);
174 }
175
176 fn current_theme(&self) -> Arc<Theme> {
177 self.settings_tx.lock().borrow().theme.clone()
178 }
179
180 fn set_theme(&self, theme: Arc<Theme>, cx: &mut MutableAppContext) {
181 self.settings_tx.lock().borrow_mut().theme = theme;
182 cx.refresh_windows();
183 }
184
185 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
186 let background = cx.background().clone();
187 let candidates = self
188 .themes
189 .list()
190 .enumerate()
191 .map(|(id, name)| StringMatchCandidate {
192 id,
193 char_bag: name.as_str().into(),
194 string: name,
195 })
196 .collect::<Vec<_>>();
197 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
198
199 self.matches = if query.is_empty() {
200 candidates
201 .into_iter()
202 .enumerate()
203 .map(|(index, candidate)| StringMatch {
204 candidate_id: index,
205 string: candidate.string,
206 positions: Vec::new(),
207 score: 0.0,
208 })
209 .collect()
210 } else {
211 smol::block_on(match_strings(
212 &candidates,
213 &query,
214 false,
215 100,
216 &Default::default(),
217 background,
218 ))
219 };
220
221 self.selected_index = self
222 .selected_index
223 .min(self.matches.len().saturating_sub(1));
224
225 cx.notify();
226 }
227
228 fn on_event(
229 workspace: &mut Workspace,
230 _: ViewHandle<ThemeSelector>,
231 event: &Event,
232 cx: &mut ViewContext<Workspace>,
233 ) {
234 match event {
235 Event::Dismissed => {
236 workspace.dismiss_modal(cx);
237 }
238 }
239 }
240
241 fn on_query_editor_event(
242 &mut self,
243 _: ViewHandle<Editor>,
244 event: &editor::Event,
245 cx: &mut ViewContext<Self>,
246 ) {
247 match event {
248 editor::Event::Edited => {
249 self.update_matches(cx);
250 self.select_if_matching(&self.current_theme().name);
251 self.show_selected_theme(cx);
252 }
253 editor::Event::Blurred => cx.emit(Event::Dismissed),
254 _ => {}
255 }
256 }
257
258 fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
259 if self.matches.is_empty() {
260 let settings = self.settings.borrow();
261 return Container::new(
262 Label::new(
263 "No matches".into(),
264 settings.theme.selector.empty.label.clone(),
265 )
266 .boxed(),
267 )
268 .with_style(settings.theme.selector.empty.container)
269 .named("empty matches");
270 }
271
272 let handle = cx.handle();
273 let list = UniformList::new(
274 self.list_state.clone(),
275 self.matches.len(),
276 move |mut range, items, cx| {
277 let cx = cx.as_ref();
278 let selector = handle.upgrade(cx).unwrap();
279 let selector = selector.read(cx);
280 let start = range.start;
281 range.end = cmp::min(range.end, selector.matches.len());
282 items.extend(
283 selector.matches[range]
284 .iter()
285 .enumerate()
286 .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
287 );
288 },
289 );
290
291 Container::new(list.boxed())
292 .with_margin_top(6.0)
293 .named("matches")
294 }
295
296 fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
297 let settings = self.settings.borrow();
298 let theme = &settings.theme;
299
300 let container = Container::new(
301 Label::new(
302 theme_match.string.clone(),
303 if index == self.selected_index {
304 theme.selector.active_item.label.clone()
305 } else {
306 theme.selector.item.label.clone()
307 },
308 )
309 .with_highlights(theme_match.positions.clone())
310 .boxed(),
311 )
312 .with_style(if index == self.selected_index {
313 theme.selector.active_item.container
314 } else {
315 theme.selector.item.container
316 });
317
318 container.boxed()
319 }
320}
321
322impl Entity for ThemeSelector {
323 type Event = Event;
324
325 fn release(&mut self, cx: &mut MutableAppContext) {
326 if !self.selection_completed {
327 self.set_theme(self.original_theme.clone(), cx);
328 }
329 }
330}
331
332impl View for ThemeSelector {
333 fn ui_name() -> &'static str {
334 "ThemeSelector"
335 }
336
337 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
338 let settings = self.settings.borrow();
339
340 Align::new(
341 ConstrainedBox::new(
342 Container::new(
343 Flex::new(Axis::Vertical)
344 .with_child(
345 ChildView::new(&self.query_editor)
346 .contained()
347 .with_style(settings.theme.selector.input_editor.container)
348 .boxed(),
349 )
350 .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
351 .boxed(),
352 )
353 .with_style(settings.theme.selector.container)
354 .boxed(),
355 )
356 .with_max_width(600.0)
357 .with_max_height(400.0)
358 .boxed(),
359 )
360 .top()
361 .named("theme selector")
362 }
363
364 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
365 cx.focus(&self.query_editor);
366 }
367
368 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
369 let mut cx = Self::default_keymap_context();
370 cx.set.insert("menu".into());
371 cx
372 }
373}
374
375impl<'a> From<&'a AppState> for ThemeSelectorParams {
376 fn from(state: &'a AppState) -> Self {
377 Self {
378 settings_tx: state.settings_tx.clone(),
379 settings: state.settings.clone(),
380 themes: state.themes.clone(),
381 }
382 }
383}