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