theme_selector.rs

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