mod decorated_icon;mod icon_decoration; use std::path::{Path, PathBuf};use std::sync::Arc; pub use decorated_icon::*;use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation, img, svg};pub use icon_decoration::*;pub use icons::*; use crate::traits::transformable::Transformable;use crate::{Indicator, prelude::*}; #[derive(IntoElement)]pub enum AnyIcon { Icon(Icon), AnimatedIcon(AnimationElement<Icon>),} impl AnyIcon { /// Returns a new [`AnyIcon`] after applying the given mapping function /// to the contained [`Icon`]. pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self { match self { Self::Icon(icon) => Self::Icon(f(icon)), Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)), } }} impl From<Icon> for AnyIcon { fn from(value: Icon) -> Self { Self::Icon(value) }} impl From<AnimationElement<Icon>> for AnyIcon { fn from(value: AnimationElement<Icon>) -> Self { Self::AnimatedIcon(value) }} impl RenderOnce for AnyIcon { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { match self { Self::Icon(icon) => icon.into_any_element(), Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(), } }} #[derive(Default, PartialEq, Copy, Clone)]pub enum IconSize { /// 10px Indicator, /// 12px XSmall, /// 14px Small, #[default] /// 16px Medium, /// 48px XLarge, Custom(Rems),} impl IconSize { pub fn rems(self) -> Rems { match self { IconSize::Indicator => rems_from_px(10.), IconSize::XSmall => rems_from_px(12.), IconSize::Small => rems_from_px(14.), IconSize::Medium => rems_from_px(16.), IconSize::XLarge => rems_from_px(48.), IconSize::Custom(size) => size, } } /// Returns the individual components of the square that contains this [`IconSize`]. /// /// The returned tuple contains: /// 1. The length of one side of the square /// 2. The padding of one side of the square pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) { let icon_size = self.rems() * window.rem_size(); let padding = match self { IconSize::Indicator => DynamicSpacing::Base00.px(cx), IconSize::XSmall => DynamicSpacing::Base02.px(cx), IconSize::Small => DynamicSpacing::Base02.px(cx), IconSize::Medium => DynamicSpacing::Base02.px(cx), IconSize::XLarge => DynamicSpacing::Base02.px(cx), // TODO: Wire into dynamic spacing IconSize::Custom(size) => size.to_pixels(window.rem_size()), }; (icon_size, padding) } /// Returns the length of a side of the square that contains this [`IconSize`], with padding. pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels { let (icon_size, padding) = self.square_components(window, cx); icon_size + padding * 2. }} impl From<IconName> for Icon { fn from(icon: IconName) -> Self { Icon::new(icon) }} /// The source of an icon.enum IconSource { /// An SVG embedded in the Zed binary. Embedded(SharedString), /// An image file located at the specified path. /// /// Currently our SVG renderer is missing support for rendering polychrome SVGs. /// /// In order to support icon themes, we render the icons as images instead. External(Arc<Path>), /// An SVG not embedded in the Zed binary. ExternalSvg(SharedString),} #[derive(IntoElement, RegisterComponent)]pub struct Icon { source: IconSource, color: Color, size: Rems, transformation: Transformation,} impl Icon { pub fn new(icon: IconName) -> Self { Self { source: IconSource::Embedded(icon.path().into()), color: Color::default(), size: IconSize::default().rems(), transformation: Transformation::default(), } } /// Create an icon from a path. Uses a heuristic to determine if it's embedded or external: /// - Paths starting with "icons/" are treated as embedded SVGs /// - Other paths are treated as external raster images (from icon themes) pub fn from_path(path: impl Into<SharedString>) -> Self { let path = path.into(); let source = if path.starts_with("icons/") { IconSource::Embedded(path) } else { IconSource::External(Arc::from(PathBuf::from(path.as_ref()))) }; Self { source, color: Color::default(), size: IconSize::default().rems(), transformation: Transformation::default(), } } pub fn from_external_svg(svg: SharedString) -> Self { Self { source: IconSource::ExternalSvg(svg), color: Color::default(), size: IconSize::default().rems(), transformation: Transformation::default(), } } pub fn color(mut self, color: Color) -> Self { self.color = color; self } pub fn size(mut self, size: IconSize) -> Self { self.size = size.rems(); self } /// Sets a custom size for the icon, in [`Rems`]. /// /// Not to be exposed outside of the `ui` crate. pub(crate) fn custom_size(mut self, size: Rems) -> Self { self.size = size; self }} impl Transformable for Icon { fn transform(mut self, transformation: Transformation) -> Self { self.transformation = transformation; self }} impl RenderOnce for Icon { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { match self.source { IconSource::Embedded(path) => svg() .with_transformation(self.transformation) .size(self.size) .flex_none() .path(path) .text_color(self.color.color(cx)) .into_any_element(), IconSource::ExternalSvg(path) => svg() .external_path(path) .with_transformation(self.transformation) .size(self.size) .flex_none() .text_color(self.color.color(cx)) .into_any_element(), IconSource::External(path) => img(path) .size(self.size) .flex_none() .text_color(self.color.color(cx)) .into_any_element(), } }} #[derive(IntoElement)]pub struct IconWithIndicator { icon: Icon, indicator: Option<Indicator>, indicator_border_color: Option<Hsla>,} impl IconWithIndicator { pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self { Self { icon, indicator, indicator_border_color: None, } } pub fn indicator(mut self, indicator: Option<Indicator>) -> Self { self.indicator = indicator; self } pub fn indicator_color(mut self, color: Color) -> Self { if let Some(indicator) = self.indicator.as_mut() { indicator.color = color; } self } pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self { self.indicator_border_color = color; self }} impl RenderOnce for IconWithIndicator { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let indicator_border_color = self .indicator_border_color .unwrap_or_else(|| cx.theme().colors().elevated_surface_background); div() .relative() .child(self.icon) .when_some(self.indicator, |this, indicator| { this.child( div() .absolute() .size_2p5() .border_2() .border_color(indicator_border_color) .rounded_full() .bottom_neg_0p5() .right_neg_0p5() .child(indicator), ) }) }} impl Component for Icon { fn scope() -> ComponentScope { ComponentScope::Images } fn description() -> Option<&'static str> { Some( "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.", ) } fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> { Some( v_flex() .gap_6() .children(vec![ example_group_with_title( "Sizes", vec![single_example( "XSmall, Small, Default, Large", h_flex() .gap_1() .child(Icon::new(IconName::Star).size(IconSize::XSmall)) .child(Icon::new(IconName::Star).size(IconSize::Small)) .child(Icon::new(IconName::Star)) .child(Icon::new(IconName::Star).size(IconSize::XLarge)) .into_any_element(), )], ), example_group(vec![single_example( "All Icons", h_flex() .image_cache(gpui::retain_all("all icons")) .flex_wrap() .gap_2() .children(<IconName as strum::IntoEnumIterator>::iter().map( |icon_name: IconName| { let name: SharedString = format!("{icon_name:?}").into(); v_flex() .min_w_0() .w_24() .p_1p5() .gap_2() .border_1() .border_color(cx.theme().colors().border_variant) .bg(cx.theme().colors().element_disabled) .rounded_sm() .items_center() .child(Icon::new(icon_name)) .child(Label::new(name).size(LabelSize::XSmall).truncate()) }, )) .into_any_element(), )]), ]) .into_any_element(), ) }}
mod decorated_icon;
mod icon_decoration;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub use decorated_icon::*;
use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation, img, svg};
pub use icon_decoration::*;
pub use icons::*;
use crate::traits::transformable::Transformable;
use crate::{Indicator, prelude::*};
#[derive(IntoElement)]
pub enum AnyIcon {
Icon(Icon),
AnimatedIcon(AnimationElement<Icon>),
}
impl AnyIcon {
/// Returns a new [`AnyIcon`] after applying the given mapping function
/// to the contained [`Icon`].
pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
match self {
Self::Icon(icon) => Self::Icon(f(icon)),
Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
}
}
}
impl From<Icon> for AnyIcon {
fn from(value: Icon) -> Self {
Self::Icon(value)
}
}
impl From<AnimationElement<Icon>> for AnyIcon {
fn from(value: AnimationElement<Icon>) -> Self {
Self::AnimatedIcon(value)
}
}
impl RenderOnce for AnyIcon {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
match self {
Self::Icon(icon) => icon.into_any_element(),
Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
}
}
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconSize {
/// 10px
Indicator,
/// 12px
XSmall,
/// 14px
Small,
#[default]
/// 16px
Medium,
/// 48px
XLarge,
Custom(Rems),
}
impl IconSize {
pub fn rems(self) -> Rems {
match self {
IconSize::Indicator => rems_from_px(10.),
IconSize::XSmall => rems_from_px(12.),
IconSize::Small => rems_from_px(14.),
IconSize::Medium => rems_from_px(16.),
IconSize::XLarge => rems_from_px(48.),
IconSize::Custom(size) => size,
}
}
/// Returns the individual components of the square that contains this [`IconSize`].
///
/// The returned tuple contains:
/// 1. The length of one side of the square
/// 2. The padding of one side of the square
pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
let icon_size = self.rems() * window.rem_size();
let padding = match self {
IconSize::Indicator => DynamicSpacing::Base00.px(cx),
IconSize::XSmall => DynamicSpacing::Base02.px(cx),
IconSize::Small => DynamicSpacing::Base02.px(cx),
IconSize::Medium => DynamicSpacing::Base02.px(cx),
IconSize::XLarge => DynamicSpacing::Base02.px(cx),
// TODO: Wire into dynamic spacing
IconSize::Custom(size) => size.to_pixels(window.rem_size()),
};
(icon_size, padding)
}
/// Returns the length of a side of the square that contains this [`IconSize`], with padding.
pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
let (icon_size, padding) = self.square_components(window, cx);
icon_size + padding * 2.
}
}
impl From<IconName> for Icon {
fn from(icon: IconName) -> Self {
Icon::new(icon)
}
}
/// The source of an icon.
enum IconSource {
/// An SVG embedded in the Zed binary.
Embedded(SharedString),
/// An image file located at the specified path.
///
/// Currently our SVG renderer is missing support for rendering polychrome SVGs.
///
/// In order to support icon themes, we render the icons as images instead.
External(Arc<Path>),
/// An SVG not embedded in the Zed binary.
ExternalSvg(SharedString),
}
#[derive(IntoElement, RegisterComponent)]
pub struct Icon {
source: IconSource,
color: Color,
size: Rems,
transformation: Transformation,
}
impl Icon {
pub fn new(icon: IconName) -> Self {
Self {
source: IconSource::Embedded(icon.path().into()),
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),
}
}
/// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
/// - Paths starting with "icons/" are treated as embedded SVGs
/// - Other paths are treated as external raster images (from icon themes)
pub fn from_path(path: impl Into<SharedString>) -> Self {
let path = path.into();
let source = if path.starts_with("icons/") {
IconSource::Embedded(path)
} else {
IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
};
Self {
source,
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),
}
}
pub fn from_external_svg(svg: SharedString) -> Self {
Self {
source: IconSource::ExternalSvg(svg),
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size.rems();
self
}
/// Sets a custom size for the icon, in [`Rems`].
///
/// Not to be exposed outside of the `ui` crate.
pub(crate) fn custom_size(mut self, size: Rems) -> Self {
self.size = size;
self
}
}
impl Transformable for Icon {
fn transform(mut self, transformation: Transformation) -> Self {
self.transformation = transformation;
self
}
}
impl RenderOnce for Icon {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
match self.source {
IconSource::Embedded(path) => svg()
.with_transformation(self.transformation)
.size(self.size)
.flex_none()
.path(path)
.text_color(self.color.color(cx))
.into_any_element(),
IconSource::ExternalSvg(path) => svg()
.external_path(path)
.with_transformation(self.transformation)
.size(self.size)
.flex_none()
.text_color(self.color.color(cx))
.into_any_element(),
IconSource::External(path) => img(path)
.size(self.size)
.flex_none()
.text_color(self.color.color(cx))
.into_any_element(),
}
}
}
#[derive(IntoElement)]
pub struct IconWithIndicator {
icon: Icon,
indicator: Option<Indicator>,
indicator_border_color: Option<Hsla>,
}
impl IconWithIndicator {
pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
Self {
icon,
indicator,
indicator_border_color: None,
}
}
pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
self.indicator = indicator;
self
}
pub fn indicator_color(mut self, color: Color) -> Self {
if let Some(indicator) = self.indicator.as_mut() {
indicator.color = color;
}
self
}
pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
self.indicator_border_color = color;
self
}
}
impl RenderOnce for IconWithIndicator {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let indicator_border_color = self
.indicator_border_color
.unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
div()
.relative()
.child(self.icon)
.when_some(self.indicator, |this, indicator| {
this.child(
div()
.absolute()
.size_2p5()
.border_2()
.border_color(indicator_border_color)
.rounded_full()
.bottom_neg_0p5()
.right_neg_0p5()
.child(indicator),
)
})
}
}
impl Component for Icon {
fn scope() -> ComponentScope {
ComponentScope::Images
}
fn description() -> Option<&'static str> {
Some(
"A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Sizes",
vec![single_example(
"XSmall, Small, Default, Large",
h_flex()
.gap_1()
.child(Icon::new(IconName::Star).size(IconSize::XSmall))
.child(Icon::new(IconName::Star).size(IconSize::Small))
.child(Icon::new(IconName::Star))
.child(Icon::new(IconName::Star).size(IconSize::XLarge))
.into_any_element(),
)],
),
example_group(vec![single_example(
"All Icons",
h_flex()
.image_cache(gpui::retain_all("all icons"))
.flex_wrap()
.gap_2()
.children(<IconName as strum::IntoEnumIterator>::iter().map(
|icon_name: IconName| {
let name: SharedString = format!("{icon_name:?}").into();
v_flex()
.min_w_0()
.w_24()
.p_1p5()
.gap_2()
.border_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().element_disabled)
.rounded_sm()
.items_center()
.child(Icon::new(icon_name))
.child(Label::new(name).size(LabelSize::XSmall).truncate())
},
))
.into_any_element(),
)]),
])
.into_any_element(),
)
}
}