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