@@ -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<Hsla>,
+pub struct IconDecoration {
+ kind: IconDecorationKind,
+ color: Hsla,
+ knockout_color: Hsla,
+ position: Point<Pixels>,
}
-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<Hsla>) -> 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<Pixels>) -> 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<ComponentExampleGroup<Self>> {
+ let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
- 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<IconDecoration>,
+}
+
+impl DecoratedIcon {
+ pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> 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<ComponentExampleGroup<Self>> {
+ 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<ComponentExampleGroup<Icon>> {
+ fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,
@@ -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))