From 47ca3438034ec6c7e5f8b17a5e9274ebc4cc24eb Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 7 May 2024 16:36:13 -0400 Subject: [PATCH] Add DecoratedIcon (#11512) This allows us to create icons with dynamic decorations drawn on top like these: ![image](https://github.com/zed-industries/zed/assets/1714999/1d1a22df-8f90-47f2-abbd-ed7afa8fc641) ### Examples: ```rust div() .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), ) ``` Release Notes: - N/A --- assets/icons/indicator.svg | 3 + assets/icons/indicator_x.svg | 3 + assets/icons/strikethrough.svg | 3 + crates/ui/src/components/icon.rs | 91 ++++++++++++++++++++++++ crates/ui/src/components/stories/icon.rs | 19 ++++- 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 assets/icons/indicator.svg create mode 100644 assets/icons/indicator_x.svg create mode 100644 assets/icons/strikethrough.svg diff --git a/assets/icons/indicator.svg b/assets/icons/indicator.svg new file mode 100644 index 0000000000..40f9151fd5 --- /dev/null +++ b/assets/icons/indicator.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/indicator_x.svg b/assets/icons/indicator_x.svg new file mode 100644 index 0000000000..d812c40fd2 --- /dev/null +++ b/assets/icons/indicator_x.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/strikethrough.svg b/assets/icons/strikethrough.svg new file mode 100644 index 0000000000..d7d0905912 --- /dev/null +++ b/assets/icons/strikethrough.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index dfb74b4fda..d94b573cbe 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -41,6 +41,17 @@ impl RenderOnce for AnyIcon { } } +/// The decoration for an icon. +/// +/// For example, this can show an indicator, an "x", +/// or a diagonal strkethrough 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 { Indicator, @@ -119,6 +130,8 @@ pub enum IconName { FolderX, Github, Hash, + Indicator, + IndicatorX, InlayHint, Link, MagicWand, @@ -159,6 +172,7 @@ pub enum IconName { SupermavenDisabled, SupermavenError, SupermavenInit, + Strikethrough, Tab, Terminal, Trash, @@ -229,6 +243,8 @@ impl IconName { IconName::FolderX => "icons/stop_sharing.svg", IconName::Github => "icons/github.svg", IconName::Hash => "icons/hash.svg", + IconName::Indicator => "icons/indicator.svg", + IconName::IndicatorX => "icons/indicator_x.svg", IconName::InlayHint => "icons/inlay_hint.svg", IconName::Link => "icons/link.svg", IconName::MagicWand => "icons/magic_wand.svg", @@ -269,6 +285,7 @@ impl IconName { IconName::SupermavenDisabled => "icons/supermaven_disabled.svg", IconName::SupermavenError => "icons/supermaven_error.svg", IconName::SupermavenInit => "icons/supermaven_init.svg", + IconName::Strikethrough => "icons/strikethrough.svg", IconName::Tab => "icons/tab.svg", IconName::Terminal => "icons/terminal.svg", IconName::Trash => "icons/trash.svg", @@ -344,6 +361,80 @@ impl RenderOnce for Icon { } } +#[derive(IntoElement)] +pub struct DecoratedIcon { + icon: Icon, + decoration: IconDecoration, + decoration_color: Color, + parent_background: Option, +} + +impl DecoratedIcon { + pub fn new(icon: Icon, decoration: IconDecoration) -> Self { + Self { + icon, + decoration, + decoration_color: Color::Default, + parent_background: None, + } + } + + pub fn decoration_color(mut self, color: Color) -> Self { + self.decoration_color = color; + self + } + + pub fn parent_background(mut self, background: Option) -> Self { + self.parent_background = background; + self + } +} + +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) + }; + + div() + .relative() + .size(self.icon.size) + .child(self.icon) + .child(decoration_knockout(decoration_icon)) + .child(decoration_svg(decoration_icon)) + } +} + #[derive(IntoElement)] pub struct IconWithIndicator { icon: Icon, diff --git a/crates/ui/src/components/stories/icon.rs b/crates/ui/src/components/stories/icon.rs index f6e750de2a..bdd253b567 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::*; +use crate::{prelude::*, DecoratedIcon, IconDecoration}; use crate::{Icon, IconName}; pub struct IconStory; @@ -13,6 +13,23 @@ 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))) }