theme.rs

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