diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 8a351bbb86535b0f494f049be27b97d6f39d81e3..c7c05451f559b41d174306af3e19918239f15705 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -1,7 +1,13 @@ #![allow(missing_docs)] -use gpui::{svg, AnimationElement, Hsla, IntoElement, Point, Rems, Transformation}; + +mod decorated_icon; +mod icon_decoration; + +pub use decorated_icon::*; +use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation}; +pub use icon_decoration::*; use serde::{Deserialize, Serialize}; -use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr}; +use strum::{EnumIter, EnumString, IntoStaticStr}; use ui_macros::DerivePathStr; use crate::{ @@ -144,7 +150,8 @@ pub enum IconName { CaseSensitive, Check, ChevronDown, - ChevronDownSmall, // This chevron indicates a popover menu. + /// This chevron indicates a popover menu. + ChevronDownSmall, ChevronLeft, ChevronRight, ChevronUp, @@ -379,260 +386,6 @@ 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, - knockout_hover_color: Hsla, - position: Point, - group_name: Option, -} - -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, - knockout_hover_color: knockout_color, - position, - group_name: None, - } - } - - /// 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 color of the decoration that is used on hover - pub fn knockout_hover_color(mut self, color: Hsla) -> Self { - self.knockout_hover_color = color; - self - } - - /// Sets the position of the decoration - pub fn position(mut self, position: Point) -> Self { - self.position = position; - self - } - - /// Sets the name of the group the decoration belongs to - pub fn group_name(mut self, name: Option) -> Self { - self.group_name = name; - 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) - .when(self.group_name.is_none(), |this| { - this.hover(|style| style.text_color(self.knockout_hover_color)) - }) - .when_some(self.group_name.clone(), |this, group_name| { - this.group_hover(group_name, |style| { - style.text_color(self.knockout_hover_color) - }) - }), - ) - } -} - -impl ComponentPreview for IconDecoration { - fn examples(cx: &mut 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 { - div() - .relative() - .size(self.icon.size) - .child(self.icon) - .when_some(self.decoration, |this, decoration| this.child(decoration)) - } -} - -impl ComponentPreview for DecoratedIcon { - fn examples(cx: &mut 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)] - } -} - #[derive(IntoElement)] pub struct IconWithIndicator { icon: Icon, diff --git a/crates/ui/src/components/icon/decorated_icon.rs b/crates/ui/src/components/icon/decorated_icon.rs new file mode 100644 index 0000000000000000000000000000000000000000..eec78594d6a4b5d4d2055a3a77c45af5c373e54b --- /dev/null +++ b/crates/ui/src/components/icon/decorated_icon.rs @@ -0,0 +1,87 @@ +use gpui::{IntoElement, Point}; + +use crate::{ + prelude::*, traits::component_preview::ComponentPreview, IconDecoration, IconDecorationKind, +}; + +#[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) + .children(self.decoration) + } +} + +impl ComponentPreview for DecoratedIcon { + fn examples(cx: &mut 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)] + } +} diff --git a/crates/ui/src/components/icon/icon_decoration.rs b/crates/ui/src/components/icon/icon_decoration.rs new file mode 100644 index 0000000000000000000000000000000000000000..af235c2e5fc72a7da5ea4065eb73c846227f6475 --- /dev/null +++ b/crates/ui/src/components/icon/icon_decoration.rs @@ -0,0 +1,169 @@ +use gpui::{svg, Hsla, IntoElement, Point}; +use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr}; +use ui_macros::DerivePathStr; + +use crate::{prelude::*, traits::component_preview::ComponentPreview}; + +const ICON_DECORATION_SIZE: Pixels = px(11.); + +/// 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 { + XFg, + XBg, + DotFg, + DotBg, + TriangleFg, + TriangleBg, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString)] +pub enum IconDecorationKind { + 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, + knockout_hover_color: Hsla, + position: Point, + group_name: Option, +} + +impl IconDecoration { + /// Creates a new [`IconDecoration`]. + 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, + knockout_hover_color: knockout_color, + position, + group_name: None, + } + } + + /// 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 color of the decoration that is used on hover. + pub fn knockout_hover_color(mut self, color: Hsla) -> Self { + self.knockout_hover_color = color; + self + } + + /// Sets the position of the decoration. + pub fn position(mut self, position: Point) -> Self { + self.position = position; + self + } + + /// Sets the name of the group the decoration belongs to + pub fn group_name(mut self, name: Option) -> Self { + self.group_name = name; + self + } +} + +impl RenderOnce for IconDecoration { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + let foreground = svg() + .absolute() + .bottom_0() + .right_0() + .size(ICON_DECORATION_SIZE) + .path(self.kind.fg().path()) + .text_color(self.color); + + let background = svg() + .absolute() + .bottom_0() + .right_0() + .size(ICON_DECORATION_SIZE) + .path(self.kind.bg().path()) + .text_color(self.knockout_color) + .map(|this| match self.group_name { + Some(group_name) => this.group_hover(group_name, |style| { + style.text_color(self.knockout_hover_color) + }), + None => this.hover(|style| style.text_color(self.knockout_hover_color)), + }); + + div() + .size(ICON_DECORATION_SIZE) + .flex_none() + .absolute() + .bottom(self.position.y) + .right(self.position.x) + .child(foreground) + .child(background) + } +} + +impl ComponentPreview for IconDecoration { + fn examples(cx: &mut WindowContext) -> Vec> { + let all_kinds = IconDecorationKind::iter().collect::>(); + + let examples = all_kinds + .iter() + .map(|kind| { + single_example( + format!("{kind:?}"), + IconDecoration::new(*kind, cx.theme().colors().surface_background, cx), + ) + }) + .collect(); + + vec![example_group(examples)] + } +}