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

            
38
use alloc::{
39
    boxed::Box,
40
    collections::BTreeMap,
41
    string::{String, ToString},
42
    sync::Arc,
43
    vec::Vec,
44
};
45
use core::fmt;
46
use core::mem::ManuallyDrop;
47

            
48
#[cfg(feature = "std")]
49
use std::sync::Mutex;
50

            
51
#[cfg(not(feature = "std"))]
52
use 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"))]
57
mod 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

            
112
use azul_css::{AzString, system::SystemStyle};
113

            
114
use crate::{
115
    dom::{Dom, NodeType},
116
    refany::{OptionRefAny, RefAny},
117
    styled_dom::StyledDom,
118
};
119

            
120
// Type name constants for RefAny-based icon type detection in debug output
121
const IMAGE_ICON_DATA_TYPE_NAME: &str = "ImageIconData";
122
const FONT_ICON_DATA_TYPE_NAME: &str = "FontIconData";
123

            
124
// Icon Resolver Callback
125

            
126
/// Callback type for resolving icon data to a StyledDom.
127
///
128
/// Parameters:
129
/// - `icon_data`: The RefAny data from the icon pack (cloned, or None if not found)
130
/// - `original_icon_dom`: The original icon node's StyledDom (contains inline styles, a11y info, icon_name)
131
/// - `system_style`: Current system style (theme, colors, etc.)
132
///
133
/// Returns: A StyledDom that will replace the icon node.
134
/// The resolver should copy relevant styles from original_icon_dom to the result.
135
/// Return an empty StyledDom to show a placeholder or nothing.
136
///
137
/// Note: icon_name is accessible via `original_icon_dom.node_data[0].get_node_type()` → `NodeType::Icon(name)`
138
pub type IconResolverCallbackType = extern "C" fn(
139
    icon_data: OptionRefAny,
140
    original_icon_dom: &StyledDom,
141
    system_style: &SystemStyle,
142
) -> StyledDom;
143

            
144
/// Default resolver that returns an empty StyledDom (shows placeholder)
145
pub extern "C" fn default_icon_resolver(
146
    _icon_data: OptionRefAny,
147
    _original_icon_dom: &StyledDom,
148
    _system_style: &SystemStyle,
149
) -> StyledDom {
150
    // Default: return empty DOM (icon won't be visible)
151
    StyledDom::default()
152
}
153

            
154
// Icon Provider Inner (single mutex)
155

            
156
/// Inner data for IconProviderHandle - all fields behind single mutex
157
#[derive(Clone)]
158
pub struct IconProviderInner {
159
    /// Nested map: pack_name → (icon_name → RefAny)
160
    /// Differentiation between Image/Font/SVG is via RefAny::downcast
161
    pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
162
    /// The resolver callback
163
    pub resolver: IconResolverCallbackType,
164
}
165

            
166
impl Default for IconProviderInner {
167
    fn default() -> Self {
168
        Self {
169
            icons: BTreeMap::new(),
170
            resolver: default_icon_resolver,
171
        }
172
    }
173
}
174

            
175
// Icon Provider Handle
176

            
177
/// Icon provider stored in AppConfig.
178
///
179
/// This is a Box<IconProviderInner> for C FFI compatibility.
180
/// When App::run() is called, it gets converted to Arc<Mutex<IconProviderInner>>
181
/// and cloned to each window.
182
///
183
/// Icons are stored in a nested map: pack_name → (icon_name → RefAny)
184
/// This allows:
185
/// - Multiple packs with different sources (app-images, material-icons, etc.)
186
/// - Easy unregistration of entire packs
187
/// - First-match-wins lookup across all packs
188
#[repr(C)]
189
pub struct IconProviderHandle {
190
    /// Boxed inner data - Box<T> is repr(C) compatible (single pointer).
191
    /// `ManuallyDrop` so the Box is freed ONLY by our `Drop` (gated on
192
    /// `run_destructor`), never by drop-glue. The codegen Az wrapper nests an
193
    /// `AzIconProviderHandle` field (in `AzAppConfig`) whose own `Drop` re-runs
194
    /// `_delete` -> `drop_in_place::<IconProviderHandle>` on the SAME bytes; with
195
    /// a bare `Box` the glue freed it a second time -> double free. Same
196
    /// convention as GlContextPtr / CssPropertyCachePtr.
197
    pub inner: ManuallyDrop<Box<IconProviderInner>>,
198
    pub run_destructor: bool,
199
}
200

            
201
impl Clone for IconProviderHandle {
202
    fn clone(&self) -> Self {
203
        Self {
204
            inner: ManuallyDrop::new(Box::new((**self.inner).clone())),
205
            run_destructor: true,
206
        }
207
    }
208
}
209

            
210
impl Drop for IconProviderHandle {
211
127
    fn drop(&mut self) {
212
        // First drop (run_destructor still true) frees the Box and clears the flag
213
        // in the shared bytes; the codegen's redundant second drop sees false -> no-op.
214
127
        if self.run_destructor {
215
127
            self.run_destructor = false;
216
127
            unsafe {
217
127
                ManuallyDrop::drop(&mut self.inner);
218
127
            }
219
        }
220
127
    }
221
}
222

            
223
impl fmt::Debug for IconProviderHandle {
224
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225
        let pack_count = self.inner.icons.len();
226
        let icon_count: usize = self.inner.icons.values().map(|p| p.len()).sum();
227
        
228
        f.debug_struct("IconProviderHandle")
229
            .field("pack_count", &pack_count)
230
            .field("icon_count", &icon_count)
231
            .finish()
232
    }
