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_contact_button: IconButton,
283 pub tree_branch: Interactive<TreeBranch>,
284 pub section_icon_size: f32,
285 pub invite_row: Interactive<ContainedLabel>,
286}
287
288#[derive(Deserialize, Default)]
289pub struct InviteLink {
290 #[serde(flatten)]
291 pub container: ContainerStyle,
292 #[serde(flatten)]
293 pub label: LabelStyle,
294 pub icon: Icon,
295}
296
297#[derive(Deserialize, Default, Clone, Copy)]
298pub struct TreeBranch {
299 pub width: f32,
300 pub color: Color,
301}
302
303#[derive(Deserialize, Default)]
304pub struct ContactFinder {
305 pub row_height: f32,
306 pub contact_avatar: ImageStyle,
307 pub contact_username: ContainerStyle,
308 pub contact_button: IconButton,
309 pub disabled_contact_button: IconButton,
310}
311
312#[derive(Deserialize, Default)]
313pub struct Icon {
314 #[serde(flatten)]
315 pub container: ContainerStyle,
316 pub color: Color,
317 pub width: f32,
318 pub path: String,
319}
320
321#[derive(Clone, Deserialize, Default)]
322pub struct IconButton {
323 #[serde(flatten)]
324 pub container: ContainerStyle,
325 pub color: Color,
326 pub icon_width: f32,
327 pub button_width: f32,
328}
329
330#[derive(Deserialize, Default)]
331pub struct ProjectRow {
332 #[serde(flatten)]
333 pub container: ContainerStyle,
334 pub name: ContainedText,
335 pub guests: ContainerStyle,
336 pub guest_avatar: ImageStyle,
337 pub guest_avatar_spacing: f32,
338}
339
340#[derive(Deserialize, Default)]
341pub struct ChatMessage {
342 #[serde(flatten)]
343 pub container: ContainerStyle,
344 pub body: TextStyle,
345 pub sender: ContainedText,
346 pub timestamp: ContainedText,
347}
348
349#[derive(Deserialize, Default)]
350pub struct ChannelSelect {
351 #[serde(flatten)]
352 pub container: ContainerStyle,
353 pub header: ChannelName,
354 pub item: ChannelName,
355 pub active_item: ChannelName,
356 pub hovered_item: ChannelName,
357 pub hovered_active_item: ChannelName,
358 pub menu: ContainerStyle,
359}
360
361#[derive(Deserialize, Default)]
362pub struct ChannelName {
363 #[serde(flatten)]
364 pub container: ContainerStyle,
365 pub hash: ContainedText,
366 pub name: TextStyle,
367}
368
369#[derive(Deserialize, Default)]
370pub struct Picker {
371 #[serde(flatten)]
372 pub container: ContainerStyle,
373 pub empty: ContainedLabel,
374 pub input_editor: FieldEditor,
375 pub item: Interactive<ContainedLabel>,
376}
377
378#[derive(Clone, Debug, Deserialize, Default)]
379pub struct ContainedText {
380 #[serde(flatten)]
381 pub container: ContainerStyle,
382 #[serde(flatten)]
383 pub text: TextStyle,
384}
385
386#[derive(Clone, Debug, Deserialize, Default)]
387pub struct ContainedLabel {
388 #[serde(flatten)]
389 pub container: ContainerStyle,
390 #[serde(flatten)]
391 pub label: LabelStyle,
392}
393
394#[derive(Clone, Deserialize, Default)]
395pub struct ProjectDiagnostics {
396 #[serde(flatten)]
397 pub container: ContainerStyle,
398 pub empty_message: TextStyle,
399 pub tab_icon_width: f32,
400 pub tab_icon_spacing: f32,
401 pub tab_summary_spacing: f32,
402}
403
404#[derive(Deserialize, Default)]
405pub struct ContactNotification {
406 pub header_avatar: ImageStyle,
407 pub header_message: ContainedText,
408 pub header_height: f32,
409 pub body_message: ContainedText,
410 pub button: Interactive<ContainedText>,
411 pub dismiss_button: Interactive<IconButton>,
412}
413
414#[derive(Clone, Deserialize, Default)]
415pub struct Editor {
416 pub text_color: Color,
417 #[serde(default)]
418 pub background: Color,
419 pub selection: SelectionStyle,
420 pub gutter_background: Color,
421 pub gutter_padding_factor: f32,
422 pub active_line_background: Color,
423 pub highlighted_line_background: Color,
424 pub rename_fade: f32,
425 pub document_highlight_read_background: Color,
426 pub document_highlight_write_background: Color,
427 pub diff_background_deleted: Color,
428 pub diff_background_inserted: Color,
429 pub line_number: Color,
430 pub line_number_active: Color,
431 pub guest_selections: Vec<SelectionStyle>,
432 pub syntax: Arc<SyntaxTheme>,
433 pub diagnostic_path_header: DiagnosticPathHeader,
434 pub diagnostic_header: DiagnosticHeader,
435 pub error_diagnostic: DiagnosticStyle,
436 pub invalid_error_diagnostic: DiagnosticStyle,
437 pub warning_diagnostic: DiagnosticStyle,
438 pub invalid_warning_diagnostic: DiagnosticStyle,
439 pub information_diagnostic: DiagnosticStyle,
440 pub invalid_information_diagnostic: DiagnosticStyle,
441 pub hint_diagnostic: DiagnosticStyle,
442 pub invalid_hint_diagnostic: DiagnosticStyle,
443 pub autocomplete: AutocompleteStyle,
444 pub code_actions_indicator: Color,
445 pub unnecessary_code_fade: f32,
446}
447
448#[derive(Clone, Deserialize, Default)]
449pub struct DiagnosticPathHeader {
450 #[serde(flatten)]
451 pub container: ContainerStyle,
452 pub filename: ContainedText,
453 pub path: ContainedText,
454 pub text_scale_factor: f32,
455}
456
457#[derive(Clone, Deserialize, Default)]
458pub struct DiagnosticHeader {
459 #[serde(flatten)]
460 pub container: ContainerStyle,
461 pub message: ContainedLabel,
462 pub code: ContainedText,
463 pub text_scale_factor: f32,
464 pub icon_width_factor: f32,
465 pub jump_icon: Interactive<IconButton>,
466}
467
468#[derive(Clone, Deserialize, Default)]
469pub struct DiagnosticStyle {
470 pub message: LabelStyle,
471 #[serde(default)]
472 pub header: ContainerStyle,
473 pub text_scale_factor: f32,
474}
475
476#[derive(Clone, Deserialize, Default)]
477pub struct AutocompleteStyle {
478 #[serde(flatten)]
479 pub container: ContainerStyle,
480 pub item: ContainerStyle,
481 pub selected_item: ContainerStyle,
482 pub hovered_item: ContainerStyle,
483 pub match_highlight: HighlightStyle,
484}
485
486#[derive(Clone, Copy, Default, Deserialize)]
487pub struct SelectionStyle {
488 pub cursor: Color,
489 pub selection: Color,
490}
491
492#[derive(Clone, Deserialize, Default)]
493pub struct FieldEditor {
494 #[serde(flatten)]
495 pub container: ContainerStyle,
496 pub text: TextStyle,
497 #[serde(default)]
498 pub placeholder_text: Option<TextStyle>,
499 pub selection: SelectionStyle,
500}
501
502#[derive(Debug, Default, Clone, Copy)]
503pub struct Interactive<T> {
504 pub default: T,
505 pub hover: Option<T>,
506 pub active: Option<T>,
507 pub active_hover: Option<T>,
508}
509
510impl<T> Interactive<T> {
511 pub fn style_for(&self, state: MouseState, active: bool) -> &T {
512 if active {
513 if state.hovered {
514 self.active_hover
515 .as_ref()
516 .or(self.active.as_ref())
517 .unwrap_or(&self.default)
518 } else {
519 self.active.as_ref().unwrap_or(&self.default)
520 }
521 } else {
522 if state.hovered {
523 self.hover.as_ref().unwrap_or(&self.default)
524 } else {
525 &self.default
526 }
527 }
528 }
529}
530
531impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
532 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
533 where
534 D: serde::Deserializer<'de>,
535 {
536 #[derive(Deserialize)]
537 struct Helper {
538 #[serde(flatten)]
539 default: Value,
540 hover: Option<Value>,
541 active: Option<Value>,
542 active_hover: Option<Value>,
543 }
544
545 let json = Helper::deserialize(deserializer)?;
546
547 let deserialize_state = |state_json: Option<Value>| -> Result<Option<T>, D::Error> {
548 if let Some(mut state_json) = state_json {
549 if let Value::Object(state_json) = &mut state_json {
550 if let Value::Object(default) = &json.default {
551 for (key, value) in default {
552 if !state_json.contains_key(key) {
553 state_json.insert(key.clone(), value.clone());
554 }
555 }
556 }
557 }
558 Ok(Some(
559 serde_json::from_value::<T>(state_json).map_err(serde::de::Error::custom)?,
560 ))
561 } else {
562 Ok(None)
563 }
564 };
565
566 let hover = deserialize_state(json.hover)?;
567 let active = deserialize_state(json.active)?;
568 let active_hover = deserialize_state(json.active_hover)?;
569 let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
570
571 Ok(Interactive {
572 default,
573 hover,
574 active,
575 active_hover,
576 })
577 }
578}
579
580impl Editor {
581 pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
582 let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
583 if style_ix == 0 {
584 &self.selection
585 } else {
586 &self.guest_selections[style_ix - 1]
587 }
588 }
589}
590
591#[derive(Default)]
592pub struct SyntaxTheme {
593 pub highlights: Vec<(String, HighlightStyle)>,
594}
595
596impl SyntaxTheme {
597 pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
598 Self { highlights }
599 }
600}
601
602impl<'de> Deserialize<'de> for SyntaxTheme {
603 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
604 where
605 D: serde::Deserializer<'de>,
606 {
607 let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
608
609 let mut result = Self::new(Vec::new());
610 for (key, style) in syntax_data {
611 match result
612 .highlights
613 .binary_search_by(|(needle, _)| needle.cmp(&key))
614 {
615 Ok(i) | Err(i) => {
616 result.highlights.insert(i, (key, style));
617 }
618 }
619 }
620
621 Ok(result)
622 }
623}