Cargo.lock 🔗
@@ -12093,6 +12093,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
+ "strum 0.25.0",
"util",
"uuid",
]
Nate Butler created
This PR adds a theme preview tab to help get an at a glance overview of
the styles in a theme.

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(-)
@@ -12093,6 +12093,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
+ "strum 0.25.0",
"util",
"uuid",
]
@@ -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 {
@@ -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
@@ -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<Item = (ThemeColorField, Hsla)> + '_ {
+ 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.
@@ -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(),
+ }
+ }
}
@@ -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::*;
@@ -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);
+ }
+}
@@ -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 {
+ 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<SharedString> {
+ 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<crate::WorkspaceId>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<gpui::View<Self>>
+ 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>,
+ ) -> 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<Self>) -> 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))
+ }
+}
@@ -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<AppState>, cx: &mut AppContext) {
init_settings(cx);
notifications::init(cx);
+ theme_preview::init(cx);
cx.on_action(Workspace::close_global);
cx.on_action(reload);