Add new DecoratedIcon
component (#20516)
This PR creates a new, revamped `DecoratedIcon` component that enables using different SVGs, one for the knockout background and another for the actual icon. That's different than what we were doing before—copying the SVG and using slightly different positioning—because we wanted to unlock an aligned knockout effect, which was particularly hard to do with non-simple shapes such as an X. Release Notes: - N/A --------- Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
This commit is contained in:
parent
149e5fde36
commit
82427e1ffb
15 changed files with 253 additions and 96 deletions
3
assets/icons/knockouts/dot_bg.svg
Normal file
3
assets/icons/knockouts/dot_bg.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.1" y="2.1" width="6.8" height="6.8" rx="3.4" stroke="black" stroke-width="1.8"/>
|
||||
</svg>
|
After Width: | Height: | Size: 195 B |
3
assets/icons/knockouts/dot_fg.svg
Normal file
3
assets/icons/knockouts/dot_fg.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="3" width="5" height="5" rx="2.5" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 166 B |
3
assets/icons/knockouts/triangle_bg.svg
Normal file
3
assets/icons/knockouts/triangle_bg.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.364 7.6025L1.64681 8.75H3H8H9.35319L8.636 7.6025L6.136 3.6025L5.5 2.5849L4.864 3.6025L2.364 7.6025Z" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 252 B |
3
assets/icons/knockouts/triangle_fg.svg
Normal file
3
assets/icons/knockouts/triangle_fg.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 8H8L5.5 4L3 8Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 146 B |
10
assets/icons/knockouts/x_bg.svg
Normal file
10
assets/icons/knockouts/x_bg.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2050_903)">
|
||||
<path d="M1.83327 2.89393L1.19687 3.53033L1.83327 4.16672L3.16654 5.5L1.83327 6.83327L1.19687 7.46967L1.83327 8.10606L2.89393 9.16672L3.53033 9.80312L4.16672 9.16672L5.5 7.83345L6.83327 9.16672L7.46967 9.80312L8.10606 9.16672L9.16672 8.10606L9.80312 7.46967L9.16672 6.83327L7.83345 5.5L9.16672 4.16672L9.80312 3.53033L9.16672 2.89393L8.10606 1.83327L7.46967 1.19687L6.83327 1.83327L5.5 3.16654L4.16672 1.83327L3.53033 1.19687L2.89393 1.83327L1.83327 2.89393Z" stroke="black" stroke-width="1.8"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2050_903">
|
||||
<rect width="11" height="11" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 743 B |
3
assets/icons/knockouts/x_fg.svg
Normal file
3
assets/icons/knockouts/x_fg.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 3L5.5 5.5M8 8L5.5 5.5M5.5 5.5L3 8M5.5 5.5L8 3" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 198 B |
|
@ -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<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Styles",
|
||||
|
|
|
@ -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<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
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<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example(
|
||||
"Unselected",
|
||||
|
|
|
@ -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<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
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",
|
||||
|
|
|
@ -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 IconDecoration {
|
||||
kind: IconDecorationKind,
|
||||
color: Hsla,
|
||||
knockout_color: Hsla,
|
||||
position: Point<Pixels>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 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<ComponentExampleGroup<Self>> {
|
||||
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
|
||||
|
||||
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: IconDecoration,
|
||||
decoration_color: Color,
|
||||
parent_background: Option<Hsla>,
|
||||
decoration: Option<IconDecoration>,
|
||||
}
|
||||
|
||||
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<Hsla>) -> Self {
|
||||
self.parent_background = background;
|
||||
self
|
||||
pub fn new(icon: Icon, decoration: Option<IconDecoration>) -> 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<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,
|
||||
|
|
|
@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
|
|||
"An indicator visually represents a status or state."
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Types",
|
||||
|
|
|
@ -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::<Icon>())
|
||||
.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)))
|
||||
}
|
||||
|
|
|
@ -160,7 +160,7 @@ impl ComponentPreview for Table {
|
|||
ExampleLabelSide::Top
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group(vec![
|
||||
single_example(
|
||||
|
|
|
@ -30,10 +30,10 @@ pub trait ComponentPreview: IntoElement {
|
|||
ExampleLabelSide::default()
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>>;
|
||||
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
|
||||
|
||||
fn component_previews() -> Vec<AnyElement> {
|
||||
Self::examples()
|
||||
fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue