Icons & Assets
GPUI Kit 通过 gpui_kit::assets 和 gpui_kit::component 提供 IconName 与 Icon。gpui-kit 默认 feature 会启用这两层,但应用仍需注册 AssetSource 才能按路径加载图标。底层的 gpui-kit-assets crate 将 SVG 资源与组件代码分开,因此你可以:
- 直接使用默认内置图标资源
- 完全不引入图标资源
- 自己维护一套 SVG 资源
#共享名称与兼容性
gpui_kit::assets::IconName 提供不依赖 Component 的完整共享目录。
gpui_kit::component::IconName 保留为原来的兼容枚举:现有导入、穷尽匹配和
.view(cx) 调用均无需改动,也无需新增 trait 导入。Icon::new(...) 同时接受
两种类型;旧名称可以通过 .into() 转为共享名称。
对于新的共享枚举,需要组件 Entity 时使用 Icon::new(name).view(cx),也可导入
gpui_kit::component::IconNameExt 后使用 name.view(cx)。
IconName::ALL 列出完整的 1,830 个名称,IconName::Accessibility.path() 返回
icons/accessibility.svg。默认资源源只包含原来的 101 个组件图标;额外图标请使用
下文的自定义资源源,或者显式注册 AllAssets 使用完整资源包。
#从默认资源源开始
gpui_kit::assets::Assets 是默认资源源,包含 default-icons.txt 中列出的原有 101 个组件图标。
在 Cargo.toml 中添加总入口 crate;它的默认 feature 包含 component 与 assets:
[dependencies]
gpui-kit = "0.7.1"
对于原生桌面应用,应在打开窗口前注册资源源。下面是可作为 src/main.rs 使用的完整示例:
use gpui_kit::*;
use gpui_kit::assets::Assets;
use gpui_kit::component::{Icon, IconName};
struct Example;
impl Render for Example {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().child(Icon::new(IconName::Inbox))
}
}
fn main() {
gpui_kit::application().with_assets(Assets).run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| Example)
})
.expect("Failed to open window");
});
}
with_assets 为整个应用安装一个资源源。gpui_kit::init(cx) 在创建窗口前初始化组件层。IconName::Inbox 对应 icons/inbox.svg,GPUI 将这个精确路径交给资源源。仅添加 crate 依赖不会注册资源源。完整应用初始化流程见快速开始。
#用 icon_assets! 选择更多目录图标
应用已经依赖 gpui-kit 时,直接使用 gpui_kit::assets::icon_assets!;默认启用的 assets feature 已经提供宏和共享目录。只为使用该宏,无须再添加一项 gpui-kit-assets 依赖。 如果某个 crate 有意独立使用资产层,而不依赖门面 crate,可以直接依赖 gpui-kit-assets,调用 gpui_kit_assets::icon_assets!。
按应用实际绘制的图标选择资源源:
| 需求 | 传给 with_assets 的资源源 | 原生程序嵌入的图标数据 |
|---|---|---|
| 只用默认组件图标 | Assets | 101 个默认 SVG |
| 默认图标加少量目录图标 | 组合 Assets 与 icon_assets! | 默认图标加选中的图标 |
| 使用全部目录图标 | AllAssets | 全部 1,830 个 SVG |
| 不使用 Component,只用少量目录图标 | 宏生成的资源源 | 仅选中的图标 |
宏参数必须是 IconName 枚举成员的标识符,不是字符串、路径,也不是自备 SVG 的文件名。例如,Accessibility 选中 icons/accessibility.svg。宏生成的 ExtraIcons 是实现了 AssetSource 的单元结构体:load 对选中路径返回借用的嵌入字节,对其他路径返回 Ok(None);list(prefix) 列出匹配前缀的已选路径。选取发生在编译期;枚举成员拼写错误或不存在会编译失败。需要在其他模块使用时,宏也接受可见性修饰符,例如 icon_assets!(pub ExtraIcons, [Accessibility]);。
下面是完整的 src/main.rs,选取两个额外的 Lucide 图标,同时保留默认组件图标。示例适用于原生桌面应用;使用上文的 gpui-kit 依赖即可,无须拷贝 SVG 或新增 crate。
use gpui_kit::*;
use gpui_kit::assets::{Assets as ComponentAssets, icon_assets};
use gpui_kit::component::Icon;
use std::borrow::Cow;
icon_assets!(ExtraIcons, [Accessibility, AlarmClock]);
struct AppAssets;
impl AssetSource for AppAssets {
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
if let Some(bytes) = ExtraIcons.load(path)? {
return Ok(Some(bytes));
}
ComponentAssets.load(path)
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
let mut paths = ComponentAssets.list(path)?;
paths.extend(ExtraIcons.list(path)?);
paths.sort();
paths.dedup();
Ok(paths)
}
}
struct Example;
impl Render for Example {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.child(Icon::new(gpui_kit::assets::IconName::Accessibility))
.child(Icon::new(gpui_kit::assets::IconName::AlarmClock))
}
}
fn main() {
gpui_kit::application().with_assets(AppAssets).run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| Example)
})
.expect("Failed to open window");
});
}
with_assets 只能注册一个资源源,因此 AppAssets 同时负责两个查找。必须先查询 ExtraIcons:原生平台的 ComponentAssets.load 对未知路径会报错,阻止后续回退。list 合并、排序并去重两个来源,使列出的资源键与 load 能提供的键一致。只有应用不需要默认组件图标时,才适合单独注册宏生成的资源源。
从手写的 Lucide 图标资源查找迁移时,可用一条 icon_assets!(ExtraIcons, [...]); 声明替换复制的 SVG 字节或文件名匹配分支。其他应用文件继续留在独立资源源中,查询该来源及 ExtraIcons 后再回退到默认 Assets,并合并三个来源的 list 结果。确认没有其他代码按路径读取旧 SVG 文件后,再删除旧副本。icons/brand-mark.svg 等自备文件不属于目录,仍需下文的自定义资源源。IconName::ALL 只列出名称,并不表示已注册资源源可以加载每个名称。
快速检查时,调用 AppAssets.load("icons/accessibility.svg") 和 AppAssets.load("icons/inbox.svg");原生平台上两者都应返回 Some。AppAssets.list("icons/") 也应包含这两个路径。额外图标空白时,检查绘制的 IconName 是否列在宏中,以及注册的是 AppAssets 而非 ComponentAssets。在 WebAssembly 上,宏生成的资源源仍可嵌入选中图标,但内置 Assets 需要带 endpoint 构造,并作为组合资源源的字段保存。
#自定义资源
icon_assets! 只能选择 GPUI Kit 已命名目录中的图标。应用自己的标志、插画、照片或其他图片,应由应用的 AssetSource 提供。仓库中的 assets 目录提供目录 SVG;把新文件放入应用不会生成新的 IconName 枚举成员。icons/brand-mark.svg 这样的资源键相对于嵌入目录,不是 URL,也不依赖程序运行时的工作目录。
下面的示例嵌入一个自有 SVG 和一个图片目录。在应用的 Cargo.toml 旁建立这些文件:
Cargo.toml
src/main.rs
assets/icons/brand-mark.svg
assets/images/cover.png
除 gpui-kit 外,添加 rust-embed:
[dependencies]
gpui-kit = "0.7.1"
rust-embed = { version = "8.7", features = ["include-exclude"] }
下面是完整的原生平台 src/main.rs:优先提供应用文件,再回退到 GPUI Kit 的默认组件图标。images/**/* 会嵌入该目录下的每个文件,所以目录中只应放要发布的资源;也可以改用更窄的 #[include] 规则。示例在打开窗口前注册一个组合资源源。
use gpui_kit::*;
use gpui_kit::assets::Assets as ComponentAssets;
use gpui_kit::component::Icon;
use rust_embed::RustEmbed;
use std::borrow::Cow;
#[derive(RustEmbed)]
#[folder = "./assets"]
#[include = "icons/**/*.svg"]
#[include = "images/**/*"]
struct AppFiles;
struct AppAssets;
impl AssetSource for AppAssets {
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
if path.is_empty() {
return Ok(None);
}
if let Some(file) = AppFiles::get(path) {
return Ok(Some(file.data));
}
ComponentAssets.load(path)
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
let mut paths = ComponentAssets.list(path)?;
paths.extend(AppFiles::iter().filter_map(|p| p.starts_with(path).then(|| p.into())));
paths.sort();
paths.dedup();
Ok(paths)
}
}
struct Example;
impl Render for Example {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.child(Icon::default().path("icons/brand-mark.svg"))
.child(img("images/cover.png").w(px(240.)).h(px(160.)).object_fit(ObjectFit::Cover))
}
}
fn main() {
gpui_kit::application().with_assets(AppAssets).run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| Example)
})
.expect("Failed to open window");
});
}
AssetSource::load(path) 根据精确资源键提供字节,list(prefix) 报告匹配的键。此例中,自有文件发生键冲突时优先;其他路径交给 ComponentAssets。原生平台上的内置资源源遇到未知非空路径会报错。如果应用也使用 icon_assets!,应在这个最终回退之前查询宏生成的资源源,并按上文方式将其路径合并到 list。在 WASM 上,需以 endpoint 构造 Assets::new(endpoint),并将其值保存在组合资源源中,而不能使用原生平台的单元结构体。
#使用资源路径
使用注册资源源提供的路径。Icon::path 用于 SVG 图标;IconName 把已知名称映射到这些路径。
use gpui_kit::component::{Icon, IconName};
let built_in = Icon::new(IconName::Inbox);
let custom = Icon::default().path("icons/brand-mark.svg");
let extra = Icon::new(gpui_kit::assets::IconName::Accessibility);
extra 需要上面的选取图标资源源或 AllAssets;custom 需要包含 icons/brand-mark.svg 的资源源。需要随文字颜色变化的单色 SVG,使用 Icon::path 或 svg().path(...):GPUI 将其绘制为可着色的透明度蒙版。需要时指定尺寸和 text_color。照片、其他位图,以及要保留原始颜色的多色 SVG,使用 img():
use gpui_kit::{ObjectFit, StyledImage, img, px, svg};
let tinted_mark = svg().path("icons/brand-mark.svg").size(px(24.));
let cover = img("images/cover.png")
.w(px(240.))
.h(px(160.))
.object_fit(ObjectFit::Cover);
let color_artwork = img("images/illustration.svg").size(px(160.));
img() 支持常见的 PNG、JPEG、WebP、GIF、SVG,以及 GPUI 的 Img::extensions() 列出的其他格式。它通过字节识别位图格式,SVG 则走另一条解码路径;因此文件内容也必须有效,不能只改扩展名。默认的 ObjectFit::Contain 会在边界内完整显示图片;Cover 会铺满并可能裁剪,Fill 可能拉伸变形。object_fit 作用于 img(),不作用于单色 svg() 元素。img() 通过 GPUI 的图片缓存异步加载并解码;嵌入资源的字节会复制进加载器,嵌入本身不会省掉解码或运行时图片内存。动画 GIF 和 WebP 可以包含多个帧。
对于 "images/cover.png" 这样的字符串,GPUI 会用该键调用已注册的 AssetSource。img(std::path::Path::new("/absolute/file.png")) 读取文件系统路径,URL 字符串使用 HTTP 加载器;它们的部署方式和错误处理不同。各种来源如何加载、布局和缓存,见图片。
#单独嵌入 SVG 图标
自定义图标可以通过 Icon::data 直接传入 SVG 字节,无须维护资源路径注册表:
use gpui_kit::component::{Icon, button::Button};
Button::new("search")
.icon(Icon::default().data(include_bytes!("search.svg")))
.label("Search")
这样可以省去该图标的资源查找。内置 IconName 和组件中使用的其他路径图标仍需要资源源。
数据所有权、来源替换、加载图标与自定义图标类型的说明见
SVG 字节。
#排查资源缺失
| 现象 | 检查项 |
|---|---|
| 默认组件图标空白 | 确认打开窗口前调用了 .with_assets(Assets),或注册了包含默认图标的组合资源源;核对路径以 icons/ 开头、以 .svg 结尾。 |
| 共享目录中的图标空白 | 确认名称属于已注册资源源。Assets 只有 101 个默认图标;其他名称需要选取图标资源源或 AllAssets。 |
| 自定义图标空白 | 将 Icon::path 中的键与 #[folder] 下的相对路径逐字比较,并检查 #[include] 和大小写。可用 list("icons/") 查看资源源提供的键。 |
| 图片空白 | 确认传入的是资源键、文件系统 Path 还是 URL;检查资源源是否包含位图文件,以及字节是否为受支持的图片格式。 |
在原生平台,内置 Assets 和 AllAssets 遇到缺失路径会返回错误;空路径返回 Ok(None)。组合资源源应先查询自定义资源,再回退到内置资源。Icon::data 可免去该 SVG 的路径查找,但无效 SVG 字节仍可能无法绘制。
#打包与 WebAssembly
原生平台的 Assets 只嵌入默认 SVG,AllAssets 则嵌入完整目录。icon_assets! 嵌入选中的 SVG 字节;rust-embed 嵌入 include 规则匹配的文件。源文件路径在构建时解析,打包后的原生应用运行时不需要源项目的 assets/ 目录。传给 img() 的文件系统 Path 不同:目标文件必须在应用运行的位置存在。上面的大小测量仅供参考,请测量自己应用的 release 二进制。
在 WebAssembly 上,Assets::new(endpoint) 和 AllAssets::new(endpoint) 使用同一个按需 HTTP 资源源。对于 icons/*.svg,它请求 endpoint + "/assets/" + path,缓存成功响应,并在请求期间返回临时加载错误。请托管精确路径,并使用不带尾部斜线的 endpoint。遇到图标空白时检查浏览器的 Network 和 Console 中的状态码、CORS 和 URL;下载完成后加载器本身不会主动要求重绘。此资源源的 list() 返回空列表。上面的原生 rust-embed 示例是另一种选择,也可以用于在 WASM 中显式嵌入应用文件。部署细节见 WebAssembly。
#参考资源
- Lucide Icons - GPUI Kit 的图标目录主要基于 Lucide 开源图标库。