theme_selector.rs

  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(&current_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}