theme_selector.rs

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