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    debug::DebugLog,
57    dom::{Dom, NodeType},
58    refany::{OptionRefAny, RefAny},
59    styled_dom::StyledDom,
60};
61
62// Icon Resolver Callback
63
64/// Callback type for resolving icon data to a StyledDom.
65///
66/// Parameters:
67/// - `icon_data`: The RefAny data from the icon pack (cloned, or None if not found)
68/// - `original_icon_dom`: The original icon node's StyledDom (contains inline styles, a11y info, icon_name)
69/// - `system_style`: Current system style (theme, colors, etc.)
70///
71/// Returns: A StyledDom that will replace the icon node.
72/// The resolver should copy relevant styles from original_icon_dom to the result.
73/// Return an empty StyledDom to show a placeholder or nothing.
74///
75/// Note: icon_name is accessible via `original_icon_dom.node_data[0].get_node_type()` → `NodeType::Icon(name)`
76pub type IconResolverCallbackType = extern "C" fn(
77    icon_data: OptionRefAny,
78    original_icon_dom: &StyledDom,
79    system_style: &SystemStyle,
80) -> StyledDom;
81
82/// Default resolver that returns an empty StyledDom (shows placeholder)
83pub extern "C" fn default_icon_resolver(
84    _icon_data: OptionRefAny,
85    _original_icon_dom: &StyledDom,
86    _system_style: &SystemStyle,
87) -> StyledDom {
88    // Default: return empty DOM (icon won't be visible)
89    StyledDom::default()
90}
91
92// Icon Provider Inner (single mutex)
93
94/// Inner data for IconProviderHandle - all fields behind single mutex
95#[derive(Clone)]
96pub struct IconProviderInner {
97    /// Nested map: pack_name → (icon_name → RefAny)
98    /// Differentiation between Image/Font/SVG is via RefAny::downcast
99    pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
100    /// The resolver callback
101    pub resolver: IconResolverCallbackType,
102}
103
104impl Default for IconProviderInner {
105    fn default() -> Self {
106        Self {
107            icons: BTreeMap::new(),
108            resolver: default_icon_resolver,
109        }
110    }
111}
112
113// Icon Provider Handle
114
115/// Icon provider stored in AppConfig.
116///
117/// This is a Box<IconProviderInner> for C FFI compatibility.
118/// When App::run() is called, it gets converted to Arc<Mutex<IconProviderInner>>
119/// and cloned to each window.
120///
121/// Icons are stored in a nested map: pack_name → (icon_name → RefAny)
122/// This allows:
123/// - Multiple packs with different sources (app-images, material-icons, etc.)
124/// - Easy unregistration of entire packs
125/// - First-match-wins lookup across all packs
126#[repr(C)]
127pub struct IconProviderHandle {
128    /// Boxed inner data - Box<T> is repr(C) compatible (single pointer)
129    pub inner: Box<IconProviderInner>,
130}
131
132impl Clone for IconProviderHandle {
133    fn clone(&self) -> Self {
134        Self { inner: Box::new((*self.inner).clone()) }
135    }
136}
137
138impl fmt::Debug for IconProviderHandle {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        let pack_count = self.inner.icons.len();
141        let icon_count: usize = self.inner.icons.values().map(|p| p.len()).sum();
142        
143        f.debug_struct("IconProviderHandle")
144            .field("pack_count", &pack_count)
145            .field("icon_count", &icon_count)
146            .finish()
147    }
148}
149
150impl Default for IconProviderHandle {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl IconProviderHandle {
157    /// Create a new empty icon provider with the default (no-op) resolver.
158    /// 
159    /// Note: The default resolver in core crate returns an empty StyledDom.
160    /// Use `set_resolver()` to set a proper resolver from the layout crate,
161    /// or use `with_resolver()` to create with a custom resolver.
162    pub fn new() -> Self {
163        Self {
164            inner: Box::new(IconProviderInner {
165                icons: BTreeMap::new(),
166                resolver: default_icon_resolver,
167            })
168        }
169    }
170
171    /// Create with a custom resolver callback
172    pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
173        Self {
174            inner: Box::new(IconProviderInner {
175                icons: BTreeMap::new(),
176                resolver,
177            })
178        }
179    }
180    
181    /// Convert this handle into an Arc<Mutex<IconProviderInner>> for use in windows.
182    /// 
183    /// This consumes the Box and creates an Arc. Called by App::run() to create
184    /// the shared icon provider that gets cloned to each window.
185    pub fn into_shared(self) -> Arc<Mutex<IconProviderInner>> {
186        Arc::new(Mutex::new(*self.inner))
187    }
188
189    /// Set the resolver callback
190    pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
191        self.inner.resolver = resolver;
192    }
193
194    /// Register a single icon in a pack (creates pack if needed)
195    pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
196        let pack = self.inner.icons
197            .entry(pack_name.to_string())
198            .or_insert_with(BTreeMap::new);
199        pack.insert(icon_name.to_lowercase(), data);
200    }
201
202    /// Unregister a single icon from a pack
203    pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
204        if let Some(pack) = self.inner.icons.get_mut(pack_name) {
205            pack.remove(&icon_name.to_lowercase());
206            if pack.is_empty() {
207                self.inner.icons.remove(pack_name);
208            }
209        }
210    }
211
212    /// Unregister an entire icon pack
213    pub fn unregister_pack(&mut self, pack_name: &str) {
214        self.inner.icons.remove(pack_name);
215    }
216
217    /// Look up an icon across all packs (first match wins)
218    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
219        let icon_name_lower = icon_name.to_lowercase();
220        for pack in self.inner.icons.values() {
221            if let Some(data) = pack.get(&icon_name_lower) {
222                return Some(data.clone());
223            }
224        }
225        None
226    }
227
228    /// Check if an icon exists in any pack
229    pub fn has_icon(&self, icon_name: &str) -> bool {
230        let icon_name_lower = icon_name.to_lowercase();
231        self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
232    }
233
234    /// List all pack names
235    pub fn list_packs(&self) -> Vec<String> {
236        self.inner.icons.keys().cloned().collect()
237    }
238
239    /// List all icon names in a specific pack
240    pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
241        self.inner.icons.get(pack_name)
242            .map(|pack| pack.keys().cloned().collect())
243            .unwrap_or_default()
244    }
245
246    /// Debug lookup: returns detailed info about an icon's RefAny contents
247    pub fn debug_lookup(&self, icon_name: &str) -> AzString {
248        let icon_name_lower = icon_name.to_lowercase();
249        
250        let mut result = format!("Debug lookup for icon '{}' (normalized: '{}'):\n", icon_name, icon_name_lower);
251        
252        // Report registered packs
253        result.push_str(&format!("  Total packs: {}\n", self.inner.icons.len()));
254        for (pack_name, pack) in self.inner.icons.iter() {
255            result.push_str(&format!("    Pack '{}': {} icons\n", pack_name, pack.len()));
256            for (name, _) in pack.iter() {
257                result.push_str(&format!("      - {}\n", name));
258            }
259        }
260        
261        // Find the icon
262        let mut found_in_pack: Option<&str> = None;
263        let mut refany: Option<&RefAny> = None;
264        for (pack_name, pack) in self.inner.icons.iter() {
265            if let Some(data) = pack.get(&icon_name_lower) {
266                found_in_pack = Some(pack_name);
267                refany = Some(data);
268                break;
269            }
270        }
271        
272        match (found_in_pack, refany) {
273            (Some(pack), Some(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("ImageIconData") {
283                    result.push_str("  RefAny type: ImageIconData (image-based icon)\n");
284                } else if type_str.contains("FontIconData") {
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            _ => {
291                result.push_str(&format!("\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(),
389            });
390        }
391    }
392    
393    icons
394}
395
396/// Generate accessibility label from icon name
397fn generate_a11y_label(icon_name: &str) -> AzString {
398    AzString::from(format!("{} icon", icon_name.replace('_', " ").replace('-', " ")))
399}
400
401/// Extract a single-node StyledDom from a parent StyledDom at the given index.
402/// This creates a minimal StyledDom containing just that node for the resolver.
403fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
404    use crate::dom::{NodeDataVec, DomId};
405    use crate::styled_dom::{
406        StyledNodeVec, NodeIdVec, TagIdToNodeIdMappingVec,
407        NodeHierarchyItemVec, NodeHierarchyItemId, ParentWithNodeDepthVec, ParentWithNodeDepth,
408    };
409    use crate::style::{CascadeInfoVec, CascadeInfo};
410    use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
411    
412    let node_data = styled_dom.node_data.as_ref();
413    let styled_nodes = styled_dom.styled_nodes.as_ref();
414    
415    if node_idx >= node_data.len() {
416        return StyledDom::default();
417    }
418    
419    // Clone the single node
420    let single_node = node_data[node_idx].clone();
421    let single_styled = if node_idx < styled_nodes.len() {
422        styled_nodes[node_idx].clone()
423    } else {
424        crate::styled_dom::StyledNode::default()
425    };
426    
427    StyledDom {
428        root: styled_dom.root.clone(),
429        node_hierarchy: styled_dom.node_hierarchy.clone(),
430        node_data: NodeDataVec::from_vec(vec![single_node]),
431        styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
432        cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
433        nodes_with_window_callbacks: NodeIdVec::from_vec(Vec::new()),
434        nodes_with_not_callbacks: NodeIdVec::from_vec(Vec::new()),
435        nodes_with_datasets: NodeIdVec::from_vec(Vec::new()),
436        tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
437        non_leaf_nodes: ParentWithNodeDepthVec::from_vec(vec![ParentWithNodeDepth {
438            depth: 0,
439            node_id: styled_dom.root.clone(),
440        }]),
441        css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
442        dom_id: DomId::ROOT_ID,
443    }
444}
445
446/// Resolve all collected icons to their StyledDom representations
447fn resolve_collected_icons(
448    icons: &[CollectedIcon],
449    styled_dom: &StyledDom,
450    provider: &SharedIconProvider,
451    system_style: &SystemStyle,
452) -> Vec<IconReplacement> {
453    icons.iter().map(|icon| {
454        // Extract the original icon node as a StyledDom
455        let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
456        let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
457        IconReplacement {
458            node_idx: icon.node_idx,
459            replacement,
460        }
461    }).collect()
462}
463
464/// Check if a replacement is a single-node replacement (fast path)
465fn is_single_node_replacement(replacement: &StyledDom) -> bool {
466    replacement.node_data.as_ref().len() == 1
467}
468
469/// Apply a single-node replacement (fast path: swap NodeType and copy properties)
470fn apply_single_node_replacement(
471    styled_dom: &mut StyledDom,
472    node_idx: usize,
473    replacement: &StyledDom,
474) {
475    if replacement.node_data.as_ref().is_empty() {
476        // Empty replacement - convert to empty div
477        let node_data = styled_dom.node_data.as_mut();
478        if let Some(node) = node_data.get_mut(node_idx) {
479            node.set_node_type(NodeType::Div);
480        }
481    } else {
482        // Get the root node from the replacement and copy its properties
483        let replacement_root = &replacement.node_data.as_ref()[0];
484        let replacement_node_type = replacement_root.get_node_type().clone();
485        
486        let node_data = styled_dom.node_data.as_mut();
487        if let Some(node) = node_data.get_mut(node_idx) {
488            // Swap node type
489            node.set_node_type(replacement_node_type);
490            
491            // Copy CSS properties from replacement
492            node.css_props = replacement_root.get_css_props().clone();
493            
494            // Copy accessibility info if present
495            if let Some(a11y) = replacement_root.get_accessibility_info() {
496                node.set_accessibility_info(*a11y.clone());
497            }
498        }
499        
500        // Also update the styled_nodes to reflect the new styling
501        if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
502            let styled_nodes = styled_dom.styled_nodes.as_mut();
503            if let Some(styled) = styled_nodes.get_mut(node_idx) {
504                *styled = replacement_styled.clone();
505            }
506        }
507    }
508}
509
510/// Apply multi-node replacement using subtree splicing
511fn apply_multi_node_replacement(
512    styled_dom: &mut StyledDom,
513    node_idx: usize,
514    replacement: StyledDom,
515) {
516    let replacement_len = replacement.node_data.as_ref().len();
517    if replacement_len == 0 {
518        let node_data = styled_dom.node_data.as_mut();
519        if let Some(node) = node_data.get_mut(node_idx) {
520            node.set_node_type(NodeType::Div);
521        }
522        return;
523    }
524    
525    // For now, just apply the root node (same as single-node)
526    apply_single_node_replacement(styled_dom, node_idx, &replacement);
527    
528    if replacement_len > 1 {
529        // TODO: Full subtree splicing requires inserting nodes into arrays
530        #[cfg(debug_assertions)]
531        eprintln!(
532            "Warning: Icon replacement has {} nodes, only root node used.",
533            replacement_len
534        );
535    }
536}
537
538/// Resolve all Icon nodes in a StyledDom to their actual content.
539///
540/// This function:
541/// 1. Collects all Icon nodes from the StyledDom
542/// 2. Resolves each icon via the provider's callback (passing original icon DOM)
543/// 3. Applies replacements (single-node fast path or multi-node splicing)
544///
545/// This should be called after StyledDom creation but before layout.
546pub fn resolve_icons_in_styled_dom(
547    styled_dom: &mut StyledDom,
548    provider: &SharedIconProvider,
549    system_style: &SystemStyle,
550) {
551    resolve_icons_in_styled_dom_with_log(styled_dom, provider, system_style, None)
552}
553
554/// Same as `resolve_icons_in_styled_dom` but with optional debug logging
555pub fn resolve_icons_in_styled_dom_with_log(
556    styled_dom: &mut StyledDom,
557    provider: &SharedIconProvider,
558    system_style: &SystemStyle,
559    mut debug_log: Option<&mut DebugLog>,
560) {
561    use crate::log_debug;
562    
563    // Step 1: Collect all icon nodes
564    let icons = collect_icon_nodes(styled_dom);
565    
566    if icons.is_empty() {
567        if let Some(ref mut log) = debug_log {
568            log_debug!(log, Icon, "No icon nodes found in StyledDom");
569        }
570        return;
571    }
572    
573    if let Some(ref mut log) = debug_log {
574        log_debug!(log, Icon, "Found {} icon nodes to resolve", icons.len());
575        for icon in &icons {
576            let has_icon = provider.has_icon(icon.icon_name.as_str());
577            log_debug!(log, Icon, "  - Icon '{}' at node {}: registered={}", 
578                icon.icon_name.as_str(), icon.node_idx, has_icon);
579        }
580    }
581    
582    // Step 2: Resolve all icons to their StyledDom representations
583    // Note: We pass styled_dom to extract each icon's original node
584    let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
585    
586    if let Some(ref mut log) = debug_log {
587        for replacement in &replacements {
588            let node_count = replacement.replacement.node_data.as_ref().len();
589            let node_type = replacement.replacement.node_data.as_ref()
590                .first()
591                .map(|n| format!("{:?}", n.get_node_type()))
592                .unwrap_or_else(|| "empty".to_string());
593            log_debug!(log, Icon, "  - Replacement at {}: {} nodes, root type: {}", 
594                replacement.node_idx, node_count, node_type);
595        }
596    }
597    
598    // Step 3: Apply replacements (reverse order to preserve indices)
599    for replacement in replacements.into_iter().rev() {
600        if is_single_node_replacement(&replacement.replacement) || 
601           replacement.replacement.node_data.as_ref().is_empty() {
602            apply_single_node_replacement(
603                styled_dom, 
604                replacement.node_idx, 
605                &replacement.replacement
606            );
607        } else {
608            apply_multi_node_replacement(
609                styled_dom, 
610                replacement.node_idx, 
611                replacement.replacement
612            );
613        }
614    }
615}
616
617// FFI Option Types
618
619impl_option!(
620    IconProviderHandle,
621    OptionIconProviderHandle,
622    [Clone]
623);
624
625// Tests
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630
631    #[test]
632    fn test_icon_provider_new() {
633        let provider = IconProviderHandle::new();
634        assert!(provider.list_packs().is_empty());
635    }
636
637    // Dummy destructor for test RefAny
638    extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
639
640    #[test]
641    fn test_icon_registration() {
642        let mut provider = IconProviderHandle::new();
643        
644        // Use a dummy RefAny for testing
645        let dummy_data = crate::refany::RefAny::new_c(
646            core::ptr::null(),
647            0,
648            1,
649            0,
650            "".into(),
651            dummy_destructor,
652            0, // serialize_fn
653            0, // deserialize_fn
654        );
655        
656        provider.register_icon("images", "home", dummy_data.clone());
657        assert!(provider.has_icon("home"));
658        assert!(provider.has_icon("HOME")); // case-insensitive
659        
660        provider.unregister_icon("images", "home");
661        assert!(!provider.has_icon("home"));
662    }
663
664    #[test]
665    fn test_icon_provider_lookup() {
666        let mut provider = IconProviderHandle::new();
667        
668        let dummy_data = crate::refany::RefAny::new_c(
669            core::ptr::null(),
670            0,
671            1,
672            0,
673            "".into(),
674            dummy_destructor,
675            0, // serialize_fn
676            0, // deserialize_fn
677        );
678        
679        provider.register_icon("images", "logo", dummy_data);
680        
681        assert!(provider.has_icon("logo"));
682        assert!(!provider.has_icon("missing"));
683        
684        let lookup = provider.lookup("logo");
685        assert!(lookup.is_some());
686    }
687
688    #[test]
689    fn test_pack_operations() {
690        let mut provider = IconProviderHandle::new();
691        
692        let dummy_data = crate::refany::RefAny::new_c(
693            core::ptr::null(),
694            0,
695            1,
696            0,
697            "".into(),
698            dummy_destructor,
699            0, // serialize_fn
700            0, // deserialize_fn
701        );
702        
703        // Register icons in different packs
704        provider.register_icon("pack1", "icon1", dummy_data.clone());
705        provider.register_icon("pack2", "icon2", dummy_data);
706        
707        assert_eq!(provider.list_packs().len(), 2);
708        assert!(provider.has_icon("icon1"));
709        assert!(provider.has_icon("icon2"));
710        
711        // Unregister entire pack
712        provider.unregister_pack("pack1");
713        assert!(!provider.has_icon("icon1"));
714        assert!(provider.has_icon("icon2"));
715        assert_eq!(provider.list_packs().len(), 1);
716    }
717}