theme_selector.rs

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