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