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