From d251a9b08b46eb8c1ed1ea458426bf97909ba5c0 Mon Sep 17 00:00:00 2001 From: grubby <70445343+andreaskrath@users.noreply.github.com> Date: Thu, 20 Nov 2025 03:30:08 +0100 Subject: [PATCH] icon: Rework icons to make use of the `IconNamed` trait (#1640) This makes use of the `IconNamed` trait and a blanked implementation to convert anything that implements this to an `Icon`. This allows for easily defined custom versions of `IconName`, while minimally changing existing code (essentially only if you previously made use of the `.path()` method on the `IconName` enum; this now requires an import of the `IconNamed` trait). # Example ```rust use gpui_component::IconNamed; pub enum IconName { Encounters, Monsters, Spells, } impl IconNamed for IconName { fn path(self) -> gpui::SharedString { match self { IconName::Encounters => "icons/encounters.svg", IconName::Monsters => "icons/monsters.svg", IconName::Spells => "icons/spells.svg", } .into() } } // this allows for the following interactions (works with anything that has the `.icon(icon)` method Button::new("my-button").icon(IconName::Spells); Icon::new(IconName::Monsters); ``` If you want to directly "render" a custom `IconName` you must implement the `RenderOnce` trait and derive `IntoElement` on the `IconName`. ```rust use gpui::{IntoElement, RenderOnce}; use gpui_component::IconNamed; #[derive(IntoElement)] pub enum IconName { // The same as before } impl IconNamed for IconName { // The same as before } impl RenderOnce for IconName { fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement { gpui_component::Icon::empty().path(self.path()) } } // this allows for the following interaction div() .child(IconName::Monsters) ``` Overall I think is an improvement to the existing way to do custom `IconName` implementations. I am unsure if this change should also be reflected in the documentation on the section with "Icons & Assets", though I personally think it would make sense to highlight this way to do custom versions of `IconName` as it is considerably less involved than the current approach. Closes #1627. --------- Co-authored-by: Jason Lee --- crates/ui/src/checkbox.rs | 4 +- crates/ui/src/icon.rs | 41 +++++++++++-------- docs/docs/components/icon.md | 77 +++++++++++++++++------------------- 3 files changed, 62 insertions(+), 60 deletions(-) diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 3cf21860..1ac980d4 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,8 +1,8 @@ use std::{rc::Rc, time::Duration}; use crate::{ - text::Text, v_flex, ActiveTheme, Disableable, FocusableExt, IconName, Selectable, Sizable, - Size, StyledExt as _, + icon::IconNamed, text::Text, v_flex, ActiveTheme, Disableable, FocusableExt, IconName, + Selectable, Sizable, Size, StyledExt as _, }; use gpui::{ div, prelude::FluentBuilder as _, px, relative, rems, svg, Animation, AnimationExt, AnyElement, diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 0e61450e..e83bb4d3 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -5,6 +5,21 @@ use gpui::{ Transformation, Window, }; +/// Types implementing this trait can automatically be converted to [`Icon`]. +/// +/// This allows you to implement a custom version of [`IconName`] that functions as a drop-in +/// replacement for other UI components. +pub trait IconNamed { + /// Returns the embedded path of the icon. + fn path(self) -> SharedString; +} + +impl From for Icon { + fn from(value: T) -> Self { + Icon::build(value) + } +} + /// The name of an icon in the asset bundle. #[derive(IntoElement, Clone)] pub enum IconName { @@ -93,7 +108,14 @@ pub enum IconName { } impl IconName { - pub fn path(self) -> SharedString { + /// Return the icon as a Entity + pub fn view(self, cx: &mut App) -> Entity { + Icon::build(self).view(cx) + } +} + +impl IconNamed for IconName { + fn path(self) -> SharedString { match self { Self::ALargeSmall => "icons/a-large-small.svg", Self::ArrowDown => "icons/arrow-down.svg", @@ -180,17 +202,6 @@ impl IconName { } .into() } - - /// Return the icon as a Entity - pub fn view(self, cx: &mut App) -> Entity { - Icon::build(self).view(cx) - } -} - -impl From for Icon { - fn from(val: IconName) -> Self { - Icon::build(val) - } } impl From for AnyElement { @@ -239,16 +250,12 @@ impl Clone for Icon { } } -pub trait IconNamed { - fn path(&self) -> SharedString; -} - impl Icon { pub fn new(icon: impl Into) -> Self { icon.into() } - fn build(name: IconName) -> Self { + fn build(name: impl IconNamed) -> Self { Self::default().path(name.path()) } diff --git a/docs/docs/components/icon.md b/docs/docs/components/icon.md index 8ced945e..67e2ff80 100644 --- a/docs/docs/components/icon.md +++ b/docs/docs/components/icon.md @@ -7,6 +7,8 @@ description: Display SVG icons with various sizes, colors, and transformations. A flexible icon component that renders SVG icons from the built-in icon library. Icons are based on Lucide.dev and support customization of size, color, and rotation. The component requires SVG files to be provided by the user in the assets bundle. +Before you start, please make sure you have read: [Icons & Assets](../assets.md) to understand how use SVG in GPUI & GPUI Component application. + ## Import ```rust @@ -136,55 +138,48 @@ The Icon component supports several predefined sizes: | Large | `.large()` | `size_6()` | 24px | | Custom | `.with_size(px(n))` | - | n px | -## SVG Asset Requirements +## Build you own `IconName`. -**Important:** SVG files must be provided by the user in the assets bundle. The component expects SVG files to be located in the `icons/` directory relative to your assets root. +You can define your own `IconName` to have more specific icons for your application. We have `IconNamed` trait for you to implement for your. -For example, if you want to use `IconName::Heart`, you need to provide: +```rust +use gpui_component::IconNamed; -- `icons/heart.svg` in your assets bundle +pub enum IconName { + Encounters, + Monsters, + Spells, +} -The component automatically maps icon names to their corresponding SVG file paths: +impl IconNamed for IconName { + fn path(self) -> gpui::SharedString { + match self { + IconName::Encounters => "icons/encounters.svg", + IconName::Monsters => "icons/monsters.svg", + IconName::Spells => "icons/spells.svg", + } + .into() + } +} -- `IconName::ArrowUp` → `icons/arrow-up.svg` -- `IconName::GitHub` → `icons/github.svg` -- `IconName::CircleCheck` → `icons/circle-check.svg` +// This allows for the following interactions (works with anything that has the `.icon(icon)` method. +Button::new("my-button").icon(IconName::Spells); +Icon::new(IconName::Monsters); +``` -## API Reference +If you want to directly `render` a custom `IconName` you must implement the `RenderOnce` trait and derive `IntoElement` on the `IconName`. -### Icon +```rust +impl RenderOnce for IconName { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + Icon::empty().path(self.path()) + } +} -| Method | Description | -| --------------------------- | --------------------------------------------------- | -| `new(icon)` | Create a new icon from `IconName` or another `Icon` | -| `path(path)` | Set custom SVG file path | -| `view(cx)` | Create a view entity for the icon | -| `rotate(radians)` | Rotate the icon by specified radians | -| `transform(transformation)` | Apply custom transformation | -| `empty()` | Create an empty icon (for custom paths) | - -### IconName Methods - -| Method | Description | -| ---------- | ------------------------------------------------ | -| `path()` | Get the SVG file path for this icon | -| `view(cx)` | Create a view entity directly from the icon name | - -### Styling (via `Styled` trait) - -| Method | Description | -| ------------------- | ------------------ | -| `text_color(color)` | Set the icon color | - -### Sizing (via `Sizable` trait) - -| Method | Description | -| ----------------- | ----------------------------------- | -| `xsmall()` | Set size to extra small (12px) | -| `small()` | Set size to small (14px) | -| `medium()` | Set size to medium (16px) - default | -| `large()` | Set size to large (24px) | -| `with_size(size)` | Set custom size in pixels | +// Now you can use it directly in your element tree: +div() + .child(IconName::Monsters) +``` ## Examples