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