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