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