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