zsui 0.3.0-alpha.2

Rust-first feature-gated native UI framework for Win32, AppKit, and Linux.
Documentation
use serde::{Deserialize, Serialize};

use crate::{PlatformName, ZsIcon, ZsuiError, ZsuiResult};

pub const WINDOWS_FLUENT_ICON_FONT_FAMILY: &str = "Segoe Fluent Icons";
pub const WINDOWS_MDL2_ICON_FONT_FAMILY: &str = "Segoe MDL2 Assets";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NativeIconSourceKind {
    WindowsSystemFont,
    MacOsSystemSymbol,
    LinuxIconTheme,
    BundledFluentSvg,
    BuiltinSymbolic,
}

impl NativeIconSourceKind {
    pub const fn source_name(self) -> &'static str {
        match self {
            Self::WindowsSystemFont => "windows_system_font",
            Self::MacOsSystemSymbol => "macos_system_symbol",
            Self::LinuxIconTheme => "linux_icon_theme",
            Self::BundledFluentSvg => "bundled_fluent_svg",
            Self::BuiltinSymbolic => "builtin_symbolic",
        }
    }

    pub const fn is_platform_native(self) -> bool {
        !matches!(self, Self::BundledFluentSvg | Self::BuiltinSymbolic)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeIconSource {
    pub icon: ZsIcon,
    pub kind: NativeIconSourceKind,
    pub identifier: String,
    pub glyph: Option<String>,
}

impl NativeIconSource {
    pub fn builtin_symbolic(icon: ZsIcon) -> Self {
        Self {
            icon,
            kind: NativeIconSourceKind::BuiltinSymbolic,
            identifier: "zsui.symbolic.16".to_owned(),
            glyph: None,
        }
    }
    pub fn windows_font(icon: ZsIcon, family: &'static str, glyph: &'static str) -> Self {
        Self {
            icon,
            kind: NativeIconSourceKind::WindowsSystemFont,
            identifier: family.to_string(),
            glyph: Some(glyph.to_string()),
        }
    }

    pub fn macos_symbol(icon: ZsIcon) -> Self {
        Self {
            icon,
            kind: NativeIconSourceKind::MacOsSystemSymbol,
            identifier: icon.sf_symbol_name().to_string(),
            glyph: None,
        }
    }

    pub fn linux_theme(icon: ZsIcon) -> Self {
        Self {
            icon,
            kind: NativeIconSourceKind::LinuxIconTheme,
            identifier: icon.gtk_symbolic_name().to_string(),
            glyph: None,
        }
    }

    #[cfg(any(
        feature = "fluent-icons",
        all(target_os = "macos", feature = "macos-appkit"),
        all(
            target_os = "linux",
            any(feature = "linux-direct-host", feature = "linux-gtk")
        )
    ))]
    pub fn bundled_fluent_svg(icon: ZsIcon) -> Self {
        Self {
            icon,
            kind: NativeIconSourceKind::BundledFluentSvg,
            identifier: icon.fluent_svg_asset_name().to_string(),
            glyph: None,
        }
    }
}

pub trait NativeIconLookup {
    fn is_icon_source_available(&self, source: &NativeIconSource) -> bool;
}

impl<F> NativeIconLookup for F
where
    F: Fn(&NativeIconSource) -> bool,
{
    fn is_icon_source_available(&self, source: &NativeIconSource) -> bool {
        self(source)
    }
}

pub fn native_icon_candidates(platform: &PlatformName, icon: ZsIcon) -> Vec<NativeIconSource> {
    let mut candidates = match platform {
        PlatformName::Windows => vec![
            NativeIconSource::windows_font(
                icon,
                WINDOWS_FLUENT_ICON_FONT_FAMILY,
                icon.windows_fluent_glyph(),
            ),
            NativeIconSource::windows_font(
                icon,
                WINDOWS_MDL2_ICON_FONT_FAMILY,
                icon.windows_mdl2_glyph(),
            ),
        ],
        PlatformName::Macos => vec![NativeIconSource::macos_symbol(icon)],
        PlatformName::Linux => vec![NativeIconSource::linux_theme(icon)],
        PlatformName::Android | PlatformName::Unknown | PlatformName::Other(_) => Vec::new(),
    };
    candidates.push(NativeIconSource::builtin_symbolic(icon));
    #[cfg(any(
        feature = "fluent-icons",
        all(target_os = "macos", feature = "macos-appkit"),
        all(
            target_os = "linux",
            any(feature = "linux-direct-host", feature = "linux-gtk")
        )
    ))]
    {
        candidates
            .into_iter()
            .chain(std::iter::once(NativeIconSource::bundled_fluent_svg(icon)))
            .collect()
    }
    #[cfg(not(any(
        feature = "fluent-icons",
        all(target_os = "macos", feature = "macos-appkit"),
        all(
            target_os = "linux",
            any(feature = "linux-direct-host", feature = "linux-gtk")
        )
    )))]
    {
        candidates
    }
}

