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