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 | 281 ++++++++++++++++------ 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, 253 insertions(+), 96 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 0000000000..9f5ba034e2 --- /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 0000000000..54eaacbfa9 --- /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 0000000000..b0c5ae6e77 --- /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 0000000000..f8f8b8c2bc --- /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 0000000000..0bc5059e73 --- /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 0000000000..a3d47f1373 --- /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 26f30f5588..fdf9b537bc 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 efa907ea20..0a3fc6f650 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 5d406f67c7..eb4dd8a98e 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 c6c92ee9d9..89763c3a42 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 { } } -#[derive(IntoElement)] -pub struct DecoratedIcon { - icon: Icon, - decoration: IconDecoration, - decoration_color: Color, - parent_background: Option, +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, } -impl DecoratedIcon { - pub fn new(icon: Icon, decoration: IconDecoration) -> Self { - Self { - icon, - decoration, - decoration_color: Color::Default, - parent_background: None, +#[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, } } - pub fn decoration_color(mut self, color: Color) -> Self { - self.decoration_color = color; + 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, +} + +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 } - 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 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> { + let all_kinds = IconDecorationKind::iter().collect::>(); + + 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)] +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 { - 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) - }; - + 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 8ce075d228..b0d5b0d2da 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 bdd253b567..618634e153 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 59273cce12..0ef5eda7b7 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 1fece0804a..1cb577a97f 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 4788842d4f..fef4dfc86e 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))