pub fn resolve_native_icon(
    platform: &PlatformName,
    icon: ZsIcon,
    lookup: &impl NativeIconLookup,
) -> ZsuiResult<NativeIconSource> {
    native_icon_candidates(platform, icon)
        .into_iter()
        .find(|source| lookup.is_icon_source_available(source))
        .ok_or_else(|| {
            ZsuiError::unsupported(
                "native_icon",
                format!(
                    "no available icon source for {icon:?} on {}",
                    platform.as_str()
                ),
            )
        })
}

#[cfg(any(
    feature = "fluent-icons",
    all(target_os = "macos", feature = "macos-appkit"),
    all(
        target_os = "linux",
        any(feature = "linux-direct-host", feature = "linux-gtk")
    )
))]
pub fn bundled_fluent_icon_svg(icon: ZsIcon) -> &'static [u8] {
    icon.fluent_svg_bytes()
}

#[cfg(any(
    feature = "fluent-icons",
    all(target_os = "macos", feature = "macos-appkit"),
    all(
        target_os = "linux",
        any(feature = "linux-direct-host", feature = "linux-gtk")
    )
))]
pub const FLUENT_SYSTEM_ICONS_LICENSE: &str =
    include_str!("../third_party/fluentui-system-icons/LICENSE");

#[cfg(any(
    feature = "fluent-icons",
    all(target_os = "macos", feature = "macos-appkit"),
    all(
        target_os = "linux",
        any(feature = "linux-direct-host", feature = "linux-gtk")
    )
))]
pub const FLUENT_SYSTEM_ICONS_NOTICE: &str =
    include_str!("../third_party/fluentui-system-icons/NOTICE");

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn platform_candidates_prefer_each_system_icon_source() {
        let windows = native_icon_candidates(&PlatformName::Windows, ZsIcon::Save);
        assert_eq!(windows[0].identifier, WINDOWS_FLUENT_ICON_FONT_FAMILY);
        assert_eq!(windows[1].identifier, WINDOWS_MDL2_ICON_FONT_FAMILY);
        assert!(windows[0].kind.is_platform_native());

        let macos = native_icon_candidates(&PlatformName::Macos, ZsIcon::Save);
        assert_eq!(macos[0].identifier, "square.and.arrow.down");
        let linux = native_icon_candidates(&PlatformName::Linux, ZsIcon::Save);
        assert_eq!(linux[0].identifier, "document-save-symbolic");
    }

    #[test]
    fn resolver_uses_the_first_available_platform_candidate() {
        let resolved = resolve_native_icon(
            &PlatformName::Windows,
            ZsIcon::Copy,
            &|source: &NativeIconSource| source.identifier == WINDOWS_MDL2_ICON_FONT_FAMILY,
        )
        .unwrap();

        assert_eq!(resolved.identifier, WINDOWS_MDL2_ICON_FONT_FAMILY);
        assert_eq!(
            resolved.glyph.as_deref(),
            Some(ZsIcon::Copy.windows_mdl2_glyph())
        );
    }

    #[test]
    #[cfg(any(
        feature = "fluent-icons",
        all(target_os = "macos", feature = "macos-appkit"),
        all(
            target_os = "linux",
            any(feature = "linux-direct-host", feature = "linux-gtk")
        )
    ))]
    fn bundled_mit_svg_is_the_last_candidate_and_has_notices() {
        let candidates = native_icon_candidates(&PlatformName::Macos, ZsIcon::Settings);
        let fallback = candidates.last().unwrap();
        assert_eq!(fallback.kind, NativeIconSourceKind::BundledFluentSvg);
        assert!(bundled_fluent_icon_svg(ZsIcon::Settings).starts_with(b"<svg"));
        assert!(FLUENT_SYSTEM_ICONS_LICENSE.contains("MIT License"));
        assert!(FLUENT_SYSTEM_ICONS_NOTICE.contains("Third Party OSS"));
    }
}