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