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