IconSpec —— 贡献点图标规格

概述

`IconSpec` 是 RML 框架中**所有贡献点图标的统一规格类型**,定义在 `rml_core::contribution`。它是 `IContribution::icon()` 的返回类型,替代了早期未类型化的 `Option<SharedString>`。

设计目标: - **消除字符串歧义**:variant tag 显式声明图标种类,框架无需 `is_url`/`is_asset_path` 等字符串推断 - **统一封装**:所有图标渲染走同一入口 `rml_ui::resolve_icon` - **原生支持嵌入资源**:`Path` variant 经 `CompositeAssets` 自动路由到 RML 嵌入资源系统 - **保持 trait 中立**:`rml_core` 不依赖 `gpui-component`,故 `Named` 载荷为 `SharedString` 而非 `IconName`

三种 Variant

pub enum IconSpec {
    Named(SharedString),  // 内置命名图标
    Path(SharedString),   // SVG 资产路径(含嵌入资源)
    Url(SharedString),    // 外部 URL
}

`Named(SharedString)` —— 内置命名图标

字符串对应 gpui-component `IconName` 枚举的变体名(PascalCase,如 `"BookOpen"`、`"Settings"`)。

fn icon(&self) -> Option<IconSpec> {
    Some(IconSpec::named("BookOpen"))
}

**渲染路径**:`Icon::new(name).small()`,未匹配时 fallback `IconName::PanelLeft`。

**完整图标清单**:见 [gpui-component Icon 文档](https://longbridge.github.io/gpui-component/zh-CN/docs/components/icon) 与 `crates/ui/src/components/activity_bar/icon.rs` 中 `parse_icon_name` 的匹配表。

`Path(SharedString)` —— SVG 资产路径(推荐用于自定义图标)

SVG 资产路径,相对资产根。**这是嵌入资源的入口**。

fn icon(&self) -> Option<IconSpec> {
    Some(IconSpec::path("logo.svg"))  // 用户嵌入资源
    // 或
    // Some(IconSpec::path("icons/foo.svg"))  // gpui-component 内置图标
}

**渲染路径**:`Icon::default().path(s).small()`,经 `CompositeAssets`(`rml_app::assets`)路由解析。

**自动支持两种来源**: 1. **gpui-component 内置图标**:`icons/**/*.svg`(由 `gpui_component_assets::Assets` 提供) 2. **RML 用户嵌入资源**:`assets/<path>`(由 `rml_core::assets::load` 提供,编译期 `include_bytes!` 嵌入或运行期文件系统读取)

无需为嵌入资源引入新 variant 或新 API——`CompositeAssets` 在 `RmlApplication::run` 时自动注册为 GPUI 的 `AssetSource`,`Icon::default().path(s)` 透明地解析到嵌入资源。

`Url(SharedString)` —— 外部 URL

`http:`/`https:`/`file:` 等协议前缀的外部图片 URL。

fn icon(&self) -> Option<IconSpec> {
    Some(IconSpec::url("https://example.com/logo.png"))
}

**渲染路径**:`gpui::img(s).size_4()`,不经过 `Icon` 组件(`Icon::path()` 不接受 URL)。

**注意**:URL 图片通常自带颜色,不应用 `text_color` 着色。

Fallback 行为

| 返回值 | 渲染结果 | |--------|----------| | `Some(IconSpec::Named(s))` 且 `s` 匹配 | 对应 `IconName` | | `Some(IconSpec::Named(s))` 且 `s` 未匹配 | `IconName::PanelLeft` | | `Some(IconSpec::Path(s))` | `Icon::default().path(s)`(路径不存在则渲染空白) | | `Some(IconSpec::Url(s))` | `gpui::img(s)` | | `None` | `IconName::PanelLeft` |

渲染入口

`rml_ui::resolve_icon` 是唯一的图标渲染函数:

pub fn resolve_icon(spec: Option<IconSpec>, window: &Window) -> AnyElement

业务代码不直接调用 `resolve_icon`——它由 `ActivityBar`、`TabWindow` 等宿主组件在渲染时调用。贡献点只需返回 `IconSpec`,渲染交给框架。

嵌入资源集成详解

资源注册

RML 的 `build.rs` 在编译期扫描 `assets/` 目录,按 `.assets(path, embed)` 配置生成注册代码:

- **Embedded 模式**:`include_bytes!` 编译期嵌入二进制(推荐用于发布) - **Filesystem 模式**:运行期从磁盘读取,首次读取后 `Box::leak` 缓存为 `&'static [u8]`

资源加载

// rml_core::assets
pub fn load(path: &str) -> Option<&'static [u8]>;
pub fn load_str(path: &str) -> Option<&'static str>;
pub fn list() -> Vec<&'static str>;

与图标的桥接

`CompositeAssets`(`rml_app::assets`)实现 `gpui::AssetSource`,按以下顺序解析路径:

1. `gpui_component_assets::Assets.load(path)` —— 内置图标 2. `rml_core::assets::load(path)` —— 用户嵌入资源

`RmlApplication::run` 自动注册 `CompositeAssets`,无需手动配置。`IconSpec::Path("logo.svg")` 因此能直接解析到用户嵌入的 `assets/logo.svg`。

完整示例

use rml_core::contribution::{IContribution, IconSpec};
use gpui::SharedString;

pub struct MyPanel;

impl IContribution for MyPanel {
    fn id(&self) -> &str { "my-panel" }
    fn name(&self) -> SharedString { "My Panel".into() }
    fn icon(&self) -> Option<IconSpec> {
        // 优先使用嵌入资源中的品牌图标
        Some(IconSpec::path("brand/icon.svg"))
    }
}

设计决策记录

为何不返回 `Any`?

`Any` 在 Rust 中是反模式逃生口,仅适用于类型集合真正开放的场景(插件系统)。图标种类是封闭集合,`enum` + variant tag 提供编译期穷举检查,比 `Any` + downcast 猜测更安全。

为何 `Named` 载荷是 `SharedString` 而非 `IconName`?

`rml_core` 保持框架中立,不依赖 `gpui-component`。引入 `IconName` 会让 core 所有消费者(CLI、测试、非 UI 上下文)被迫拉入 gpui-component 编译负担。字符串→`IconName` 的映射集中在 `rml_ui::resolve_icon` 的 `parse_icon_name` 表中(单一位置),未来可通过给上游 `icon_named!` 宏加 `FromStr` impl 一次性消除。

为何不引入 `IconSpec::Svg(SharedString)` 内联 SVG 字符串?

当前无业务需求。未来如有运行期生成 SVG 的场景,可增加此 variant,编译器会强制 `resolve_icon` 更新匹配臂——这正是封闭 enum 的安全保证。

相关文档

- [activity-bar.md](./activity-bar.md) —— `IActivityPanel` 使用 `IconSpec` - [贡献点架构](../../09-architecture/contribution-system.md) —— `IContribution` trait 详解 - [gpui-component Icon](https://longbridge.github.io/gpui-component/zh-CN/docs/components/icon) —— 上游 `IconName` 完整清单