233
}
234

            
235
impl Default for IconProviderHandle {
236
    fn default() -> Self {
237
        Self::new()
238
    }
239
}
240

            
241
impl IconProviderHandle {
242
    /// Create a new empty icon provider with the default (no-op) resolver.
243
    /// 
244
    /// Note: The default resolver in core crate returns an empty StyledDom.
245
    /// Use `set_resolver()` to set a proper resolver from the layout crate,
246
    /// or use `with_resolver()` to create with a custom resolver.
247
76
    pub fn new() -> Self {
248
76
        Self {
249
76
            inner: ManuallyDrop::new(Box::new(IconProviderInner {
250
76
                icons: BTreeMap::new(),
251
76
                resolver: default_icon_resolver,
252
76
            })),
253
76
            run_destructor: true,
254
76
        }
255
76
    }
256

            
257
    /// Create with a custom resolver callback
258
51
    pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
259
51
        Self {
260
51
            inner: ManuallyDrop::new(Box::new(IconProviderInner {
261
51
                icons: BTreeMap::new(),
262
51
                resolver,
263
51
            })),
264
51
            run_destructor: true,
265
51
        }
266
51
    }
267
    
268
    /// Convert this handle into an Arc<Mutex<IconProviderInner>> for use in windows.
269
    ///
270
    /// This consumes the Box and creates an Arc. Called by App::run() to create
271
    /// the shared icon provider that gets cloned to each window.
272
    pub(crate) fn into_shared(mut self) -> Arc<Mutex<IconProviderInner>> {
273
        // Take the Box out and disarm our Drop so it doesn't free the moved-out
274
        // allocation (ManuallyDrop::take leaves `inner` logically uninitialized).
275
        let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
276
        self.run_destructor = false;
277
        Arc::new(Mutex::new(*inner))
278
    }
279

            
280
    /// Set the resolver callback
281
    pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
282
        self.inner.resolver = resolver;
283
    }
284

            
285
    /// Register a single icon in a pack (creates pack if needed).
286
    ///
287
    /// Note: `pack_name` is case-sensitive, while `icon_name` is normalized to lowercase.
288
76
    pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
289
76
        let pack = self.inner.icons
290
76
            .entry(pack_name.to_string())
291
76
            .or_default();
292
76
        pack.insert(icon_name.to_lowercase(), data);
293
76
    }
294

            
295
    /// Unregister a single icon from a pack
296
19
    pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
297
19
        if let Some(pack) = self.inner.icons.get_mut(pack_name) {
298
19
            pack.remove(&icon_name.to_lowercase());
299
19
            if pack.is_empty() {
300
19
                self.inner.icons.remove(pack_name);
301
19
            }
302
        }
303
19
    }
304

            
305
    /// Unregister an entire icon pack
306
19
    pub fn unregister_pack(&mut self, pack_name: &str) {
307
19
        self.inner.icons.remove(pack_name);
308
19
    }
309

            
310
    /// Look up an icon across all packs, returning the pack name and data reference (first match wins)
311
19
    fn lookup_with_pack(&self, icon_name: &str) -> Option<(&str, &RefAny)> {
312
19
        let icon_name_lower = icon_name.to_lowercase();
313
19
        for (pack_name, pack) in self.inner.icons.iter() {
314
19
            if let Some(data) = pack.get(&icon_name_lower) {
315
19
                return Some((pack_name.as_str(), data));
316
            }
317
        }
318
        None
319
19
    }
