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