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.
This commit is contained in:
Nate Butler 2024-10-31 11:40:38 -04:00 committed by GitHub
parent 9c77bcc827
commit a347c4def7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 813 additions and 4 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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.

View file

@ -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(),
}
}
}

View file

@ -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::*;

View file

@ -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);
}
}

View file

@ -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))
}
}

View file

@ -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);