From a347c4def7f1d88f9c4fbcd1a82cd5d465a8d386 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 31 Oct 2024 11:40:38 -0400 Subject: [PATCH] Add theme preview (#20039) This PR adds a theme preview tab to help get an at a glance overview of the styles in a theme. ![CleanShot 2024-10-31 at 11 27 18@2x](https://github.com/user-attachments/assets/798e97cf-9f80-4994-b2fd-ac1dcd58e4d9) You can open it using `debug: open theme preview`. The next major theme preview PR will move this into it's own crate, as it will grow substantially as we add content. Next for theme preview: - Update layout to two columns, with controls on the right for selecting theme, layer/elevation-index, etc. - Cover more UI elements in preview - Display theme colors in a more helpful way - Add syntax & markdown previews Release Notes: - Added a way to preview the current theme's styles with the `debug: open theme preview` command. --- Cargo.lock | 1 + crates/gpui/src/color.rs | 15 +- crates/theme/Cargo.toml | 1 + crates/theme/src/styles/colors.rs | 237 +++++++++++++- crates/ui/src/styles/elevation.rs | 35 +- crates/ui/src/utils.rs | 2 + crates/ui/src/utils/color_contrast.rs | 70 ++++ crates/workspace/src/theme_preview.rs | 454 ++++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 2 + 9 files changed, 813 insertions(+), 4 deletions(-) create mode 100644 crates/ui/src/utils/color_contrast.rs create mode 100644 crates/workspace/src/theme_preview.rs diff --git a/Cargo.lock b/Cargo.lock index 10e46225342be..bea5b56126a61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12093,6 +12093,7 @@ dependencies = [ "serde_json_lenient", "serde_repr", "settings", + "strum 0.25.0", "util", "uuid", ] diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 24edace5936c8..6a1f375b651d4 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Context}; use serde::de::{self, Deserialize, Deserializer, Visitor}; use std::{ - fmt, + fmt::{self, Display, Formatter}, hash::{Hash, Hasher}, }; @@ -279,6 +279,19 @@ impl Hash for Hsla { } } +impl Display for Hsla { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "hsla({:.2}, {:.2}%, {:.2}%, {:.2})", + self.h * 360., + self.s * 100., + self.l * 100., + self.a + ) + } +} + /// Construct an [`Hsla`] object from plain values pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { Hsla { diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index b751bea727c75..c3e3a197cbf91 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -35,6 +35,7 @@ serde_json.workspace = true serde_json_lenient.workspace = true serde_repr.workspace = true settings.workspace = true +strum.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index d9ea58813c6af..99c1656215080 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -1,11 +1,13 @@ #![allow(missing_docs)] -use gpui::{Hsla, WindowBackgroundAppearance}; +use gpui::{Hsla, SharedString, WindowBackgroundAppearance, WindowContext}; use refineable::Refineable; use std::sync::Arc; +use strum::{AsRefStr, EnumIter, IntoEnumIterator}; use crate::{ - AccentColors, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors, + AccentColors, ActiveTheme, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, + SystemColors, }; #[derive(Refineable, Clone, Debug, PartialEq)] @@ -249,6 +251,237 @@ pub struct ThemeColors { pub link_text_hover: Hsla, } +#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum ThemeColorField { + Border, + BorderVariant, + BorderFocused, + BorderSelected, + BorderTransparent, + BorderDisabled, + ElevatedSurfaceBackground, + SurfaceBackground, + Background, + ElementBackground, + ElementHover, + ElementActive, + ElementSelected, + ElementDisabled, + DropTargetBackground, + GhostElementBackground, + GhostElementHover, + GhostElementActive, + GhostElementSelected, + GhostElementDisabled, + Text, + TextMuted, + TextPlaceholder, + TextDisabled, + TextAccent, + Icon, + IconMuted, + IconDisabled, + IconPlaceholder, + IconAccent, + StatusBarBackground, + TitleBarBackground, + TitleBarInactiveBackground, + ToolbarBackground, + TabBarBackground, + TabInactiveBackground, + TabActiveBackground, + SearchMatchBackground, + PanelBackground, + PanelFocusedBorder, + PanelIndentGuide, + PanelIndentGuideHover, + PanelIndentGuideActive, + PaneFocusedBorder, + PaneGroupBorder, + ScrollbarThumbBackground, + ScrollbarThumbHoverBackground, + ScrollbarThumbBorder, + ScrollbarTrackBackground, + ScrollbarTrackBorder, + EditorForeground, + EditorBackground, + EditorGutterBackground, + EditorSubheaderBackground, + EditorActiveLineBackground, + EditorHighlightedLineBackground, + EditorLineNumber, + EditorActiveLineNumber, + EditorInvisible, + EditorWrapGuide, + EditorActiveWrapGuide, + EditorIndentGuide, + EditorIndentGuideActive, + EditorDocumentHighlightReadBackground, + EditorDocumentHighlightWriteBackground, + EditorDocumentHighlightBracketBackground, + TerminalBackground, + TerminalForeground, + TerminalBrightForeground, + TerminalDimForeground, + TerminalAnsiBackground, + TerminalAnsiBlack, + TerminalAnsiBrightBlack, + TerminalAnsiDimBlack, + TerminalAnsiRed, + TerminalAnsiBrightRed, + TerminalAnsiDimRed, + TerminalAnsiGreen, + TerminalAnsiBrightGreen, + TerminalAnsiDimGreen, + TerminalAnsiYellow, + TerminalAnsiBrightYellow, + TerminalAnsiDimYellow, + TerminalAnsiBlue, + TerminalAnsiBrightBlue, + TerminalAnsiDimBlue, + TerminalAnsiMagenta, + TerminalAnsiBrightMagenta, + TerminalAnsiDimMagenta, + TerminalAnsiCyan, + TerminalAnsiBrightCyan, + TerminalAnsiDimCyan, + TerminalAnsiWhite, + TerminalAnsiBrightWhite, + TerminalAnsiDimWhite, + LinkTextHover, +} + +impl ThemeColors { + pub fn color(&self, field: ThemeColorField) -> Hsla { + match field { + ThemeColorField::Border => self.border, + ThemeColorField::BorderVariant => self.border_variant, + ThemeColorField::BorderFocused => self.border_focused, + ThemeColorField::BorderSelected => self.border_selected, + ThemeColorField::BorderTransparent => self.border_transparent, + ThemeColorField::BorderDisabled => self.border_disabled, + ThemeColorField::ElevatedSurfaceBackground => self.elevated_surface_background, + ThemeColorField::SurfaceBackground => self.surface_background, + ThemeColorField::Background => self.background, + ThemeColorField::ElementBackground => self.element_background, + ThemeColorField::ElementHover => self.element_hover, + ThemeColorField::ElementActive => self.element_active, + ThemeColorField::ElementSelected => self.element_selected, + ThemeColorField::ElementDisabled => self.element_disabled, + ThemeColorField::DropTargetBackground => self.drop_target_background, + ThemeColorField::GhostElementBackground => self.ghost_element_background, + ThemeColorField::GhostElementHover => self.ghost_element_hover, + ThemeColorField::GhostElementActive => self.ghost_element_active, + ThemeColorField::GhostElementSelected => self.ghost_element_selected, + ThemeColorField::GhostElementDisabled => self.ghost_element_disabled, + ThemeColorField::Text => self.text, + ThemeColorField::TextMuted => self.text_muted, + ThemeColorField::TextPlaceholder => self.text_placeholder, + ThemeColorField::TextDisabled => self.text_disabled, + ThemeColorField::TextAccent => self.text_accent, + ThemeColorField::Icon => self.icon, + ThemeColorField::IconMuted => self.icon_muted, + ThemeColorField::IconDisabled => self.icon_disabled, + ThemeColorField::IconPlaceholder => self.icon_placeholder, + ThemeColorField::IconAccent => self.icon_accent, + ThemeColorField::StatusBarBackground => self.status_bar_background, + ThemeColorField::TitleBarBackground => self.title_bar_background, + ThemeColorField::TitleBarInactiveBackground => self.title_bar_inactive_background, + ThemeColorField::ToolbarBackground => self.toolbar_background, + ThemeColorField::TabBarBackground => self.tab_bar_background, + ThemeColorField::TabInactiveBackground => self.tab_inactive_background, + ThemeColorField::TabActiveBackground => self.tab_active_background, + ThemeColorField::SearchMatchBackground => self.search_match_background, + ThemeColorField::PanelBackground => self.panel_background, + ThemeColorField::PanelFocusedBorder => self.panel_focused_border, + ThemeColorField::PanelIndentGuide => self.panel_indent_guide, + ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover, + ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active, + ThemeColorField::PaneFocusedBorder => self.pane_focused_border, + ThemeColorField::PaneGroupBorder => self.pane_group_border, + ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background, + ThemeColorField::ScrollbarThumbHoverBackground => self.scrollbar_thumb_hover_background, + ThemeColorField::ScrollbarThumbBorder => self.scrollbar_thumb_border, + ThemeColorField::ScrollbarTrackBackground => self.scrollbar_track_background, + ThemeColorField::ScrollbarTrackBorder => self.scrollbar_track_border, + ThemeColorField::EditorForeground => self.editor_foreground, + ThemeColorField::EditorBackground => self.editor_background, + ThemeColorField::EditorGutterBackground => self.editor_gutter_background, + ThemeColorField::EditorSubheaderBackground => self.editor_subheader_background, + ThemeColorField::EditorActiveLineBackground => self.editor_active_line_background, + ThemeColorField::EditorHighlightedLineBackground => { + self.editor_highlighted_line_background + } + ThemeColorField::EditorLineNumber => self.editor_line_number, + ThemeColorField::EditorActiveLineNumber => self.editor_active_line_number, + ThemeColorField::EditorInvisible => self.editor_invisible, + ThemeColorField::EditorWrapGuide => self.editor_wrap_guide, + ThemeColorField::EditorActiveWrapGuide => self.editor_active_wrap_guide, + ThemeColorField::EditorIndentGuide => self.editor_indent_guide, + ThemeColorField::EditorIndentGuideActive => self.editor_indent_guide_active, + ThemeColorField::EditorDocumentHighlightReadBackground => { + self.editor_document_highlight_read_background + } + ThemeColorField::EditorDocumentHighlightWriteBackground => { + self.editor_document_highlight_write_background + } + ThemeColorField::EditorDocumentHighlightBracketBackground => { + self.editor_document_highlight_bracket_background + } + ThemeColorField::TerminalBackground => self.terminal_background, + ThemeColorField::TerminalForeground => self.terminal_foreground, + ThemeColorField::TerminalBrightForeground => self.terminal_bright_foreground, + ThemeColorField::TerminalDimForeground => self.terminal_dim_foreground, + ThemeColorField::TerminalAnsiBackground => self.terminal_ansi_background, + ThemeColorField::TerminalAnsiBlack => self.terminal_ansi_black, + ThemeColorField::TerminalAnsiBrightBlack => self.terminal_ansi_bright_black, + ThemeColorField::TerminalAnsiDimBlack => self.terminal_ansi_dim_black, + ThemeColorField::TerminalAnsiRed => self.terminal_ansi_red, + ThemeColorField::TerminalAnsiBrightRed => self.terminal_ansi_bright_red, + ThemeColorField::TerminalAnsiDimRed => self.terminal_ansi_dim_red, + ThemeColorField::TerminalAnsiGreen => self.terminal_ansi_green, + ThemeColorField::TerminalAnsiBrightGreen => self.terminal_ansi_bright_green, + ThemeColorField::TerminalAnsiDimGreen => self.terminal_ansi_dim_green, + ThemeColorField::TerminalAnsiYellow => self.terminal_ansi_yellow, + ThemeColorField::TerminalAnsiBrightYellow => self.terminal_ansi_bright_yellow, + ThemeColorField::TerminalAnsiDimYellow => self.terminal_ansi_dim_yellow, + ThemeColorField::TerminalAnsiBlue => self.terminal_ansi_blue, + ThemeColorField::TerminalAnsiBrightBlue => self.terminal_ansi_bright_blue, + ThemeColorField::TerminalAnsiDimBlue => self.terminal_ansi_dim_blue, + ThemeColorField::TerminalAnsiMagenta => self.terminal_ansi_magenta, + ThemeColorField::TerminalAnsiBrightMagenta => self.terminal_ansi_bright_magenta, + ThemeColorField::TerminalAnsiDimMagenta => self.terminal_ansi_dim_magenta, + ThemeColorField::TerminalAnsiCyan => self.terminal_ansi_cyan, + ThemeColorField::TerminalAnsiBrightCyan => self.terminal_ansi_bright_cyan, + ThemeColorField::TerminalAnsiDimCyan => self.terminal_ansi_dim_cyan, + ThemeColorField::TerminalAnsiWhite => self.terminal_ansi_white, + ThemeColorField::TerminalAnsiBrightWhite => self.terminal_ansi_bright_white, + ThemeColorField::TerminalAnsiDimWhite => self.terminal_ansi_dim_white, + ThemeColorField::LinkTextHover => self.link_text_hover, + } + } + + pub fn iter(&self) -> impl Iterator + '_ { + ThemeColorField::iter().map(move |field| (field, self.color(field))) + } + + pub fn to_vec(&self) -> Vec<(ThemeColorField, Hsla)> { + self.iter().collect() + } +} + +pub fn all_theme_colors(cx: &WindowContext) -> Vec<(Hsla, SharedString)> { + let theme = cx.theme(); + ThemeColorField::iter() + .map(|field| { + let color = theme.colors().color(field); + let name = field.as_ref().to_string(); + (color, SharedString::from(name)) + }) + .collect() +} + #[derive(Refineable, Clone, PartialEq)] pub struct ThemeStyles { /// The background appearance of the window. diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 722111b46c49f..932fd3a944af4 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -1,5 +1,8 @@ -use gpui::{hsla, point, px, BoxShadow}; +use std::fmt::{self, Display, Formatter}; + +use gpui::{hsla, point, px, BoxShadow, Hsla, WindowContext}; use smallvec::{smallvec, SmallVec}; +use theme::ActiveTheme; /// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons. /// @@ -15,6 +18,8 @@ pub enum ElevationIndex { Background, /// The primary surface – Contains panels, panes, containers, etc. Surface, + /// The same elevation as the primary surface, but used for the editable areas, like buffers + EditorSurface, /// A surface that is elevated above the primary surface. but below washes, models, and dragged elements. ElevatedSurface, /// A surface that is above all non-modal surfaces, and separates the app from focused intents, like dialogs, alerts, modals, etc. @@ -25,11 +30,26 @@ pub enum ElevationIndex { DraggedElement, } +impl Display for ElevationIndex { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + ElevationIndex::Background => write!(f, "Background"), + ElevationIndex::Surface => write!(f, "Surface"), + ElevationIndex::EditorSurface => write!(f, "Editor Surface"), + ElevationIndex::ElevatedSurface => write!(f, "Elevated Surface"), + ElevationIndex::Wash => write!(f, "Wash"), + ElevationIndex::ModalSurface => write!(f, "Modal Surface"), + ElevationIndex::DraggedElement => write!(f, "Dragged Element"), + } + } +} + impl ElevationIndex { /// Returns an appropriate shadow for the given elevation index. pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { match self { ElevationIndex::Surface => smallvec![], + ElevationIndex::EditorSurface => smallvec![], ElevationIndex::ElevatedSurface => smallvec![BoxShadow { color: hsla(0., 0., 0., 0.12), @@ -62,4 +82,17 @@ impl ElevationIndex { _ => smallvec![], } } + + /// Returns the background color for the given elevation index. + pub fn bg(&self, cx: &WindowContext) -> Hsla { + match self { + ElevationIndex::Background => cx.theme().colors().background, + ElevationIndex::Surface => cx.theme().colors().surface_background, + ElevationIndex::EditorSurface => cx.theme().colors().editor_background, + ElevationIndex::ElevatedSurface => cx.theme().colors().elevated_surface_background, + ElevationIndex::Wash => gpui::transparent_black(), + ElevationIndex::ModalSurface => cx.theme().colors().elevated_surface_background, + ElevationIndex::DraggedElement => gpui::transparent_black(), + } + } } diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index b68d6e6bbdba6..25477194dc363 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -1,7 +1,9 @@ //! UI-related utilities +mod color_contrast; mod format_distance; mod with_rem_size; +pub use color_contrast::*; pub use format_distance::*; pub use with_rem_size::*; diff --git a/crates/ui/src/utils/color_contrast.rs b/crates/ui/src/utils/color_contrast.rs new file mode 100644 index 0000000000000..2a6b4bf28111d --- /dev/null +++ b/crates/ui/src/utils/color_contrast.rs @@ -0,0 +1,70 @@ +use gpui::{Hsla, Rgba}; + +/// Calculates the contrast ratio between two colors according to WCAG 2.0 standards. +/// +/// The formula used is: +/// (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter of the two luminances and L2 is the darker. +/// +/// Returns a float representing the contrast ratio. A higher value indicates more contrast. +/// The range of the returned value is 1 to 21 (commonly written as 1:1 to 21:1). +pub fn calculate_contrast_ratio(fg: Hsla, bg: Hsla) -> f32 { + let l1 = relative_luminance(fg); + let l2 = relative_luminance(bg); + + let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) }; + + (lighter + 0.05) / (darker + 0.05) +} + +/// Calculates the relative luminance of a color. +/// +/// The relative luminance is the relative brightness of any point in a colorspace, +/// normalized to 0 for darkest black and 1 for lightest white. +fn relative_luminance(color: Hsla) -> f32 { + let rgba: Rgba = color.into(); + let r = linearize(rgba.r); + let g = linearize(rgba.g); + let b = linearize(rgba.b); + + 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +/// Linearizes an RGB component. +fn linearize(component: f32) -> f32 { + if component <= 0.03928 { + component / 12.92 + } else { + ((component + 0.055) / 1.055).powf(2.4) + } +} + +#[cfg(test)] +mod tests { + use gpui::hsla; + + use super::*; + + // Test the contrast ratio formula with some common color combinations to + // prevent regressions in either the color conversions or the formula itself. + #[test] + fn test_contrast_ratio_formula() { + // White on Black (should be close to 21:1) + let white = hsla(0.0, 0.0, 1.0, 1.0); + let black = hsla(0.0, 0.0, 0.0, 1.0); + assert!((calculate_contrast_ratio(white, black) - 21.0).abs() < 0.1); + + // Black on White (should be close to 21:1) + assert!((calculate_contrast_ratio(black, white) - 21.0).abs() < 0.1); + + // Mid-gray on Black (should be close to 5.32:1) + let mid_gray = hsla(0.0, 0.0, 0.5, 1.0); + assert!((calculate_contrast_ratio(mid_gray, black) - 5.32).abs() < 0.1); + + // White on Mid-gray (should be close to 3.95:1) + assert!((calculate_contrast_ratio(white, mid_gray) - 3.95).abs() < 0.1); + + // Same color (should be 1:1) + let red = hsla(0.0, 1.0, 0.5, 1.0); + assert!((calculate_contrast_ratio(red, red) - 1.0).abs() < 0.01); + } +} diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs new file mode 100644 index 0000000000000..620b66d7021dc --- /dev/null +++ b/crates/workspace/src/theme_preview.rs @@ -0,0 +1,454 @@ +#![allow(unused, dead_code)] +use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla}; +use theme::all_theme_colors; +use ui::{ + prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar, + AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, ElevationIndex, Facepile, + TintColor, Tooltip, +}; + +use crate::{Item, Workspace}; + +actions!(debug, [OpenThemePreview]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &OpenThemePreview, cx| { + let theme_preview = cx.new_view(ThemePreview::new); + workspace.add_item_to_active_pane(Box::new(theme_preview), None, true, cx) + }); + }) + .detach(); +} + +struct ThemePreview { + focus_handle: FocusHandle, +} + +impl ThemePreview { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } +} + +impl EventEmitter<()> for ThemePreview {} + +impl FocusableView for ThemePreview { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} +impl ThemePreview {} + +impl Item for ThemePreview { + type Event = (); + + fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {} + + fn tab_content_text(&self, cx: &WindowContext) -> Option { + let name = cx.theme().name.clone(); + Some(format!("{} Preview", name).into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.new_view(Self::new)) + } +} + +const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4"; + +impl ThemePreview { + fn preview_bg(cx: &WindowContext) -> Hsla { + cx.theme().colors().editor_background + } + + fn render_avatars(&self, cx: &ViewContext) -> impl IntoElement { + v_flex() + .gap_1() + .child( + Headline::new("Avatars") + .size(HeadlineSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .items_start() + .gap_4() + .child(Avatar::new(AVATAR_URL).size(px(24.))) + .child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true)) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)), + ) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)), + ) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), + ) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), + ) + .child( + Facepile::empty() + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ) + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ) + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ) + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ), + ), + ) + } + + fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext) -> impl IntoElement { + v_flex() + .gap_1() + .child( + Headline::new("Buttons") + .size(HeadlineSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .items_start() + .gap_px() + .child( + IconButton::new("icon_button_transparent", IconName::Check) + .style(ButtonStyle::Transparent), + ) + .child( + IconButton::new("icon_button_subtle", IconName::Check) + .style(ButtonStyle::Subtle), + ) + .child( + IconButton::new("icon_button_filled", IconName::Check) + .style(ButtonStyle::Filled), + ) + .child( + IconButton::new("icon_button_selected_accent", IconName::Check) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(true), + ) + .child(IconButton::new("icon_button_selected", IconName::Check).selected(true)) + .child( + IconButton::new("icon_button_positive", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Positive)), + ) + .child( + IconButton::new("icon_button_warning", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Warning)), + ) + .child( + IconButton::new("icon_button_negative", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Negative)), + ), + ) + .child( + h_flex() + .gap_px() + .child( + Button::new("button_transparent", "Transparent") + .style(ButtonStyle::Transparent), + ) + .child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle)) + .child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled)) + .child( + Button::new("button_selected", "Selected") + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(true), + ) + .child( + Button::new("button_selected_tinted", "Selected (Tinted)") + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(true), + ) + .child( + Button::new("button_positive", "Tint::Positive") + .style(ButtonStyle::Tinted(TintColor::Positive)), + ) + .child( + Button::new("button_warning", "Tint::Warning") + .style(ButtonStyle::Tinted(TintColor::Warning)), + ) + .child( + Button::new("button_negative", "Tint::Negative") + .style(ButtonStyle::Tinted(TintColor::Negative)), + ), + ) + } + + fn render_text(&self, layer: ElevationIndex, cx: &ViewContext) -> impl IntoElement { + let bg = layer.bg(cx); + + let label_with_contrast = |label: &str, fg: Hsla| { + let contrast = calculate_contrast_ratio(fg, bg); + format!("{} ({:.2})", label, contrast) + }; + + v_flex() + .gap_1() + .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted)) + .child( + h_flex() + .items_start() + .gap_4() + .child( + v_flex() + .gap_1() + .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted)) + .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge)) + .child(Headline::new("Large Headline").size(HeadlineSize::Large)) + .child(Headline::new("Medium Headline").size(HeadlineSize::Medium)) + .child(Headline::new("Small Headline").size(HeadlineSize::Small)) + .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)), + ) + .child( + v_flex() + .gap_1() + .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted)) + .child( + Label::new(label_with_contrast( + "Default Text", + Color::Default.color(cx), + )) + .color(Color::Default), + ) + .child( + Label::new(label_with_contrast( + "Accent Text", + Color::Accent.color(cx), + )) + .color(Color::Accent), + ) + .child( + Label::new(label_with_contrast( + "Conflict Text", + Color::Conflict.color(cx), + )) + .color(Color::Conflict), + ) + .child( + Label::new(label_with_contrast( + "Created Text", + Color::Created.color(cx), + )) + .color(Color::Created), + ) + .child( + Label::new(label_with_contrast( + "Deleted Text", + Color::Deleted.color(cx), + )) + .color(Color::Deleted), + ) + .child( + Label::new(label_with_contrast( + "Disabled Text", + Color::Disabled.color(cx), + )) + .color(Color::Disabled), + ) + .child( + Label::new(label_with_contrast( + "Error Text", + Color::Error.color(cx), + )) + .color(Color::Error), + ) + .child( + Label::new(label_with_contrast( + "Hidden Text", + Color::Hidden.color(cx), + )) + .color(Color::Hidden), + ) + .child( + Label::new(label_with_contrast( + "Hint Text", + Color::Hint.color(cx), + )) + .color(Color::Hint), + ) + .child( + Label::new(label_with_contrast( + "Ignored Text", + Color::Ignored.color(cx), + )) + .color(Color::Ignored), + ) + .child( + Label::new(label_with_contrast( + "Info Text", + Color::Info.color(cx), + )) + .color(Color::Info), + ) + .child( + Label::new(label_with_contrast( + "Modified Text", + Color::Modified.color(cx), + )) + .color(Color::Modified), + ) + .child( + Label::new(label_with_contrast( + "Muted Text", + Color::Muted.color(cx), + )) + .color(Color::Muted), + ) + .child( + Label::new(label_with_contrast( + "Placeholder Text", + Color::Placeholder.color(cx), + )) + .color(Color::Placeholder), + ) + .child( + Label::new(label_with_contrast( + "Selected Text", + Color::Selected.color(cx), + )) + .color(Color::Selected), + ) + .child( + Label::new(label_with_contrast( + "Success Text", + Color::Success.color(cx), + )) + .color(Color::Success), + ) + .child( + Label::new(label_with_contrast( + "Warning Text", + Color::Warning.color(cx), + )) + .color(Color::Warning), + ) + ) + .child( + v_flex() + .gap_1() + .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted)) + .child( + div().max_w(px(200.)).child( + "This is a longer piece of text that should wrap to multiple lines. It demonstrates how text behaves when it exceeds the width of its container." + )) + ) + ) + } + + fn render_colors(&self, layer: ElevationIndex, cx: &ViewContext) -> impl IntoElement { + let bg = layer.bg(cx); + let all_colors = all_theme_colors(cx); + + v_flex() + .gap_1() + .child( + Headline::new("Colors") + .size(HeadlineSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .flex_wrap() + .gap_1() + .children(all_colors.into_iter().map(|(color, name)| { + let id = ElementId::Name(format!("{:?}-preview", color).into()); + let name = name.clone(); + div().size_8().flex_none().child( + ButtonLike::new(id) + .child( + div() + .size_8() + .bg(color) + .border_1() + .border_color(cx.theme().colors().border) + .overflow_hidden(), + ) + .size(ButtonSize::None) + .style(ButtonStyle::Transparent) + .tooltip(move |cx| { + let name = name.clone(); + Tooltip::with_meta(name, None, format!("{:?}", color), cx) + }), + ) + })), + ) + } + + fn render_theme_layer( + &self, + layer: ElevationIndex, + cx: &ViewContext, + ) -> impl IntoElement { + v_flex() + .p_4() + .bg(layer.bg(cx)) + .text_color(cx.theme().colors().text) + .gap_2() + .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium)) + .child(self.render_avatars(cx)) + .child(self.render_buttons(layer, cx)) + .child(self.render_text(layer, cx)) + .child(self.render_colors(layer, cx)) + } +} + +impl Render for ThemePreview { + fn render(&mut self, cx: &mut ViewContext) -> impl ui::IntoElement { + v_flex() + .id("theme-preview") + .key_context("ThemePreview") + .overflow_scroll() + .size_full() + .max_h_full() + .p_4() + .track_focus(&self.focus_handle) + .bg(Self::preview_bg(cx)) + .gap_4() + .child(self.render_theme_layer(ElevationIndex::Background, cx)) + .child(self.render_theme_layer(ElevationIndex::Surface, cx)) + .child(self.render_theme_layer(ElevationIndex::EditorSurface, cx)) + .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, cx)) + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 24c681083b12c..9715381684abc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,6 +9,7 @@ pub mod searchable; pub mod shared_screen; mod status_bar; pub mod tasks; +mod theme_preview; mod toolbar; mod workspace_settings; @@ -323,6 +324,7 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); + theme_preview::init(cx); cx.on_action(Workspace::close_global); cx.on_action(reload);