tag: Add color to Tag to support more colors. (#698)

- Add `ColorName` enum.

<img width="588" alt="image"
src="https://github.com/user-attachments/assets/20670946-1a4f-45ff-b5ac-81e7eb760242"
/>

<img width="586" alt="image"
src="https://github.com/user-attachments/assets/7ce4a87f-fa6b-4794-83c5-3f18e6d09dcb"
/>
This commit is contained in:
Jason Lee 2025-03-07 21:41:04 +08:00 committed by GitHub
parent 4c9c7980ee
commit 3e4a3ed046
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 210 additions and 10 deletions

View file

@ -19,7 +19,12 @@ impl Example {
impl Render for Example {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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())
}
}

View file

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

View file

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

View file

@ -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<ColorName>) -> Self {
Self::new().with_variant(TagVariant::Color(color.into()))
}
}
impl Sizable for Tag {
fn with_size(mut self, size: impl Into<Size>) -> 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))
}
}

View file

@ -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::<Theme>()
}
/// Returns the global theme mutable reference
#[inline(always)]
pub fn global_mut(cx: &mut App) -> &mut Theme {
cx.global_mut::<Theme>()
}
/// 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)
}