320

            
321
    /// Look up an icon across all packs (first match wins)
322
19
    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
323
19
        self.lookup_with_pack(icon_name).map(|(_, data)| data.clone())
324
19
    }
325

            
326
    /// Check if an icon exists in any pack
327
171
    pub fn has_icon(&self, icon_name: &str) -> bool {
328
171
        let icon_name_lower = icon_name.to_lowercase();
329
171
        self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
330
171
    }
331

            
332
    /// List all pack names
333
108
    pub fn list_packs(&self) -> Vec<String> {
334
108
        self.inner.icons.keys().cloned().collect()
335
108
    }
336

            
337
    /// List all icon names in a specific pack
338
    pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
339
        self.inner.icons.get(pack_name)
340
            .map(|pack| pack.keys().cloned().collect())
341
            .unwrap_or_default()
342
    }
343

            
344
    /// Debug lookup: returns detailed info about an icon's RefAny contents
345
    pub fn debug_lookup(&self, icon_name: &str) -> AzString {
346
        let icon_name_lower = icon_name.to_lowercase();
347

            
348
        let mut result = format!("Debug lookup for icon '{}' (normalized: '{}'):\n", icon_name, icon_name_lower);
349

            
350
        // Report registered packs
351
        result.push_str(&format!("  Total packs: {}\n", self.inner.icons.len()));
352
        for (pack_name, pack) in self.inner.icons.iter() {
353
            result.push_str(&format!("    Pack '{}': {} icons\n", pack_name, pack.len()));
354
            for (name, _) in pack.iter() {
355
                result.push_str(&format!("      - {}\n", name));
356
            }
357
        }
358

            
359
        // Find the icon using shared lookup helper
360
        match self.lookup_with_pack(icon_name) {
361
            Some((pack, data)) => {
362
                result.push_str(&format!("\n  FOUND in pack '{}'\n", pack));
363
                let type_name = data.get_type_name();
364
                result.push_str(&format!("  RefAny type_name: '{}'\n", type_name.as_str()));
365

            
366
                let debug_info = data.sharing_info.debug_get_refcount_copied();
367
                result.push_str(&format!("  RefAny size: {} bytes\n", debug_info._internal_layout_size));
368

            
369
                let type_str = type_name.as_str();
370
                if type_str.contains(IMAGE_ICON_DATA_TYPE_NAME) {
371
                    result.push_str("  RefAny type: ImageIconData (image-based icon)\n");
372
                } else if type_str.contains(FONT_ICON_DATA_TYPE_NAME) {
373
                    result.push_str("  RefAny type: FontIconData (font-based icon)\n");
374
                } else {
375
                    result.push_str(&format!("  RefAny type: UNKNOWN ('{}')\n", type_str));
376
                }
377
            }
378
            None => {
379
                result.push_str("\n  NOT FOUND in any pack\n");
380
            }
381
        }
382

            
383
        AzString::from(result)
384
    }
385
}
386

            
387
/// Thread-safe icon provider for use in windows.
388
/// 
389
/// This is created from IconProviderHandle::into_shared() in App::run()
390
/// and cloned to each window.
391
#[derive(Clone)]
392
pub struct SharedIconProvider {
393
    inner: Arc<Mutex<IconProviderInner>>,
394
}
395

            
396
impl SharedIconProvider {
397
    /// Create from an IconProviderHandle (consumes the handle)
398
    pub fn from_handle(handle: IconProviderHandle) -> Self {
399
        Self { inner: handle.into_shared() }
400
    }
401
    
402
    /// Resolve an icon to a StyledDom using the registered callback
403
    pub fn resolve(
404
        &self, 
405
        original_icon_dom: &StyledDom,
406
        icon_name: &str,
407
        system_style: &SystemStyle,
408
    ) -> StyledDom {
409
        let (resolver, lookup_result) = {
410
            let guard = match self.inner.lock() {
411
                Ok(g) => g,
412
                Err(_) => return StyledDom::default(),
413
            };
414
            
415
            let resolver = guard.resolver;
416
            let icon_name_lower = icon_name.to_lowercase();
417
            
418
            let lookup_result = guard.icons.values()
419
                .find_map(|pack| pack.get(&icon_name_lower).cloned());
420
            
421
            (resolver, lookup_result)
422
        };
423
        
424
        resolver(lookup_result.into(), original_icon_dom, system_style)
425
    }
426
    
427
    /// Look up an icon across all packs
428
    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
429
        let icon_name_lower = icon_name.to_lowercase();
430
        self.inner.lock().ok().and_then(|guard| {
431
            for pack in guard.icons.values() {
432
                if let Some(data) = pack.get(&icon_name_lower) {
433
                    return Some(data.clone());
434
                }
435
            }
436
            None
437
        })
438
    }
