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 pub hover_popover: ContainerStyle,
448}
449
450#[derive(Clone, Deserialize, Default)]
451pub struct DiagnosticPathHeader {
452 #[serde(flatten)]
453 pub container: ContainerStyle,
454 pub filename: ContainedText,
455 pub path: ContainedText,
456 pub text_scale_factor: f32,
457}
458
459#[derive(Clone, Deserialize, Default)]
460pub struct DiagnosticHeader {
461 #[serde(flatten)]
462 pub container: ContainerStyle,
463 pub message: ContainedLabel,
464 pub code: ContainedText,
465 pub text_scale_factor: f32,
466 pub icon_width_factor: f32,
467 pub jump_icon: Interactive<IconButton>,
468}
469
470#[derive(Clone, Deserialize, Default)]
471pub struct DiagnosticStyle {
472 pub message: LabelStyle,
473 #[serde(default)]
474 pub header: ContainerStyle,
475 pub text_scale_factor: f32,
476}
477
478#[derive(Clone, Deserialize, Default)]
479pub struct AutocompleteStyle {
480 #[serde(flatten)]
481 pub container: ContainerStyle,
482 pub item: ContainerStyle,
483 pub selected_item: ContainerStyle,
484 pub hovered_item: ContainerStyle,
485 pub match_highlight: HighlightStyle,
486}
487
488#[derive(Clone, Copy, Default, Deserialize)]
489pub struct SelectionStyle {
490 pub cursor: Color,
491 pub selection: Color,
492}
493
494#[derive(Clone, Deserialize, Default)]
495pub struct FieldEditor {
496 #[serde(flatten)]
497 pub container: ContainerStyle,
498 pub text: TextStyle,
499 #[serde(default)]
500 pub placeholder_text: Option<TextStyle>,
501 pub selection: SelectionStyle,
502}
503
504#[derive(Debug, Default, Clone, Copy)]
505pub struct Interactive<T> {
506 pub default: T,
507 pub hover: Option<T>,
508 pub active: Option<T>,
509 pub active_hover: Option<T>,
510}
511
512impl<T> Interactive<T> {
513 pub fn style_for(&self, state: MouseState, active: bool) -> &T {
514 if active {
515 if state.hovered {
516 self.active_hover
517 .as_ref()
518 .or(self.active.as_ref())
519 .unwrap_or(&self.default)
520 } else {
521 self.active.as_ref().unwrap_or(&self.default)
522 }
523 } else {
524 if state.hovered {
525 self.hover.as_ref().unwrap_or(&self.default)
526 } else {
527 &self.default
528 }
529 }
530 }
531}
532
533impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
534 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
535 where
536 D: serde::Deserializer<'de>,
537 {
538 #[derive(Deserialize)]
539 struct Helper {
540 #[serde(flatten)]
541 default: Value,
542 hover: Option<Value>,
543 active: Option<Value>,
544 active_hover: Option<Value>,
545 }
546
547 let json = Helper::deserialize(deserializer)?;
548
549 let deserialize_state = |state_json: Option<Value>| -> Result<Option<T>, D::Error> {
550 if let Some(mut state_json) = state_json {
551 if let Value::Object(state_json) = &mut state_json {
552 if let Value::Object(default) = &json.default {
553 for (key, value) in default {
554 if !state_json.contains_key(key) {
555 state_json.insert(key.clone(), value.clone());
556 }
557 }
558 }
559 }
560 Ok(Some(
561 serde_json::from_value::<T>(state_json).map_err(serde::de::Error::custom)?,
562 ))
563 } else {
564 Ok(None)
565 }
566 };
567
568 let hover = deserialize_state(json.hover)?;
569 let active = deserialize_state(json.active)?;
570 let active_hover = deserialize_state(json.active_hover)?;
571 let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
572
573 Ok(Interactive {
574 default,
575 hover,
576 active,
577 active_hover,
578 })
579 }
580}
581
582impl Editor {
583 pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
584 let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
585 if style_ix == 0 {
586 &self.selection
587 } else {
588 &self.guest_selections[style_ix - 1]
589 }
590 }
591}
592
593#[derive(Default)]
594pub struct SyntaxTheme {
595 pub highlights: Vec<(String, HighlightStyle)>,
596}
597
598impl SyntaxTheme {
599 pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
600 Self { highlights }
601 }
602}
603
604impl<'de> Deserialize<'de> for SyntaxTheme {
605 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
606 where
607 D: serde::Deserializer<'de>,
608 {
609 let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
610
611 let mut result = Self::new(Vec::new());
612 for (key, style) in syntax_data {
613 match result
614 .highlights
615 .binary_search_by(|(needle, _)| needle.cmp(&key))
616 {
617 Ok(i) | Err(i) => {
618 result.highlights.insert(i, (key, style));
619 }
620 }
621 }
622
623 Ok(result)
624 }
625}