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