Add new DecoratedIcon component (#20516)

This PR creates a new, revamped `DecoratedIcon` component that enables
using different SVGs, one for the knockout background and another for
the actual icon. That's different than what we were doing before—copying
the SVG and using slightly different positioning—because we wanted to
unlock an aligned knockout effect, which was particularly hard to do
with non-simple shapes such as an X.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
This commit is contained in:
Danilo Leal 2024-11-11 19:09:02 -03:00 committed by GitHub
parent 149e5fde36
commit 82427e1ffb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 253 additions and 96 deletions

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.1" y="2.1" width="6.8" height="6.8" rx="3.4" stroke="black" stroke-width="1.8"/>
</svg>

After

Width:  |  Height:  |  Size: 195 B

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="5" height="5" rx="2.5" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 166 B

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.364 7.6025L1.64681 8.75H3H8H9.35319L8.636 7.6025L6.136 3.6025L5.5 2.5849L4.864 3.6025L2.364 7.6025Z" stroke="black" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 252 B

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8H8L5.5 4L3 8Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 146 B

View file

@ -0,0 +1,10 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2050_903)">
<path d="M1.83327 2.89393L1.19687 3.53033L1.83327 4.16672L3.16654 5.5L1.83327 6.83327L1.19687 7.46967L1.83327 8.10606L2.89393 9.16672L3.53033 9.80312L4.16672 9.16672L5.5 7.83345L6.83327 9.16672L7.46967 9.80312L8.10606 9.16672L9.16672 8.10606L9.80312 7.46967L9.16672 6.83327L7.83345 5.5L9.16672 4.16672L9.80312 3.53033L9.16672 2.89393L8.10606 1.83327L7.46967 1.19687L6.83327 1.83327L5.5 3.16654L4.16672 1.83327L3.53033 1.19687L2.89393 1.83327L1.83327 2.89393Z" stroke="black" stroke-width="1.8"/>
</g>
<defs>
<clipPath id="clip0_2050_903">
<rect width="11" height="11" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3L5.5 5.5M8 8L5.5 5.5M5.5 5.5L3 8M5.5 5.5L8 3" stroke="black" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 198 B

View file

