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;
46use core::mem::ManuallyDrop;
47
48#[cfg(feature = "std")]
49use std::sync::Mutex;
50
51#[cfg(not(feature = "std"))]
52use self::nostd_lock::Mutex;
53
54/// Minimal `no_std` spinlock that mirrors the slice of the `std::sync::Mutex`
55/// API actually used by this module (`new` + `lock` returning a `Result`).
56#[cfg(not(feature = "std"))]
57mod nostd_lock {
58    use core::cell::UnsafeCell;
59    use core::ops::{Deref, DerefMut};
60    use core::sync::atomic::{AtomicBool, Ordering};
61
62    pub struct Mutex<T> {
63        locked: AtomicBool,
64        data: UnsafeCell<T>,
65    }
66
67    unsafe impl<T: Send> Send for Mutex<T> {}
68    unsafe impl<T: Send> Sync for Mutex<T> {}
69
70    pub struct MutexGuard<'a, T> {
71        lock: &'a Mutex<T>,
72    }
73
74    impl<T> Mutex<T> {
75        pub fn new(data: T) -> Self {
76            Mutex { locked: AtomicBool::new(false), data: UnsafeCell::new(data) }
77        }
78
79        /// Returns `Ok(guard)` to mirror `std::sync::Mutex::lock`. Never poisons.
80        pub fn lock(&self) -> Result<MutexGuard<'_, T>, core::convert::Infallible> {
81            while self
82                .locked
83                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
84                .is_err()
85            {
86                core::hint::spin_loop();
87            }
88            Ok(MutexGuard { lock: self })
89        }
90    }
91
92    impl<'a, T> Deref for MutexGuard<'a, T> {
93        type Target = T;
94        fn deref(&self) -> &T {
95            unsafe { &*self.lock.data.get() }
96        }
97    }
98
99    impl<'a, T> DerefMut for MutexGuard<'a, T> {
100        fn deref_mut(&mut self) -> &mut T {
101            unsafe { &mut *self.lock.data.get() }
102        }
103    }
104
105    impl<'a, T> Drop for MutexGuard<'a, T> {
106        fn drop(&mut self) {
107            self.lock.locked.store(false, Ordering::Release);
108        }
109    }
110
111    // Mirror `std::sync::Mutex: Debug` so containers can derive Debug. Does not
112    // lock (the spinlock has no `try_lock`, and locking in `fmt` could deadlock).
113    impl<T> core::fmt::Debug for Mutex<T> {
114        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115            f.debug_struct("Mutex").finish_non_exhaustive()
116        }
117    }
118}
119
120use azul_css::{AzString, system::SystemStyle};
121
122use crate::{
123    dom::{Dom, NodeType},
124    refany::{OptionRefAny, RefAny},
125    styled_dom::StyledDom,
126};
127
128// Type name constants for RefAny-based icon type detection in debug output
129const IMAGE_ICON_DATA_TYPE_NAME: &str = "ImageIconData";
130const FONT_ICON_DATA_TYPE_NAME: &str = "FontIconData";
131
132// Icon Resolver Callback
133
134/// Callback type for resolving icon data to a `StyledDom`.
135///
136/// Parameters:
137/// - `icon_data`: The `RefAny` data from the icon pack (cloned, or None if not found)
138/// - `original_icon_dom`: The original icon node's `StyledDom` (contains inline styles, a11y info, `icon_name`)
139/// - `system_style`: Current system style (theme, colors, etc.)
140///
141/// Returns: A `StyledDom` that will replace the icon node.
142/// The resolver should copy relevant styles from `original_icon_dom` to the result.
143/// Return an empty `StyledDom` to show a placeholder or nothing.
144///
145/// Note: `icon_name` is accessible via `original_icon_dom.node_data[0].get_node_type()` → `NodeType::Icon(name)`
146pub type IconResolverCallbackType = extern "C" fn(
147    icon_data: OptionRefAny,
148    original_icon_dom: &StyledDom,
149    system_style: &SystemStyle,
150) -> StyledDom;
151
152/// Default resolver that returns an empty `StyledDom` (shows placeholder)
153#[must_use] pub extern "C" fn default_icon_resolver(
154    _icon_data: OptionRefAny,
155    _original_icon_dom: &StyledDom,
156    _system_style: &SystemStyle,
157) -> StyledDom {
158    // Default: return empty DOM (icon won't be visible)
159    StyledDom::default()
160}
161
162// Icon Provider Inner (single mutex)
163
164/// Inner data for `IconProviderHandle` - all fields behind single mutex
165#[derive(Debug, Clone)]
166pub struct IconProviderInner {
167    /// Nested map: `pack_name` → (`icon_name` → `RefAny`)
168    /// Differentiation between Image/Font/SVG is via `RefAny::downcast`
169    pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
170    /// The resolver callback
171    pub resolver: IconResolverCallbackType,
172}
173
174impl Default for IconProviderInner {
175    fn default() -> Self {
176        Self {
177            icons: BTreeMap::new(),
178            resolver: default_icon_resolver,
179        }
180    }
181}
182
183// Icon Provider Handle
184
185/// Icon provider stored in `AppConfig`.
186///
187/// This is a Box<IconProviderInner> for C FFI compatibility.
188/// When `App::run()` is called, it gets converted to Arc<Mutex<IconProviderInner>>
189/// and cloned to each window.
190///
191/// Icons are stored in a nested map: `pack_name` → (`icon_name` → `RefAny`)
192/// This allows:
193/// - Multiple packs with different sources (app-images, material-icons, etc.)
194/// - Easy unregistration of entire packs
195/// - First-match-wins lookup across all packs
196#[repr(C)]
197pub struct IconProviderHandle {
198    /// Boxed inner data - Box<T> is repr(C) compatible (single pointer).
199    /// `ManuallyDrop` so the Box is freed ONLY by our `Drop` (gated on
200    /// `run_destructor`), never by drop-glue. The codegen Az wrapper nests an
201    /// `AzIconProviderHandle` field (in `AzAppConfig`) whose own `Drop` re-runs
202    /// `_delete` -> `drop_in_place::<IconProviderHandle>` on the SAME bytes; with
203    /// a bare `Box` the glue freed it a second time -> double free. Same
204    /// convention as `GlContextPtr` / `CssPropertyCachePtr`.
205    pub inner: ManuallyDrop<Box<IconProviderInner>>,
206    pub run_destructor: bool,
207}
208
209impl Clone for IconProviderHandle {
210    fn clone(&self) -> Self {
211        Self {
212            inner: ManuallyDrop::new(Box::new((**self.inner).clone())),
213            run_destructor: true,
214        }
215    }
216}
217
218impl Drop for IconProviderHandle {
219    fn drop(&mut self) {
220        // First drop (run_destructor still true) frees the Box and clears the flag
221        // in the shared bytes; the codegen's redundant second drop sees false -> no-op.
222        if self.run_destructor {
223            self.run_destructor = false;
224            unsafe {
225                ManuallyDrop::drop(&mut self.inner);
226            }
227        }
228    }
229}
230
231impl fmt::Debug for IconProviderHandle {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        let pack_count = self.inner.icons.len();
234        let icon_count: usize = self.inner.icons.values().map(BTreeMap::len).sum();
235        
236        f.debug_struct("IconProviderHandle")
237            .field("pack_count", &pack_count)
238            .field("icon_count", &icon_count)
239            .finish_non_exhaustive()
240    }
241}
242
243impl Default for IconProviderHandle {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249impl IconProviderHandle {
250    /// Create a new empty icon provider with the default (no-op) resolver.
251    /// 
252    /// Note: The default resolver in core crate returns an empty `StyledDom`.
253    /// Use `set_resolver()` to set a proper resolver from the layout crate,
254    /// or use `with_resolver()` to create with a custom resolver.
255    #[must_use] pub fn new() -> Self {
256        Self {
257            inner: ManuallyDrop::new(Box::new(IconProviderInner {
258                icons: BTreeMap::new(),
259                resolver: default_icon_resolver,
260            })),
261            run_destructor: true,
262        }
263    }
264
265    /// Create with a custom resolver callback
266    pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
267        Self {
268            inner: ManuallyDrop::new(Box::new(IconProviderInner {
269                icons: BTreeMap::new(),
270                resolver,
271            })),
272            run_destructor: true,
273        }
274    }
275    
276    /// Convert this handle into an Arc<Mutex<IconProviderInner>> for use in windows.
277    ///
278    /// This consumes the Box and creates an Arc. Called by `App::run()` to create
279    /// the shared icon provider that gets cloned to each window.
280    pub(crate) fn into_shared(mut self) -> Arc<Mutex<IconProviderInner>> {
281        // Take the Box out and disarm our Drop so it doesn't free the moved-out
282        // allocation (ManuallyDrop::take leaves `inner` logically uninitialized).
283        let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
284        self.run_destructor = false;
285        Arc::new(Mutex::new(*inner))
286    }
287
288    /// Set the resolver callback
289    pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
290        self.inner.resolver = resolver;
291    }
292
293    /// Register a single icon in a pack (creates pack if needed).
294    ///
295    /// Note: `pack_name` is case-sensitive, while `icon_name` is normalized to lowercase.
296    pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
297        let pack = self.inner.icons
298            .entry(pack_name.to_string())
299            .or_default();
300        pack.insert(icon_name.to_lowercase(), data);
301    }
302
303    /// Unregister a single icon from a pack
304    pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
305        if let Some(pack) = self.inner.icons.get_mut(pack_name) {
306            pack.remove(&icon_name.to_lowercase());
307            if pack.is_empty() {
308                self.inner.icons.remove(pack_name);
309            }
310        }
311    }
312
313    /// Unregister an entire icon pack
314    pub fn unregister_pack(&mut self, pack_name: &str) {
315        self.inner.icons.remove(pack_name);
316    }
317
318    /// Look up an icon across all packs, returning the pack name and data reference (first match wins)
319    fn lookup_with_pack(&self, icon_name: &str) -> Option<(&str, &RefAny)> {
320        let icon_name_lower = icon_name.to_lowercase();
321        for (pack_name, pack) in &self.inner.icons {
322            if let Some(data) = pack.get(&icon_name_lower) {
323                return Some((pack_name.as_str(), data));
324            }
325        }
326        None
327    }
328
329    /// Look up an icon across all packs (first match wins)
330    #[must_use] pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
331        self.lookup_with_pack(icon_name).map(|(_, data)| data.clone())
332    }
333
334    /// Check if an icon exists in any pack
335    #[must_use] pub fn has_icon(&self, icon_name: &str) -> bool {
336        let icon_name_lower = icon_name.to_lowercase();
337        self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
338    }
339
340    /// List all pack names
341    #[must_use] pub fn list_packs(&self) -> Vec<String> {
342        self.inner.icons.keys().cloned().collect()
343    }
344
345    /// List all icon names in a specific pack
346    #[must_use] pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
347        self.inner.icons.get(pack_name)
348            .map(|pack| pack.keys().cloned().collect())
349            .unwrap_or_default()
350    }
351
352    /// Debug lookup: returns detailed info about an icon's `RefAny` contents
353    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
354    #[must_use] pub fn debug_lookup(&self, icon_name: &str) -> AzString {
355        use core::fmt::Write;
356
357        let icon_name_lower = icon_name.to_lowercase();
358
359        let mut result = format!("Debug lookup for icon '{icon_name}' (normalized: '{icon_name_lower}'):\n");
360
361        // Report registered packs
362        let _ = writeln!(result, "  Total packs: {}", self.inner.icons.len());
363        for (pack_name, pack) in &self.inner.icons {
364            let _ = writeln!(result, "    Pack '{}': {} icons", pack_name, pack.len());
365            for name in pack.keys() {
366                let _ = writeln!(result, "      - {name}");
367            }
368        }
369
370        // Find the icon using shared lookup helper
371        match self.lookup_with_pack(icon_name) {
372            Some((pack, data)) => {
373                let _ = writeln!(result, "\n  FOUND in pack '{pack}'");
374                let type_name = data.get_type_name();
375                let _ = writeln!(result, "  RefAny type_name: '{}'", type_name.as_str());
376
377                let debug_info = data.sharing_info.debug_get_refcount_copied();
378                let _ = writeln!(result, "  RefAny size: {} bytes", debug_info._internal_layout_size);
379
380                let type_str = type_name.as_str();
381                if type_str.contains(IMAGE_ICON_DATA_TYPE_NAME) {
382                    result.push_str("  RefAny type: ImageIconData (image-based icon)\n");
383                } else if type_str.contains(FONT_ICON_DATA_TYPE_NAME) {
384                    result.push_str("  RefAny type: FontIconData (font-based icon)\n");
385                } else {
386                    let _ = writeln!(result, "  RefAny type: UNKNOWN ('{type_str}')");
387                }
388            }
389            None => {
390                result.push_str("\n  NOT FOUND in any pack\n");
391            }
392        }
393
394        AzString::from(result)
395    }
396}
397
398/// Thread-safe icon provider for use in windows.
399/// 
400/// This is created from `IconProviderHandle::into_shared()` in `App::run()`
401/// and cloned to each window.
402#[derive(Debug, Clone)]
403pub struct SharedIconProvider {
404    inner: Arc<Mutex<IconProviderInner>>,
405}
406
407impl SharedIconProvider {
408    /// Create from an `IconProviderHandle` (consumes the handle)
409    #[must_use] pub fn from_handle(handle: IconProviderHandle) -> Self {
410        Self { inner: handle.into_shared() }
411    }
412    
413    /// Resolve an icon to a `StyledDom` using the registered callback
414    #[must_use] pub fn resolve(
415        &self, 
416        original_icon_dom: &StyledDom,
417        icon_name: &str,
418        system_style: &SystemStyle,
419    ) -> StyledDom {
420        let (resolver, lookup_result) = {
421            let Ok(guard) = self.inner.lock() else {
422                return StyledDom::default();
423            };
424            
425            let resolver = guard.resolver;
426            let icon_name_lower = icon_name.to_lowercase();
427            
428            let lookup_result = guard.icons.values()
429                .find_map(|pack| pack.get(&icon_name_lower).cloned());
430            
431            (resolver, lookup_result)
432        };
433        
434        resolver(lookup_result.into(), original_icon_dom, system_style)
435    }
436    
437    /// Look up an icon across all packs
438    #[must_use] pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
439        let icon_name_lower = icon_name.to_lowercase();
440        self.inner.lock().ok().and_then(|guard| {
441            for pack in guard.icons.values() {
442                if let Some(data) = pack.get(&icon_name_lower) {
443                    return Some(data.clone());
444                }
445            }
446            None
447        })
448    }
449    
450    /// Check if an icon exists
451    #[must_use] pub fn has_icon(&self, icon_name: &str) -> bool {
452        let icon_name_lower = icon_name.to_lowercase();
453        self.inner.lock()
454            .map(|guard| guard.icons.values().any(|p| p.contains_key(&icon_name_lower)))
455            .unwrap_or(false)
456    }
457}
458
459// Icon Resolution in StyledDom
460
461/// Collected icon node info for replacement
462struct CollectedIcon {
463    /// Index in the `node_data` array
464    node_idx: usize,
465    /// The icon name
466    icon_name: AzString,
467}
468
469/// Replacement result after resolving an icon
470struct IconReplacement {
471    /// Index of the icon node to replace
472    node_idx: usize,
473    /// The resolved `StyledDom` (may be empty, single node, or multi-node tree)
474    replacement: StyledDom,
475}
476
477/// Collect all Icon nodes from the `StyledDom`
478fn collect_icon_nodes(styled_dom: &StyledDom) -> Vec<CollectedIcon> {
479    let mut icons = Vec::new();
480    
481    let node_data = styled_dom.node_data.as_ref();
482    for (idx, node) in node_data.iter().enumerate() {
483        if let NodeType::Icon(icon_name) = node.get_node_type() {
484            icons.push(CollectedIcon {
485                node_idx: idx,
486                icon_name: icon_name.clone_self(),
487            });
488        }
489    }
490    
491    icons
492}
493
494/// Extract a single-node `StyledDom` from a parent `StyledDom` at the given index.
495/// This creates a minimal `StyledDom` containing just that node for the resolver.
496fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
497    use crate::dom::{NodeDataVec, DomId};
498    use crate::id::NodeId;
499    use crate::styled_dom::{
500        StyledNodeVec, NodeHierarchyItemIdVec, TagIdToNodeIdMappingVec,
501        NodeHierarchyItemVec, NodeHierarchyItem, NodeHierarchyItemId,
502        ParentWithNodeDepthVec, ParentWithNodeDepth,
503    };
504    use crate::style::{CascadeInfoVec, CascadeInfo};
505    use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
506    
507    let node_data = styled_dom.node_data.as_ref();
508    let styled_nodes = styled_dom.styled_nodes.as_ref();
509    
510    if node_idx >= node_data.len() {
511        return StyledDom::default();
512    }
513    
514    // Clone the single node
515    let single_node = node_data[node_idx].clone();
516    let single_styled = if node_idx < styled_nodes.len() {
517        styled_nodes[node_idx].clone()
518    } else {
519        crate::styled_dom::StyledNode::default()
520    };
521    
522    StyledDom {
523        root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
524        node_hierarchy: NodeHierarchyItemVec::from_vec(vec![NodeHierarchyItem {
525            parent: 0,
526            previous_sibling: 0,
527            next_sibling: 0,
528            last_child: 0,
529        }]),
530        node_data: NodeDataVec::from_vec(vec![single_node]),
531        styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
532        cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
533        nodes_with_window_callbacks: NodeHierarchyItemIdVec::from_vec(Vec::new()),
534        nodes_with_datasets: NodeHierarchyItemIdVec::from_vec(Vec::new()),
535        tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
536        non_leaf_nodes: ParentWithNodeDepthVec::from_vec(Vec::new()),
537        css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
538        dom_id: DomId::ROOT_ID,
539    }
540}
541
542/// Resolve all collected icons to their `StyledDom` representations
543fn resolve_collected_icons(
544    icons: &[CollectedIcon],
545    styled_dom: &StyledDom,
546    provider: &SharedIconProvider,
547    system_style: &SystemStyle,
548) -> Vec<IconReplacement> {
549    icons.iter().map(|icon| {
550        // Extract the original icon node as a StyledDom
551        let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
552        let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
553        IconReplacement {
554            node_idx: icon.node_idx,
555            replacement,
556        }
557    }).collect()
558}
559
560/// Check if a replacement is a single-node replacement (fast path)
561fn is_single_node_replacement(replacement: &StyledDom) -> bool {
562    replacement.node_data.as_ref().len() == 1
563}
564
565/// Apply a single-node replacement (fast path: swap `NodeType` and copy properties)
566fn apply_single_node_replacement(
567    styled_dom: &mut StyledDom,
568    node_idx: usize,
569    replacement: &StyledDom,
570) {
571    if replacement.node_data.as_ref().is_empty() {
572        // Empty replacement - convert to empty div
573        let node_data = styled_dom.node_data.as_mut();
574        if let Some(node) = node_data.get_mut(node_idx) {
575            node.set_node_type(NodeType::Div);
576        }
577    } else {
578        // Get the root node from the replacement and copy its properties
579        let replacement_root = &replacement.node_data.as_ref()[0];
580        let replacement_node_type = replacement_root.get_node_type().clone();
581        
582        let node_data = styled_dom.node_data.as_mut();
583        if let Some(node) = node_data.get_mut(node_idx) {
584            // Swap node type
585            node.set_node_type(replacement_node_type);
586            
587            // Copy inline style from replacement
588            node.set_style(replacement_root.get_style().clone());
589            
590            // Copy accessibility info if present
591            if let Some(a11y) = replacement_root.get_accessibility_info() {
592                node.set_accessibility_info(a11y.clone());
593            }
594        }
595        
596        // Also update the styled_nodes to reflect the new styling
597        if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
598            let styled_nodes = styled_dom.styled_nodes.as_mut();
599            if let Some(styled) = styled_nodes.get_mut(node_idx) {
600                *styled = replacement_styled.clone();
601            }
602        }
603    }
604}
605
606/// Apply multi-node replacement using subtree splicing
607fn apply_multi_node_replacement(
608    styled_dom: &mut StyledDom,
609    node_idx: usize,
610    replacement: &StyledDom,
611) {
612    let replacement_len = replacement.node_data.as_ref().len();
613    if replacement_len == 0 {
614        let node_data = styled_dom.node_data.as_mut();
615        if let Some(node) = node_data.get_mut(node_idx) {
616            node.set_node_type(NodeType::Div);
617        }
618        return;
619    }
620    
621    // For now, just apply the root node (same as single-node)
622    apply_single_node_replacement(styled_dom, node_idx, replacement);
623    
624    if replacement_len > 1 {
625        // TODO: Full subtree splicing requires inserting nodes into arrays
626        #[cfg(all(debug_assertions, feature = "std"))]
627        eprintln!(
628            "Warning: Icon replacement has {replacement_len} nodes, only root node used."
629        );
630    }
631}
632
633/// Resolve all Icon nodes in a `StyledDom` to their actual content.
634///
635/// This function:
636/// 1. Collects all Icon nodes from the `StyledDom`
637/// 2. Resolves each icon via the provider's callback (passing original icon DOM)
638/// 3. Applies replacements (single-node fast path or multi-node splicing)
639///
640/// This should be called after `StyledDom` creation but before layout.
641pub fn resolve_icons_in_styled_dom(
642    styled_dom: &mut StyledDom,
643    provider: &SharedIconProvider,
644    system_style: &SystemStyle,
645) {
646    // Step 1: Collect all icon nodes
647    let icons = collect_icon_nodes(styled_dom);
648
649    if icons.is_empty() {
650        return;
651    }
652
653    // Step 2: Resolve all icons to their StyledDom representations
654    // Note: We pass styled_dom to extract each icon's original node
655    let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
656
657    // Step 3: Apply replacements (reverse order to preserve indices)
658    for replacement in replacements.into_iter().rev() {
659        if is_single_node_replacement(&replacement.replacement) ||
660           replacement.replacement.node_data.as_ref().is_empty() {
661            apply_single_node_replacement(
662                styled_dom,
663                replacement.node_idx,
664                &replacement.replacement
665            );
666        } else {
667            apply_multi_node_replacement(
668                styled_dom,
669                replacement.node_idx,
670                &replacement.replacement
671            );
672        }
673    }
674}
675
676// FFI Option Types
677
678impl_option!(
679    IconProviderHandle,
680    OptionIconProviderHandle,
681    [Clone]
682);
683
684#[cfg(test)]
685#[allow(clippy::float_cmp, clippy::too_many_lines)]
686mod autotest_generated {
687    use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering};
688
689    use super::*;
690    use crate::{dom::NodeDataVec, styled_dom::StyledNodeVec};
691
692    // Test payloads. The names `ImageIconData` / `FontIconData` are load-bearing:
693    // `debug_lookup` sniffs `RefAny::get_type_name()` (i.e. `core::any::type_name`)
694    // for those substrings.
695    #[derive(Debug, Clone, PartialEq)]
696    struct TestIconData {
697        id: u32,
698    }
699    #[derive(Debug)]
700    struct ImageIconData {
701        _w: u32,
702    }
703    #[derive(Debug)]
704    struct FontIconData {
705        _codepoint: u32,
706    }
707
708    /// Empty / control / unicode / huge names, all of which are legal icon names
709    /// (the API places no constraints on them).
710    fn adversarial_names() -> Vec<String> {
711        vec![
712            String::new(),
713            String::from(" "),
714            String::from("   "),
715            String::from("\t\n\r"),
716            String::from("\0"),
717            String::from("a\0b"),
718            String::from("\u{1b}[0m"),
719            String::from("../../etc/passwd"),
720            String::from("home;garbage"),
721            String::from("{\"json\":true}"),
722            String::from("-0"),
723            String::from("NaN"),
724            String::from("inf"),
725            String::from("9223372036854775807"),
726            String::from("\u{1F600}"),               // emoji
727            String::from("e\u{0301}\u{0301}"),       // combining marks
728            String::from("\u{202e}RTL\u{202d}"),     // bidi override
729            String::from("\u{130}"),                 // LATIN CAPITAL I WITH DOT ABOVE
730            String::from("\u{FFFD}\u{10FFFF}"),      // replacement + max scalar
731            "[".repeat(10_000),                      // deeply "nested" junk
732            "x".repeat(100_000),                     // huge
733        ]
734    }
735
736    fn styled_dom_with_icons(names: &[&str]) -> StyledDom {
737        let mut body = Dom::create_body();
738        for n in names {
739            body.add_child(Dom::create_icon(*n));
740        }
741        StyledDom::create_from_dom(body)
742    }
743
744    /// A `StyledDom` with *zero* nodes — `StyledDom::default()` has one (a Body),
745    /// so the truly-empty case has to be built by hand.
746    fn zero_node_styled_dom() -> StyledDom {
747        let mut sd = StyledDom::default();
748        sd.node_data = NodeDataVec::from_vec(Vec::new());
749        sd.styled_nodes = StyledNodeVec::from_vec(Vec::new());
750        sd
751    }
752
753    fn node_type_at(sd: &StyledDom, idx: usize) -> NodeType {
754        sd.node_data.as_ref()[idx].get_node_type().clone()
755    }
756
757    fn icon_indices(sd: &StyledDom) -> Vec<usize> {
758        collect_icon_nodes(sd).iter().map(|i| i.node_idx).collect()
759    }
760
761    // Resolvers
762
763    extern "C" fn div_resolver(
764        _icon_data: OptionRefAny,
765        _original_icon_dom: &StyledDom,
766        _system_style: &SystemStyle,
767    ) -> StyledDom {
768        StyledDom::create_from_dom(Dom::create_div())
769    }
770
771    extern "C" fn zero_node_resolver(
772        _icon_data: OptionRefAny,
773        _original_icon_dom: &StyledDom,
774        _system_style: &SystemStyle,
775    ) -> StyledDom {
776        zero_node_styled_dom()
777    }
778
779    // Statics for `shared_resolve_receives_icon_data_and_original_dom` ONLY.
780    // (`extern "C" fn` cannot capture, and tests run in parallel — never share
781    // one recording resolver between two tests.)
782    static REC_CALLS: AtomicUsize = AtomicUsize::new(0);
783    static REC_SAW_DATA: AtomicBool = AtomicBool::new(false);
784    static REC_SAW_ICON_NODE: AtomicBool = AtomicBool::new(false);
785    static REC_NAME_LEN: AtomicUsize = AtomicUsize::new(0);
786
787    extern "C" fn recording_resolver(
788        icon_data: OptionRefAny,
789        original_icon_dom: &StyledDom,
790        _system_style: &SystemStyle,
791    ) -> StyledDom {
792        REC_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
793        if matches!(icon_data, OptionRefAny::Some(_)) {
794            REC_SAW_DATA.store(true, AtomicOrdering::SeqCst);
795        }
796        if let Some(node) = original_icon_dom.node_data.as_ref().first() {
797            if let NodeType::Icon(name) = node.get_node_type() {
798                REC_SAW_ICON_NODE.store(true, AtomicOrdering::SeqCst);
799                REC_NAME_LEN.store(name.as_ref().as_str().len(), AtomicOrdering::SeqCst);
800            }
801        }
802        StyledDom::create_from_dom(Dom::create_div())
803    }
804
805    // Mutex (the no_std spinlock under `no_std`, `std::sync::Mutex` otherwise)
806
807    #[test]
808    fn mutex_new_then_lock_roundtrips_the_value() {
809        let m = Mutex::new(42u32);
810        assert_eq!(*m.lock().unwrap(), 42);
811        *m.lock().unwrap() = u32::MAX;
812        assert_eq!(*m.lock().unwrap(), u32::MAX);
813    }
814
815    #[test]
816    fn mutex_lock_on_empty_and_large_payloads() {
817        let empty: Mutex<Vec<u8>> = Mutex::new(Vec::new());
818        assert!(empty.lock().unwrap().is_empty());
819
820        let big = Mutex::new(vec![0u8; 1_000_000]);
821        assert_eq!(big.lock().unwrap().len(), 1_000_000);
822
823        // Sequential re-lock must not deadlock (guard dropped at end of statement).
824        for _ in 0..1_000 {
825            assert!(big.lock().is_ok());
826        }
827    }
828
829    // default_icon_resolver
830
831    #[test]
832    fn default_resolver_returns_one_body_node_for_none_and_some() {
833        let orig = StyledDom::default();
834        let style = SystemStyle::default();
835
836        let none = default_icon_resolver(OptionRefAny::None, &orig, &style);
837        // NOTE: the doc calls this an "empty StyledDom", but `StyledDom::default()`
838        // carries exactly one node (a Body), so the result is single-node, NOT empty.
839        assert_eq!(none.node_data.as_ref().len(), 1);
840        assert!(is_single_node_replacement(&none));
841
842        let some = default_icon_resolver(
843            OptionRefAny::Some(RefAny::new(TestIconData { id: 1 })),
844            &orig,
845            &style,
846        );
847        assert_eq!(some.node_data.as_ref().len(), 1);
848    }
849
850    #[test]
851    fn default_resolver_no_panic_on_zero_node_original_dom() {
852        let orig = zero_node_styled_dom();
853        let style = SystemStyle::default();
854        let out = default_icon_resolver(OptionRefAny::None, &orig, &style);
855        assert_eq!(out.node_data.as_ref().len(), 1);
856    }
857
858    // IconProviderHandle: construction / invariants
859
860    #[test]
861    fn new_handle_is_empty_and_all_queries_are_negative() {
862        let h = IconProviderHandle::new();
863        assert!(h.list_packs().is_empty());
864        assert!(h.list_icons_in_pack("anything").is_empty());
865        assert!(!h.has_icon("home"));
866        assert!(h.lookup("home").is_none());
867        assert!(h.lookup_with_pack("home").is_none());
868        assert!(h.debug_lookup("home").as_str().contains("Total packs: 0"));
869    }
870
871    #[test]
872    fn default_handle_matches_new_handle() {
873        let a = IconProviderHandle::default();
874        let b = IconProviderHandle::new();
875        assert_eq!(a.list_packs(), b.list_packs());
876        assert_eq!(a.has_icon(""), b.has_icon(""));
877    }
878
879    #[test]
880    fn with_resolver_installs_the_callback() {
881        let mut h = IconProviderHandle::with_resolver(div_resolver);
882        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
883        let shared = SharedIconProvider::from_handle(h);
884        let out = shared.resolve(&StyledDom::default(), "home", &SystemStyle::default());
885        assert!(matches!(node_type_at(&out, 0), NodeType::Div));
886    }
887
888    #[test]
889    fn set_resolver_overrides_the_default_resolver() {
890        let mut h = IconProviderHandle::new();
891        h.set_resolver(div_resolver);
892        let shared = SharedIconProvider::from_handle(h);
893        // Unregistered icon: resolver still runs, just with `None` data.
894        let out = shared.resolve(&StyledDom::default(), "missing", &SystemStyle::default());
895        assert!(matches!(node_type_at(&out, 0), NodeType::Div));
896    }
897
898    #[test]
899    fn clone_of_handle_is_deep() {
900        let mut a = IconProviderHandle::new();
901        a.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
902
903        let mut b = a.clone();
904        b.register_icon("p", "settings", RefAny::new(TestIconData { id: 2 }));
905        b.unregister_icon("p", "home");
906
907        assert!(a.has_icon("home"));
908        assert!(!a.has_icon("settings"));
909        assert!(b.has_icon("settings"));
910        assert!(!b.has_icon("home"));
911    }
912
913    #[test]
914    fn drop_of_clones_and_originals_is_safe() {
915        // Guards the ManuallyDrop / run_destructor convention (see the type's docs).
916        let mut a = IconProviderHandle::new();
917        a.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
918        for _ in 0..100 {
919            let c = a.clone();
920            drop(c);
921        }
922        assert!(a.has_icon("home"));
923        drop(a);
924    }
925
926    // register / unregister
927
928    #[test]
929    fn register_icon_lowercases_icon_name_but_not_pack_name() {
930        let mut h = IconProviderHandle::new();
931        h.register_icon("MyPack", "HoMe", RefAny::new(TestIconData { id: 1 }));
932
933        assert_eq!(h.list_packs(), vec![String::from("MyPack")]);
934        assert!(h.list_icons_in_pack("MyPack").contains(&String::from("home")));
935        // Pack name is case-sensitive:
936        assert!(h.list_icons_in_pack("mypack").is_empty());
937        // Icon name is not:
938        assert!(h.has_icon("HOME"));
939        assert!(h.has_icon("home"));
940        assert!(h.has_icon("hOmE"));
941    }
942
943    #[test]
944    fn registering_the_same_icon_twice_overwrites_instead_of_duplicating() {
945        let mut h = IconProviderHandle::new();
946        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
947        h.register_icon("p", "HOME", RefAny::new(TestIconData { id: 2 }));
948
949        assert_eq!(h.list_icons_in_pack("p").len(), 1);
950        let mut data = h.lookup("home").expect("icon must exist");
951        assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 2);
952    }
953
954    #[test]
955    fn unregister_icon_drops_the_pack_once_it_is_empty() {
956        let mut h = IconProviderHandle::new();
957        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
958        h.register_icon("p", "settings", RefAny::new(TestIconData { id: 2 }));
959
960        h.unregister_icon("p", "HOME"); // case-insensitive on the icon name
961        assert_eq!(h.list_packs(), vec![String::from("p")]);
962        assert!(!h.has_icon("home"));
963
964        h.unregister_icon("p", "settings");
965        assert!(h.list_packs().is_empty(), "pack must be pruned when empty");
966    }
967
968    #[test]
969    fn unregister_of_unknown_pack_or_icon_is_a_no_op() {
970        let mut h = IconProviderHandle::new();
971        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
972
973        h.unregister_icon("nonexistent-pack", "home");
974        h.unregister_icon("p", "nonexistent-icon");
975        h.unregister_pack("nonexistent-pack");
976        h.unregister_pack("");
977        h.unregister_icon("", "");
978
979        assert!(h.has_icon("home"));
980        assert_eq!(h.list_packs().len(), 1);
981    }
982
983    #[test]
984    fn unregister_pack_removes_all_of_its_icons() {
985        let mut h = IconProviderHandle::new();
986        h.register_icon("p", "a", RefAny::new(TestIconData { id: 1 }));
987        h.register_icon("p", "b", RefAny::new(TestIconData { id: 2 }));
988        h.register_icon("q", "a", RefAny::new(TestIconData { id: 3 }));
989
990        h.unregister_pack("p");
991        assert_eq!(h.list_packs(), vec![String::from("q")]);
992        // "a" still resolvable via the other pack.
993        assert!(h.has_icon("a"));
994        assert!(!h.has_icon("b"));
995    }
996
997    #[test]
998    fn adversarial_names_roundtrip_through_register_lookup_unregister() {
999        for (i, name) in adversarial_names().iter().enumerate() {
1000            let mut h = IconProviderHandle::new();
1001            h.register_icon("p", name, RefAny::new(TestIconData { id: i as u32 }));
1002
1003            assert!(h.has_icon(name), "has_icon failed for name #{i}");
1004            let mut data = h.lookup(name).unwrap_or_else(|| panic!("lookup failed for name #{i}"));
1005            assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i as u32);
1006
1007            h.unregister_icon("p", name);
1008            assert!(!h.has_icon(name), "unregister failed for name #{i}");
1009            assert!(h.list_packs().is_empty());
1010        }
1011    }
1012
1013    #[test]
1014    fn empty_pack_name_and_empty_icon_name_are_legal_keys() {
1015        let mut h = IconProviderHandle::new();
1016        h.register_icon("", "", RefAny::new(TestIconData { id: 9 }));
1017
1018        assert_eq!(h.list_packs(), vec![String::new()]);
1019        assert_eq!(h.list_icons_in_pack(""), vec![String::new()]);
1020        assert!(h.has_icon(""));
1021        let (pack, _) = h.lookup_with_pack("").expect("empty key must be found");
1022        assert_eq!(pack, "");
1023    }
1024
1025    // lookup / lookup_with_pack (parser-shaped adversarial cases)
1026
1027    #[test]
1028    fn lookup_empty_input_returns_none() {
1029        let mut h = IconProviderHandle::new();
1030        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1031        assert!(h.lookup("").is_none());
1032        assert!(h.lookup_with_pack("").is_none());
1033        assert!(!h.has_icon(""));
1034    }
1035
1036    #[test]
1037    fn lookup_whitespace_only_is_not_trimmed() {
1038        let mut h = IconProviderHandle::new();
1039        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1040        for ws in ["   ", "\t\n", "\r", "\u{a0}"] {
1041            assert!(h.lookup(ws).is_none(), "{ws:?} must not match");
1042        }
1043        // ...and a whitespace-only *registered* name matches only itself, verbatim.
1044        h.register_icon("p", "   ", RefAny::new(TestIconData { id: 2 }));
1045        assert!(h.lookup("   ").is_some());
1046        assert!(h.lookup(" ").is_none());
1047        assert!(h.lookup("").is_none());
1048    }
1049
1050    #[test]
1051    fn lookup_garbage_and_leading_trailing_junk_returns_none() {
1052        let mut h = IconProviderHandle::new();
1053        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1054
1055        for junk in [
1056            " home ",
1057            "home ",
1058            " home",
1059            "home;garbage",
1060            "home\0",
1061            "\0home",
1062            "ho\nme",
1063            "home/../../etc/passwd",
1064            "\u{1b}[31mhome\u{1b}[0m",
1065            "{\"icon\":\"home\"}",
1066        ] {
1067            assert!(h.lookup(junk).is_none(), "{junk:?} must not match 'home'");
1068            assert!(!h.has_icon(junk));
1069        }
1070        assert!(h.lookup("home").is_some(), "positive control");
1071    }
1072
1073    #[test]
1074    fn lookup_of_extremely_long_name_terminates_and_matches_exactly() {
1075        let mut h = IconProviderHandle::new();
1076        let long = "x".repeat(1_000_000);
1077        h.register_icon("p", &long, RefAny::new(TestIconData { id: 7 }));
1078
1079        assert!(h.lookup(&long).is_some());
1080        assert!(h.has_icon(&long));
1081        // One char shorter -> no match, still no panic/hang.
1082        assert!(h.lookup(&"x".repeat(999_999)).is_none());
1083        // A 1M-char miss against a small map.
1084        let mut h2 = IconProviderHandle::new();
1085        h2.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1086        assert!(h2.lookup(&long).is_none());
1087    }
1088
1089    #[test]
1090    fn lookup_of_boundary_numeric_strings_is_deterministic() {
1091        let mut h = IconProviderHandle::new();
1092        for (i, n) in ["0", "-0", "9223372036854775807", "-9223372036854775808", "nan", "inf"]
1093            .iter()
1094            .enumerate()
1095        {
1096            h.register_icon("p", n, RefAny::new(TestIconData { id: i as u32 }));
1097        }
1098        // Numeric-looking names are plain string keys: no numeric parsing, no coercion.
1099        assert!(h.lookup("0").is_some());
1100        assert!(h.lookup("-0").is_some());
1101        assert!(h.lookup("0.0").is_none());
1102        assert!(h.lookup("00").is_none());
1103        assert!(h.lookup("+0").is_none());
1104        assert!(h.lookup("9223372036854775808").is_none()); // i64::MAX + 1
1105        // ...but case folding still applies.
1106        assert!(h.lookup("NaN").is_some());
1107        assert!(h.lookup("INF").is_some());
1108    }
1109
1110    #[test]
1111    fn lookup_of_unicode_names_folds_case_without_panicking() {
1112        let mut h = IconProviderHandle::new();
1113        h.register_icon("p", "\u{1F600}", RefAny::new(TestIconData { id: 1 }));
1114        h.register_icon("p", "\u{C4}", RefAny::new(TestIconData { id: 2 })); // Ä
1115        h.register_icon("p", "I", RefAny::new(TestIconData { id: 3 }));
1116
1117        assert!(h.lookup("\u{1F600}").is_some(), "emoji key must round-trip");
1118        assert!(h.lookup("\u{E4}").is_some(), "ä must match registered Ä");
1119        assert!(h.lookup("i").is_some(), "I folds to i");
1120
1121        // `str::to_lowercase` is full-Unicode: "İ" (U+0130) folds to TWO scalars
1122        // ("i" + U+0307), so the *stored key is not the registered string*.
1123        let mut h2 = IconProviderHandle::new();
1124        h2.register_icon("p", "\u{130}", RefAny::new(TestIconData { id: 4 }));
1125        assert!(h2.lookup("\u{130}").is_some(), "self-lookup must still work");
1126        let keys = h2.list_icons_in_pack("p");
1127        assert_eq!(keys, vec![String::from("\u{130}").to_lowercase()]);
1128        assert!(!keys.contains(&String::from("\u{130}")), "key is stored folded, not verbatim");
1129        assert!(h2.lookup("i").is_none(), "the bare ASCII 'i' must not match İ");
1130    }
1131
1132    #[test]
1133    fn lookup_of_deeply_nested_input_does_not_stack_overflow() {
1134        let h = IconProviderHandle::new();
1135        // Lookup is a map probe, not a recursive-descent parse: depth is irrelevant,
1136        // but assert it explicitly so a future parsing implementation stays flat.
1137        for depth in [1_000usize, 10_000, 100_000] {
1138            let nested = "[".repeat(depth);
1139            assert!(h.lookup(&nested).is_none());
1140            assert!(!h.has_icon(&nested));
1141            assert!(h.debug_lookup(&nested).as_str().contains("NOT FOUND"));
1142        }
1143    }
1144
1145    #[test]
1146    fn lookup_valid_minimal_positive_control_roundtrips_the_payload() {
1147        let mut h = IconProviderHandle::new();
1148        h.register_icon("p", "a", RefAny::new(TestIconData { id: 123 }));
1149
1150        let mut data = h.lookup("a").expect("registered icon must be found");
1151        assert_eq!(*data.downcast_ref::<TestIconData>().unwrap(), TestIconData { id: 123 });
1152        // Wrong-type downcast must fail rather than reinterpret the bytes.
1153        assert!(data.downcast_ref::<u64>().is_none());
1154    }
1155
1156    #[test]
1157    fn lookup_with_pack_first_match_is_the_lexicographically_first_pack() {
1158        let mut h = IconProviderHandle::new();
1159        // Register in reverse-alphabetical order: insertion order must NOT decide.
1160        h.register_icon("zzz", "home", RefAny::new(TestIconData { id: 26 }));
1161        h.register_icon("mmm", "home", RefAny::new(TestIconData { id: 13 }));
1162        h.register_icon("aaa", "home", RefAny::new(TestIconData { id: 1 }));
1163
1164        let (pack, _) = h.lookup_with_pack("HOME").expect("must be found");
1165        assert_eq!(pack, "aaa", "BTreeMap order => first match is the first pack by name");
1166
1167        let mut data = h.lookup("home").unwrap();
1168        assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 1);
1169
1170        // Removing the winner promotes the next pack in order.
1171        h.unregister_pack("aaa");
1172        let (pack, _) = h.lookup_with_pack("home").unwrap();
1173        assert_eq!(pack, "mmm");
1174    }
1175
1176    // has_icon
1177
1178    #[test]
1179    fn has_icon_true_false_and_edge_inputs() {
1180        let mut h = IconProviderHandle::new();
1181        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1182
1183        assert!(h.has_icon("home"));
1184        assert!(!h.has_icon("definitely-not-registered"));
1185
1186        for name in adversarial_names() {
1187            // Deterministic bool, no panic: none of these were registered.
1188            assert!(!h.has_icon(&name));
1189        }
1190        assert!(h.has_icon("home"), "state unchanged by the queries above");
1191    }
1192
1193    // getters: list_packs / list_icons_in_pack
1194
1195    #[test]
1196    fn list_packs_is_sorted_and_case_sensitive() {
1197        let mut h = IconProviderHandle::new();
1198        for p in ["zeta", "alpha", "Alpha", "mid", ""] {
1199            h.register_icon(p, "home", RefAny::new(TestIconData { id: 0 }));
1200        }
1201        // BTreeMap => byte-order sorted; "Alpha" != "alpha" (case-sensitive).
1202        assert_eq!(
1203            h.list_packs(),
1204            vec![
1205                String::new(),
1206                String::from("Alpha"),
1207                String::from("alpha"),
1208                String::from("mid"),
1209                String::from("zeta"),
1210            ]
1211        );
1212    }
1213
1214    #[test]
1215    fn list_icons_in_pack_returns_folded_keys_and_empty_for_unknown_packs() {
1216        let mut h = IconProviderHandle::new();
1217        h.register_icon("p", "Zoom", RefAny::new(TestIconData { id: 1 }));
1218        h.register_icon("p", "HOME", RefAny::new(TestIconData { id: 2 }));
1219
1220        assert_eq!(h.list_icons_in_pack("p"), vec![String::from("home"), String::from("zoom")]);
1221        assert!(h.list_icons_in_pack("P").is_empty());
1222        assert!(h.list_icons_in_pack("").is_empty());
1223        assert!(h.list_icons_in_pack(&"x".repeat(100_000)).is_empty());
1224    }
1225
1226    // debug_lookup
1227
1228    #[test]
1229    fn debug_lookup_reports_not_found_for_missing_icons() {
1230        let mut h = IconProviderHandle::new();
1231        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1232
1233        let out = h.debug_lookup("settings");
1234        let s = out.as_str();
1235        assert!(s.contains("NOT FOUND in any pack"));
1236        assert!(s.contains("Total packs: 1"));
1237        assert!(s.contains("Pack 'p': 1 icons"));
1238    }
1239
1240    #[test]
1241    fn debug_lookup_classifies_image_font_and_unknown_refany_types() {
1242        let mut h = IconProviderHandle::new();
1243        h.register_icon("p", "img", RefAny::new(ImageIconData { _w: 16 }));
1244        h.register_icon("p", "fnt", RefAny::new(FontIconData { _codepoint: 0xF015 }));
1245        h.register_icon("p", "other", RefAny::new(TestIconData { id: 1 }));
1246
1247        let img = h.debug_lookup("img");
1248        assert!(img.as_str().contains("FOUND in pack 'p'"));
1249        assert!(img.as_str().contains("RefAny type: ImageIconData"));
1250
1251        let fnt = h.debug_lookup("FNT"); // case-folded lookup path
1252        assert!(fnt.as_str().contains("RefAny type: FontIconData"));
1253
1254        let other = h.debug_lookup("other");
1255        assert!(other.as_str().contains("RefAny type: UNKNOWN"));
1256    }
1257
1258    #[test]
1259    fn debug_lookup_survives_adversarial_names() {
1260        let mut h = IconProviderHandle::new();
1261        for (i, name) in adversarial_names().iter().enumerate() {
1262            h.register_icon("p", name, RefAny::new(TestIconData { id: i as u32 }));
1263        }
1264        for name in adversarial_names() {
1265            let out = h.debug_lookup(&name);
1266            assert!(out.as_str().contains("FOUND in pack 'p'"), "must find {name:?}");
1267        }
1268        assert!(h.debug_lookup("never-registered").as_str().contains("NOT FOUND"));
1269    }
1270
1271    // SharedIconProvider
1272
1273    #[test]
1274    fn from_handle_preserves_every_registered_icon() {
1275        let mut h = IconProviderHandle::new();
1276        for i in 0..64u32 {
1277            h.register_icon("p", &format!("icon{i}"), RefAny::new(TestIconData { id: i }));
1278        }
1279        let shared = SharedIconProvider::from_handle(h);
1280
1281        for i in 0..64u32 {
1282            let name = format!("ICON{i}");
1283            assert!(shared.has_icon(&name));
1284            let mut data = shared.lookup(&name).expect("must survive into_shared()");
1285            assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i);
1286        }
1287        assert!(!shared.has_icon("icon64"));
1288        assert!(shared.lookup("").is_none());
1289    }
1290
1291    #[test]
1292    fn shared_provider_lookup_and_has_icon_agree_on_adversarial_input() {
1293        let mut h = IconProviderHandle::new();
1294        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1295        let shared = SharedIconProvider::from_handle(h);
1296
1297        for name in adversarial_names() {
1298            assert_eq!(
1299                shared.has_icon(&name),
1300                shared.lookup(&name).is_some(),
1301                "has_icon/lookup disagree for {name:?}"
1302            );
1303        }
1304        assert!(shared.has_icon("HoMe") && shared.lookup("HoMe").is_some());
1305    }
1306
1307    #[test]
1308    fn shared_provider_clone_shares_the_same_icon_table() {
1309        let mut h = IconProviderHandle::new();
1310        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1311        let a = SharedIconProvider::from_handle(h);
1312        let b = a.clone();
1313
1314        assert!(b.has_icon("home"));
1315        drop(a);
1316        assert!(b.has_icon("home"), "clone must keep the Arc alive");
1317        let mut data = b.lookup("home").unwrap();
1318        assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 1);
1319    }
1320
1321    #[test]
1322    fn shared_resolve_receives_icon_data_and_original_dom() {
1323        let mut h = IconProviderHandle::with_resolver(recording_resolver);
1324        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1325        let shared = SharedIconProvider::from_handle(h);
1326
1327        let original = styled_dom_with_icons(&["home"]);
1328        let icon_idx = icon_indices(&original)[0];
1329        let single = extract_single_node_styled_dom(&original, icon_idx);
1330
1331        let out = shared.resolve(&single, "HOME", &SystemStyle::default());
1332
1333        assert!(REC_CALLS.load(AtomicOrdering::SeqCst) >= 1);
1334        assert!(REC_SAW_DATA.load(AtomicOrdering::SeqCst), "case-folded lookup must pass Some(data)");
1335        assert!(REC_SAW_ICON_NODE.load(AtomicOrdering::SeqCst), "node 0 of the original dom is the Icon");
1336        assert_eq!(REC_NAME_LEN.load(AtomicOrdering::SeqCst), "home".len());
1337        assert!(matches!(node_type_at(&out, 0), NodeType::Div));
1338    }
1339
1340    #[test]
1341    fn shared_resolve_runs_the_resolver_even_when_the_icon_is_missing() {
1342        let h = IconProviderHandle::with_resolver(div_resolver);
1343        let shared = SharedIconProvider::from_handle(h);
1344        // Empty name, huge name, unicode name: resolver still returns its DOM.
1345        let huge = "x".repeat(100_000);
1346        for name in ["", "\u{1F600}", huge.as_str()] {
1347            let out = shared.resolve(&StyledDom::default(), name, &SystemStyle::default());
1348            assert!(matches!(node_type_at(&out, 0), NodeType::Div));
1349        }
1350    }
1351
1352    #[cfg(feature = "std")]
1353    #[test]
1354    fn shared_provider_survives_concurrent_lookups() {
1355        let mut h = IconProviderHandle::new();
1356        for i in 0..16u32 {
1357            h.register_icon("p", &format!("icon{i}"), RefAny::new(TestIconData { id: i }));
1358        }
1359        let shared = SharedIconProvider::from_handle(h);
1360
1361        let mut threads = Vec::new();
1362        for _ in 0..4 {
1363            let s = shared.clone();
1364            threads.push(std::thread::spawn(move || {
1365                let mut hits = 0usize;
1366                for i in 0..200u32 {
1367                    let name = format!("icon{}", i % 16);
1368                    if s.has_icon(&name) {
1369                        hits += 1;
1370                    }
1371                    let mut data = s.lookup(&name).expect("registered icon");
1372                    assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i % 16);
1373                }
1374                hits
1375            }));
1376        }
1377        for t in threads {
1378            assert_eq!(t.join().unwrap(), 200);
1379        }
1380        assert!(shared.has_icon("icon0"), "table intact after contention");
1381    }
1382
1383    // collect_icon_nodes
1384
1385    #[test]
1386    fn collect_icon_nodes_is_empty_when_there_are_no_icons() {
1387        assert!(collect_icon_nodes(&StyledDom::default()).is_empty());
1388        assert!(collect_icon_nodes(&zero_node_styled_dom()).is_empty());
1389        assert!(collect_icon_nodes(&StyledDom::create_from_dom(Dom::create_div())).is_empty());
1390    }
1391
1392    #[test]
1393    fn collect_icon_nodes_finds_every_icon_in_ascending_index_order_with_verbatim_names() {
1394        let names = ["HOME", "\u{1F600}", ""];
1395        let sd = styled_dom_with_icons(&names);
1396        let collected = collect_icon_nodes(&sd);
1397
1398        assert_eq!(collected.len(), names.len());
1399        for (i, c) in collected.iter().enumerate() {
1400            // Node names are NOT folded at DOM-construction time (only at lookup).
1401            assert_eq!(c.icon_name.as_str(), names[i]);
1402            if i > 0 {
1403                assert!(c.node_idx > collected[i - 1].node_idx, "indices must ascend");
1404            }
1405        }
1406    }
1407
1408    #[test]
1409    fn collect_icon_nodes_handles_a_very_long_icon_name() {
1410        let long = "x".repeat(100_000);
1411        let sd = styled_dom_with_icons(&[&long]);
1412        let collected = collect_icon_nodes(&sd);
1413        assert_eq!(collected.len(), 1);
1414        assert_eq!(collected[0].icon_name.as_str().len(), 100_000);
1415    }
1416
1417    // extract_single_node_styled_dom (numeric / index boundaries)
1418
1419    #[test]
1420    fn extract_single_node_at_index_zero() {
1421        let sd = styled_dom_with_icons(&["home"]);
1422        let out = extract_single_node_styled_dom(&sd, 0);
1423        assert_eq!(out.node_data.as_ref().len(), 1);
1424        assert_eq!(out.styled_nodes.as_ref().len(), 1);
1425        assert_eq!(node_type_at(&out, 0), node_type_at(&sd, 0));
1426    }
1427
1428    #[test]
1429    fn extract_single_node_of_the_icon_keeps_the_icon_node_type() {
1430        let sd = styled_dom_with_icons(&["home"]);
1431        let idx = icon_indices(&sd)[0];
1432        let out = extract_single_node_styled_dom(&sd, idx);
1433        assert_eq!(out.node_data.as_ref().len(), 1);
1434        assert!(matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1435    }
1436
1437    #[test]
1438    fn extract_single_node_out_of_bounds_falls_back_to_default_without_panicking() {
1439        let sd = styled_dom_with_icons(&["home"]);
1440        let len = sd.node_data.as_ref().len();
1441
1442        for idx in [len, len + 1, usize::MAX / 2, usize::MAX - 1, usize::MAX] {
1443            let out = extract_single_node_styled_dom(&sd, idx);
1444            // Falls back to StyledDom::default() -> exactly one (Body) node.
1445            assert_eq!(out.node_data.as_ref().len(), 1, "idx {idx} must not panic");
1446            assert!(!matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1447        }
1448        // Zero-node input: even index 0 is out of bounds.
1449        let empty = zero_node_styled_dom();
1450        assert_eq!(extract_single_node_styled_dom(&empty, 0).node_data.as_ref().len(), 1);
1451    }
1452
1453    #[test]
1454    fn extract_single_node_tolerates_styled_nodes_shorter_than_node_data() {
1455        let sd_full = styled_dom_with_icons(&["home"]);
1456        let idx = icon_indices(&sd_full)[0];
1457
1458        let mut sd = sd_full;
1459        sd.styled_nodes = StyledNodeVec::from_vec(Vec::new()); // desynced arrays
1460
1461        let out = extract_single_node_styled_dom(&sd, idx);
1462        assert_eq!(out.node_data.as_ref().len(), 1);
1463        assert_eq!(out.styled_nodes.as_ref().len(), 1, "must synthesize a default StyledNode");
1464        assert!(matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1465    }
1466
1467    // is_single_node_replacement
1468
1469    #[test]
1470    fn is_single_node_replacement_true_false_and_edges() {
1471        assert!(is_single_node_replacement(&StyledDom::default()));
1472        assert!(is_single_node_replacement(&StyledDom::create_from_dom(Dom::create_div())));
1473
1474        // Zero nodes is NOT "single node" (callers treat it as the empty case).
1475        assert!(!is_single_node_replacement(&zero_node_styled_dom()));
1476
1477        let multi = StyledDom::create_from_dom(
1478            Dom::create_div().with_child(Dom::create_div()).with_child(Dom::create_div()),
1479        );
1480        assert!(multi.node_data.as_ref().len() > 1);
1481        assert!(!is_single_node_replacement(&multi));
1482    }
1483
1484    // apply_single_node_replacement (index boundaries)
1485
1486    #[test]
1487    fn apply_single_node_replacement_with_zero_node_dom_turns_the_icon_into_a_div() {
1488        let mut sd = styled_dom_with_icons(&["home"]);
1489        let idx = icon_indices(&sd)[0];
1490        let empty = zero_node_styled_dom();
1491
1492        apply_single_node_replacement(&mut sd, idx, &empty);
1493        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1494        assert!(collect_icon_nodes(&sd).is_empty());
1495    }
1496
1497    #[test]
1498    fn apply_single_node_replacement_copies_the_replacement_root_node_type() {
1499        let mut sd = styled_dom_with_icons(&["home"]);
1500        let idx = icon_indices(&sd)[0];
1501        let before_len = sd.node_data.as_ref().len();
1502        let repl = StyledDom::create_from_dom(Dom::create_div());
1503
1504        apply_single_node_replacement(&mut sd, idx, &repl);
1505        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1506        assert_eq!(sd.node_data.as_ref().len(), before_len, "node count must not change");
1507    }
1508
1509    #[test]
1510    fn apply_single_node_replacement_out_of_bounds_index_is_a_no_op() {
1511        let repl = StyledDom::create_from_dom(Dom::create_div());
1512        let empty = zero_node_styled_dom();
1513
1514        let base = styled_dom_with_icons(&["home"]);
1515        let icon_idx = icon_indices(&base)[0];
1516        let len = base.node_data.as_ref().len();
1517
1518        for idx in [len, len + 1, usize::MAX / 2, usize::MAX] {
1519            let mut sd = styled_dom_with_icons(&["home"]);
1520            apply_single_node_replacement(&mut sd, idx, &repl);
1521            apply_single_node_replacement(&mut sd, idx, &empty);
1522            assert_eq!(sd.node_data.as_ref().len(), len, "idx {idx} must not resize");
1523            assert!(
1524                matches!(node_type_at(&sd, icon_idx), NodeType::Icon(_)),
1525                "idx {idx} must leave the icon untouched"
1526            );
1527        }
1528    }
1529
1530    // apply_multi_node_replacement (index boundaries)
1531
1532    #[test]
1533    fn apply_multi_node_replacement_with_zero_node_dom_turns_the_icon_into_a_div() {
1534        let mut sd = styled_dom_with_icons(&["home"]);
1535        let idx = icon_indices(&sd)[0];
1536
1537        apply_multi_node_replacement(&mut sd, idx, &zero_node_styled_dom());
1538        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1539    }
1540
1541    #[test]
1542    fn apply_multi_node_replacement_applies_only_the_root_and_does_not_splice() {
1543        let mut sd = styled_dom_with_icons(&["home"]);
1544        let idx = icon_indices(&sd)[0];
1545        let before_len = sd.node_data.as_ref().len();
1546
1547        let repl = StyledDom::create_from_dom(
1548            Dom::create_div().with_child(Dom::create_div()).with_child(Dom::create_div()),
1549        );
1550        assert!(repl.node_data.as_ref().len() > 1);
1551
1552        apply_multi_node_replacement(&mut sd, idx, &repl);
1553
1554        // Documented TODO: subtree splicing is not implemented, only the root is used.
1555        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1556        assert_eq!(sd.node_data.as_ref().len(), before_len, "children are dropped, not spliced");
1557    }
1558
1559    #[test]
1560    fn apply_multi_node_replacement_out_of_bounds_index_is_a_no_op() {
1561        let repl = StyledDom::create_from_dom(Dom::create_div().with_child(Dom::create_div()));
1562        let base = styled_dom_with_icons(&["home"]);
1563        let icon_idx = icon_indices(&base)[0];
1564        let len = base.node_data.as_ref().len();
1565
1566        for idx in [len, usize::MAX] {
1567            let mut sd = styled_dom_with_icons(&["home"]);
1568            apply_multi_node_replacement(&mut sd, idx, &repl);
1569            apply_multi_node_replacement(&mut sd, idx, &zero_node_styled_dom());
1570            assert_eq!(sd.node_data.as_ref().len(), len);
1571            assert!(matches!(node_type_at(&sd, icon_idx), NodeType::Icon(_)));
1572        }
1573    }
1574
1575    // resolve_collected_icons
1576
1577    #[test]
1578    fn resolve_collected_icons_preserves_indices_and_resolves_each_icon() {
1579        let mut h = IconProviderHandle::with_resolver(div_resolver);
1580        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1581        let shared = SharedIconProvider::from_handle(h);
1582
1583        let sd = styled_dom_with_icons(&["home", "missing", "\u{1F600}"]);
1584        let icons = collect_icon_nodes(&sd);
1585        let replacements =
1586            resolve_collected_icons(&icons, &sd, &shared, &SystemStyle::default());
1587
1588        assert_eq!(replacements.len(), icons.len());
1589        for (r, i) in replacements.iter().zip(icons.iter()) {
1590            assert_eq!(r.node_idx, i.node_idx);
1591            // The custom resolver ignores the data, so even unregistered icons resolve.
1592            assert!(matches!(node_type_at(&r.replacement, 0), NodeType::Div));
1593        }
1594    }
1595
1596    #[test]
1597    fn resolve_collected_icons_with_no_icons_returns_no_replacements() {
1598        let shared = SharedIconProvider::from_handle(IconProviderHandle::new());
1599        let sd = StyledDom::default();
1600        let out = resolve_collected_icons(&[], &sd, &shared, &SystemStyle::default());
1601        assert!(out.is_empty());
1602    }
1603
1604    // resolve_icons_in_styled_dom (end to end)
1605
1606    #[test]
1607    fn resolve_icons_in_styled_dom_is_a_no_op_without_icons() {
1608        let shared = SharedIconProvider::from_handle(IconProviderHandle::with_resolver(div_resolver));
1609        let mut sd = StyledDom::create_from_dom(Dom::create_body().with_child(Dom::create_text("hi")));
1610        let before_len = sd.node_data.as_ref().len();
1611        let before_root = node_type_at(&sd, 0);
1612
1613        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1614
1615        assert_eq!(sd.node_data.as_ref().len(), before_len);
1616        assert_eq!(node_type_at(&sd, 0), before_root);
1617    }
1618
1619    #[test]
1620    fn resolve_icons_in_styled_dom_replaces_every_icon_case_insensitively() {
1621        let mut h = IconProviderHandle::with_resolver(div_resolver);
1622        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1623        let shared = SharedIconProvider::from_handle(h);
1624
1625        // Mixed case in the DOM, lowercase in the pack, plus one unregistered icon.
1626        let mut sd = styled_dom_with_icons(&["HOME", "unregistered", "HoMe"]);
1627        let idxs = icon_indices(&sd);
1628        let before_len = sd.node_data.as_ref().len();
1629
1630        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1631
1632        assert_eq!(sd.node_data.as_ref().len(), before_len);
1633        assert!(collect_icon_nodes(&sd).is_empty(), "no Icon node may survive resolution");
1634        for idx in idxs {
1635            assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1636        }
1637    }
1638
1639    #[test]
1640    fn resolve_icons_in_styled_dom_with_the_default_resolver_removes_the_icon_nodes() {
1641        // The default resolver returns `StyledDom::default()` (one Body node), so
1642        // icons are replaced by that root's node type rather than being cleared.
1643        let shared = SharedIconProvider::from_handle(IconProviderHandle::new());
1644        let mut sd = styled_dom_with_icons(&["home"]);
1645        let idx = icon_indices(&sd)[0];
1646
1647        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1648
1649        assert!(!matches!(node_type_at(&sd, idx), NodeType::Icon(_)));
1650        assert!(collect_icon_nodes(&sd).is_empty());
1651    }
1652
1653    #[test]
1654    fn resolve_icons_in_styled_dom_handles_a_zero_node_replacement() {
1655        let shared =
1656            SharedIconProvider::from_handle(IconProviderHandle::with_resolver(zero_node_resolver));
1657        let mut sd = styled_dom_with_icons(&["home", "other"]);
1658        let idxs = icon_indices(&sd);
1659        let before_len = sd.node_data.as_ref().len();
1660
1661        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1662
1663        assert_eq!(sd.node_data.as_ref().len(), before_len);
1664        for idx in idxs {
1665            assert!(matches!(node_type_at(&sd, idx), NodeType::Div), "empty => Div placeholder");
1666        }
1667    }
1668
1669    #[test]
1670    fn resolve_icons_in_styled_dom_scales_to_many_icons() {
1671        let shared = SharedIconProvider::from_handle(IconProviderHandle::with_resolver(div_resolver));
1672        let names: Vec<String> = (0..500).map(|i| format!("icon{i}")).collect();
1673        let refs: Vec<&str> = names.iter().map(String::as_str).collect();
1674        let mut sd = styled_dom_with_icons(&refs);
1675        let before_len = sd.node_data.as_ref().len();
1676
1677        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1678
1679        assert_eq!(sd.node_data.as_ref().len(), before_len);
1680        assert!(collect_icon_nodes(&sd).is_empty());
1681    }
1682}