theme.rs

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