439
    
440
    /// Check if an icon exists
441
    pub fn has_icon(&self, icon_name: &str) -> bool {
442
        let icon_name_lower = icon_name.to_lowercase();
443
        self.inner.lock()
444
            .map(|guard| guard.icons.values().any(|p| p.contains_key(&icon_name_lower)))
445
            .unwrap_or(false)
446
    }
447
}
448

            
449
// Icon Resolution in StyledDom
450

            
451
/// Collected icon node info for replacement
452
struct CollectedIcon {
453
    /// Index in the node_data array
454
    node_idx: usize,
455
    /// The icon name
456
    icon_name: AzString,
457
}
458

            
459
/// Replacement result after resolving an icon
460
struct IconReplacement {
461
    /// Index of the icon node to replace
462
    node_idx: usize,
463
    /// The resolved StyledDom (may be empty, single node, or multi-node tree)
464
    replacement: StyledDom,
465
}
466

            
467
/// Collect all Icon nodes from the StyledDom
468
fn collect_icon_nodes(styled_dom: &StyledDom) -> Vec<CollectedIcon> {
469
    let mut icons = Vec::new();
470
    
471
    let node_data = styled_dom.node_data.as_ref();
472
    for (idx, node) in node_data.iter().enumerate() {
473
        if let NodeType::Icon(icon_name) = node.get_node_type() {
474
            icons.push(CollectedIcon {
475
                node_idx: idx,
476
                icon_name: icon_name.clone_self(),
477
            });
478
        }
479
    }
480
    
481
    icons
482
}
483

            
484
/// Extract a single-node StyledDom from a parent StyledDom at the given index.
485
/// This creates a minimal StyledDom containing just that node for the resolver.
486
fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
487
    use crate::dom::{NodeDataVec, DomId};
488
    use crate::id::NodeId;
489
    use crate::styled_dom::{
490
        StyledNodeVec, NodeHierarchyItemIdVec, TagIdToNodeIdMappingVec,
491
        NodeHierarchyItemVec, NodeHierarchyItem, NodeHierarchyItemId,
492
        ParentWithNodeDepthVec, ParentWithNodeDepth,
493
    };
494
    use crate::style::{CascadeInfoVec, CascadeInfo};
495
    use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
496
    
497
    let node_data = styled_dom.node_data.as_ref();
498
    let styled_nodes = styled_dom.styled_nodes.as_ref();
499
    
500
    if node_idx >= node_data.len() {
501
        return StyledDom::default();
502
    }
503
    
504
    // Clone the single node
505
    let single_node = node_data[node_idx].clone();
506
    let single_styled = if node_idx < styled_nodes.len() {
507
        styled_nodes[node_idx].clone()
508
    } else {
509
        crate::styled_dom::StyledNode::default()
510
    };
511
    
512
    StyledDom {
513
        root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
514
        node_hierarchy: NodeHierarchyItemVec::from_vec(vec![NodeHierarchyItem {
515
            parent: 0,
516
            previous_sibling: 0,
517
            next_sibling: 0,
518
            last_child: 0,
519
        }]),
520
        node_data: NodeDataVec::from_vec(vec![single_node]),
521
        styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
522
        cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
523
        nodes_with_window_callbacks: NodeHierarchyItemIdVec::from_vec(Vec::new()),
524
        nodes_with_datasets: NodeHierarchyItemIdVec::from_vec(Vec::new()),
525
        tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
526
        non_leaf_nodes: ParentWithNodeDepthVec::from_vec(Vec::new()),
527
        css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
528
        dom_id: DomId::ROOT_ID,
529
    }
