//! SVG builders for the floating tab bar's action icons. //! //! The touch tab bar (`hud_plugin::tab_bar`) is icon-first, but the bundled //! FiraMono face has no icon glyphs (and the Geometric Shapes block U+25xx //! is missing on Android entirely), so the icons are authored here as tiny //! stroke-based SVGs and rasterised through the same `usvg`/`resvg` pipeline //! as the card themes (`assets::rasterize_svg`). //! //! Icons are drawn in **white** on a transparent background inside a //! 24-unit logical box; the spawn site tints them to the theme colour via //! `ImageNode::color` (a per-node multiply), so one texture serves every //! button state. use bevy::image::Image; use bevy::math::UVec2; use super::svg_loader::{SvgLoaderError, rasterize_svg}; /// Rasterisation size for one tab-bar icon texture. Icons display at /// 24 logical px; 96 px texels keep them crisp at the Fold's 1.2 scale /// factor (and any future 3× phone) while staying trivially small. pub const HUD_ICON_TEXTURE_PX: u32 = 96; /// The five actions the floating touch tab bar can show. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HudIcon { /// Counter-clockwise "take back" arrow. Undo, /// Two offset card outlines — draw from the stock. Draw, /// Lightbulb — highlight a suggested move. Hint, /// Two vertical bars. Pause, /// Three-line hamburger — the floating menu button. Menu, } impl HudIcon { /// Build the icon's SVG document. All icons share one visual language: /// 2-unit white strokes, round caps, transparent fill, 24-unit box. pub fn svg(self) -> String { let body = match self { // Arrow head pointing left into a half-circle sweeping back // over the top — the conventional undo mark. HudIcon::Undo => concat!( r##""##, r##""##, ), // Back card peeks out behind the front card; the front card is // drawn second so its stroke reads on top. HudIcon::Draw => concat!( r##""##, r##""##, ), // Bulb dome with a flat lamp base beneath. HudIcon::Hint => concat!( r##""##, r##""##, ), HudIcon::Pause => concat!( r##""##, r##""##, ), HudIcon::Menu => concat!( r##""##, r##""##, r##""##, ), }; format!( concat!( r##""##, r##"{body}"##, r##""##, ), body = body ) } /// Rasterise this icon into a Bevy [`Image`] at /// [`HUD_ICON_TEXTURE_PX`] square. pub fn rasterize(self) -> Result { rasterize_svg(self.svg().as_bytes(), UVec2::splat(HUD_ICON_TEXTURE_PX)) } } #[cfg(test)] mod tests { use super::*; const ALL: [HudIcon; 5] = [ HudIcon::Undo, HudIcon::Draw, HudIcon::Hint, HudIcon::Pause, HudIcon::Menu, ]; /// Every icon must survive the real usvg/resvg pipeline at the real /// texture size — a malformed path here would otherwise only surface /// as a blank button on device. #[test] fn every_icon_rasterizes_at_texture_size() { for icon in ALL { let img = icon.rasterize().expect("icon SVG must rasterise"); assert_eq!(img.width(), HUD_ICON_TEXTURE_PX, "{icon:?} width"); assert_eq!(img.height(), HUD_ICON_TEXTURE_PX, "{icon:?} height"); } } /// Rasterised icons must contain visible (non-transparent) pixels — /// guards against a stroke accidentally drawn outside the viewBox. #[test] fn every_icon_has_visible_pixels() { for icon in ALL { let img = icon.rasterize().expect("icon SVG must rasterise"); let data = img.data.as_ref().expect("rasterised image has data"); let opaque = data.chunks_exact(4).filter(|px| px[3] > 0).count(); assert!( opaque > 100, "{icon:?} renders only {opaque} visible pixels" ); } } }