1use editor::{Editor, EditorSettings};
2use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
3use gpui::{
4 action,
5 elements::*,
6 keymap::{self, menu, Binding},
7 AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
8 ViewContext, ViewHandle,
9};
10use parking_lot::Mutex;
11use postage::watch;
12use std::{cmp, sync::Arc};
13use theme::ThemeRegistry;
14use workspace::{Settings, Workspace};
15
16#[derive(Clone)]
17pub struct ThemeSelectorParams {
18 pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
19 pub settings: watch::Receiver<Settings>,
20 pub themes: Arc<ThemeRegistry>,
21}
22
23pub struct ThemeSelector {
24 settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
25 settings: watch::Receiver<Settings>,
26 themes: Arc<ThemeRegistry>,
27 matches: Vec<StringMatch>,
28 query_editor: ViewHandle<Editor>,
29 list_state: UniformListState,
30 selected_index: usize,
31}
32
33action!(Confirm);
34action!(Toggle, ThemeSelectorParams);
35action!(Reload, ThemeSelectorParams);
36
37pub fn init(params: ThemeSelectorParams, cx: &mut MutableAppContext) {
38 cx.add_action(ThemeSelector::confirm);
39 cx.add_action(ThemeSelector::select_prev);
40 cx.add_action(ThemeSelector::select_next);
41 cx.add_action(ThemeSelector::toggle);
42 cx.add_action(ThemeSelector::reload);
43
44 cx.add_bindings(vec![
45 Binding::new("cmd-k cmd-t", Toggle(params.clone()), None),
46 Binding::new("cmd-k t", Reload(params.clone()), None),
47 Binding::new("escape", Toggle(params.clone()), Some("ThemeSelector")),
48 Binding::new("enter", Confirm, Some("ThemeSelector")),
49 ]);
50}
51
52pub enum Event {
53 Dismissed,
54}
55
56impl ThemeSelector {
57 fn new(
58 settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
59 settings: watch::Receiver<Settings>,
60 registry: Arc<ThemeRegistry>,
61 cx: &mut ViewContext<Self>,
62 ) -> Self {
63 let query_editor = cx.add_view(|cx| {
64 Editor::single_line(
65 {
66 let settings = settings.clone();
67 move |_| {
68 let settings = settings.borrow();
69 EditorSettings {
70 tab_size: settings.tab_size,
71 style: settings.theme.selector.input_editor.as_editor(),
72 }
73 }
74 },
75 cx,
76 )
77 });
78
79 cx.subscribe(&query_editor, Self::on_query_editor_event)
80 .detach();
81
82 let mut this = Self {
83 settings,
84 settings_tx,
85 themes: registry,
86 query_editor,
87 matches: Vec::new(),
88 list_state: Default::default(),
89 selected_index: 0,
90 };
91 this.update_matches(cx);
92 this
93 }
94
95 fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
96 workspace.toggle_modal(cx, |cx, _| {
97 let selector = cx.add_view(|cx| {
98 Self::new(
99 action.0.settings_tx.clone(),
100 action.0.settings.clone(),
101 action.0.themes.clone(),
102 cx,
103 )
104 });
105 cx.subscribe(&selector, Self::on_event).detach();
106 selector
107 });
108 }
109
110 fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
111 let current_theme_name = action.0.settings.borrow().theme.name.clone();
112 action.0.themes.clear();
113 match action.0.themes.get(¤t_theme_name) {
114 Ok(theme) => {
115 cx.refresh_windows();
116 action.0.settings_tx.lock().borrow_mut().theme = theme;
117 log::info!("reloaded theme {}", current_theme_name);
118 }
119 Err(error) => {
120 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
121 }
122 }
123 }
124
125 fn confirm(&mut self, _: &Confirm, 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) => {
129 self.settings_tx.lock().borrow_mut().theme = theme;
130 cx.refresh_windows();
131 cx.emit(Event::Dismissed);
132 }
133 Err(error) => log::error!("error loading theme {}: {}", mat.string, error),
134 }
135 }
136 }
137
138 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
139 if self.selected_index > 0 {
140 self.selected_index -= 1;
141 }
142 self.list_state.scroll_to(self.selected_index);
143 cx.notify();
144 }
145
146 fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
147 if self.selected_index + 1 < self.matches.len() {
148 self.selected_index += 1;
149 }
150 self.list_state.scroll_to(self.selected_index);
151 cx.notify();
152 }
153
154 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
155 let background = cx.background().clone();
156 let candidates = self
157 .themes
158 .list()
159 .map(|name| StringMatchCandidate {
160 char_bag: name.as_str().into(),
161 string: name,
162 })
163 .collect::<Vec<_>>();
164 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
165
166 self.matches = if query.is_empty() {
167 candidates
168 .into_iter()
169 .map(|candidate| StringMatch {
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 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::Edited => self.update_matches(cx),
209 editor::Event::Blurred => cx.emit(Event::Dismissed),
210 _ => {}
211 }
212 }
213
214 fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
215 if self.matches.is_empty() {
216 let settings = self.settings.borrow();
217 return Container::new(
218 Label::new(
219 "No matches".into(),
220 settings.theme.selector.empty.label.clone(),
221 )
222 .boxed(),
223 )
224 .with_style(settings.theme.selector.empty.container)
225 .named("empty matches");
226 }
227
228 let handle = cx.handle();
229 let list = UniformList::new(
230 self.list_state.clone(),
231 self.matches.len(),
232 move |mut range, items, cx| {
233 let cx = cx.as_ref();
234 let selector = handle.upgrade(cx).unwrap();
235 let selector = selector.read(cx);
236 let start = range.start;
237 range.end = cmp::min(range.end, selector.matches.len());
238 items.extend(
239 selector.matches[range]
240 .iter()
241 .enumerate()
242 .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
243 );
244 },
245 );
246
247 Container::new(list.boxed())
248 .with_margin_top(6.0)
249 .named("matches")
250 }
251
252 fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
253 let settings = self.settings.borrow();
254 let theme = &settings.theme;
255
256 let container = Container::new(
257 Label::new(
258 theme_match.string.clone(),
259 if index == self.selected_index {
260 theme.selector.active_item.label.clone()
261 } else {
262 theme.selector.item.label.clone()
263 },
264 )
265 .with_highlights(theme_match.positions.clone())
266 .boxed(),
267 )
268 .with_style(if index == self.selected_index {
269 theme.selector.active_item.container
270 } else {
271 theme.selector.item.container
272 });
273
274 container.boxed()
275 }
276}
277
278impl Entity for ThemeSelector {
279 type Event = Event;
280}
281
282impl View for ThemeSelector {
283 fn ui_name() -> &'static str {
284 "ThemeSelector"
285 }
286
287 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
288 let settings = self.settings.borrow();
289
290 Align::new(
291 ConstrainedBox::new(
292 Container::new(
293 Flex::new(Axis::Vertical)
294 .with_child(ChildView::new(self.query_editor.id()).boxed())
295 .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
296 .boxed(),
297 )
298 .with_style(settings.theme.selector.container)
299 .boxed(),
300 )
301 .with_max_width(600.0)
302 .with_max_height(400.0)
303 .boxed(),
304 )
305 .top()
306 .named("theme selector")
307 }
308
309 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
310 cx.focus(&self.query_editor);
311 }
312
313 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
314 let mut cx = Self::default_keymap_context();
315 cx.set.insert("menu".into());
316 cx
317 }
318}