530
}
531

            
532
/// Resolve all collected icons to their StyledDom representations
533
fn resolve_collected_icons(
534
    icons: &[CollectedIcon],
535
    styled_dom: &StyledDom,
536
    provider: &SharedIconProvider,
537
    system_style: &SystemStyle,
538
) -> Vec<IconReplacement> {
539
    icons.iter().map(|icon| {
540
        // Extract the original icon node as a StyledDom
541
        let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
542
        let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
543
        IconReplacement {
544
            node_idx: icon.node_idx,
545
            replacement,
546
        }
547
    }).collect()
548
}
549

            
550
/// Check if a replacement is a single-node replacement (fast path)
551
fn is_single_node_replacement(replacement: &StyledDom) -> bool {
552
    replacement.node_data.as_ref().len() == 1
553
}
554

            
555
/// Apply a single-node replacement (fast path: swap NodeType and copy properties)
556
fn apply_single_node_replacement(
557
    styled_dom: &mut StyledDom,
558
    node_idx: usize,
559
    replacement: &StyledDom,
560
) {
561
    if replacement.node_data.as_ref().is_empty() {
562
        // Empty replacement - convert to empty div
563
        let node_data = styled_dom.node_data.as_mut();
564
        if let Some(node) = node_data.get_mut(node_idx) {
565
            node.set_node_type(NodeType::Div);
566
        }
567
    } else {
568
        // Get the root node from the replacement and copy its properties
569
        let replacement_root = &replacement.node_data.as_ref()[0];
570
        let replacement_node_type = replacement_root.get_node_type().clone();
571
        
572
        let node_data = styled_dom.node_data.as_mut();
573
        if let Some(node) = node_data.get_mut(node_idx) {
574
            // Swap node type
575
            node.set_node_type(replacement_node_type);
576
            
577
            // Copy inline style from replacement
578
            node.set_style(replacement_root.get_style().clone());
579
            
580
            // Copy accessibility info if present
581
            if let Some(a11y) = replacement_root.get_accessibility_info() {
582
                node.set_accessibility_info(*a11y.clone());
583
            }
584
        }
585
        
586
        // Also update the styled_nodes to reflect the new styling
587
        if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
588
            let styled_nodes = styled_dom.styled_nodes.as_mut();
589
            if let Some(styled) = styled_nodes.get_mut(node_idx) {
590
                *styled = replacement_styled.clone();
591
            }
592
        }
593
    }
594
}
595

            
596
/// Apply multi-node replacement using subtree splicing
597
fn apply_multi_node_replacement(
598
    styled_dom: &mut StyledDom,
599
    node_idx: usize,
600
    replacement: StyledDom,
601
) {
602
    let replacement_len = replacement.node_data.as_ref().len();
603
    if replacement_len == 0 {
604
        let node_data = styled_dom.node_data.as_mut();
605
        if let Some(node) = node_data.get_mut(node_idx) {
606
            node.set_node_type(NodeType::Div);
607
        }
608
        return;
609
    }
610
    
611
    // For now, just apply the root node (same as single-node)
612
    apply_single_node_replacement(styled_dom, node_idx, &replacement);
613
    
614
    if replacement_len > 1 {
615
        // TODO: Full subtree splicing requires inserting nodes into arrays
616
        #[cfg(all(debug_assertions, feature = "std"))]
617
        eprintln!(
618
            "Warning: Icon replacement has {} nodes, only root node used.",
619
            replacement_len
620
        );
621
    }
622
}
623

            
624
/// Resolve all Icon nodes in a StyledDom to their actual content.
625
///
626
/// This function:
627
/// 1. Collects all Icon nodes from the StyledDom
628
/// 2. Resolves each icon via the provider's callback (passing original icon DOM)
629
/// 3. Applies replacements (single-node fast path or multi-node splicing)
630
///
631
/// This should be called after StyledDom creation but before layout.
632
pub fn resolve_icons_in_styled_dom(
633
    styled_dom: &mut StyledDom,
634
    provider: &SharedIconProvider,
635
    system_style: &SystemStyle,
636
) {
637
    // Step 1: Collect all icon nodes
638
    let icons = collect_icon_nodes(styled_dom);
639

            
640
    if icons.is_empty() {
641
        return;
642
    }
643

            
644
    // Step 2: Resolve all icons to their StyledDom representations
645
    // Note: We pass styled_dom to extract each icon's original node
646
    let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
647

            
648
    // Step 3: Apply replacements (reverse order to preserve indices)
649
    for replacement in replacements.into_iter().rev() {
650
        if is_single_node_replacement(&replacement.replacement) ||
651
           replacement.replacement.node_data.as_ref().is_empty() {
652
            apply_single_node_replacement(
653
                styled_dom,
654
                replacement.node_idx,
655
                &replacement.replacement
656
            );
657
        } else {
658
            apply_multi_node_replacement(
659
                styled_dom,
660
                replacement.node_idx,
661
                replacement.replacement
662
            );
663
        }
664
    }
665
}
666

            
667
// FFI Option Types
668

            
669
impl_option!(
670
    IconProviderHandle,
671
    OptionIconProviderHandle,
672
    [Clone]
673
);