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 <huacnlee@gmail.com>
This commit is contained in:
grubby 2025-11-20 03:30:08 +01:00 committed by GitHub
parent 2e3387a76f
commit d251a9b08b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 60 deletions

View file

@ -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,

View file

@ -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<T: IconNamed> From<T> 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<Icon>
pub fn view(self, cx: &mut App) -> Entity<Icon> {
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<Icon>
pub fn view(self, cx: &mut App) -> Entity<Icon> {
Icon::build(self).view(cx)
}
}
impl From<IconName> for Icon {
fn from(val: IconName) -> Self {
Icon::build(val)
}
}
impl From<IconName> 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<Icon>) -> Self {
icon.into()
}
fn build(name: IconName) -> Self {
fn build(name: impl IconNamed) -> Self {
Self::default().path(name.path())
}

View file

@ -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