Skip to main content

iris_hub/services/
icon_service.rs

1//! # Icon Service
2//! 
3//! Serviço responsável pelo carregamento, renderização e cache
4//! de ícones SVG das tecnologias.
5//! 
6//! ## Funcionalidades
7//! - Ícones SVG embutidos no executável
8//! - Renderização de SVG para texturas egui
9//! - Cache de texturas para performance
10//! - Listagem de ícones disponíveis
11
12use std::collections::HashMap;
13use std::sync::OnceLock;
14use eframe::egui::{self, ColorImage, TextureHandle};
15
16use crate::core::IconInfo;
17
18// Inclui os ícones gerados pelo build.rs
19include!(concat!(env!("OUT_DIR"), "/embedded_icons.rs"));
20
21// Cache estático dos ícones embutidos
22static ICONS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
23
24fn get_icons() -> &'static HashMap<&'static str, &'static str> {
25    ICONS.get_or_init(|| get_embedded_icons())
26}
27
28/// Cache de texturas dos ícones.
29/// 
30/// Armazena texturas já renderizadas para evitar
31/// re-renderização a cada frame.
32pub struct IconCache {
33    /// Mapa de texturas (nome_do_icone -> TextureHandle)
34    textures: HashMap<String, TextureHandle>,
35}
36
37impl IconCache {
38    /// Cria um novo cache vazio
39    pub fn new() -> Self {
40        Self {
41            textures: HashMap::new(),
42        }
43    }
44    
45    /// Obtém um ícone do cache ou carrega do disco.
46    /// 
47    /// Se o ícone já estiver no cache, retorna imediatamente.
48    /// Caso contrário, carrega o SVG, renderiza e adiciona ao cache.
49    /// 
50    /// # Argumentos
51    /// * `ctx` - Contexto do egui para criação de texturas
52    /// * `icon_name` - Nome do ícone (ex: "react", "python")
53    /// 
54    /// # Retorno
55    /// `Some(TextureHandle)` se o ícone foi carregado com sucesso,
56    /// `None` se o ícone não existe ou falhou ao carregar.
57        pub fn get_or_load(&mut self, ctx: &egui::Context, icon_name: &str) -> Option<TextureHandle> {
58        // Verifica se já está no cache
59        if let Some(texture) = self.textures.get(icon_name) {
60            return Some(texture.clone());
61        }
62        
63        // Busca o ícone embutido
64        let icons = get_icons();
65        if let Some(svg_data) = icons.get(icon_name) {
66            if let Some(image) = render_svg_to_image(svg_data, 32, 32) {
67                let texture = ctx.load_texture(
68                    icon_name,
69                    image,
70                    egui::TextureOptions::LINEAR,
71                );
72                self.textures.insert(icon_name.to_string(), texture.clone());
73                return Some(texture);
74            }
75        }
76        
77        None
78    }
79    
80    /// Limpa o cache de texturas
81    pub fn clear(&mut self) {
82        self.textures.clear();
83    }
84    
85    /// Retorna o número de texturas em cache
86    pub fn len(&self) -> usize {
87        self.textures.len()
88    }
89    
90    /// Verifica se o cache está vazio
91    pub fn is_empty(&self) -> bool {
92        self.textures.is_empty()
93    }
94}
95
96impl Default for IconCache {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102/// Renderiza um SVG para uma imagem ColorImage do egui.
103/// 
104/// Utiliza a biblioteca resvg para renderização de alta qualidade.
105/// 
106/// # Argumentos
107/// * `svg_data` - Conteúdo do arquivo SVG
108/// * `width` - Largura desejada em pixels
109/// * `height` - Altura desejada em pixels
110/// 
111/// # Retorno
112/// `Some(ColorImage)` se a renderização foi bem sucedida,
113/// `None` em caso de erro.
114pub fn render_svg_to_image(svg_data: &str, width: u32, height: u32) -> Option<ColorImage> {
115    let opts = resvg::usvg::Options::default();
116    let tree = resvg::usvg::Tree::from_str(svg_data, &opts).ok()?;
117    
118    let size = tree.size();
119    let scale_x = width as f32 / size.width();
120    let scale_y = height as f32 / size.height();
121    let scale = scale_x.min(scale_y);
122    
123    let scaled_width = (size.width() * scale) as u32;
124    let scaled_height = (size.height() * scale) as u32;
125    
126    let mut pixmap = resvg::tiny_skia::Pixmap::new(scaled_width, scaled_height)?;
127    
128    let transform = resvg::tiny_skia::Transform::from_scale(scale, scale);
129    resvg::render(&tree, transform, &mut pixmap.as_mut());
130    
131    let pixels: Vec<egui::Color32> = pixmap
132        .pixels()
133        .iter()
134        .map(|p| egui::Color32::from_rgba_premultiplied(p.red(), p.green(), p.blue(), p.alpha()))
135        .collect();
136    
137    Some(ColorImage {
138        size: [scaled_width as usize, scaled_height as usize],
139        pixels,
140    })
141}
142
143/// Carrega a lista de ícones disponíveis do diretório assets/langs.
144/// 
145/// Busca por arquivos com padrão `*-original.svg` e extrai o nome
146/// da tecnologia do nome do arquivo.
147/// 
148/// # Retorno
149/// Vetor de `IconInfo` ordenado alfabeticamente pelo nome.
150/// 
151/// # Exemplo
152/// ```rust
153/// let icons = load_available_icons();
154/// for icon in icons {
155///     println!("Ícone disponível: {}", icon.name);
156/// }
157/// ```
158pub fn load_available_icons() -> Vec<IconInfo> {
159    let icons_map = get_icons();
160    let mut icons: Vec<IconInfo> = icons_map
161        .keys()
162        .map(|name| {
163            let filename = format!("{}-original.svg", name);
164            IconInfo::new(name.to_string(), filename)
165        })
166        .collect();
167    
168    icons.sort_by(|a, b| a.name.cmp(&b.name));
169    icons
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    
176    #[test]
177    fn test_icon_cache_creation() {
178        let cache = IconCache::new();
179        assert!(cache.is_empty());
180    }
181    
182    #[test]
183    fn test_render_invalid_svg() {
184        let result = render_svg_to_image("invalid svg content", 32, 32);
185        assert!(result.is_none());
186    }
187}