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