1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
2use gpui::{
3 actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext,
4 RenderContext, View, ViewContext, ViewHandle,
5};
6use picker::{Picker, PickerDelegate};
7use settings::Settings;
8use std::sync::Arc;
9use theme::{Theme, ThemeRegistry};
10use workspace::Workspace;
11
12pub struct ThemeSelector {
13 registry: Arc<ThemeRegistry>,
14 theme_names: Vec<String>,
15 matches: Vec<StringMatch>,
16 original_theme: Arc<Theme>,
17 picker: ViewHandle<Picker<Self>>,
18 selection_completed: bool,
19 selected_index: usize,
20}
21
22actions!(theme_selector, [Toggle, Reload]);
23
24pub fn init(cx: &mut MutableAppContext) {
25 cx.add_action(ThemeSelector::toggle);
26 Picker::<ThemeSelector>::init(cx);
27}
28
29pub enum Event {
30 Dismissed,
31}
32
33impl ThemeSelector {
34 fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
35 let handle = cx.weak_handle();
36 let picker = cx.add_view(|cx| Picker::new(handle, cx));
37 let original_theme = cx.global::<Settings>().theme.clone();
38 let mut theme_names = registry.list().collect::<Vec<_>>();
39 theme_names.sort_unstable_by(|a, b| {
40 a.ends_with("dark")
41 .cmp(&b.ends_with("dark"))
42 .then_with(|| a.cmp(&b))
43 });
44 let matches = theme_names
45 .iter()
46 .map(|name| StringMatch {
47 candidate_id: 0,
48 score: 0.0,
49 positions: Default::default(),
50 string: name.clone(),
51 })
52 .collect();
53 let mut this = Self {
54 registry,
55 theme_names,
56 matches,
57 picker,
58 original_theme: original_theme.clone(),
59 selected_index: 0,
60 selection_completed: false,
61 };
62 this.select_if_matching(&original_theme.name);
63 this
64 }
65
66 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
67 let themes = workspace.themes();
68 workspace.toggle_modal(cx, |_, cx| {
69 let this = cx.add_view(|cx| Self::new(themes, cx));
70 cx.subscribe(&this, Self::on_event).detach();
71 this
72 });
73 }
74
75 #[cfg(debug_assertions)]
76 pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
77 let current_theme_name = cx.global::<Settings>().theme.name.clone();
78 themes.clear();
79 match themes.get(¤t_theme_name) {
80 Ok(theme) => {
81 Self::set_theme(theme, cx);
82 log::info!("reloaded theme {}", current_theme_name);
83 }
84 Err(error) => {
85 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
86 }
87 }
88 }
89
90 fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
91 if let Some(mat) = self.matches.get(self.selected_index) {
92 match self.registry.get(&mat.string) {
93 Ok(theme) => Self::set_theme(theme, cx),
94 Err(error) => {
95 log::error!("error loading theme {}: {}", mat.string, error)
96 }
97 }
98 }
99 }
100
101 fn select_if_matching(&mut self, theme_name: &str) {
102 self.selected_index = self
103 .matches
104 .iter()
105 .position(|mat| mat.string == theme_name)
106 .unwrap_or(self.selected_index);
107 }
108
109 fn on_event(
110 workspace: &mut Workspace,
111 _: ViewHandle<ThemeSelector>,
112 event: &Event,
113 cx: &mut ViewContext<Workspace>,
114 ) {
115 match event {
116 Event::Dismissed => {
117 workspace.dismiss_modal(cx);
118 }
119 }
120 }
121
122 fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
123 cx.update_global::<Settings, _, _>(|settings, cx| {
124 settings.theme = theme;
125 cx.refresh_windows();
126 });
127 }
128}
129
130impl PickerDelegate for ThemeSelector {
131 fn match_count(&self) -> usize {
132 self.matches.len()
133 }
134
135 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
136 self.selection_completed = true;
137 cx.emit(Event::Dismissed);
138 }
139
140 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
141 if !self.selection_completed {
142 Self::set_theme(self.original_theme.clone(), cx);
143 self.selection_completed = true;
144 }
145 cx.emit(Event::Dismissed);
146 }
147
148 fn selected_index(&self) -> usize {
149 self.selected_index
150 }
151
152 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
153 self.selected_index = ix;
154 self.show_selected_theme(cx);
155 }
156
157 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
158 let background = cx.background().clone();
159 let candidates = self
160 .theme_names
161 .iter()
162 .enumerate()
163 .map(|(id, name)| StringMatchCandidate {
164 id,
165 char_bag: name.as_str().into(),
166 string: name.clone(),
167 })
168 .collect::<Vec<_>>();
169
170 cx.spawn(|this, mut cx| async move {
171 let matches = if query.is_empty() {
172 candidates
173 .into_iter()
174 .enumerate()
175 .map(|(index, candidate)| StringMatch {
176 candidate_id: index,
177 string: candidate.string,
178 positions: Vec::new(),
179 score: 0.0,
180 })
181 .collect()
182 } else {
183 match_strings(
184 &candidates,
185 &query,
186 false,
187 100,
188 &Default::default(),
189 background,
190 )
191 .await
192 };
193
194 this.update(&mut cx, |this, cx| {
195 this.matches = matches;
196 this.selected_index = this
197 .selected_index
198 .min(this.matches.len().saturating_sub(1));
199 this.show_selected_theme(cx);
200 cx.notify();
201 });
202 })
203 }
204
205 fn render_match(
206 &self,
207 ix: usize,
208 mouse_state: &MouseState,
209 selected: bool,
210 cx: &AppContext,
211 ) -> ElementBox {
212 let settings = cx.global::<Settings>();
213 let theme = &settings.theme;
214 let theme_match = &self.matches[ix];
215 let style = theme.picker.item.style_for(mouse_state, selected);
216
217 Label::new(theme_match.string.clone(), style.label.clone())
218 .with_highlights(theme_match.positions.clone())
219 .contained()
220 .with_style(style.container)
221 .boxed()
222 }
223}
224
225impl Entity for ThemeSelector {
226 type Event = Event;
227
228 fn release(&mut self, cx: &mut MutableAppContext) {
229 if !self.selection_completed {
230 Self::set_theme(self.original_theme.clone(), cx);
231 }
232 }
233}
234
235impl View for ThemeSelector {
236 fn ui_name() -> &'static str {
237 "ThemeSelector"
238 }
239
240 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
241 ChildView::new(self.picker.clone()).boxed()
242 }
243
244 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
245 cx.focus(&self.picker);
246 }
247}