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::{AppState, 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 Arc::new(move |_| {
68 let settings = settings.borrow();
69 EditorSettings {
70 tab_size: settings.tab_size,
71 style: settings.theme.selector.input_editor.as_editor(),
72 soft_wrap: editor::SoftWrap::None,
73 }
74 })
75 },
76 cx,
77 )
78 });
79
80 cx.subscribe(&query_editor, Self::on_query_editor_event)
81 .detach();
82
83 let mut this = Self {
84 settings,
85 settings_tx,
86 themes: registry,
87 query_editor,
88 matches: Vec::new(),
89 list_state: Default::default(),
90 selected_index: 0,
91 };
92 this.update_matches(cx);
93 this
94 }
95
96 fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
97 workspace.toggle_modal(cx, |cx, _| {
98 let selector = cx.add_view(|cx| {
99 Self::new(
100 action.0.settings_tx.clone(),
101 action.0.settings.clone(),
102 action.0.themes.clone(),
103 cx,
104 )
105 });
106 cx.subscribe(&selector, Self::on_event).detach();
107 selector
108 });
109 }
110
111 fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
112 let current_theme_name = action.0.settings.borrow().theme.name.clone();
113 action.0.themes.clear();
114 match action.0.themes.get(¤t_theme_name) {
115 Ok(theme) => {
116 cx.refresh_windows();
117 action.0.settings_tx.lock().borrow_mut().theme = theme;
118 log::info!("reloaded theme {}", current_theme_name);
119 }
120 Err(error) => {
121 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
122 }
123 }
124 }
125
126 fn confirm(&mut self, _: &Confirm, 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) => {
130 self.settings_tx.lock().borrow_mut().theme = theme;
131 cx.refresh_windows();
132 cx.emit(Event::Dismissed);
133 }
134 Err(error) => log::error!("error loading theme {}: {}", mat.string, error),
135 }
136 }
137 }
138
139 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
140 if self.selected_index > 0 {
141 self.selected_index -= 1;
142 }
143 self.list_state.scroll_to(self.selected_index);
144 cx.notify();
145 }
146
147 fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
148 if self.selected_index + 1 < self.matches.len() {
149 self.selected_index += 1;
150 }
151 self.list_state.scroll_to(self.selected_index);
152 cx.notify();
153 }
154
155 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
156 let background = cx.background().clone();
157 let candidates = self
158 .themes
159 .list()
160 .enumerate()
161 .map(|(id, name)| StringMatchCandidate {
162 id,
163 char_bag: name.as_str().into(),
164 string: name,
165 })
166 .collect::<Vec<_>>();
167 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
168
169 self.matches = if query.is_empty() {
170 candidates
171 .into_iter()
172 .enumerate()
173 .map(|(index, candidate)| StringMatch {
174 candidate_id: index,
175 string: candidate.string,
176 positions: Vec::new(),
177 score: 0.0,
178 })
179 .collect()
180 } else {
181 smol::block_on(match_strings(
182 &candidates,
183 &query,
184 false,
185 100,
186 &Default::default(),
187 background,
188 ))
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::Edited => self.update_matches(cx),
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 = self.settings.borrow();
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 = 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(
244 selector.matches[range]
245 .iter()
246 .enumerate()
247 .map(move |(i, path_match)| selector.render_match(path_match, start + i)),
248 );
249 },
250 );
251
252 Container::new(list.boxed())
253 .with_margin_top(6.0)
254 .named("matches")
255 }
256
257 fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
258 let settings = self.settings.borrow();
259 let theme = &settings.theme;
260
261 let container = Container::new(
262 Label::new(
263 theme_match.string.clone(),
264 if index == self.selected_index {
265 theme.selector.active_item.label.clone()
266 } else {
267 theme.selector.item.label.clone()
268 },
269 )
270 .with_highlights(theme_match.positions.clone())
271 .boxed(),
272 )
273 .with_style(if index == self.selected_index {
274 theme.selector.active_item.container
275 } else {
276 theme.selector.item.container
277 });
278
279 container.boxed()
280 }
281}
282
283impl Entity for ThemeSelector {
284 type Event = Event;
285}
286
287impl View for ThemeSelector {
288 fn ui_name() -> &'static str {
289 "ThemeSelector"
290 }
291
292 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
293 let settings = self.settings.borrow();
294
295 Align::new(
296 ConstrainedBox::new(
297 Container::new(
298 Flex::new(Axis::Vertical)
299 .with_child(ChildView::new(self.query_editor.id()).boxed())
300 .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
301 .boxed(),
302 )
303 .with_style(settings.theme.selector.container)
304 .boxed(),
305 )
306 .with_max_width(600.0)
307 .with_max_height(400.0)
308 .boxed(),
309 )
310 .top()
311 .named("theme selector")
312 }
313
314 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
315 cx.focus(&self.query_editor);
316 }
317
318 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
319 let mut cx = Self::default_keymap_context();
320 cx.set.insert("menu".into());
321 cx
322 }
323}
324
325impl<'a> From<&'a AppState> for ThemeSelectorParams {
326 fn from(state: &'a AppState) -> Self {
327 Self {
328 settings_tx: state.settings_tx.clone(),
329 settings: state.settings.clone(),
330 themes: state.themes.clone(),
331 }
332 }
333}