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