feat: add spinner component

This commit is contained in:
2026-11-03 16:39:13 +07:00
parent 978aecb651
commit 7399d92f36
11 changed files with 595 additions and 114 deletions
+32 -13
View File
@@ -4,7 +4,7 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
Styled as _, Window, div, px, svg,
Styled as _, Transformation, Window, div, px, svg,
};
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
@@ -34,6 +34,7 @@ pub enum IconRole {
WindowMinimize,
WindowMaximize,
WindowRestore,
Loader,
}
impl IconRole {
@@ -58,6 +59,7 @@ impl IconRole {
Self::WindowMinimize,
Self::WindowMaximize,
Self::WindowRestore,
Self::Loader,
]
}
@@ -82,6 +84,7 @@ impl IconRole {
Self::WindowMinimize => "icons/minus.svg",
Self::WindowMaximize => "icons/square.svg",
Self::WindowRestore => "icons/copy.svg",
Self::Loader => "icons/loader.svg",
}
}
@@ -106,6 +109,7 @@ impl IconRole {
Self::WindowMinimize => "window-minimize",
Self::WindowMaximize => "window-maximize",
Self::WindowRestore => "window-restore",
Self::Loader => "loader",
}
}
@@ -180,6 +184,7 @@ impl IconRole {
Self::WindowMinimize => &["window-minimize-symbolic", "window-minimize"],
Self::WindowMaximize => &["window-maximize-symbolic", "window-maximize"],
Self::WindowRestore => &["window-restore-symbolic", "window-restore"],
Self::Loader => &["process-working-symbolic", "process-working"],
}
}
}
@@ -190,6 +195,7 @@ pub struct Icon {
role: IconRole,
size: Pixels,
color: Option<Hsla>,
transformation: Option<Transformation>,
}
/// Creates an icon for the role at the default size of 16px.
@@ -198,6 +204,7 @@ pub fn icon(role: IconRole) -> Icon {
role,
size: px(16.0),
color: None,
transformation: None,
}
}
@@ -212,6 +219,12 @@ impl Icon {
self
}
/// An extra SVG transformation, e.g. a spinner's rotation.
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
self.transformation = Some(transformation);
self
}
/// The asset key a resolved freedesktop file is served under.
#[cfg(target_os = "linux")]
fn freedesktop_asset_path(&self) -> Option<SharedString> {
@@ -232,27 +245,29 @@ impl RenderOnce for Icon {
}
impl Icon {
/// Windows: the Segoe Fluent codepoint drawn as text in the system font.
/// Windows: the Segoe Fluent codepoint drawn as text in the system font;
/// roles without a codepoint fall back to the bundled SVG.
#[cfg(target_os = "windows")]
fn render_glyph(self) -> impl IntoElement {
div()
.flex_none()
.size(self.size)
.flex()
.items_center()
.justify_center()
.when_some(segoe::glyph(self.role), |el, glyph| {
el.child(
match segoe::glyph(self.role) {
Some(glyph) => div()
.flex_none()
.size(self.size)
.flex()
.items_center()
.justify_center()
.child(
div()
.font_family(segoe::FONT_FAMILY)
.text_size(self.size)
.when_some(self.color, |el, color| el.text_color(color))
.child(glyph.to_string()),
)
})
.into_any_element(),
None => self.render_svg().into_any_element(),
}
}
#[cfg(not(target_os = "windows"))]
fn render_svg(self) -> impl IntoElement {
#[cfg(target_os = "linux")]
let path: SharedString = self
@@ -266,7 +281,10 @@ impl Icon {
svg()
.size_full()
.path(path)
.when_some(self.color, |el, color| el.text_color(color)),
.when_some(self.color, |el, color| el.text_color(color))
.when_some(self.transformation, |el, transformation| {
el.with_transformation(transformation)
}),
)
}
}
@@ -294,6 +312,7 @@ fn asset_bytes(path: &str) -> Option<&'static [u8]> {
"icons/eye-off.svg" => Some(include_bytes!("../assets/eye-off.svg")),
"icons/panel-left.svg" => Some(include_bytes!("../assets/panel-left.svg")),
"icons/panel-right.svg" => Some(include_bytes!("../assets/panel-right.svg")),
"icons/loader.svg" => Some(include_bytes!("../assets/loader.svg")),
_ => None,
}
}
+8 -2
View File
@@ -6,6 +6,9 @@ use crate::IconRole;
/// The Segoe Fluent Icons codepoint for the role.
pub(crate) fn glyph(role: IconRole) -> Option<char> {
let codepoint = match role {
// No Segoe spinner glyph; the icon falls back to the bundled SVG,
// which the spinner rotates.
IconRole::Loader => return None,
IconRole::Check => 0xE73E, // CheckMark
IconRole::Close => 0xE8BB, // ChromeClose
IconRole::ChevronDown => 0xE70D, // ChevronDown
@@ -37,9 +40,12 @@ mod tests {
use super::*;
#[test]
fn every_role_maps_to_a_codepoint() {
fn every_role_maps_a_codepoint_or_falls_back_to_the_bundled_svg() {
for role in crate::IconRole::all() {
assert!(glyph(*role).is_some(), "missing codepoint for {role:?}");
assert!(
glyph(*role).is_some() || crate::asset_bytes(role.asset_path()).is_some(),
"no Segoe codepoint or bundled asset for {role:?}"
);
}
}