diff --git a/crates/story/examples/text.rs b/crates/story/examples/text.rs index 7fcbb715..30fc6ef6 100644 --- a/crates/story/examples/text.rs +++ b/crates/story/examples/text.rs @@ -19,7 +19,12 @@ impl Example { impl Render for Example { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().p_4().size_full().child(self.root.clone()) + div() + .p_4() + .id("example") + .overflow_y_scroll() + .size_full() + .child(self.root.clone()) } } diff --git a/crates/story/src/text_story.rs b/crates/story/src/text_story.rs index 3fae5868..c0b44824 100644 --- a/crates/story/src/text_story.rs +++ b/crates/story/src/text_story.rs @@ -11,7 +11,7 @@ use gpui_component::{ link::Link, red_500, tag::Tag, - v_flex, yellow_500, yellow_800, IconName, Kbd, Sizable, StyledExt, + v_flex, yellow_500, yellow_800, ColorName, IconName, Kbd, Sizable, StyledExt, }; use crate::section; @@ -218,6 +218,18 @@ impl Render for TextStory { ), ), ) + .child( + section("Tag::color", cx).child( + v_flex().gap_4().child( + h_flex().gap_2().flex_wrap().children( + ColorName::all() + .into_iter() + .filter(|color| *color != ColorName::Gray) + .map(|color| Tag::color(color).child(color.to_string())), + ), + ), + ), + ) .child( section("Kbd", cx).child( h_flex() diff --git a/crates/ui/src/colors.rs b/crates/ui/src/colors.rs index 539a2290..a37e9361 100644 --- a/crates/ui/src/colors.rs +++ b/crates/ui/src/colors.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; -use gpui::Hsla; +use gpui::{Hsla, SharedString}; use serde::{de::Error, Deserialize, Deserializer}; use crate::hsl; @@ -181,6 +181,126 @@ mod color_scales { } } +/// Enum representing the available color names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ColorName { + Gray, + Red, + Orange, + Amber, + Yellow, + Lime, + Green, + Emerald, + Teal, + Cyan, + Sky, + Blue, + Indigo, + Violet, + Purple, + Fuchsia, + Pink, + Rose, +} + +impl Display for ColorName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<&str> for ColorName { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + "gray" => ColorName::Gray, + "red" => ColorName::Red, + "orange" => ColorName::Orange, + "amber" => ColorName::Amber, + "yellow" => ColorName::Yellow, + "lime" => ColorName::Lime, + "green" => ColorName::Green, + "emerald" => ColorName::Emerald, + "teal" => ColorName::Teal, + "cyan" => ColorName::Cyan, + "sky" => ColorName::Sky, + "blue" => ColorName::Blue, + "indigo" => ColorName::Indigo, + "violet" => ColorName::Violet, + "purple" => ColorName::Purple, + "fuchsia" => ColorName::Fuchsia, + "pink" => ColorName::Pink, + "rose" => ColorName::Rose, + _ => ColorName::Gray, + } + } +} + +impl From for ColorName { + fn from(value: SharedString) -> Self { + value.as_ref().into() + } +} + +impl ColorName { + /// Returns all available color names. + pub fn all() -> [Self; 18] { + [ + ColorName::Gray, + ColorName::Red, + ColorName::Orange, + ColorName::Amber, + ColorName::Yellow, + ColorName::Lime, + ColorName::Green, + ColorName::Emerald, + ColorName::Teal, + ColorName::Cyan, + ColorName::Sky, + ColorName::Blue, + ColorName::Indigo, + ColorName::Violet, + ColorName::Purple, + ColorName::Fuchsia, + ColorName::Pink, + ColorName::Rose, + ] + } + + /// Returns the color for the given scale. + /// + /// The `scale` is any of `[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]` + /// falls back to 500 if out of range. + pub fn scale(&self, scale: usize) -> Hsla { + let colors = match self { + ColorName::Gray => &DEFAULT_COLOR.gray, + ColorName::Red => &DEFAULT_COLOR.red, + ColorName::Orange => &DEFAULT_COLOR.orange, + ColorName::Amber => &DEFAULT_COLOR.amber, + ColorName::Yellow => &DEFAULT_COLOR.yellow, + ColorName::Lime => &DEFAULT_COLOR.lime, + ColorName::Green => &DEFAULT_COLOR.green, + ColorName::Emerald => &DEFAULT_COLOR.emerald, + ColorName::Teal => &DEFAULT_COLOR.teal, + ColorName::Cyan => &DEFAULT_COLOR.cyan, + ColorName::Sky => &DEFAULT_COLOR.sky, + ColorName::Blue => &DEFAULT_COLOR.blue, + ColorName::Indigo => &DEFAULT_COLOR.indigo, + ColorName::Violet => &DEFAULT_COLOR.violet, + ColorName::Purple => &DEFAULT_COLOR.purple, + ColorName::Fuchsia => &DEFAULT_COLOR.fuchsia, + ColorName::Pink => &DEFAULT_COLOR.pink, + ColorName::Rose => &DEFAULT_COLOR.rose, + }; + + if let Some(color) = colors.get(&scale) { + color.hsla + } else { + colors.get(&500).unwrap().hsla + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] pub(crate) struct ShadcnColors { pub(crate) black: ShadcnColor, @@ -426,4 +546,20 @@ mod tests { assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00"); assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF"); } + + #[test] + fn test_color_name() { + assert_eq!(ColorName::Purple.to_string(), "Purple"); + assert_eq!(format!("{}", ColorName::Green), "Green"); + assert_eq!(format!("{:?}", ColorName::Yellow), "Yellow"); + + let color = ColorName::Green; + assert_eq!(color.scale(500).to_hex(), "#21C55E"); + assert_eq!(color.scale(1500).to_hex(), "#21C55E"); + + for name in ColorName::all().iter() { + let name1: ColorName = name.to_string().as_str().into(); + assert_eq!(name1, *name); + } + } } diff --git a/crates/ui/src/tag.rs b/crates/ui/src/tag.rs index 855fdf49..b543ffd0 100644 --- a/crates/ui/src/tag.rs +++ b/crates/ui/src/tag.rs @@ -1,4 +1,4 @@ -use crate::{theme::ActiveTheme as _, Sizable, Size}; +use crate::{theme::ActiveTheme as _, ColorName, Sizable, Size}; use gpui::{ div, prelude::FluentBuilder as _, relative, transparent_black, AnyElement, App, Div, Hsla, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, Styled, Window, @@ -7,16 +7,18 @@ use gpui::{ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TagVariant { #[default] + Outline, Primary, Secondary, - Outline, Danger, + Color(ColorName), Custom { color: Hsla, foreground: Hsla, border: Hsla, }, } + impl TagVariant { fn bg(&self, cx: &App) -> Hsla { match self { @@ -24,6 +26,13 @@ impl TagVariant { Self::Secondary => cx.theme().secondary, Self::Outline => transparent_black(), Self::Danger => cx.theme().danger, + Self::Color(color) => { + if cx.theme().is_dark() { + color.scale(950).opacity(0.5) + } else { + color.scale(50) + } + } Self::Custom { color, .. } => *color, } } @@ -34,6 +43,13 @@ impl TagVariant { Self::Secondary => cx.theme().secondary, Self::Outline => cx.theme().border, Self::Danger => cx.theme().danger, + Self::Color(color) => { + if cx.theme().is_dark() { + color.scale(800).opacity(0.5) + } else { + color.scale(200) + } + } Self::Custom { border, .. } => *border, } } @@ -44,6 +60,13 @@ impl TagVariant { Self::Secondary => cx.theme().secondary_foreground, Self::Outline => cx.theme().foreground, Self::Danger => cx.theme().danger_foreground, + Self::Color(color) => { + if cx.theme().is_dark() { + color.scale(300) + } else { + color.scale(600) + } + } Self::Custom { foreground, .. } => *foreground, } } @@ -72,22 +95,29 @@ impl Tag { self } + /// Create a new tag with default variant ([`TagVariant::Primary`]). pub fn primary() -> Self { Self::new().with_variant(TagVariant::Primary) } + /// Create a new tag with default variant ([`TagVariant::Secondary`]). pub fn secondary() -> Self { Self::new().with_variant(TagVariant::Secondary) } + /// Create a new tag with default variant ([`TagVariant::Outline`]). + /// + /// See also [`Tag::new`]. pub fn outline() -> Self { Self::new().with_variant(TagVariant::Outline) } + /// Create a new tag with default variant ([`TagVariant::Danger`]). pub fn danger() -> Self { Self::new().with_variant(TagVariant::Danger) } + /// Create a new tag with default variant ([`TagVariant::Custom`]). pub fn custom(color: Hsla, foreground: Hsla, border: Hsla) -> Self { Self::new().with_variant(TagVariant::Custom { color, @@ -95,6 +125,11 @@ impl Tag { border, }) } + + /// Create a new tag with default variant ([`TagVariant::Color`]). + pub fn color(color: impl Into) -> Self { + Self::new().with_variant(TagVariant::Color(color.into())) + } } impl Sizable for Tag { fn with_size(mut self, size: impl Into) -> Self { @@ -109,6 +144,10 @@ impl ParentElement for Tag { } impl RenderOnce for Tag { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let bg = self.variant.bg(cx); + let fg = self.variant.fg(cx); + let border = self.variant.border(cx); + self.base .line_height(relative(1.3)) .text_xs() @@ -116,9 +155,9 @@ impl RenderOnce for Tag { Size::XSmall | Size::Small => this.px_1p5().py_0().rounded(cx.theme().radius / 2.), _ => this.px_2p5().py_0p5().rounded(cx.theme().radius), }) - .bg(self.variant.bg(cx)) - .text_color(self.variant.fg(cx)) - .border_color(self.variant.border(cx)) + .bg(bg) + .text_color(fg) + .border_color(border) .hover(|this| this.opacity(0.9)) } } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index cd747ce6..bb990fed 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -411,15 +411,23 @@ impl Global for Theme {} impl Theme { /// Returns the global theme reference + #[inline(always)] pub fn global(cx: &App) -> &Theme { cx.global::() } /// Returns the global theme mutable reference + #[inline(always)] pub fn global_mut(cx: &mut App) -> &mut Theme { cx.global_mut::() } + /// Returns true if the theme is dark. + #[inline(always)] + pub fn is_dark(&self) -> bool { + self.mode.is_dark() + } + /// Apply a mask color to the theme. pub fn apply_color(&mut self, mask_color: Hsla) { self.title_bar = self.title_bar.apply(mask_color); @@ -576,7 +584,7 @@ pub enum ThemeMode { } impl ThemeMode { - #[inline] + #[inline(always)] pub fn is_dark(&self) -> bool { matches!(self, Self::Dark) }