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