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