herogpui-components 0.9.0

HeroUI-style component library for GPUI
Documentation
//! Embedded assets used by HeroGPUI's built-in component chrome.

use std::borrow::Cow;

use gpui::{AssetSource, Result, SharedString};

/// Asset source for the SVG icons used by HeroGPUI components.
///
/// Register it with [`gpui::Application::with_assets`] before opening a window.
pub struct HeroGpuiAssets;

impl HeroGpuiAssets {
    /// Combines HeroGPUI's icons with an application's own asset source.
    pub fn with_fallback<A: AssetSource>(fallback: A) -> HeroGpuiAssetSource<A> {
        HeroGpuiAssetSource { fallback }
    }
}

/// HeroGPUI's embedded icons followed by an application's asset source.
pub struct HeroGpuiAssetSource<A> {
    fallback: A,
}

macro_rules! svg_assets {
    ($($name:literal => $svg:literal),* $(,)?) => {
        const EMBEDDED: &[(&str, &[u8])] = &[
            $(($name, $svg.as_bytes())),*
        ];

        impl AssetSource for HeroGpuiAssets {
            fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
                Ok(EMBEDDED
                    .iter()
                    .find(|(asset_path, _)| *asset_path == path)
                    .map(|(_, data)| Cow::Borrowed(*data)))
            }

            fn list(&self, path: &str) -> Result<Vec<SharedString>> {
                Ok(EMBEDDED
                    .iter()
                    .filter(|(asset_path, _)| asset_path.starts_with(path))
                    .map(|(asset_path, _)| SharedString::from(*asset_path))
                    .collect())
            }
        }

        impl<A: AssetSource> AssetSource for HeroGpuiAssetSource<A> {
            fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
                match HeroGpuiAssets.load(path)? {
                    Some(data) => Ok(Some(data)),
                    None => self.fallback.load(path),
                }
            }

            fn list(&self, path: &str) -> Result<Vec<SharedString>> {
                let mut assets = HeroGpuiAssets.list(path)?;
                for asset in self.fallback.list(path)? {
                    if !assets.contains(&asset) {
                        assets.push(asset);
                    }
                }
                Ok(assets)
            }
        }
    };
}

svg_assets! {
    "herogpui/icons/alert_triangle.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>"#,
    "herogpui/icons/arrow_left.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>"#,
    "herogpui/icons/arrow_right.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>"#,
    "herogpui/icons/check.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>"#,
    "herogpui/icons/chevron_down.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>"#,
    "herogpui/icons/chevron_left.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>"#,
    "herogpui/icons/chevron_right.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>"#,
    "herogpui/icons/chevron_up.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>"#,
    "herogpui/icons/close_circle.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9 9l6 6"/><path d="M15 9l-6 6"/></svg>"#,
    "herogpui/icons/close.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>"#,
    "herogpui/icons/copy.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>"#,
    "herogpui/icons/dots_vertical.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="12" cy="19" r="1.8"/></svg>"#,
    "herogpui/icons/ellipsis.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><line x1="12" y1="10.5" x2="12" y2="16.5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><circle cx="12" cy="7.2" r="1.35"/></svg>"#,
    "herogpui/icons/external_link.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>"#,
    "herogpui/icons/eye_off.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>"#,
    "herogpui/icons/eye.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>"#,
    "herogpui/icons/globe.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3c2.6 2.4 4 5.6 4 9s-1.4 6.6-4 9c-2.6-2.4-4-5.6-4-9s1.4-6.6 4-9z"/></svg>"#,
    "herogpui/icons/heart_fill.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 20s-7-4.4-7-9.4A4.6 4.6 0 0 1 12 7.6 4.6 4.6 0 0 1 19 10.6c0 5-7 9.4-7 9.4z"/></svg>"#,
    "herogpui/icons/heart.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.4A4.6 4.6 0 0 1 12 7.6 4.6 4.6 0 0 1 19 10.6c0 5-7 9.4-7 9.4z"/></svg>"#,
    "herogpui/icons/key.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="12" r="4"/><path d="M12 12h9"/><path d="M17 12v3.5"/><path d="M20 12v2.5"/></svg>"#,
    "herogpui/icons/mail.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M4 7.5l7.4 5.2a1 1 0 0 0 1.2 0L20 7.5"/></svg>"#,
    "herogpui/icons/minus.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>"#,
    "herogpui/icons/moon.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>"#,
    "herogpui/icons/plus.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>"#,
    "herogpui/icons/search.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>"#,
    "herogpui/icons/spinner.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="none"><circle cx="25" cy="25" r="20" stroke="currentColor" stroke-opacity="0.25" stroke-width="5"/><path d="M45 25 A20 20 0 0 0 25 5" stroke="currentColor" stroke-width="5" stroke-linecap="round"/></svg>"#,
    "herogpui/icons/sun.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4" fill="currentColor"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>"#,
    "herogpui/icons/tooltip_arrow.svg" => r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none"><path fill="currentColor" d="M0 0C5.48483 8 6.5 8 12 0Z"/></svg>"#,
}

#[cfg(test)]
mod tests {
    use std::borrow::Cow;

    use gpui::AssetSource as _;

    use super::{HeroGpuiAssets, EMBEDDED};

    struct AppAssets;

    impl gpui::AssetSource for AppAssets {
        fn load(&self, path: &str) -> gpui::Result<Option<Cow<'static, [u8]>>> {
            Ok((path == "app/logo.svg").then(|| Cow::Borrowed(&b"logo"[..])))
        }

        fn list(&self, path: &str) -> gpui::Result<Vec<gpui::SharedString>> {
            Ok((path == "app")
                .then(|| "app/logo.svg".into())
                .into_iter()
                .collect())
        }
    }

    #[test]
    fn embedded_assets_load_and_list() {
        let assets = HeroGpuiAssets;

        assert_eq!(assets.list("herogpui/icons").unwrap().len(), EMBEDDED.len());
        for (path, expected) in EMBEDDED {
            assert_eq!(assets.load(path).unwrap().as_deref(), Some(*expected));
        }
        assert!(assets.load("herogpui/icons/missing.svg").unwrap().is_none());
    }

    #[test]
    fn fallback_preserves_application_assets() {
        let assets = HeroGpuiAssets::with_fallback(AppAssets);

        assert!(assets.load("herogpui/icons/check.svg").unwrap().is_some());
        assert_eq!(
            assets.load("app/logo.svg").unwrap().as_deref(),
            Some(&b"logo"[..])
        );
        assert_eq!(
            assets.list("app").unwrap(),
            vec![gpui::SharedString::from("app/logo.svg")]
        );
    }
}