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        StyledDom {
748            node_data: NodeDataVec::from_vec(Vec::new()),
749            styled_nodes: StyledNodeVec::from_vec(Vec::new()),
750            ..StyledDom::default()
751        }
752    }
753
754    fn node_type_at(sd: &StyledDom, idx: usize) -> NodeType {
755        sd.node_data.as_ref()[idx].get_node_type().clone()
756    }
757
758    fn icon_indices(sd: &StyledDom) -> Vec<usize> {
759        collect_icon_nodes(sd).iter().map(|i| i.node_idx).collect()
760    }
761
762    // Resolvers
763
764    extern "C" fn div_resolver(
765        _icon_data: OptionRefAny,
766        _original_icon_dom: &StyledDom,
767        _system_style: &SystemStyle,
768    ) -> StyledDom {
769        StyledDom::create_from_dom(Dom::create_div())
770    }
771
772    extern "C" fn zero_node_resolver(
773        _icon_data: OptionRefAny,
774        _original_icon_dom: &StyledDom,
775        _system_style: &SystemStyle,
776    ) -> StyledDom {
777        zero_node_styled_dom()
778    }
779
780    // Statics for `shared_resolve_receives_icon_data_and_original_dom` ONLY.
781    // (`extern "C" fn` cannot capture, and tests run in parallel — never share
782    // one recording resolver between two tests.)
783    static REC_CALLS: AtomicUsize = AtomicUsize::new(0);
784    static REC_SAW_DATA: AtomicBool = AtomicBool::new(false);
785    static REC_SAW_ICON_NODE: AtomicBool = AtomicBool::new(false);
786    static REC_NAME_LEN: AtomicUsize = AtomicUsize::new(0);
787
788    extern "C" fn recording_resolver(
789        icon_data: OptionRefAny,
790        original_icon_dom: &StyledDom,
791        _system_style: &SystemStyle,
792    ) -> StyledDom {
793        REC_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
794        if matches!(icon_data, OptionRefAny::Some(_)) {
795            REC_SAW_DATA.store(true, AtomicOrdering::SeqCst);
796        }
797        if let Some(node) = original_icon_dom.node_data.as_ref().first() {
798            if let NodeType::Icon(name) = node.get_node_type() {
799                REC_SAW_ICON_NODE.store(true, AtomicOrdering::SeqCst);
800                REC_NAME_LEN.store(name.as_ref().as_str().len(), AtomicOrdering::SeqCst);
801            }
802        }
803        StyledDom::create_from_dom(Dom::create_div())
804    }
805
806    // Mutex (the no_std spinlock under `no_std`, `std::sync::Mutex` otherwise)
807
808    #[test]
809    fn mutex_new_then_lock_roundtrips_the_value() {
810        let m = Mutex::new(42u32);
811        assert_eq!(*m.lock().unwrap(), 42);
812        *m.lock().unwrap() = u32::MAX;
813        assert_eq!(*m.lock().unwrap(), u32::MAX);
814    }
815
816    #[test]
817    fn mutex_lock_on_empty_and_large_payloads() {
818        let empty: Mutex<Vec<u8>> = Mutex::new(Vec::new());
819        assert!(empty.lock().unwrap().is_empty());
820
821        let big = Mutex::new(vec![0u8; 1_000_000]);
822        assert_eq!(big.lock().unwrap().len(), 1_000_000);
823
824        // Sequential re-lock must not deadlock (guard dropped at end of statement).
825        for _ in 0..1_000 {
826            assert!(big.lock().is_ok());
827        }
828    }
829
830    // default_icon_resolver
831
832    #[test]
833    fn default_resolver_returns_one_body_node_for_none_and_some() {
834        let orig = StyledDom::default();
835        let style = SystemStyle::default();
836
837        let none = default_icon_resolver(OptionRefAny::None, &orig, &style);
838        // NOTE: the doc calls this an "empty StyledDom", but `StyledDom::default()`
839        // carries exactly one node (a Body), so the result is single-node, NOT empty.
840        assert_eq!(none.node_data.as_ref().len(), 1);
841        assert!(is_single_node_replacement(&none));
842
843        let some = default_icon_resolver(
844            OptionRefAny::Some(RefAny::new(TestIconData { id: 1 })),
845            &orig,
846            &style,
847        );
848        assert_eq!(some.node_data.as_ref().len(), 1);
849    }
850
851    #[test]
852    fn default_resolver_no_panic_on_zero_node_original_dom() {
853        let orig = zero_node_styled_dom();
854        let style = SystemStyle::default();
855        let out = default_icon_resolver(OptionRefAny::None, &orig, &style);
856        assert_eq!(out.node_data.as_ref().len(), 1);
857    }
858
859    // IconProviderHandle: construction / invariants
860
861    #[test]
862    fn new_handle_is_empty_and_all_queries_are_negative() {
863        let h = IconProviderHandle::new();
864        assert!(h.list_packs().is_empty());
865        assert!(h.list_icons_in_pack("anything").is_empty());
866        assert!(!h.has_icon("home"));
867        assert!(h.lookup("home").is_none());
868        assert!(h.lookup_with_pack("home").is_none());
869        assert!(h.debug_lookup("home").as_str().contains("Total packs: 0"));
870    }
871
872    #[test]
873    fn default_handle_matches_new_handle() {
874        let a = IconProviderHandle::default();
875        let b = IconProviderHandle::new();
876        assert_eq!(a.list_packs(), b.list_packs());
877        assert_eq!(a.has_icon(""), b.has_icon(""));
878    }
879
880    #[test]
881    fn with_resolver_installs_the_callback() {
882        let mut h = IconProviderHandle::with_resolver(div_resolver);
883        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
884        let shared = SharedIconProvider::from_handle(h);
885        let out = shared.resolve(&StyledDom::default(), "home", &SystemStyle::default());
886        assert!(matches!(node_type_at(&out, 0), NodeType::Div));
887    }
888
889    #[test]
890    fn set_resolver_overrides_the_default_resolver() {
891        let mut h = IconProviderHandle::new();
892        h.set_resolver(div_resolver);
893        let shared = SharedIconProvider::from_handle(h);
894        // Unregistered icon: resolver still runs, just with `None` data.
895        let out = shared.resolve(&StyledDom::default(), "missing", &SystemStyle::default());
896        assert!(matches!(node_type_at(&out, 0), NodeType::Div));
897    }
898
899    #[test]
900    fn clone_of_handle_is_deep() {
901        let mut a = IconProviderHandle::new();
902        a.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
903
904        let mut b = a.clone();
905        b.register_icon("p", "settings", RefAny::new(TestIconData { id: 2 }));
906        b.unregister_icon("p", "home");
907
908        assert!(a.has_icon("home"));
909        assert!(!a.has_icon("settings"));
910        assert!(b.has_icon("settings"));
911        assert!(!b.has_icon("home"));
912    }
913
914    #[test]
915    fn drop_of_clones_and_originals_is_safe() {
916        // Guards the ManuallyDrop / run_destructor convention (see the type's docs).
917        let mut a = IconProviderHandle::new();
918        a.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
919        for _ in 0..100 {
920            let c = a.clone();
921            drop(c);
922        }
923        assert!(a.has_icon("home"));
924        drop(a);
925    }
926
927    // register / unregister
928
929    #[test]
930    fn register_icon_lowercases_icon_name_but_not_pack_name() {
931        let mut h = IconProviderHandle::new();
932        h.register_icon("MyPack", "HoMe", RefAny::new(TestIconData { id: 1 }));
933
934        assert_eq!(h.list_packs(), vec![String::from("MyPack")]);
935        assert!(h.list_icons_in_pack("MyPack").contains(&String::from("home")));
936        // Pack name is case-sensitive:
937        assert!(h.list_icons_in_pack("mypack").is_empty());
938        // Icon name is not:
939        assert!(h.has_icon("HOME"));
940        assert!(h.has_icon("home"));
941        assert!(h.has_icon("hOmE"));
942    }
943
944    #[test]
945    fn registering_the_same_icon_twice_overwrites_instead_of_duplicating() {
946        let mut h = IconProviderHandle::new();
947        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
948        h.register_icon("p", "HOME", RefAny::new(TestIconData { id: 2 }));
949
950        assert_eq!(h.list_icons_in_pack("p").len(), 1);
951        let mut data = h.lookup("home").expect("icon must exist");
952        assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 2);
953    }
954
955    #[test]
956    fn unregister_icon_drops_the_pack_once_it_is_empty() {
957        let mut h = IconProviderHandle::new();
958        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
959        h.register_icon("p", "settings", RefAny::new(TestIconData { id: 2 }));
960
961        h.unregister_icon("p", "HOME"); // case-insensitive on the icon name
962        assert_eq!(h.list_packs(), vec![String::from("p")]);
963        assert!(!h.has_icon("home"));
964
965        h.unregister_icon("p", "settings");
966        assert!(h.list_packs().is_empty(), "pack must be pruned when empty");
967    }
968
969    #[test]
970    fn unregister_of_unknown_pack_or_icon_is_a_no_op() {
971        let mut h = IconProviderHandle::new();
972        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
973
974        h.unregister_icon("nonexistent-pack", "home");
975        h.unregister_icon("p", "nonexistent-icon");
976        h.unregister_pack("nonexistent-pack");
977        h.unregister_pack("");
978        h.unregister_icon("", "");
979
980        assert!(h.has_icon("home"));
981        assert_eq!(h.list_packs().len(), 1);
982    }
983
984    #[test]
985    fn unregister_pack_removes_all_of_its_icons() {
986        let mut h = IconProviderHandle::new();
987        h.register_icon("p", "a", RefAny::new(TestIconData { id: 1 }));
988        h.register_icon("p", "b", RefAny::new(TestIconData { id: 2 }));
989        h.register_icon("q", "a", RefAny::new(TestIconData { id: 3 }));
990
991        h.unregister_pack("p");
992        assert_eq!(h.list_packs(), vec![String::from("q")]);
993        // "a" still resolvable via the other pack.
994        assert!(h.has_icon("a"));
995        assert!(!h.has_icon("b"));
996    }
997
998    #[test]
999    fn adversarial_names_roundtrip_through_register_lookup_unregister() {
1000        for (i, name) in adversarial_names().iter().enumerate() {
1001            let mut h = IconProviderHandle::new();
1002            h.register_icon("p", name, RefAny::new(TestIconData { id: i as u32 }));
1003
1004            assert!(h.has_icon(name), "has_icon failed for name #{i}");
1005            let mut data = h.lookup(name).unwrap_or_else(|| panic!("lookup failed for name #{i}"));
1006            assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i as u32);
1007
1008            h.unregister_icon("p", name);
1009            assert!(!h.has_icon(name), "unregister failed for name #{i}");
1010            assert!(h.list_packs().is_empty());
1011        }
1012    }
1013
1014    #[test]
1015    fn empty_pack_name_and_empty_icon_name_are_legal_keys() {
1016        let mut h = IconProviderHandle::new();
1017        h.register_icon("", "", RefAny::new(TestIconData { id: 9 }));
1018
1019        assert_eq!(h.list_packs(), vec![String::new()]);
1020        assert_eq!(h.list_icons_in_pack(""), vec![String::new()]);
1021        assert!(h.has_icon(""));
1022        let (pack, _) = h.lookup_with_pack("").expect("empty key must be found");
1023        assert_eq!(pack, "");
1024    }
1025
1026    // lookup / lookup_with_pack (parser-shaped adversarial cases)
1027
1028    #[test]
1029    fn lookup_empty_input_returns_none() {
1030        let mut h = IconProviderHandle::new();
1031        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1032        assert!(h.lookup("").is_none());
1033        assert!(h.lookup_with_pack("").is_none());
1034        assert!(!h.has_icon(""));
1035    }
1036
1037    #[test]
1038    fn lookup_whitespace_only_is_not_trimmed() {
1039        let mut h = IconProviderHandle::new();
1040        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1041        for ws in ["   ", "\t\n", "\r", "\u{a0}"] {
1042            assert!(h.lookup(ws).is_none(), "{ws:?} must not match");
1043        }
1044        // ...and a whitespace-only *registered* name matches only itself, verbatim.
1045        h.register_icon("p", "   ", RefAny::new(TestIconData { id: 2 }));
1046        assert!(h.lookup("   ").is_some());
1047        assert!(h.lookup(" ").is_none());
1048        assert!(h.lookup("").is_none());
1049    }
1050
1051    #[test]
1052    fn lookup_garbage_and_leading_trailing_junk_returns_none() {
1053        let mut h = IconProviderHandle::new();
1054        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1055
1056        for junk in [
1057            " home ",
1058            "home ",
1059            " home",
1060            "home;garbage",
1061            "home\0",
1062            "\0home",
1063            "ho\nme",
1064            "home/../../etc/passwd",
1065            "\u{1b}[31mhome\u{1b}[0m",
1066            "{\"icon\":\"home\"}",
1067        ] {
1068            assert!(h.lookup(junk).is_none(), "{junk:?} must not match 'home'");
1069            assert!(!h.has_icon(junk));
1070        }
1071        assert!(h.lookup("home").is_some(), "positive control");
1072    }
1073
1074    #[test]
1075    fn lookup_of_extremely_long_name_terminates_and_matches_exactly() {
1076        let mut h = IconProviderHandle::new();
1077        let long = "x".repeat(1_000_000);
1078        h.register_icon("p", &long, RefAny::new(TestIconData { id: 7 }));
1079
1080        assert!(h.lookup(&long).is_some());
1081        assert!(h.has_icon(&long));
1082        // One char shorter -> no match, still no panic/hang.
1083        assert!(h.lookup(&"x".repeat(999_999)).is_none());
1084        // A 1M-char miss against a small map.
1085        let mut h2 = IconProviderHandle::new();
1086        h2.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1087        assert!(h2.lookup(&long).is_none());
1088    }
1089
1090    #[test]
1091    fn lookup_of_boundary_numeric_strings_is_deterministic() {
1092        let mut h = IconProviderHandle::new();
1093        for (i, n) in ["0", "-0", "9223372036854775807", "-9223372036854775808", "nan", "inf"]
1094            .iter()
1095            .enumerate()
1096        {
1097            h.register_icon("p", n, RefAny::new(TestIconData { id: i as u32 }));
1098        }
1099        // Numeric-looking names are plain string keys: no numeric parsing, no coercion.
1100        assert!(h.lookup("0").is_some());
1101        assert!(h.lookup("-0").is_some());
1102        assert!(h.lookup("0.0").is_none());
1103        assert!(h.lookup("00").is_none());
1104        assert!(h.lookup("+0").is_none());
1105        assert!(h.lookup("9223372036854775808").is_none()); // i64::MAX + 1
1106        // ...but case folding still applies.
1107        assert!(h.lookup("NaN").is_some());
1108        assert!(h.lookup("INF").is_some());
1109    }
1110
1111    #[test]
1112    fn lookup_of_unicode_names_folds_case_without_panicking() {
1113        let mut h = IconProviderHandle::new();
1114        h.register_icon("p", "\u{1F600}", RefAny::new(TestIconData { id: 1 }));
1115        h.register_icon("p", "\u{C4}", RefAny::new(TestIconData { id: 2 })); // Ä
1116        h.register_icon("p", "I", RefAny::new(TestIconData { id: 3 }));
1117
1118        assert!(h.lookup("\u{1F600}").is_some(), "emoji key must round-trip");
1119        assert!(h.lookup("\u{E4}").is_some(), "ä must match registered Ä");
1120        assert!(h.lookup("i").is_some(), "I folds to i");
1121
1122        // `str::to_lowercase` is full-Unicode: "İ" (U+0130) folds to TWO scalars
1123        // ("i" + U+0307), so the *stored key is not the registered string*.
1124        let mut h2 = IconProviderHandle::new();
1125        h2.register_icon("p", "\u{130}", RefAny::new(TestIconData { id: 4 }));
1126        assert!(h2.lookup("\u{130}").is_some(), "self-lookup must still work");
1127        let keys = h2.list_icons_in_pack("p");
1128        assert_eq!(keys, vec![String::from("\u{130}").to_lowercase()]);
1129        assert!(!keys.contains(&String::from("\u{130}")), "key is stored folded, not verbatim");
1130        assert!(h2.lookup("i").is_none(), "the bare ASCII 'i' must not match İ");
1131    }
1132
1133    #[test]
1134    fn lookup_of_deeply_nested_input_does_not_stack_overflow() {
1135        let h = IconProviderHandle::new();
1136        // Lookup is a map probe, not a recursive-descent parse: depth is irrelevant,
1137        // but assert it explicitly so a future parsing implementation stays flat.
1138        for depth in [1_000usize, 10_000, 100_000] {
1139            let nested = "[".repeat(depth);
1140            assert!(h.lookup(&nested).is_none());
1141            assert!(!h.has_icon(&nested));
1142            assert!(h.debug_lookup(&nested).as_str().contains("NOT FOUND"));
1143        }
1144    }
1145
1146    #[test]
1147    fn lookup_valid_minimal_positive_control_roundtrips_the_payload() {
1148        let mut h = IconProviderHandle::new();
1149        h.register_icon("p", "a", RefAny::new(TestIconData { id: 123 }));
1150
1151        let mut data = h.lookup("a").expect("registered icon must be found");
1152        assert_eq!(*data.downcast_ref::<TestIconData>().unwrap(), TestIconData { id: 123 });
1153        // Wrong-type downcast must fail rather than reinterpret the bytes.
1154        assert!(data.downcast_ref::<u64>().is_none());
1155    }
1156
1157    #[test]
1158    fn lookup_with_pack_first_match_is_the_lexicographically_first_pack() {
1159        let mut h = IconProviderHandle::new();
1160        // Register in reverse-alphabetical order: insertion order must NOT decide.
1161        h.register_icon("zzz", "home", RefAny::new(TestIconData { id: 26 }));
1162        h.register_icon("mmm", "home", RefAny::new(TestIconData { id: 13 }));
1163        h.register_icon("aaa", "home", RefAny::new(TestIconData { id: 1 }));
1164
1165        let (pack, _) = h.lookup_with_pack("HOME").expect("must be found");
1166        assert_eq!(pack, "aaa", "BTreeMap order => first match is the first pack by name");
1167
1168        let mut data = h.lookup("home").unwrap();
1169        assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 1);
1170
1171        // Removing the winner promotes the next pack in order.
1172        h.unregister_pack("aaa");
1173        let (pack, _) = h.lookup_with_pack("home").unwrap();
1174        assert_eq!(pack, "mmm");
1175    }
1176
1177    // has_icon
1178
1179    #[test]
1180    fn has_icon_true_false_and_edge_inputs() {
1181        let mut h = IconProviderHandle::new();
1182        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1183
1184        assert!(h.has_icon("home"));
1185        assert!(!h.has_icon("definitely-not-registered"));
1186
1187        for name in adversarial_names() {
1188            // Deterministic bool, no panic: none of these were registered.
1189            assert!(!h.has_icon(&name));
1190        }
1191        assert!(h.has_icon("home"), "state unchanged by the queries above");
1192    }
1193
1194    // getters: list_packs / list_icons_in_pack
1195
1196    #[test]
1197    fn list_packs_is_sorted_and_case_sensitive() {
1198        let mut h = IconProviderHandle::new();
1199        for p in ["zeta", "alpha", "Alpha", "mid", ""] {
1200            h.register_icon(p, "home", RefAny::new(TestIconData { id: 0 }));
1201        }
1202        // BTreeMap => byte-order sorted; "Alpha" != "alpha" (case-sensitive).
1203        assert_eq!(
1204            h.list_packs(),
1205            vec![
1206                String::new(),
1207                String::from("Alpha"),
1208                String::from("alpha"),
1209                String::from("mid"),
1210                String::from("zeta"),
1211            ]
1212        );
1213    }
1214
1215    #[test]
1216    fn list_icons_in_pack_returns_folded_keys_and_empty_for_unknown_packs() {
1217        let mut h = IconProviderHandle::new();
1218        h.register_icon("p", "Zoom", RefAny::new(TestIconData { id: 1 }));
1219        h.register_icon("p", "HOME", RefAny::new(TestIconData { id: 2 }));
1220
1221        assert_eq!(h.list_icons_in_pack("p"), vec![String::from("home"), String::from("zoom")]);
1222        assert!(h.list_icons_in_pack("P").is_empty());
1223        assert!(h.list_icons_in_pack("").is_empty());
1224        assert!(h.list_icons_in_pack(&"x".repeat(100_000)).is_empty());
1225    }
1226
1227    // debug_lookup
1228
1229    #[test]
1230    fn debug_lookup_reports_not_found_for_missing_icons() {
1231        let mut h = IconProviderHandle::new();
1232        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1233
1234        let out = h.debug_lookup("settings");
1235        let s = out.as_str();
1236        assert!(s.contains("NOT FOUND in any pack"));
1237        assert!(s.contains("Total packs: 1"));
1238        assert!(s.contains("Pack 'p': 1 icons"));
1239    }
1240
1241    #[test]
1242    fn debug_lookup_classifies_image_font_and_unknown_refany_types() {
1243        let mut h = IconProviderHandle::new();
1244        h.register_icon("p", "img", RefAny::new(ImageIconData { _w: 16 }));
1245        h.register_icon("p", "fnt", RefAny::new(FontIconData { _codepoint: 0xF015 }));
1246        h.register_icon("p", "other", RefAny::new(TestIconData { id: 1 }));
1247
1248        let img = h.debug_lookup("img");
1249        assert!(img.as_str().contains("FOUND in pack 'p'"));
1250        assert!(img.as_str().contains("RefAny type: ImageIconData"));
1251
1252        let fnt = h.debug_lookup("FNT"); // case-folded lookup path
1253        assert!(fnt.as_str().contains("RefAny type: FontIconData"));
1254
1255        let other = h.debug_lookup("other");
1256        assert!(other.as_str().contains("RefAny type: UNKNOWN"));
1257    }
1258
1259    #[test]
1260    fn debug_lookup_survives_adversarial_names() {
1261        let mut h = IconProviderHandle::new();
1262        for (i, name) in adversarial_names().iter().enumerate() {
1263            h.register_icon("p", name, RefAny::new(TestIconData { id: i as u32 }));
1264        }
1265        for name in adversarial_names() {
1266            let out = h.debug_lookup(&name);
1267            assert!(out.as_str().contains("FOUND in pack 'p'"), "must find {name:?}");
1268        }
1269        assert!(h.debug_lookup("never-registered").as_str().contains("NOT FOUND"));
1270    }
1271
1272    // SharedIconProvider
1273
1274    #[test]
1275    fn from_handle_preserves_every_registered_icon() {
1276        let mut h = IconProviderHandle::new();
1277        for i in 0..64u32 {
1278            h.register_icon("p", &format!("icon{i}"), RefAny::new(TestIconData { id: i }));
1279        }
1280        let shared = SharedIconProvider::from_handle(h);
1281
1282        for i in 0..64u32 {
1283            let name = format!("ICON{i}");
1284            assert!(shared.has_icon(&name));
1285            let mut data = shared.lookup(&name).expect("must survive into_shared()");
1286            assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i);
1287        }
1288        assert!(!shared.has_icon("icon64"));
1289        assert!(shared.lookup("").is_none());
1290    }
1291
1292    #[test]
1293    fn shared_provider_lookup_and_has_icon_agree_on_adversarial_input() {
1294        let mut h = IconProviderHandle::new();
1295        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1296        let shared = SharedIconProvider::from_handle(h);
1297
1298        for name in adversarial_names() {
1299            assert_eq!(
1300                shared.has_icon(&name),
1301                shared.lookup(&name).is_some(),
1302                "has_icon/lookup disagree for {name:?}"
1303            );
1304        }
1305        assert!(shared.has_icon("HoMe") && shared.lookup("HoMe").is_some());
1306    }
1307
1308    #[test]
1309    fn shared_provider_clone_shares_the_same_icon_table() {
1310        let mut h = IconProviderHandle::new();
1311        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1312        let a = SharedIconProvider::from_handle(h);
1313        let b = a.clone();
1314
1315        assert!(b.has_icon("home"));
1316        drop(a);
1317        assert!(b.has_icon("home"), "clone must keep the Arc alive");
1318        let mut data = b.lookup("home").unwrap();
1319        assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 1);
1320    }
1321
1322    #[test]
1323    fn shared_resolve_receives_icon_data_and_original_dom() {
1324        let mut h = IconProviderHandle::with_resolver(recording_resolver);
1325        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1326        let shared = SharedIconProvider::from_handle(h);
1327
1328        let original = styled_dom_with_icons(&["home"]);
1329        let icon_idx = icon_indices(&original)[0];
1330        let single = extract_single_node_styled_dom(&original, icon_idx);
1331
1332        let out = shared.resolve(&single, "HOME", &SystemStyle::default());
1333
1334        assert!(REC_CALLS.load(AtomicOrdering::SeqCst) >= 1);
1335        assert!(REC_SAW_DATA.load(AtomicOrdering::SeqCst), "case-folded lookup must pass Some(data)");
1336        assert!(REC_SAW_ICON_NODE.load(AtomicOrdering::SeqCst), "node 0 of the original dom is the Icon");
1337        assert_eq!(REC_NAME_LEN.load(AtomicOrdering::SeqCst), "home".len());
1338        assert!(matches!(node_type_at(&out, 0), NodeType::Div));
1339    }
1340
1341    #[test]
1342    fn shared_resolve_runs_the_resolver_even_when_the_icon_is_missing() {
1343        let h = IconProviderHandle::with_resolver(div_resolver);
1344        let shared = SharedIconProvider::from_handle(h);
1345        // Empty name, huge name, unicode name: resolver still returns its DOM.
1346        let huge = "x".repeat(100_000);
1347        for name in ["", "\u{1F600}", huge.as_str()] {
1348            let out = shared.resolve(&StyledDom::default(), name, &SystemStyle::default());
1349            assert!(matches!(node_type_at(&out, 0), NodeType::Div));
1350        }
1351    }
1352
1353    #[cfg(feature = "std")]
1354    #[test]
1355    fn shared_provider_survives_concurrent_lookups() {
1356        let mut h = IconProviderHandle::new();
1357        for i in 0..16u32 {
1358            h.register_icon("p", &format!("icon{i}"), RefAny::new(TestIconData { id: i }));
1359        }
1360        let shared = SharedIconProvider::from_handle(h);
1361
1362        let mut threads = Vec::new();
1363        for _ in 0..4 {
1364            let s = shared.clone();
1365            threads.push(std::thread::spawn(move || {
1366                let mut hits = 0usize;
1367                for i in 0..200u32 {
1368                    let name = format!("icon{}", i % 16);
1369                    if s.has_icon(&name) {
1370                        hits += 1;
1371                    }
1372                    let mut data = s.lookup(&name).expect("registered icon");
1373                    assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i % 16);
1374                }
1375                hits
1376            }));
1377        }
1378        for t in threads {
1379            assert_eq!(t.join().unwrap(), 200);
1380        }
1381        assert!(shared.has_icon("icon0"), "table intact after contention");
1382    }
1383
1384    // collect_icon_nodes
1385
1386    #[test]
1387    fn collect_icon_nodes_is_empty_when_there_are_no_icons() {
1388        assert!(collect_icon_nodes(&StyledDom::default()).is_empty());
1389        assert!(collect_icon_nodes(&zero_node_styled_dom()).is_empty());
1390        assert!(collect_icon_nodes(&StyledDom::create_from_dom(Dom::create_div())).is_empty());
1391    }
1392
1393    #[test]
1394    fn collect_icon_nodes_finds_every_icon_in_ascending_index_order_with_verbatim_names() {
1395        let names = ["HOME", "\u{1F600}", ""];
1396        let sd = styled_dom_with_icons(&names);
1397        let collected = collect_icon_nodes(&sd);
1398
1399        assert_eq!(collected.len(), names.len());
1400        for (i, c) in collected.iter().enumerate() {
1401            // Node names are NOT folded at DOM-construction time (only at lookup).
1402            assert_eq!(c.icon_name.as_str(), names[i]);
1403            if i > 0 {
1404                assert!(c.node_idx > collected[i - 1].node_idx, "indices must ascend");
1405            }
1406        }
1407    }
1408
1409    #[test]
1410    fn collect_icon_nodes_handles_a_very_long_icon_name() {
1411        let long = "x".repeat(100_000);
1412        let sd = styled_dom_with_icons(&[&long]);
1413        let collected = collect_icon_nodes(&sd);
1414        assert_eq!(collected.len(), 1);
1415        assert_eq!(collected[0].icon_name.as_str().len(), 100_000);
1416    }
1417
1418    // extract_single_node_styled_dom (numeric / index boundaries)
1419
1420    #[test]
1421    fn extract_single_node_at_index_zero() {
1422        let sd = styled_dom_with_icons(&["home"]);
1423        let out = extract_single_node_styled_dom(&sd, 0);
1424        assert_eq!(out.node_data.as_ref().len(), 1);
1425        assert_eq!(out.styled_nodes.as_ref().len(), 1);
1426        assert_eq!(node_type_at(&out, 0), node_type_at(&sd, 0));
1427    }
1428
1429    #[test]
1430    fn extract_single_node_of_the_icon_keeps_the_icon_node_type() {
1431        let sd = styled_dom_with_icons(&["home"]);
1432        let idx = icon_indices(&sd)[0];
1433        let out = extract_single_node_styled_dom(&sd, idx);
1434        assert_eq!(out.node_data.as_ref().len(), 1);
1435        assert!(matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1436    }
1437
1438    #[test]
1439    fn extract_single_node_out_of_bounds_falls_back_to_default_without_panicking() {
1440        let sd = styled_dom_with_icons(&["home"]);
1441        let len = sd.node_data.as_ref().len();
1442
1443        for idx in [len, len + 1, usize::MAX / 2, usize::MAX - 1, usize::MAX] {
1444            let out = extract_single_node_styled_dom(&sd, idx);
1445            // Falls back to StyledDom::default() -> exactly one (Body) node.
1446            assert_eq!(out.node_data.as_ref().len(), 1, "idx {idx} must not panic");
1447            assert!(!matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1448        }
1449        // Zero-node input: even index 0 is out of bounds.
1450        let empty = zero_node_styled_dom();
1451        assert_eq!(extract_single_node_styled_dom(&empty, 0).node_data.as_ref().len(), 1);
1452    }
1453
1454    #[test]
1455    fn extract_single_node_tolerates_styled_nodes_shorter_than_node_data() {
1456        let sd_full = styled_dom_with_icons(&["home"]);
1457        let idx = icon_indices(&sd_full)[0];
1458
1459        let mut sd = sd_full;
1460        sd.styled_nodes = StyledNodeVec::from_vec(Vec::new()); // desynced arrays
1461
1462        let out = extract_single_node_styled_dom(&sd, idx);
1463        assert_eq!(out.node_data.as_ref().len(), 1);
1464        assert_eq!(out.styled_nodes.as_ref().len(), 1, "must synthesize a default StyledNode");
1465        assert!(matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1466    }
1467
1468    // is_single_node_replacement
1469
1470    #[test]
1471    fn is_single_node_replacement_true_false_and_edges() {
1472        assert!(is_single_node_replacement(&StyledDom::default()));
1473        assert!(is_single_node_replacement(&StyledDom::create_from_dom(Dom::create_div())));
1474
1475        // Zero nodes is NOT "single node" (callers treat it as the empty case).
1476        assert!(!is_single_node_replacement(&zero_node_styled_dom()));
1477
1478        let multi = StyledDom::create_from_dom(
1479            Dom::create_div().with_child(Dom::create_div()).with_child(Dom::create_div()),
1480        );
1481        assert!(multi.node_data.as_ref().len() > 1);
1482        assert!(!is_single_node_replacement(&multi));
1483    }
1484
1485    // apply_single_node_replacement (index boundaries)
1486
1487    #[test]
1488    fn apply_single_node_replacement_with_zero_node_dom_turns_the_icon_into_a_div() {
1489        let mut sd = styled_dom_with_icons(&["home"]);
1490        let idx = icon_indices(&sd)[0];
1491        let empty = zero_node_styled_dom();
1492
1493        apply_single_node_replacement(&mut sd, idx, &empty);
1494        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1495        assert!(collect_icon_nodes(&sd).is_empty());
1496    }
1497
1498    #[test]
1499    fn apply_single_node_replacement_copies_the_replacement_root_node_type() {
1500        let mut sd = styled_dom_with_icons(&["home"]);
1501        let idx = icon_indices(&sd)[0];
1502        let before_len = sd.node_data.as_ref().len();
1503        let repl = StyledDom::create_from_dom(Dom::create_div());
1504
1505        apply_single_node_replacement(&mut sd, idx, &repl);
1506        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1507        assert_eq!(sd.node_data.as_ref().len(), before_len, "node count must not change");
1508    }
1509
1510    #[test]
1511    fn apply_single_node_replacement_out_of_bounds_index_is_a_no_op() {
1512        let repl = StyledDom::create_from_dom(Dom::create_div());
1513        let empty = zero_node_styled_dom();
1514
1515        let base = styled_dom_with_icons(&["home"]);
1516        let icon_idx = icon_indices(&base)[0];
1517        let len = base.node_data.as_ref().len();
1518
1519        for idx in [len, len + 1, usize::MAX / 2, usize::MAX] {
1520            let mut sd = styled_dom_with_icons(&["home"]);
1521            apply_single_node_replacement(&mut sd, idx, &repl);
1522            apply_single_node_replacement(&mut sd, idx, &empty);
1523            assert_eq!(sd.node_data.as_ref().len(), len, "idx {idx} must not resize");
1524            assert!(
1525                matches!(node_type_at(&sd, icon_idx), NodeType::Icon(_)),
1526                "idx {idx} must leave the icon untouched"
1527            );
1528        }
1529    }
1530
1531    // apply_multi_node_replacement (index boundaries)
1532
1533    #[test]
1534    fn apply_multi_node_replacement_with_zero_node_dom_turns_the_icon_into_a_div() {
1535        let mut sd = styled_dom_with_icons(&["home"]);
1536        let idx = icon_indices(&sd)[0];
1537
1538        apply_multi_node_replacement(&mut sd, idx, &zero_node_styled_dom());
1539        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1540    }
1541
1542    #[test]
1543    fn apply_multi_node_replacement_applies_only_the_root_and_does_not_splice() {
1544        let mut sd = styled_dom_with_icons(&["home"]);
1545        let idx = icon_indices(&sd)[0];
1546        let before_len = sd.node_data.as_ref().len();
1547
1548        let repl = StyledDom::create_from_dom(
1549            Dom::create_div().with_child(Dom::create_div()).with_child(Dom::create_div()),
1550        );
1551        assert!(repl.node_data.as_ref().len() > 1);
1552
1553        apply_multi_node_replacement(&mut sd, idx, &repl);
1554
1555        // Documented TODO: subtree splicing is not implemented, only the root is used.
1556        assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1557        assert_eq!(sd.node_data.as_ref().len(), before_len, "children are dropped, not spliced");
1558    }
1559
1560    #[test]
1561    fn apply_multi_node_replacement_out_of_bounds_index_is_a_no_op() {
1562        let repl = StyledDom::create_from_dom(Dom::create_div().with_child(Dom::create_div()));
1563        let base = styled_dom_with_icons(&["home"]);
1564        let icon_idx = icon_indices(&base)[0];
1565        let len = base.node_data.as_ref().len();
1566
1567        for idx in [len, usize::MAX] {
1568            let mut sd = styled_dom_with_icons(&["home"]);
1569            apply_multi_node_replacement(&mut sd, idx, &repl);
1570            apply_multi_node_replacement(&mut sd, idx, &zero_node_styled_dom());
1571            assert_eq!(sd.node_data.as_ref().len(), len);
1572            assert!(matches!(node_type_at(&sd, icon_idx), NodeType::Icon(_)));
1573        }
1574    }
1575
1576    // resolve_collected_icons
1577
1578    #[test]
1579    fn resolve_collected_icons_preserves_indices_and_resolves_each_icon() {
1580        let mut h = IconProviderHandle::with_resolver(div_resolver);
1581        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1582        let shared = SharedIconProvider::from_handle(h);
1583
1584        let sd = styled_dom_with_icons(&["home", "missing", "\u{1F600}"]);
1585        let icons = collect_icon_nodes(&sd);
1586        let replacements =
1587            resolve_collected_icons(&icons, &sd, &shared, &SystemStyle::default());
1588
1589        assert_eq!(replacements.len(), icons.len());
1590        for (r, i) in replacements.iter().zip(icons.iter()) {
1591            assert_eq!(r.node_idx, i.node_idx);
1592            // The custom resolver ignores the data, so even unregistered icons resolve.
1593            assert!(matches!(node_type_at(&r.replacement, 0), NodeType::Div));
1594        }
1595    }
1596
1597    #[test]
1598    fn resolve_collected_icons_with_no_icons_returns_no_replacements() {
1599        let shared = SharedIconProvider::from_handle(IconProviderHandle::new());
1600        let sd = StyledDom::default();
1601        let out = resolve_collected_icons(&[], &sd, &shared, &SystemStyle::default());
1602        assert!(out.is_empty());
1603    }
1604
1605    // resolve_icons_in_styled_dom (end to end)
1606
1607    #[test]
1608    fn resolve_icons_in_styled_dom_is_a_no_op_without_icons() {
1609        let shared = SharedIconProvider::from_handle(IconProviderHandle::with_resolver(div_resolver));
1610        let mut sd = StyledDom::create_from_dom(Dom::create_body().with_child(Dom::create_text("hi")));
1611        let before_len = sd.node_data.as_ref().len();
1612        let before_root = node_type_at(&sd, 0);
1613
1614        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1615
1616        assert_eq!(sd.node_data.as_ref().len(), before_len);
1617        assert_eq!(node_type_at(&sd, 0), before_root);
1618    }
1619
1620    #[test]
1621    fn resolve_icons_in_styled_dom_replaces_every_icon_case_insensitively() {
1622        let mut h = IconProviderHandle::with_resolver(div_resolver);
1623        h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1624        let shared = SharedIconProvider::from_handle(h);
1625
1626        // Mixed case in the DOM, lowercase in the pack, plus one unregistered icon.
1627        let mut sd = styled_dom_with_icons(&["HOME", "unregistered", "HoMe"]);
1628        let idxs = icon_indices(&sd);
1629        let before_len = sd.node_data.as_ref().len();
1630
1631        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1632
1633        assert_eq!(sd.node_data.as_ref().len(), before_len);
1634        assert!(collect_icon_nodes(&sd).is_empty(), "no Icon node may survive resolution");
1635        for idx in idxs {
1636            assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1637        }
1638    }
1639
1640    #[test]
1641    fn resolve_icons_in_styled_dom_with_the_default_resolver_removes_the_icon_nodes() {
1642        // The default resolver returns `StyledDom::default()` (one Body node), so
1643        // icons are replaced by that root's node type rather than being cleared.
1644        let shared = SharedIconProvider::from_handle(IconProviderHandle::new());
1645        let mut sd = styled_dom_with_icons(&["home"]);
1646        let idx = icon_indices(&sd)[0];
1647
1648        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1649
1650        assert!(!matches!(node_type_at(&sd, idx), NodeType::Icon(_)));
1651        assert!(collect_icon_nodes(&sd).is_empty());
1652    }
1653
1654    #[test]
1655    fn resolve_icons_in_styled_dom_handles_a_zero_node_replacement() {
1656        let shared =
1657            SharedIconProvider::from_handle(IconProviderHandle::with_resolver(zero_node_resolver));
1658        let mut sd = styled_dom_with_icons(&["home", "other"]);
1659        let idxs = icon_indices(&sd);
1660        let before_len = sd.node_data.as_ref().len();
1661
1662        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1663
1664        assert_eq!(sd.node_data.as_ref().len(), before_len);
1665        for idx in idxs {
1666            assert!(matches!(node_type_at(&sd, idx), NodeType::Div), "empty => Div placeholder");
1667        }
1668    }
1669
1670    #[test]
1671    fn resolve_icons_in_styled_dom_scales_to_many_icons() {
1672        let shared = SharedIconProvider::from_handle(IconProviderHandle::with_resolver(div_resolver));
1673        let names: Vec<String> = (0..500).map(|i| format!("icon{i}")).collect();
1674        let refs: Vec<&str> = names.iter().map(String::as_str).collect();
1675        let mut sd = styled_dom_with_icons(&refs);
1676        let before_len = sd.node_data.as_ref().len();
1677
1678        resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1679
1680        assert_eq!(sd.node_data.as_ref().len(), before_len);
1681        assert!(collect_icon_nodes(&sd).is_empty());
1682    }
1683}