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