From 82427e1ffba509b2ac96046ad5276edaf186d934 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Mon, 11 Nov 2024 19:09:02 -0300
Subject: [PATCH] Add new `DecoratedIcon` component (#20516)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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>
---
assets/icons/knockouts/dot_bg.svg | 3 +
assets/icons/knockouts/dot_fg.svg | 3 +
assets/icons/knockouts/triangle_bg.svg | 3 +
assets/icons/knockouts/triangle_fg.svg | 3 +
assets/icons/knockouts/x_bg.svg | 10 +
assets/icons/knockouts/x_fg.svg | 3 +
crates/ui/src/components/button/button.rs | 2 +-
crates/ui/src/components/checkbox.rs | 4 +-
crates/ui/src/components/facepile.rs | 2 +-
crates/ui/src/components/icon.rs | 269 +++++++++++++++++-----
crates/ui/src/components/indicator.rs | 2 +-
crates/ui/src/components/stories/icon.rs | 18 +-
crates/ui/src/components/table.rs | 2 +-
crates/ui/src/traits/component_preview.rs | 8 +-
crates/workspace/src/theme_preview.rs | 5 +-
15 files changed, 247 insertions(+), 90 deletions(-)
create mode 100644 assets/icons/knockouts/dot_bg.svg
create mode 100644 assets/icons/knockouts/dot_fg.svg
create mode 100644 assets/icons/knockouts/triangle_bg.svg
create mode 100644 assets/icons/knockouts/triangle_fg.svg
create mode 100644 assets/icons/knockouts/x_bg.svg
create mode 100644 assets/icons/knockouts/x_fg.svg
diff --git a/assets/icons/knockouts/dot_bg.svg b/assets/icons/knockouts/dot_bg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9f5ba034e2c0e37e69668e73503db863d3e4fb8f
--- /dev/null
+++ b/assets/icons/knockouts/dot_bg.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/knockouts/dot_fg.svg b/assets/icons/knockouts/dot_fg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..54eaacbfa9335d7fee573f2ce5f054e6e8a9dfdc
--- /dev/null
+++ b/assets/icons/knockouts/dot_fg.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/knockouts/triangle_bg.svg b/assets/icons/knockouts/triangle_bg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b0c5ae6e7793133ef9e498d80cb736b605f127dd
--- /dev/null
+++ b/assets/icons/knockouts/triangle_bg.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/knockouts/triangle_fg.svg b/assets/icons/knockouts/triangle_fg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f8f8b8c2bcddf6f80db749ea77e7cd0f109202cb
--- /dev/null
+++ b/assets/icons/knockouts/triangle_fg.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/knockouts/x_bg.svg b/assets/icons/knockouts/x_bg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0bc5059e73da5685c505ac1471e5c11022ff42dc
--- /dev/null
+++ b/assets/icons/knockouts/x_bg.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/knockouts/x_fg.svg b/assets/icons/knockouts/x_fg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a3d47f13735e734ca2f801618a4edbee02ea457b
--- /dev/null
+++ b/assets/icons/knockouts/x_fg.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs
index 26f30f5588e17513efa82a9ca11676c9a1d5a9a0..fdf9b537bce77014441ca57dcc308543866b7953 100644
--- a/crates/ui/src/components/button/button.rs
+++ b/crates/ui/src/components/button/button.rs
@@ -445,7 +445,7 @@ impl ComponentPreview for Button {
"A button allows users to take actions, and make choices, with a single tap."
}
- fn examples() -> Vec> {
+ fn examples(_: &WindowContext) -> Vec> {
vec![
example_group_with_title(
"Styles",
diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs
index efa907ea20fb5b84786210dee5e7b9db26c55793..0a3fc6f6502545a3b0c85e3474128587cc6f17ef 100644
--- a/crates/ui/src/components/checkbox.rs
+++ b/crates/ui/src/components/checkbox.rs
@@ -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."
}
- fn examples() -> Vec> {
+ fn examples(_: &WindowContext) -> Vec> {
vec![
example_group_with_title(
"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."
}
- fn examples() -> Vec> {
+ fn examples(_: &WindowContext) -> Vec> {
vec![example_group(vec![
single_example(
"Unselected",
diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs
index 5d406f67c7af56cc4018e2fa876a65473c076a2a..eb4dd8a98e003e306924a83a04783f253570b4a8 100644
--- a/crates/ui/src/components/facepile.rs
+++ b/crates/ui/src/components/facepile.rs
@@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
\n\nFacepiles are used to display a group of people or things,\
such as a list of participants in a collaboration session."
}
- fn examples() -> Vec> {
+ fn examples(_: &WindowContext) -> Vec> {
let few_faces: [&'static str; 3] = [
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index c6c92ee9d99384acebf6b7a23f1f66376977c7fd..89763c3a421ec4c26e6f425c5612f1c1cc4373b9 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -1,7 +1,7 @@
#![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 strum::{EnumIter, EnumString, IntoStaticStr};
+use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
use ui_macros::DerivePathStr;
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)]
pub enum IconSize {
/// 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 DecoratedIcon {
- icon: Icon,
- decoration: IconDecoration,
- decoration_color: Color,
- parent_background: Option,
+pub struct IconDecoration {
+ kind: IconDecorationKind,
+ color: Hsla,
+ knockout_color: Hsla,
+ position: Point,
}
-impl DecoratedIcon {
- pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
+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 {
- icon,
- decoration,
- decoration_color: Color::Default,
- parent_background: None,
+ kind,
+ color,
+ knockout_color,
+ position,
}
}
- pub fn decoration_color(mut self, color: Color) -> Self {
- self.decoration_color = color;
+ /// Sets the kind of decoration
+ pub fn kind(mut self, kind: IconDecorationKind) -> Self {
+ self.kind = kind;
self
}
- pub fn parent_background(mut self, background: Option) -> Self {
- self.parent_background = background;
+ /// 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) -> Self {
+ self.position = position;
self
}
}
-impl RenderOnce for DecoratedIcon {
- fn render(self, cx: &mut WindowContext) -> impl IntoElement {
- let background = self
- .parent_background
- .unwrap_or(cx.theme().colors().background);
+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),
+ )
+ }
+}
- let size = self.icon.size;
+impl ComponentPreview for IconDecoration {
+ fn examples(cx: &WindowContext) -> Vec> {
+ let all_kinds = IconDecorationKind::iter().collect::>();
- let decoration_icon = match self.decoration {
- IconDecoration::Strikethrough => IconName::Strikethrough,
- IconDecoration::IndicatorDot => IconName::Indicator,
- IconDecoration::X => IconName::IndicatorX,
- };
+ let examples = all_kinds
+ .iter()
+ .map(|kind| {
+ let name = format!("{:?}", kind).to_string();
- 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))
- };
+ single_example(
+ name,
+ IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
+ )
+ })
+ .collect();
- 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)
- };
+ vec![example_group(examples)]
+ }
+}
+#[derive(IntoElement)]
+pub struct DecoratedIcon {
+ icon: Icon,
+ decoration: Option,
+}
+
+impl DecoratedIcon {
+ pub fn new(icon: Icon, decoration: Option) -> Self {
+ Self { icon, decoration }
+ }
+}
+
+impl RenderOnce for DecoratedIcon {
+ fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
div()
.relative()
.size(self.icon.size)
.child(self.icon)
- .child(decoration_knockout(decoration_icon))
- .child(decoration_svg(decoration_icon))
+ .when_some(self.decoration, |this, decoration| this.child(decoration))
+ }
+}
+
+impl ComponentPreview for DecoratedIcon {
+ fn examples(cx: &WindowContext) -> Vec> {
+ 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 {
- fn examples() -> Vec> {
+ fn examples(_cx: &WindowContext) -> Vec> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,
diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs
index 8ce075d228935a1abde76ce6a3a3f4060e5e5312..b0d5b0d2da96e78175421a9fd2bc142b180bdc15 100644
--- a/crates/ui/src/components/indicator.rs
+++ b/crates/ui/src/components/indicator.rs
@@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
"An indicator visually represents a status or state."
}
- fn examples() -> Vec> {
+ fn examples(_: &WindowContext) -> Vec> {
vec![
example_group_with_title(
"Types",
diff --git a/crates/ui/src/components/stories/icon.rs b/crates/ui/src/components/stories/icon.rs
index bdd253b567e13d2564ccec2414e8bc33ae5f8b6b..618634e1536cc13cfa7c44666103c43880916bef 100644
--- a/crates/ui/src/components/stories/icon.rs
+++ b/crates/ui/src/components/stories/icon.rs
@@ -2,7 +2,7 @@ use gpui::Render;
use story::Story;
use strum::IntoEnumIterator;
-use crate::{prelude::*, DecoratedIcon, IconDecoration};
+use crate::prelude::*;
use crate::{Icon, IconName};
pub struct IconStory;
@@ -14,22 +14,6 @@ impl Render for IconStory {
Story::container()
.child(Story::title_for::())
.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(div().flex().gap_3().children(icons.map(Icon::new)))
}
diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs
index 59273cce1278c47af0599a65af9c270df7f938f5..0ef5eda7b7bf1506239c05ce8d3f5a6ded4fad78 100644
--- a/crates/ui/src/components/table.rs
+++ b/crates/ui/src/components/table.rs
@@ -160,7 +160,7 @@ impl ComponentPreview for Table {
ExampleLabelSide::Top
}
- fn examples() -> Vec> {
+ fn examples(_: &WindowContext) -> Vec> {
vec![
example_group(vec![
single_example(
diff --git a/crates/ui/src/traits/component_preview.rs b/crates/ui/src/traits/component_preview.rs
index 1fece0804a607949f986cf9686eafaf267ba1f5a..1cb577a97fb64a34654a2b36223c8fd55cbfd544 100644
--- a/crates/ui/src/traits/component_preview.rs
+++ b/crates/ui/src/traits/component_preview.rs
@@ -30,10 +30,10 @@ pub trait ComponentPreview: IntoElement {
ExampleLabelSide::default()
}
- fn examples() -> Vec>;
+ fn examples(_cx: &WindowContext) -> Vec>;
- fn component_previews() -> Vec {
- Self::examples()
+ fn component_previews(cx: &WindowContext) -> Vec {
+ Self::examples(cx)
.into_iter()
.map(|example| Self::render_example_group(example))
.collect()
@@ -73,7 +73,7 @@ pub trait ComponentPreview: IntoElement {
)
}),
)
- .children(Self::component_previews())
+ .children(Self::component_previews(cx))
.into_any_element()
}
diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs
index 4788842d4fff8de3adcaeee7ae9c96d734ad6bc2..fef4dfc86e0009ce2a070bd81df1deed756647a1 100644
--- a/crates/workspace/src/theme_preview.rs
+++ b/crates/workspace/src/theme_preview.rs
@@ -5,7 +5,8 @@ use theme::all_theme_colors;
use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
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};
@@ -509,6 +510,8 @@ impl ThemePreview {
.overflow_scroll()
.size_full()
.gap_2()
+ .child(IconDecoration::render_component_previews(cx))
+ .child(DecoratedIcon::render_component_previews(cx))
.child(Checkbox::render_component_previews(cx))
.child(CheckboxWithLabel::render_component_previews(cx))
.child(Facepile::render_component_previews(cx))