@ -445,7 +445,7 @@ impl ComponentPreview for Button {
"A button allows users to take actions, and make choices, with a single tap." "A button allows users to take actions, and make choices, with a single tap."
} }
fn examples() -> Vec<ComponentExampleGroup<Self>> { fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![ vec![
example_group_with_title( example_group_with_title(
"Styles", "Styles",

View file

@ -118,7 +118,7 @@ impl ComponentPreview for Checkbox {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state." "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
} }
fn examples() -> Vec<ComponentExampleGroup<Self>> { fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![ vec![
example_group_with_title( example_group_with_title(
"Default", "Default",
@ -214,7 +214,7 @@ impl ComponentPreview for CheckboxWithLabel {
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text." "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
} }
fn examples() -> Vec<ComponentExampleGroup<Self>> { fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![ vec![example_group(vec![
single_example( single_example(
"Unselected", "Unselected",

View file

@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
\n\nFacepiles are used to display a group of people or things,\ \n\nFacepiles are used to display a group of people or things,\
such as a list of participants in a collaboration session." such as a list of participants in a collaboration session."
} }
fn examples() -> Vec<ComponentExampleGroup<Self>> { fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let few_faces: [&'static str; 3] = [ let few_faces: [&'static str; 3] = [
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4", "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4", "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",

View file

@ -1,7 +1,7 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation}; use gpui::{svg, AnimationElement, Hsla, IntoElement, Point, Rems, Transformation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoStaticStr}; use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
use ui_macros::DerivePathStr; use ui_macros::DerivePathStr;
use crate::{ use crate::{
@ -48,17 +48,6 @@ impl RenderOnce for AnyIcon {
} }
} }
/// The decoration for an icon.
///
/// For example, this can show an indicator, an "x",
/// or a diagonal strikethrough to indicate something is disabled.
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
pub enum IconDecoration {
Strikethrough,
IndicatorDot,
X,
}
#[derive(Default, PartialEq, Copy, Clone)] #[derive(Default, PartialEq, Copy, Clone)]
pub enum IconSize { pub enum IconSize {
/// 10px /// 10px
@ -367,77 +356,233 @@ impl RenderOnce for Icon {
} }
} }
const ICON_DECORATION_SIZE: f32 = 11.0;
/// An icon silhouette used to knockout the background of an element
/// for an icon to sit on top of it, emulating a stroke/border.
#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, DerivePathStr)]
#[strum(serialize_all = "snake_case")]
#[path_str(prefix = "icons/knockouts", suffix = ".svg")]
pub enum KnockoutIconName {
// /icons/knockouts/x1.svg
XFg,
XBg,
DotFg,
DotBg,
TriangleFg,
TriangleBg,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString)]
pub enum IconDecorationKind {
// Slash,
X,
Dot,
Triangle,
}
impl IconDecorationKind {
fn fg(&self) -> KnockoutIconName {
match self {
Self::X => KnockoutIconName::XFg,
Self::Dot => KnockoutIconName::DotFg,
Self::Triangle => KnockoutIconName::TriangleFg,
}
}
fn bg(&self) -> KnockoutIconName {
match self {
Self::X => KnockoutIconName::XBg,
Self::Dot => KnockoutIconName::DotBg,
Self::Triangle => KnockoutIconName::TriangleBg,
}
}
}
/// The decoration for an icon.
///
/// For example, this can show an indicator, an "x",
/// or a diagonal strikethrough to indicate something is disabled.
#[derive(IntoElement)]
pub struct IconDecoration {
kind: IconDecorationKind,
color: Hsla,
knockout_color: Hsla,
position: Point<Pixels>,
}
impl IconDecoration {
/// Create a new icon decoration
pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &WindowContext) -> Self {
let color = cx.theme().colors().icon;
let position = Point::default();
Self {
kind,
color,
knockout_color,
position,
}
}
/// Sets the kind of decoration
pub fn kind(mut self, kind: IconDecorationKind) -> Self {
self.kind = kind;
self
}
/// Sets the color of the decoration
pub fn color(mut self, color: Hsla) -> Self {
self.color = color;
self
}
/// Sets the color of the decoration's knockout
///
/// Match this to the background of the element
/// the icon will be rendered on
pub fn knockout_color(mut self, color: Hsla) -> Self {
self.knockout_color = color;
self
}
/// Sets the position of the decoration
pub fn position(mut self, position: Point<Pixels>) -> Self {
self.position = position;
self
}
}
impl RenderOnce for IconDecoration {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
div()
.size(px(ICON_DECORATION_SIZE))
.flex_none()
.absolute()
.bottom(self.position.y)
.right(self.position.x)
.child(
// foreground
svg()
.absolute()
.bottom_0()
.right_0()
.size(px(ICON_DECORATION_SIZE))
.path(self.kind.fg().path())
.text_color(self.color),
)
.child(
// background
svg()
.absolute()
.bottom_0()
.right_0()
.size(px(ICON_DECORATION_SIZE))
.path(self.kind.bg().path())
.text_color(self.knockout_color),
)
}
}
impl ComponentPreview for IconDecoration {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
let examples = all_kinds
.iter()
.map(|kind| {
let name = format!("{:?}", kind).to_string();
single_example(
name,
IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
)
})
.collect();
vec![example_group(examples)]
}
}
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct DecoratedIcon { pub struct DecoratedIcon {
icon: Icon, icon: Icon,
decoration: IconDecoration, decoration: Option<IconDecoration>,
decoration_color: Color,
parent_background: Option<Hsla>,
} }
impl DecoratedIcon { impl DecoratedIcon {
pub fn new(icon: Icon, decoration: IconDecoration) -> Self { pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> Self {
Self { Self { icon, decoration }
icon,
decoration,
decoration_color: Color::Default,
parent_background: None,
}
}
pub fn decoration_color(mut self, color: Color) -> Self {
self.decoration_color = color;
self
}
pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
self.parent_background = background;
self
} }
} }
impl RenderOnce for DecoratedIcon { impl RenderOnce for DecoratedIcon {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let background = self
.parent_background
.unwrap_or(cx.theme().colors().background);
let size = self.icon.size;
let decoration_icon = match self.decoration {
IconDecoration::Strikethrough => IconName::Strikethrough,
IconDecoration::IndicatorDot => IconName::Indicator,
IconDecoration::X => IconName::IndicatorX,
};
let decoration_svg = |icon: IconName| {
svg()
.absolute()
.top_0()
.left_0()
.path(icon.path())
.size(size)
.flex_none()
.text_color(self.decoration_color.color(cx))
};
let decoration_knockout = |icon: IconName| {
svg()
.absolute()
.top(-rems_from_px(2.))
.left(-rems_from_px(3.))
.path(icon.path())
.size(size + rems_from_px(2.))
.flex_none()
.text_color(background)
};
div() div()
.relative() .relative()
.size(self.icon.size) .size(self.icon.size)
.child(self.icon) .child(self.icon)
.child(decoration_knockout(decoration_icon)) .when_some(self.decoration, |this, decoration| this.child(decoration))
.child(decoration_svg(decoration_icon)) }
}
impl ComponentPreview for DecoratedIcon {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let icon_1 = Icon::new(IconName::FileDoc);
let icon_2 = Icon::new(IconName::FileDoc);
let icon_3 = Icon::new(IconName::FileDoc);
let icon_4 = Icon::new(IconName::FileDoc);
let decoration_x = IconDecoration::new(
IconDecorationKind::X,
cx.theme().colors().surface_background,
cx,
)
.color(cx.theme().status().error)
.position(Point {
x: px(-2.),
y: px(-2.),
});
let decoration_triangle = IconDecoration::new(
IconDecorationKind::Triangle,
cx.theme().colors().surface_background,
cx,
)
.color(cx.theme().status().error)
.position(Point {
x: px(-2.),
y: px(-2.),
});
let decoration_dot = IconDecoration::new(
IconDecorationKind::Dot,
cx.theme().colors().surface_background,
cx,
)
.color(cx.theme().status().error)
.position(Point {
x: px(-2.),
y: px(-2.),
});
let examples = vec![
single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
single_example(
"with_decoration",
DecoratedIcon::new(icon_2, Some(decoration_x)),
),
single_example(
"with_decoration",
DecoratedIcon::new(icon_3, Some(decoration_triangle)),
),
single_example(
"with_decoration",
DecoratedIcon::new(icon_4, Some(decoration_dot)),
),
];
vec![example_group(examples)]
} }
} }
@ -501,7 +646,7 @@ impl RenderOnce for IconWithIndicator {
} }
impl ComponentPreview for Icon { impl ComponentPreview for Icon {
fn examples() -> Vec<ComponentExampleGroup<Icon>> { fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
let arrow_icons = vec![ let arrow_icons = vec![
IconName::ArrowDown, IconName::ArrowDown,
IconName::ArrowLeft, IconName::ArrowLeft,

View file

@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
"An indicator visually represents a status or state." "An indicator visually represents a status or state."
} }
fn examples() -> Vec<ComponentExampleGroup<Self>> { fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![ vec![
example_group_with_title( example_group_with_title(
"Types", "Types",

View file

@ -2,7 +2,7 @@ use gpui::Render;
use story::Story; use story::Story;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::{prelude::*, DecoratedIcon, IconDecoration}; use crate::prelude::*;
use crate::{Icon, IconName}; use crate::{Icon, IconName};
pub struct IconStory; pub struct IconStory;
@ -14,22 +14,6 @@ impl Render for IconStory {
Story::container() Story::container()
.child(Story::title_for::<Icon>()) .child(Story::title_for::<Icon>())
.child(Story::label("DecoratedIcon")) .child(Story::label("DecoratedIcon"))
.child(DecoratedIcon::new(
Icon::new(IconName::Bell).color(Color::Muted),
IconDecoration::IndicatorDot,
))
.child(
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot)
.decoration_color(Color::Accent),
)
.child(DecoratedIcon::new(
Icon::new(IconName::Bell).color(Color::Muted),
IconDecoration::Strikethrough,
))
.child(
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::X)
.decoration_color(Color::Error),
)
.child(Story::label("All Icons")) .child(Story::label("All Icons"))
.child(div().flex().gap_3().children(icons.map(Icon::new))) .child(div().flex().gap_3().children(icons.map(Icon::new)))
} }

View file

@ -160,7 +160,7 @@ impl ComponentPreview for Table {
ExampleLabelSide::Top ExampleLabelSide::Top
} }
fn examples() -> Vec<ComponentExampleGroup<Self>> { fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![ vec![
example_group(vec![ example_group(vec![
single_example( single_example(

View file

@ -30,10 +30,10 @@ pub trait ComponentPreview: IntoElement {
ExampleLabelSide::default() ExampleLabelSide::default()
} }
fn examples() -> Vec<ComponentExampleGroup<Self>>; fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn component_previews() -> Vec<AnyElement> { fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
Self::examples() Self::examples(cx)
.into_iter() .into_iter()
.map(|example| Self::render_example_group(example)) .map(|example| Self::render_example_group(example))
.collect() .collect()
@ -73,7 +73,7 @@ pub trait ComponentPreview: IntoElement {
) )
}), }),
) )
.children(Self::component_previews()) .children(Self::component_previews(cx))
.into_any_element() .into_any_element()
} }

View file

@ -5,7 +5,8 @@ use theme::all_theme_colors;
use ui::{ use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus, element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, ElevationIndex, Facepile, Indicator, Table, TintColor, Tooltip, Checkbox, CheckboxWithLabel, DecoratedIcon, ElevationIndex, Facepile, IconDecoration,
Indicator, Table, TintColor, Tooltip,
}; };
use crate::{Item, Workspace}; use crate::{Item, Workspace};
@ -509,6 +510,8 @@ impl ThemePreview {
.overflow_scroll() .overflow_scroll()
.size_full() .size_full()
.gap_2() .gap_2()
.child(IconDecoration::render_component_previews(cx))
.child(DecoratedIcon::render_component_previews(cx))
.child(Checkbox::render_component_previews(cx)) .child(Checkbox::render_component_previews(cx))
.child(CheckboxWithLabel::render_component_previews(cx)) .child(CheckboxWithLabel::render_component_previews(cx))
.child(Facepile::render_component_previews(cx)) .child(Facepile::render_component_previews(cx))