theme_selector.rs

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