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.  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:
parent
9c77bcc827
commit
a347c4def7
9 changed files with 813 additions and 4 deletions
|
@ -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::*;
|
||||
|
|
70
crates/ui/src/utils/color_contrast.rs
Normal file
70
crates/ui/src/utils/color_contrast.rs
Normal 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);
|
||||
}
|
||||
}
|
454
crates/workspace/src/theme_preview.rs
Normal file
454
crates/workspace/src/theme_preview.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue