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