Skip to main content

azul_core/
icon.rs

1//! Generic icon provider system for Azul
2//!
3//! This module defines a generic, callback-based icon resolution infrastructure.
4//! The actual parsing/loading implementations live in `azul-layout`.
5//!
6//! # Architecture
7//!
8//! The icon system is fully generic using RefAny:
9//!
10//! 1. `IconProviderHandle` - stores icons in nested map: pack_name → (icon_name → RefAny)
11//! 2. The resolver callback turns (icon_data, original_dom) into a StyledDom
12//! 3. Differentiation between Image/Font/SVG/etc. is via RefAny::downcast
13//! 4. Supports any icon source: images, fonts, SVGs, animated icons, etc.
14//!
15//! # Resolution Flow
16//!
17//! 1. User creates Icon nodes: `Dom::create_icon("home")`
18//! 2. Before layout, `resolve_icons_in_styled_dom()` is called
19//! 3. Each Icon node is looked up across all packs (first match wins)
20//! 4. The resolver callback is invoked with the found RefAny data + original DOM
21//! 5. The callback returns a StyledDom subtree that replaces the icon node
22//!
23//! # Custom Resolvers
24//!
25//! Users can provide custom C callbacks for complete control:
26//!
27//! ```c
28//! AzStyledDom my_resolver(
29//!     AzRefAny* icon_data,           // NULL if icon not found
30//!     AzStyledDom* original_icon_dom, // Contains icon_name, styles, a11y
31//!     AzSystemStyle* system_style
32//! ) {
33//!     // Custom resolution logic - icon_data contains your registered data
34//!     return create_my_icon_dom(...);
35//! }
36//! ```
37
38use alloc::{
39    boxed::Box,
40    collections::BTreeMap,
41    string::{String, ToString},
42    sync::Arc,
43    vec::Vec,
44};
45use core::fmt;
46
47#[cfg(feature = "std")]
48use std::sync::Mutex;
49
50#[cfg(not(feature = "std"))]
51use spin::Mutex;
52
53use azul_css::{AzString, system::SystemStyle};
54
55use crate::{
56    dom::{Dom, NodeType},
57    refany::{OptionRefAny, RefAny},
58    styled_dom::StyledDom,
59};
60
61// Type name constants for RefAny-based icon type detection in debug output
62const IMAGE_ICON_DATA_TYPE_NAME: &str = "ImageIconData";
63const FONT_ICON_DATA_TYPE_NAME: &str = "FontIconData";
64
65// Icon Resolver Callback
66
67/// Callback type for resolving icon data to a StyledDom.
68///
69/// Parameters:
70/// - `icon_data`: The RefAny data from the icon pack (cloned, or None if not found)
71/// - `original_icon_dom`: The original icon node's StyledDom (contains inline styles, a11y info, icon_name)
72/// - `system_style`: Current system style (theme, colors, etc.)
73///
74/// Returns: A StyledDom that will replace the icon node.
75/// The resolver should copy relevant styles from original_icon_dom to the result.
76/// Return an empty StyledDom to show a placeholder or nothing.
77///
78/// Note: icon_name is accessible via `original_icon_dom.node_data[0].get_node_type()` → `NodeType::Icon(name)`
79pub type IconResolverCallbackType = extern "C" fn(
80    icon_data: OptionRefAny,
81    original_icon_dom: &StyledDom,
82    system_style: &SystemStyle,
83) -> StyledDom;
84
85/// Default resolver that returns an empty StyledDom (shows placeholder)
86pub extern "C" fn default_icon_resolver(
87    _icon_data: OptionRefAny,
88    _original_icon_dom: &StyledDom,
89    _system_style: &SystemStyle,
90) -> StyledDom {
91    // Default: return empty DOM (icon won't be visible)
92    StyledDom::default()
93}
94
95// Icon Provider Inner (single mutex)
96
97/// Inner data for IconProviderHandle - all fields behind single mutex
98#[derive(Clone)]
99pub struct IconProviderInner {
100    /// Nested map: pack_name → (icon_name → RefAny)
101    /// Differentiation between Image/Font/SVG is via RefAny::downcast
102    pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
103    /// The resolver callback
104    pub resolver: IconResolverCallbackType,
105}
106
107impl Default for IconProviderInner {
108    fn default() -> Self {
109        Self {
110            icons: BTreeMap::new(),
111            resolver: default_icon_resolver,
112        }
113    }
114}
115
116// Icon Provider Handle
117
118/// Icon provider stored in AppConfig.
119///
120/// This is a Box<IconProviderInner> for C FFI compatibility.
121/// When App::run() is called, it gets converted to Arc<Mutex<IconProviderInner>>
122/// and cloned to each window.
123///
124/// Icons are stored in a nested map: pack_name → (icon_name → RefAny)
125/// This allows:
126/// - Multiple packs with different sources (app-images, material-icons, etc.)
127/// - Easy unregistration of entire packs
128/// - First-match-wins lookup across all packs
129#[repr(C)]
130pub struct IconProviderHandle {
131    /// Boxed inner data - Box<T> is repr(C) compatible (single pointer)
132    pub inner: Box<IconProviderInner>,
133}
134
135impl Clone for IconProviderHandle {
136    fn clone(&self) -> Self {
137        Self { inner: Box::new((*self.inner).clone()) }
138    }
139}
140
141impl fmt::Debug for IconProviderHandle {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        let pack_count = self.inner.icons.len();
144        let icon_count: usize = self.inner.icons.values().map(|p| p.len()).sum();
145        
146        f.debug_struct("IconProviderHandle")
147            .field("pack_count", &pack_count)
148            .field("icon_count", &icon_count)
149            .finish()
150    }
151}
152
153impl Default for IconProviderHandle {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl IconProviderHandle {
160    /// Create a new empty icon provider with the default (no-op) resolver.
161    /// 
162    /// Note: The default resolver in core crate returns an empty StyledDom.
163    /// Use `set_resolver()` to set a proper resolver from the layout crate,
164    /// or use `with_resolver()` to create with a custom resolver.
165    pub fn new() -> Self {
166        Self {
167            inner: Box::new(IconProviderInner {
168                icons: BTreeMap::new(),
169                resolver: default_icon_resolver,
170            })
171        }
172    }
173
174    /// Create with a custom resolver callback
175    pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
176        Self {
177            inner: Box::new(IconProviderInner {
178                icons: BTreeMap::new(),
179                resolver,
180            })
181        }
182    }
183    
184    /// Convert this handle into an Arc<Mutex<IconProviderInner>> for use in windows.
185    ///
186    /// This consumes the Box and creates an Arc. Called by App::run() to create
187    /// the shared icon provider that gets cloned to each window.
188    pub(crate) fn into_shared(self) -> Arc<Mutex<IconProviderInner>> {
189        Arc::new(Mutex::new(*self.inner))
190    }
191
192    /// Set the resolver callback
193    pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
194        self.inner.resolver = resolver;
195    }
196
197    /// Register a single icon in a pack (creates pack if needed).
198    ///
199    /// Note: `pack_name` is case-sensitive, while `icon_name` is normalized to lowercase.
200    pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
201        let pack = self.inner.icons
202            .entry(pack_name.to_string())
203            .or_default();
204        pack.insert(icon_name.to_lowercase(), data);
205    }
206
207    /// Unregister a single icon from a pack
208    pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
209        if let Some(pack) = self.inner.icons.get_mut(pack_name) {
210            pack.remove(&icon_name.to_lowercase());
211            if pack.is_empty() {
212                self.inner.icons.remove(pack_name);
213            }
214        }
215    }
216
217    /// Unregister an entire icon pack
218    pub fn unregister_pack(&mut self, pack_name: &str) {
219        self.inner.icons.remove(pack_name);
220    }
221
222    /// Look up an icon across all packs, returning the pack name and data reference (first match wins)
223    fn lookup_with_pack(&self, icon_name: &str) -> Option<(&str, &RefAny)> {
224        let icon_name_lower = icon_name.to_lowercase();
225        for (pack_name, pack) in self.inner.icons.iter() {
226            if let Some(data) = pack.get(&icon_name_lower) {
227                return Some((pack_name.as_str(), data));
228            }
229        }
230        None
231    }
232
233    /// Look up an icon across all packs (first match wins)
234    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
235        self.lookup_with_pack(icon_name).map(|(_, data)| data.clone())
236    }
237
238    /// Check if an icon exists in any pack
239    pub fn has_icon(&self, icon_name: &str) -> bool {
240        let icon_name_lower = icon_name.to_lowercase();
241        self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
242    }
243
244    /// List all pack names
245    pub fn list_packs(&self) -> Vec<String> {
246        self.inner.icons.keys().cloned().collect()
247    }
248
249    /// List all icon names in a specific pack
250    pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
251        self.inner.icons.get(pack_name)
252            .map(|pack| pack.keys().cloned().collect())
253            .unwrap_or_default()
254    }
255
256    /// Debug lookup: returns detailed info about an icon's RefAny contents
257    pub fn debug_lookup(&self, icon_name: &str) -> AzString {
258        let icon_name_lower = icon_name.to_lowercase();
259
260        let mut result = format!("Debug lookup for icon '{}' (normalized: '{}'):\n", icon_name, icon_name_lower);
261
262        // Report registered packs
263        result.push_str(&format!("  Total packs: {}\n", self.inner.icons.len()));
264        for (pack_name, pack) in self.inner.icons.iter() {
265            result.push_str(&format!("    Pack '{}': {} icons\n", pack_name, pack.len()));
266            for (name, _) in pack.iter() {
267                result.push_str(&format!("      - {}\n", name));
268            }
269        }
270
271        // Find the icon using shared lookup helper
272        match self.lookup_with_pack(icon_name) {
273            Some((pack, data)) => {
274                result.push_str(&format!("\n  FOUND in pack '{}'\n", pack));
275                let type_name = data.get_type_name();
276                result.push_str(&format!("  RefAny type_name: '{}'\n", type_name.as_str()));
277
278                let debug_info = data.sharing_info.debug_get_refcount_copied();
279                result.push_str(&format!("  RefAny size: {} bytes\n", debug_info._internal_layout_size));
280
281                let type_str = type_name.as_str();
282                if type_str.contains(IMAGE_ICON_DATA_TYPE_NAME) {
283                    result.push_str("  RefAny type: ImageIconData (image-based icon)\n");
284                } else if type_str.contains(FONT_ICON_DATA_TYPE_NAME) {
285                    result.push_str("  RefAny type: FontIconData (font-based icon)\n");
286                } else {
287                    result.push_str(&format!("  RefAny type: UNKNOWN ('{}')\n", type_str));
288                }
289            }
290            None => {
291                result.push_str("\n  NOT FOUND in any pack\n");
292            }
293        }
294
295        AzString::from(result)
296    }
297}
298
299/// Thread-safe icon provider for use in windows.
300/// 
301/// This is created from IconProviderHandle::into_shared() in App::run()
302/// and cloned to each window.
303#[derive(Clone)]
304pub struct SharedIconProvider {
305    inner: Arc<Mutex<IconProviderInner>>,
306}
307
308impl SharedIconProvider {
309    /// Create from an IconProviderHandle (consumes the handle)
310    pub fn from_handle(handle: IconProviderHandle) -> Self {
311        Self { inner: handle.into_shared() }
312    }
313    
314    /// Resolve an icon to a StyledDom using the registered callback
315    pub fn resolve(
316        &self, 
317        original_icon_dom: &StyledDom,
318        icon_name: &str,
319        system_style: &SystemStyle,
320    ) -> StyledDom {
321        let (resolver, lookup_result) = {
322            let guard = match self.inner.lock() {
323                Ok(g) => g,
324                Err(_) => return StyledDom::default(),
325            };
326            
327            let resolver = guard.resolver;
328            let icon_name_lower = icon_name.to_lowercase();
329            
330            let lookup_result = guard.icons.values()
331                .find_map(|pack| pack.get(&icon_name_lower).cloned());
332            
333            (resolver, lookup_result)
334        };
335        
336        resolver(lookup_result.into(), original_icon_dom, system_style)
337    }
338    
339    /// Look up an icon across all packs
340    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
341        let icon_name_lower = icon_name.to_lowercase();
342        self.inner.lock().ok().and_then(|guard| {
343            for pack in guard.icons.values() {
344                if let Some(data) = pack.get(&icon_name_lower) {
345                    return Some(data.clone());
346                }
347            }
348            None
349        })
350    }
351    
352    /// Check if an icon exists
353    pub fn has_icon(&self, icon_name: &str) -> bool {
354        let icon_name_lower = icon_name.to_lowercase();
355        self.inner.lock()
356            .map(|guard| guard.icons.values().any(|p| p.contains_key(&icon_name_lower)))
357            .unwrap_or(false)
358    }
359}
360
361// Icon Resolution in StyledDom
362
363/// Collected icon node info for replacement
364struct CollectedIcon {
365    /// Index in the node_data array
366    node_idx: usize,
367    /// The icon name
368    icon_name: AzString,
369}
370
371/// Replacement result after resolving an icon
372struct IconReplacement {
373    /// Index of the icon node to replace
374    node_idx: usize,
375    /// The resolved StyledDom (may be empty, single node, or multi-node tree)
376    replacement: StyledDom,
377}
378
379/// Collect all Icon nodes from the StyledDom
380fn collect_icon_nodes(styled_dom: &StyledDom) -> Vec<CollectedIcon> {
381    let mut icons = Vec::new();
382    
383    let node_data = styled_dom.node_data.as_ref();
384    for (idx, node) in node_data.iter().enumerate() {
385        if let NodeType::Icon(icon_name) = node.get_node_type() {
386            icons.push(CollectedIcon {
387                node_idx: idx,
388                icon_name: icon_name.clone_self(),
389            });
390        }
391    }
392    
393    icons
394}
395
396/// Extract a single-node StyledDom from a parent StyledDom at the given index.
397/// This creates a minimal StyledDom containing just that node for the resolver.
398fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
399    use crate::dom::{NodeDataVec, DomId};
400    use crate::id::NodeId;
401    use crate::styled_dom::{
402        StyledNodeVec, NodeHierarchyItemIdVec, TagIdToNodeIdMappingVec,
403        NodeHierarchyItemVec, NodeHierarchyItem, NodeHierarchyItemId,
404        ParentWithNodeDepthVec, ParentWithNodeDepth,
405    };
406    use crate::style::{CascadeInfoVec, CascadeInfo};
407    use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
408    
409    let node_data = styled_dom.node_data.as_ref();
410    let styled_nodes = styled_dom.styled_nodes.as_ref();
411    
412    if node_idx >= node_data.len() {
413        return StyledDom::default();
414    }
415    
416    // Clone the single node
417    let single_node = node_data[node_idx].clone();
418    let single_styled = if node_idx < styled_nodes.len() {
419        styled_nodes[node_idx].clone()
420    } else {
421        crate::styled_dom::StyledNode::default()
422    };
423    
424    StyledDom {
425        root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
426        node_hierarchy: NodeHierarchyItemVec::from_vec(vec![NodeHierarchyItem {
427            parent: 0,
428            previous_sibling: 0,
429            next_sibling: 0,
430            last_child: 0,
431        }]),
432        node_data: NodeDataVec::from_vec(vec![single_node]),
433        styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
434        cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
435        nodes_with_window_callbacks: NodeHierarchyItemIdVec::from_vec(Vec::new()),
436        nodes_with_datasets: NodeHierarchyItemIdVec::from_vec(Vec::new()),
437        tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
438        non_leaf_nodes: ParentWithNodeDepthVec::from_vec(Vec::new()),
439        css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
440        dom_id: DomId::ROOT_ID,
441    }
442}
443
444/// Resolve all collected icons to their StyledDom representations
445fn resolve_collected_icons(
446    icons: &[CollectedIcon],
447    styled_dom: &StyledDom,
448    provider: &SharedIconProvider,
449    system_style: &SystemStyle,
450) -> Vec<IconReplacement> {
451    icons.iter().map(|icon| {
452        // Extract the original icon node as a StyledDom
453        let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
454        let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
455        IconReplacement {
456            node_idx: icon.node_idx,
457            replacement,
458        }
459    }).collect()
460}
461
462/// Check if a replacement is a single-node replacement (fast path)
463fn is_single_node_replacement(replacement: &StyledDom) -> bool {
464    replacement.node_data.as_ref().len() == 1
465}
466
467/// Apply a single-node replacement (fast path: swap NodeType and copy properties)
468fn apply_single_node_replacement(
469    styled_dom: &mut StyledDom,
470    node_idx: usize,
471    replacement: &StyledDom,
472) {
473    if replacement.node_data.as_ref().is_empty() {
474        // Empty replacement - convert to empty div
475        let node_data = styled_dom.node_data.as_mut();
476        if let Some(node) = node_data.get_mut(node_idx) {
477            node.set_node_type(NodeType::Div);
478        }
479    } else {
480        // Get the root node from the replacement and copy its properties
481        let replacement_root = &replacement.node_data.as_ref()[0];
482        let replacement_node_type = replacement_root.get_node_type().clone();
483        
484        let node_data = styled_dom.node_data.as_mut();
485        if let Some(node) = node_data.get_mut(node_idx) {
486            // Swap node type
487            node.set_node_type(replacement_node_type);
488            
489            // Copy inline style from replacement
490            node.set_style(replacement_root.get_style().clone());
491            
492            // Copy accessibility info if present
493            if let Some(a11y) = replacement_root.get_accessibility_info() {
494                node.set_accessibility_info(*a11y.clone());
495            }
496        }
497        
498        // Also update the styled_nodes to reflect the new styling
499        if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
500            let styled_nodes = styled_dom.styled_nodes.as_mut();
501            if let Some(styled) = styled_nodes.get_mut(node_idx) {
502                *styled = replacement_styled.clone();
503            }
504        }
505    }
506}
507
508/// Apply multi-node replacement using subtree splicing
509fn apply_multi_node_replacement(
510    styled_dom: &mut StyledDom,
511    node_idx: usize,
512    replacement: StyledDom,
513) {
514    let replacement_len = replacement.node_data.as_ref().len();
515    if replacement_len == 0 {
516        let node_data = styled_dom.node_data.as_mut();
517        if let Some(node) = node_data.get_mut(node_idx) {
518            node.set_node_type(NodeType::Div);
519        }
520        return;
521    }
522    
523    // For now, just apply the root node (same as single-node)
524    apply_single_node_replacement(styled_dom, node_idx, &replacement);
525    
526    if replacement_len > 1 {
527        // TODO: Full subtree splicing requires inserting nodes into arrays
528        #[cfg(debug_assertions)]
529        eprintln!(
530            "Warning: Icon replacement has {} nodes, only root node used.",
531            replacement_len
532        );
533    }
534}
535
536/// Resolve all Icon nodes in a StyledDom to their actual content.
537///
538/// This function:
539/// 1. Collects all Icon nodes from the StyledDom
540/// 2. Resolves each icon via the provider's callback (passing original icon DOM)
541/// 3. Applies replacements (single-node fast path or multi-node splicing)
542///
543/// This should be called after StyledDom creation but before layout.
544pub fn resolve_icons_in_styled_dom(
545    styled_dom: &mut StyledDom,
546    provider: &SharedIconProvider,
547    system_style: &SystemStyle,
548) {
549    // Step 1: Collect all icon nodes
550    let icons = collect_icon_nodes(styled_dom);
551
552    if icons.is_empty() {
553        return;
554    }
555
556    // Step 2: Resolve all icons to their StyledDom representations
557    // Note: We pass styled_dom to extract each icon's original node
558    let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
559
560    // Step 3: Apply replacements (reverse order to preserve indices)
561    for replacement in replacements.into_iter().rev() {
562        if is_single_node_replacement(&replacement.replacement) ||
563           replacement.replacement.node_data.as_ref().is_empty() {
564            apply_single_node_replacement(
565                styled_dom,
566                replacement.node_idx,
567                &replacement.replacement
568            );
569        } else {
570            apply_multi_node_replacement(
571                styled_dom,
572                replacement.node_idx,
573                replacement.replacement
574            );
575        }
576    }
577}
578
579// FFI Option Types
580
581impl_option!(
582    IconProviderHandle,
583    OptionIconProviderHandle,
584    [Clone]
585);