1mod theme_registry;
2
3use gpui::{
4 color::Color,
5 elements::{ContainerStyle, ImageStyle, LabelStyle, TooltipStyle},
6 fonts::{HighlightStyle, TextStyle},
7 Border, MouseState,
8};
9use serde::{de::DeserializeOwned, Deserialize};
10use serde_json::Value;
11use std::{collections::HashMap, sync::Arc};
12
13pub use theme_registry::*;
14
15pub const DEFAULT_THEME_NAME: &'static str = "cave-dark";
16
17#[derive(Deserialize, Default)]
18pub struct Theme {
19 #[serde(default)]
20 pub name: String,
21 pub workspace: Workspace,
22 pub context_menu: ContextMenu,
23 pub chat_panel: ChatPanel,
24 pub contacts_panel: ContactsPanel,
25 pub contact_finder: ContactFinder,
26 pub project_panel: ProjectPanel,
27 pub command_palette: CommandPalette,
28 pub picker: Picker,
29 pub editor: Editor,
30 pub search: Search,
31 pub project_diagnostics: ProjectDiagnostics,
32 pub breadcrumbs: ContainedText,
33 pub contact_notification: ContactNotification,
34 pub tooltip: TooltipStyle,
35}
36
37#[derive(Deserialize, Default)]
38pub struct Workspace {
39 pub background: Color,
40 pub titlebar: Titlebar,
41 pub tab: Tab,
42 pub active_tab: Tab,
43 pub pane_divider: Border,
44 pub leader_border_opacity: f32,
45 pub leader_border_width: f32,
46 pub sidebar_resize_handle: ContainerStyle,
47 pub status_bar: StatusBar,
48 pub toolbar: Toolbar,
49 pub disconnected_overlay: ContainedText,
50 pub modal: ContainerStyle,
51 pub notification: ContainerStyle,
52 pub notifications: Notifications,
53 pub joining_project_avatar: ImageStyle,
54 pub joining_project_message: ContainedText,
55}
56
57#[derive(Clone, Deserialize, Default)]
58pub struct Titlebar {
59 #[serde(flatten)]
60 pub container: ContainerStyle,
61 pub height: f32,
62 pub title: TextStyle,
63 pub avatar_width: f32,
64 pub avatar_margin: f32,
65 pub avatar_ribbon: AvatarRibbon,
66 pub offline_icon: OfflineIcon,
67 pub share_icon: Interactive<ShareIcon>,
68 pub avatar: ImageStyle,
69 pub sign_in_prompt: Interactive<ContainedText>,
70 pub outdated_warning: ContainedText,
71}
72
73#[derive(Clone, Deserialize, Default)]
74pub struct AvatarRibbon {
75 #[serde(flatten)]
76 pub container: ContainerStyle,
77 pub width: f32,
78 pub height: f32,
79}
80
81#[derive(Clone, Deserialize, Default)]
82pub struct OfflineIcon {
83 #[serde(flatten)]
84 pub container: ContainerStyle,
85 pub width: f32,
86 pub color: Color,
87}
88
89#[derive(Clone, Deserialize, Default)]
90pub struct ShareIcon {
91 #[serde(flatten)]
92 pub container: ContainerStyle,
93 pub color: Color,
94}
95
96#[derive(Clone, Deserialize, Default)]
97pub struct Tab {
98 pub height: f32,
99 #[serde(flatten)]
100 pub container: ContainerStyle,
101 #[serde(flatten)]
102 pub label: LabelStyle,
103 pub spacing: f32,
104 pub icon_width: f32,
105 pub icon_close: Color,
106 pub icon_close_active: Color,
107 pub icon_dirty: Color,
108 pub icon_conflict: Color,
109}
110
111#[derive(Clone, Deserialize, Default)]
112pub struct Toolbar {
113 #[serde(flatten)]
114 pub container: ContainerStyle,
115 pub height: f32,
116 pub item_spacing: f32,
117}
118
119#[derive(Clone, Deserialize, Default)]
120pub struct Notifications {
121 #[serde(flatten)]
122 pub container: ContainerStyle,
123 pub width: f32,
124}
125
126#[derive(Clone, Deserialize, Default)]
127pub struct Search {
128 #[serde(flatten)]
129 pub container: ContainerStyle,
130 pub editor: FindEditor,
131 pub invalid_editor: ContainerStyle,
132 pub option_button_group: ContainerStyle,
133 pub option_button: Interactive<ContainedText>,
134 pub match_background: Color,
135 pub match_index: ContainedText,
136 pub results_status: TextStyle,
137 pub tab_icon_width: f32,
138 pub tab_icon_spacing: f32,
139}
140
141#[derive(Clone, Deserialize, Default)]
142pub struct FindEditor {
143 #[serde(flatten)]
144 pub input: FieldEditor,
145 pub min_width: f32,
146 pub max_width: f32,
147}
148
149#[derive(Deserialize, Default)]
150pub struct StatusBar {
151 #[serde(flatten)]
152 pub container: ContainerStyle,
153 pub height: f32,
154 pub item_spacing: f32,
155 pub cursor_position: TextStyle,
156 pub auto_update_progress_message: TextStyle,
157 pub auto_update_done_message: TextStyle,
158 pub lsp_status: Interactive<StatusBarLspStatus>,
159 pub sidebar_buttons: StatusBarSidebarButtons,
160 pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
161 pub diagnostic_message: Interactive<ContainedText>,
162}
163
164#[derive(Deserialize, Default)]
165pub struct StatusBarSidebarButtons {
166 pub group_left: ContainerStyle,
167 pub group_right: ContainerStyle,
168 pub item: Interactive<SidebarItem>,
169 pub badge: ContainerStyle,
170}
171
172#[derive(Deserialize, Default)]
173pub struct StatusBarDiagnosticSummary {
174 pub container_ok: ContainerStyle,
175 pub container_warning: ContainerStyle,
176 pub container_error: ContainerStyle,
177 pub text: TextStyle,
178 pub icon_color_ok: Color,
179 pub icon_color_warning: Color,
180 pub icon_color_error: Color,
181 pub height: f32,
182 pub icon_width: f32,
183 pub icon_spacing: f32,
184 pub summary_spacing: f32,
185}
186
187#[derive(Deserialize, Default)]
188pub struct StatusBarLspStatus {
189 #[serde(flatten)]
190 pub container: ContainerStyle,
191 pub height: f32,
192 pub icon_spacing: f32,
193 pub icon_color: Color,
194 pub icon_width: f32,
195 pub message: TextStyle,
196}
197
198#[derive(Deserialize, Default)]
199pub struct Sidebar {
200 pub resize_handle: ContainerStyle,
201}
202
203#[derive(Clone, Copy, Deserialize, Default)]
204pub struct SidebarItem {
205 #[serde(flatten)]
206 pub container: ContainerStyle,
207 pub icon_color: Color,
208 pub icon_size: f32,
209}
210
211#[derive(Deserialize, Default)]
212pub struct ChatPanel {
213 #[serde(flatten)]
214 pub container: ContainerStyle,
215 pub message: ChatMessage,
216 pub pending_message: ChatMessage,
217 pub channel_select: ChannelSelect,
218 pub input_editor: FieldEditor,
219 pub sign_in_prompt: TextStyle,
220 pub hovered_sign_in_prompt: TextStyle,
221}
222
223#[derive(Deserialize, Default)]
224pub struct ProjectPanel {
225 #[serde(flatten)]
226 pub container: ContainerStyle,
227 pub entry: Interactive<ProjectPanelEntry>,
228 pub cut_entry_fade: f32,
229 pub ignored_entry_fade: f32,
230 pub filename_editor: FieldEditor,
231 pub indent_width: f32,
232}
233
234#[derive(Clone, Debug, Deserialize, Default)]
235pub struct ProjectPanelEntry {
236 pub height: f32,
237 #[serde(flatten)]
238 pub container: ContainerStyle,
239 pub text: TextStyle,
240 pub icon_color: Color,
241 pub icon_size: f32,
242 pub icon_spacing: f32,
243}
244
245#[derive(Clone, Debug, Deserialize, Default)]
246pub struct ContextMenu {
247 #[serde(flatten)]
248 pub container: ContainerStyle,
249 pub item: Interactive<ContextMenuItem>,
250 pub separator: ContainerStyle,
251}
252
253#[derive(Clone, Debug, Deserialize, Default)]
254pub struct ContextMenuItem {
255 #[serde(flatten)]
256 pub container: ContainerStyle,
257 pub label: TextStyle,
258 pub keystroke: ContainedText,
259}
260
261#[derive(Debug, Deserialize, Default)]
262pub struct CommandPalette {
263 pub key: Interactive<ContainedLabel>,
264 pub keystroke_spacing: f32,
265}
266
267#[derive(Deserialize, Default)]
268pub struct ContactsPanel {
269 #[serde(flatten)]
270 pub container: ContainerStyle,
271 pub user_query_editor: FieldEditor,
272 pub user_query_editor_height: f32,
273 pub add_contact_button: IconButton,
274 pub header_row: Interactive<ContainedText>,
275 pub contact_row: Interactive<ContainerStyle>,
276 pub project_row: Interactive<ProjectRow>,
277 pub row_height: f32,
278 pub contact_avatar: ImageStyle,
279 pub contact_username: ContainedText,
280 pub contact_button: Interactive<IconButton>,
281 pub contact_button_spacing: f32,
282 pub disabled_button: IconButton,
283 pub tree_branch: Interactive<TreeBranch>,
284 pub private_button: Interactive<IconButton>,
285 pub section_icon_size: f32,
286 pub invite_row: Interactive<ContainedLabel>,
287}
288
289#[derive(Deserialize, Default)]
290pub struct InviteLink {
291 #[serde(flatten)]
292 pub container: ContainerStyle,
293 #[serde(flatten)]
294 pub label: LabelStyle,
295 pub icon: Icon,
296}
297
298#[derive(Deserialize, Default, Clone, Copy)]
299pub struct TreeBranch {
300 pub width: f32,
301 pub color: Color,
302}
303
304#[derive(Deserialize, Default)]
305pub struct ContactFinder {
306 pub row_height: f32,
307 pub contact_avatar: ImageStyle,
308 pub contact_username: ContainerStyle,
309 pub contact_button: IconButton,
310 pub disabled_contact_button: IconButton,
311}
312
313#[derive(Deserialize, Default)]
314pub struct Icon {
315 #[serde(flatten)]
316 pub container: ContainerStyle,
317 pub color: Color,
318 pub width: f32,
319 pub path: String,
320}
321
322#[derive(Deserialize, Clone, Copy, Default)]
323pub struct IconButton {
324 #[serde(flatten)]
325 pub container: ContainerStyle,
326 pub color: Color,
327 pub icon_width: f32,
328 pub button_width: f32,
329}
330
331#[derive(Deserialize, Default)]
332pub struct ProjectRow {
333 #[serde(flatten)]
334 pub container: ContainerStyle,
335 pub name: ContainedText,
336 pub guests: ContainerStyle,
337 pub guest_avatar: ImageStyle,
338 pub guest_avatar_spacing: f32,
339}
340
341#[derive(Deserialize, Default)]
342pub struct ChatMessage {
343 #[serde(flatten)]
344 pub container: ContainerStyle,
345 pub body: TextStyle,
346 pub sender: ContainedText,
347 pub timestamp: ContainedText,
348}
349
350#[derive(Deserialize, Default)]
351pub struct ChannelSelect {
352 #[serde(flatten)]
353 pub container: ContainerStyle,
354 pub header: ChannelName,
355 pub item: ChannelName,
356 pub active_item: ChannelName,
357 pub hovered_item: ChannelName,
358 pub hovered_active_item: ChannelName,
359 pub menu: ContainerStyle,
360}
361
362#[derive(Deserialize, Default)]
363pub struct ChannelName {
364 #[serde(flatten)]
365 pub container: ContainerStyle,
366 pub hash: ContainedText,
367 pub name: TextStyle,
368}
369
370#[derive(Deserialize, Default)]
371pub struct Picker {
372 #[serde(flatten)]
373 pub container: ContainerStyle,
374 pub empty: ContainedLabel,
375 pub input_editor: FieldEditor,
376 pub item: Interactive<ContainedLabel>,
377}
378
379#[derive(Clone, Debug, Deserialize, Default)]
380pub struct ContainedText {
381 #[serde(flatten)]
382 pub container: ContainerStyle,
383 #[serde(flatten)]
384 pub text: TextStyle,
385}
386
387#[derive(Clone, Debug, Deserialize, Default)]
388pub struct ContainedLabel {
389 #[serde(flatten)]
390 pub container: ContainerStyle,
391 #[serde(flatten)]
392 pub label: LabelStyle,
393}
394
395#[derive(Clone, Deserialize, Default)]
396pub struct ProjectDiagnostics {
397 #[serde(flatten)]
398 pub container: ContainerStyle,
399 pub empty_message: TextStyle,
400 pub tab_icon_width: f32,
401 pub tab_icon_spacing: f32,
402 pub tab_summary_spacing: f32,
403}
404
405#[derive(Deserialize, Default)]
406pub struct ContactNotification {
407 pub header_avatar: ImageStyle,
408 pub header_message: ContainedText,
409 pub header_height: f32,
410 pub body_message: ContainedText,
411 pub button: Interactive<ContainedText>,
412 pub dismiss_button: Interactive<IconButton>,
413}
414
415#[derive(Clone, Deserialize, Default)]
416pub struct Editor {
417 pub text_color: Color,
418 #[serde(default)]
419 pub background: Color,
420 pub selection: SelectionStyle,
421 pub gutter_background: Color,
422 pub gutter_padding_factor: f32,
423 pub active_line_background: Color,
424 pub highlighted_line_background: Color,
425 pub rename_fade: f32,
426 pub document_highlight_read_background: Color,
427 pub document_highlight_write_background: Color,
428 pub diff_background_deleted: Color,
429 pub diff_background_inserted: Color,
430 pub line_number: Color,
431 pub line_number_active: Color,
432 pub guest_selections: Vec<SelectionStyle>,
433 pub syntax: Arc<SyntaxTheme>,
434 pub diagnostic_path_header: DiagnosticPathHeader,
435 pub diagnostic_header: DiagnosticHeader,
436 pub error_diagnostic: DiagnosticStyle,
437 pub invalid_error_diagnostic: DiagnosticStyle,
438 pub warning_diagnostic: DiagnosticStyle,
439 pub invalid_warning_diagnostic: DiagnosticStyle,
440 pub information_diagnostic: DiagnosticStyle,
441 pub invalid_information_diagnostic: DiagnosticStyle,
442 pub hint_diagnostic: DiagnosticStyle,
443 pub invalid_hint_diagnostic: DiagnosticStyle,
444 pub autocomplete: AutocompleteStyle,
445 pub code_actions_indicator: Color,
446 pub unnecessary_code_fade: f32,
447}
448
449#[derive(Clone, Deserialize, Default)]
450pub struct DiagnosticPathHeader {
451 #[serde(flatten)]
452 pub container: ContainerStyle,
453 pub filename: ContainedText,
454 pub path: ContainedText,
455 pub text_scale_factor: f32,
456}
457
458#[derive(Clone, Deserialize, Default)]
459pub struct DiagnosticHeader {
460 #[serde(flatten)]
461 pub container: ContainerStyle,
462 pub message: ContainedLabel,
463 pub code: ContainedText,
464 pub text_scale_factor: f32,
465 pub icon_width_factor: f32,
466 pub jump_icon: Interactive<IconButton>,
467}
468
469#[derive(Clone, Deserialize, Default)]
470pub struct DiagnosticStyle {
471 pub message: LabelStyle,
472 #[serde(default)]
473 pub header: ContainerStyle,
474 pub text_scale_factor: f32,
475}
476
477#[derive(Clone, Deserialize, Default)]
478pub struct AutocompleteStyle {
479 #[serde(flatten)]
480 pub container: ContainerStyle,
481 pub item: ContainerStyle,
482 pub selected_item: ContainerStyle,
483 pub hovered_item: ContainerStyle,
484 pub match_highlight: HighlightStyle,
485}
486
487#[derive(Clone, Copy, Default, Deserialize)]
488pub struct SelectionStyle {
489 pub cursor: Color,
490 pub selection: Color,
491}
492
493#[derive(Clone, Deserialize, Default)]
494pub struct FieldEditor {
495 #[serde(flatten)]
496 pub container: ContainerStyle,
497 pub text: TextStyle,
498 #[serde(default)]
499 pub placeholder_text: Option<TextStyle>,
500 pub selection: SelectionStyle,
501}
502
503#[derive(Debug, Default, Clone, Copy)]
504pub struct Interactive<T> {
505 pub default: T,
506 pub hover: Option<T>,
507 pub active: Option<T>,
508 pub active_hover: Option<T>,
509}
510
511impl<T> Interactive<T> {
512 pub fn style_for(&self, state: MouseState, active: bool) -> &T {
513 if active {
514 if state.hovered {
515 self.active_hover
516 .as_ref()
517 .or(self.active.as_ref())
518 .unwrap_or(&self.default)
519 } else {
520 self.active.as_ref().unwrap_or(&self.default)
521 }
522 } else {
523 if state.hovered {
524 self.hover.as_ref().unwrap_or(&self.default)
525 } else {
526 &self.default
527 }
528 }
529 }
530}
531
532impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
533 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
534 where
535 D: serde::Deserializer<'de>,
536 {
537 #[derive(Deserialize)]
538 struct Helper {
539 #[serde(flatten)]
540 default: Value,
541 hover: Option<Value>,
542 active: Option<Value>,
543 active_hover: Option<Value>,
544 }
545
546 let json = Helper::deserialize(deserializer)?;
547
548 let deserialize_state = |state_json: Option<Value>| -> Result<Option<T>, D::Error> {
549 if let Some(mut state_json) = state_json {
550 if let Value::Object(state_json) = &mut state_json {
551 if let Value::Object(default) = &json.default {
552 for (key, value) in default {
553 if !state_json.contains_key(key) {
554 state_json.insert(key.clone(), value.clone());
555 }
556 }
557 }
558 }
559 Ok(Some(
560 serde_json::from_value::<T>(state_json).map_err(serde::de::Error::custom)?,
561 ))
562 } else {
563 Ok(None)
564 }
565 };
566
567 let hover = deserialize_state(json.hover)?;
568 let active = deserialize_state(json.active)?;
569 let active_hover = deserialize_state(json.active_hover)?;
570 let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
571
572 Ok(Interactive {
573 default,
574 hover,
575 active,
576 active_hover,
577 })
578 }
579}
580
581impl Editor {
582 pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
583 let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
584 if style_ix == 0 {
585 &self.selection
586 } else {
587 &self.guest_selections[style_ix - 1]
588 }
589 }
590}
591
592#[derive(Default)]
593pub struct SyntaxTheme {
594 pub highlights: Vec<(String, HighlightStyle)>,
595}
596
597impl SyntaxTheme {
598 pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
599 Self { highlights }
600 }
601}
602
603impl<'de> Deserialize<'de> for SyntaxTheme {
604 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
605 where
606 D: serde::Deserializer<'de>,
607 {
608 let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
609
610 let mut result = Self::new(Vec::new());
611 for (key, style) in syntax_data {
612 match result
613 .highlights
614 .binary_search_by(|(needle, _)| needle.cmp(&key))
615 {
616 Ok(i) | Err(i) => {
617 result.highlights.insert(i, (key, style));
618 }
619 }
620 }
621
622 Ok(result)
623 }
624}