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

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

            
50
#[cfg(not(feature = "std"))]
51
use self::nostd_lock::Mutex;
52

            
53
/// Minimal `no_std` spinlock that mirrors the slice of the `std::sync::Mutex`
54
/// API actually used by this module (`new` + `lock` returning a `Result`).
55
#[cfg(not(feature = "std"))]
56
mod nostd_lock {
57
    use core::cell::UnsafeCell;
58
    use core::ops::{Deref, DerefMut};
59
    use core::sync::atomic::{AtomicBool, Ordering};
60

            
61
    pub struct Mutex<T> {
62
        locked: AtomicBool,
63
        data: UnsafeCell<T>,
64
    }
65

            
66
    unsafe impl<T: Send> Send for Mutex<T> {}
67
    unsafe impl<T: Send> Sync for Mutex<T> {}
68

            
69
    pub struct MutexGuard<'a, T> {
70
        lock: &'a Mutex<T>,
71
    }
72

            
73
    impl<T> Mutex<T> {
74
        pub fn new(data: T) -> Self {
75
            Mutex { locked: AtomicBool::new(false), data: UnsafeCell::new(data) }
76
        }
77

            
78
        /// Returns `Ok(guard)` to mirror `std::sync::Mutex::lock`. Never poisons.
79
        pub fn lock(&self) -> Result<MutexGuard<'_, T>, core::convert::Infallible> {
80
            while self
81
                .locked
82
                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
83
                .is_err()
84
            {
85
                core::hint::spin_loop();
86
            }
87
            Ok(MutexGuard { lock: self })
88
        }
89
    }
90

            
91
    impl<'a, T> Deref for MutexGuard<'a, T> {
92
        type Target = T;
93
        fn deref(&self) -> &T {
94
            unsafe { &*self.lock.data.get() }
95
        }
96
    }
97

            
98
    impl<'a, T> DerefMut for MutexGuard<'a, T> {
99
        fn deref_mut(&mut self) -> &mut T {
100
            unsafe { &mut *self.lock.data.get() }
101
        }
102
    }
103

            
104
    impl<'a, T> Drop for MutexGuard<'a, T> {
105
        fn drop(&mut self) {
106
            self.lock.locked.store(false, Ordering::Release);
107
        }
108
    }
109
}
110

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

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

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

            
123
// Icon Resolver Callback
124

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

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

            
153
// Icon Provider Inner (single mutex)
154

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

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

            
174
// Icon Provider Handle
175

            
176
/// Icon provider stored in AppConfig.
177
///
178
/// This is a Box<IconProviderInner> for C FFI compatibility.
179
/// When App::run() is called, it gets converted to Arc<Mutex<IconProviderInner>>
180
/// and cloned to each window.
181
///
182
/// Icons are stored in a nested map: pack_name → (icon_name → RefAny)
183
/// This allows:
184
/// - Multiple packs with different sources (app-images, material-icons, etc.)
185
/// - Easy unregistration of entire packs
186
/// - First-match-wins lookup across all packs
187
#[repr(C)]
188
pub struct IconProviderHandle {
189
    /// Boxed inner data - Box<T> is repr(C) compatible (single pointer)
190
    pub inner: Box<IconProviderInner>,
191
}
192

            
193
impl Clone for IconProviderHandle {
194
    fn clone(&self) -> Self {
195
        Self { inner: Box::new((*self.inner).clone()) }
196
    }
197
}
198

            
199
impl fmt::Debug for IconProviderHandle {
200
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201
        let pack_count = self.inner.icons.len();
202
        let icon_count: usize = self.inner.icons.values().map(|p| p.len()).sum();
203
        
204
        f.debug_struct("IconProviderHandle")
205
            .field("pack_count", &pack_count)
206
            .field("icon_count", &icon_count)
207
            .finish()
208
    }
209
}
210

            
211
impl Default for IconProviderHandle {
212
    fn default() -> Self {
213
        Self::new()
214
    }
215
}
216

            
217
impl IconProviderHandle {
218
    /// Create a new empty icon provider with the default (no-op) resolver.
219
    /// 
220
    /// Note: The default resolver in core crate returns an empty StyledDom.
221
    /// Use `set_resolver()` to set a proper resolver from the layout crate,
222
    /// or use `with_resolver()` to create with a custom resolver.
223
68
    pub fn new() -> Self {
224
68
        Self {
225
68
            inner: Box::new(IconProviderInner {
226
68
                icons: BTreeMap::new(),
227
68
                resolver: default_icon_resolver,
228
68
            })
229
68
        }
230
68
    }
231

            
232
    /// Create with a custom resolver callback
233
42
    pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
234
42
        Self {
235
42
            inner: Box::new(IconProviderInner {
236
42
                icons: BTreeMap::new(),
237
42
                resolver,
238
42
            })
239
42
        }
240
42
    }
241
    
242
    /// Convert this handle into an Arc<Mutex<IconProviderInner>> for use in windows.
243
    ///
244
    /// This consumes the Box and creates an Arc. Called by App::run() to create
245
    /// the shared icon provider that gets cloned to each window.
246
    pub(crate) fn into_shared(self) -> Arc<Mutex<IconProviderInner>> {
247
        Arc::new(Mutex::new(*self.inner))
248
    }
249

            
250
    /// Set the resolver callback
251
    pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
252
        self.inner.resolver = resolver;
253
    }
254

            
255
    /// Register a single icon in a pack (creates pack if needed).
256
    ///
257
    /// Note: `pack_name` is case-sensitive, while `icon_name` is normalized to lowercase.
258
68
    pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
259
68
        let pack = self.inner.icons
260
68
            .entry(pack_name.to_string())
261
68
            .or_default();
262
68
        pack.insert(icon_name.to_lowercase(), data);
263
68
    }
264

            
265
    /// Unregister a single icon from a pack
266
17
    pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
267
17
        if let Some(pack) = self.inner.icons.get_mut(pack_name) {
268
17
            pack.remove(&icon_name.to_lowercase());
269
17
            if pack.is_empty() {
270
17
                self.inner.icons.remove(pack_name);
271
17
            }
272
        }
273
17
    }
274

            
275
    /// Unregister an entire icon pack
276
17
    pub fn unregister_pack(&mut self, pack_name: &str) {
277
17
        self.inner.icons.remove(pack_name);
278
17
    }
279

            
280
    /// Look up an icon across all packs, returning the pack name and data reference (first match wins)
281
17
    fn lookup_with_pack(&self, icon_name: &str) -> Option<(&str, &RefAny)> {
282
17
        let icon_name_lower = icon_name.to_lowercase();
283
17
        for (pack_name, pack) in self.inner.icons.iter() {
284
17
            if let Some(data) = pack.get(&icon_name_lower) {
285
17
                return Some((pack_name.as_str(), data));
286
            }
287
        }
288
        None
289
17
    }
290

            
291
    /// Look up an icon across all packs (first match wins)
292
17
    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
293
17
        self.lookup_with_pack(icon_name).map(|(_, data)| data.clone())
294
17
    }
295

            
296
    /// Check if an icon exists in any pack
297
153
    pub fn has_icon(&self, icon_name: &str) -> bool {
298
153
        let icon_name_lower = icon_name.to_lowercase();
299
153
        self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
300
153
    }
301

            
302
    /// List all pack names
303
93
    pub fn list_packs(&self) -> Vec<String> {
304
93
        self.inner.icons.keys().cloned().collect()
305
93
    }
306

            
307
    /// List all icon names in a specific pack
308
    pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
309
        self.inner.icons.get(pack_name)
310
            .map(|pack| pack.keys().cloned().collect())
311
            .unwrap_or_default()
312
    }
313

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

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

            
320
        // Report registered packs
321
        result.push_str(&format!("  Total packs: {}\n", self.inner.icons.len()));
322
        for (pack_name, pack) in self.inner.icons.iter() {
323
            result.push_str(&format!("    Pack '{}': {} icons\n", pack_name, pack.len()));
324
            for (name, _) in pack.iter() {
325
                result.push_str(&format!("      - {}\n", name));
326
            }
327
        }
328

            
329
        // Find the icon using shared lookup helper
330
        match self.lookup_with_pack(icon_name) {
331
            Some((pack, data)) => {
332
                result.push_str(&format!("\n  FOUND in pack '{}'\n", pack));
333
                let type_name = data.get_type_name();
334
                result.push_str(&format!("  RefAny type_name: '{}'\n", type_name.as_str()));
335

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

            
339
                let type_str = type_name.as_str();
340
                if type_str.contains(IMAGE_ICON_DATA_TYPE_NAME) {
341
                    result.push_str("  RefAny type: ImageIconData (image-based icon)\n");
342
                } else if type_str.contains(FONT_ICON_DATA_TYPE_NAME) {
343
                    result.push_str("  RefAny type: FontIconData (font-based icon)\n");
344
                } else {
345
                    result.push_str(&format!("  RefAny type: UNKNOWN ('{}')\n", type_str));
346
                }
347
            }
348
            None => {
349
                result.push_str("\n  NOT FOUND in any pack\n");
350
            }
351
        }
352

            
353
        AzString::from(result)
354
    }
355
}
356

            
357
/// Thread-safe icon provider for use in windows.
358
/// 
359
/// This is created from IconProviderHandle::into_shared() in App::run()
360
/// and cloned to each window.
361
#[derive(Clone)]
362
pub struct SharedIconProvider {
363
    inner: Arc<Mutex<IconProviderInner>>,
364
}
365

            
366
impl SharedIconProvider {
367
    /// Create from an IconProviderHandle (consumes the handle)
368
    pub fn from_handle(handle: IconProviderHandle) -> Self {
369
        Self { inner: handle.into_shared() }
370
    }
371
    
372
    /// Resolve an icon to a StyledDom using the registered callback
373
    pub fn resolve(
374
        &self, 
375
        original_icon_dom: &StyledDom,
376
        icon_name: &str,
377
        system_style: &SystemStyle,
378
    ) -> StyledDom {
379
        let (resolver, lookup_result) = {
380
            let guard = match self.inner.lock() {
381
                Ok(g) => g,
382
                Err(_) => return StyledDom::default(),
383
            };
384
            
385
            let resolver = guard.resolver;
386
            let icon_name_lower = icon_name.to_lowercase();
387
            
388
            let lookup_result = guard.icons.values()
389
                .find_map(|pack| pack.get(&icon_name_lower).cloned());
390
            
391
            (resolver, lookup_result)
392
        };
393
        
394
        resolver(lookup_result.into(), original_icon_dom, system_style)
395
    }
396
    
397
    /// Look up an icon across all packs
398
    pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
399
        let icon_name_lower = icon_name.to_lowercase();
400
        self.inner.lock().ok().and_then(|guard| {
401
            for pack in guard.icons.values() {
402
                if let Some(data) = pack.get(&icon_name_lower) {
403
                    return Some(data.clone());
404
                }
405
            }
406
            None
407
        })
408
    }
409
    
410
    /// Check if an icon exists
411
    pub fn has_icon(&self, icon_name: &str) -> bool {
412
        let icon_name_lower = icon_name.to_lowercase();
413
        self.inner.lock()
414
            .map(|guard| guard.icons.values().any(|p| p.contains_key(&icon_name_lower)))
415
            .unwrap_or(false)
416
    }
417
}
418

            
419
// Icon Resolution in StyledDom
420

            
421
/// Collected icon node info for replacement
422
struct CollectedIcon {
423
    /// Index in the node_data array
424
    node_idx: usize,
425
    /// The icon name
426
    icon_name: AzString,
427
}
428

            
429
/// Replacement result after resolving an icon
430
struct IconReplacement {
431
    /// Index of the icon node to replace
432
    node_idx: usize,
433
    /// The resolved StyledDom (may be empty, single node, or multi-node tree)
434
    replacement: StyledDom,
435
}
436

            
437
/// Collect all Icon nodes from the StyledDom
438
fn collect_icon_nodes(styled_dom: &StyledDom) -> Vec<CollectedIcon> {
439
    let mut icons = Vec::new();
440
    
441
    let node_data = styled_dom.node_data.as_ref();
442
    for (idx, node) in node_data.iter().enumerate() {
443
        if let NodeType::Icon(icon_name) = node.get_node_type() {
444
            icons.push(CollectedIcon {
445
                node_idx: idx,
446
                icon_name: icon_name.clone_self(),
447
            });
448
        }
449
    }
450
    
451
    icons
452
}
453

            
454
/// Extract a single-node StyledDom from a parent StyledDom at the given index.
455
/// This creates a minimal StyledDom containing just that node for the resolver.
456
fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
457
    use crate::dom::{NodeDataVec, DomId};
458
    use crate::id::NodeId;
459
    use crate::styled_dom::{
460
        StyledNodeVec, NodeHierarchyItemIdVec, TagIdToNodeIdMappingVec,
461
        NodeHierarchyItemVec, NodeHierarchyItem, NodeHierarchyItemId,
462
        ParentWithNodeDepthVec, ParentWithNodeDepth,
463
    };
464
    use crate::style::{CascadeInfoVec, CascadeInfo};
465
    use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
466
    
467
    let node_data = styled_dom.node_data.as_ref();
468
    let styled_nodes = styled_dom.styled_nodes.as_ref();
469
    
470
    if node_idx >= node_data.len() {
471
        return StyledDom::default();
472
    }
473
    
474
    // Clone the single node
475
    let single_node = node_data[node_idx].clone();
476
    let single_styled = if node_idx < styled_nodes.len() {
477
        styled_nodes[node_idx].clone()
478
    } else {
479
        crate::styled_dom::StyledNode::default()
480
    };
481
    
482
    StyledDom {
483
        root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
484
        node_hierarchy: NodeHierarchyItemVec::from_vec(vec![NodeHierarchyItem {
485
            parent: 0,
486
            previous_sibling: 0,
487
            next_sibling: 0,
488
            last_child: 0,
489
        }]),
490
        node_data: NodeDataVec::from_vec(vec![single_node]),
491
        styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
492
        cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
493
        nodes_with_window_callbacks: NodeHierarchyItemIdVec::from_vec(Vec::new()),
494
        nodes_with_datasets: NodeHierarchyItemIdVec::from_vec(Vec::new()),
495
        tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
496
        non_leaf_nodes: ParentWithNodeDepthVec::from_vec(Vec::new()),
497
        css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
498
        dom_id: DomId::ROOT_ID,
499
    }
500
}
501

            
502
/// Resolve all collected icons to their StyledDom representations
503
fn resolve_collected_icons(
504
    icons: &[CollectedIcon],
505
    styled_dom: &StyledDom,
506
    provider: &SharedIconProvider,
507
    system_style: &SystemStyle,
508
) -> Vec<IconReplacement> {
509
    icons.iter().map(|icon| {
510
        // Extract the original icon node as a StyledDom
511
        let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
512
        let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
513
        IconReplacement {
514
            node_idx: icon.node_idx,
515
            replacement,
516
        }
517
    }).collect()
518
}
519

            
520
/// Check if a replacement is a single-node replacement (fast path)
521
fn is_single_node_replacement(replacement: &StyledDom) -> bool {
522
    replacement.node_data.as_ref().len() == 1
523
}
524

            
525
/// Apply a single-node replacement (fast path: swap NodeType and copy properties)
526
fn apply_single_node_replacement(
527
    styled_dom: &mut StyledDom,
528
    node_idx: usize,
529
    replacement: &StyledDom,
530
) {
531
    if replacement.node_data.as_ref().is_empty() {
532
        // Empty replacement - convert to empty div
533
        let node_data = styled_dom.node_data.as_mut();
534
        if let Some(node) = node_data.get_mut(node_idx) {
535
            node.set_node_type(NodeType::Div);
536
        }
537
    } else {
538
        // Get the root node from the replacement and copy its properties
539
        let replacement_root = &replacement.node_data.as_ref()[0];
540
        let replacement_node_type = replacement_root.get_node_type().clone();
541
        
542
        let node_data = styled_dom.node_data.as_mut();
543
        if let Some(node) = node_data.get_mut(node_idx) {
544
            // Swap node type
545
            node.set_node_type(replacement_node_type);
546
            
547
            // Copy inline style from replacement
548
            node.set_style(replacement_root.get_style().clone());
549
            
550
            // Copy accessibility info if present
551
            if let Some(a11y) = replacement_root.get_accessibility_info() {
552
                node.set_accessibility_info(*a11y.clone());
553
            }
554
        }
555
        
556
        // Also update the styled_nodes to reflect the new styling
557
        if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
558
            let styled_nodes = styled_dom.styled_nodes.as_mut();
559
            if let Some(styled) = styled_nodes.get_mut(node_idx) {
560
                *styled = replacement_styled.clone();
561
            }
562
        }
563
    }
564
}
565

            
566
/// Apply multi-node replacement using subtree splicing
567
fn apply_multi_node_replacement(
568
    styled_dom: &mut StyledDom,
569
    node_idx: usize,
570
    replacement: StyledDom,
571
) {
572
    let replacement_len = replacement.node_data.as_ref().len();
573
    if replacement_len == 0 {
574
        let node_data = styled_dom.node_data.as_mut();
575
        if let Some(node) = node_data.get_mut(node_idx) {
576
            node.set_node_type(NodeType::Div);
577
        }
578
        return;
579
    }
580
    
581
    // For now, just apply the root node (same as single-node)
582
    apply_single_node_replacement(styled_dom, node_idx, &replacement);
583
    
584
    if replacement_len > 1 {
585
        // TODO: Full subtree splicing requires inserting nodes into arrays
586
        #[cfg(all(debug_assertions, feature = "std"))]
587
        eprintln!(
588
            "Warning: Icon replacement has {} nodes, only root node used.",
589
            replacement_len
590
        );
591
    }
592
}
593

            
594
/// Resolve all Icon nodes in a StyledDom to their actual content.
595
///
596
/// This function:
597
/// 1. Collects all Icon nodes from the StyledDom
598
/// 2. Resolves each icon via the provider's callback (passing original icon DOM)
599
/// 3. Applies replacements (single-node fast path or multi-node splicing)
600
///
601
/// This should be called after StyledDom creation but before layout.
602
pub fn resolve_icons_in_styled_dom(
603
    styled_dom: &mut StyledDom,
604
    provider: &SharedIconProvider,
605
    system_style: &SystemStyle,
606
) {
607
    // Step 1: Collect all icon nodes
608
    let icons = collect_icon_nodes(styled_dom);
609

            
610
    if icons.is_empty() {
611
        return;
612
    }
613

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

            
618
    // Step 3: Apply replacements (reverse order to preserve indices)
619
    for replacement in replacements.into_iter().rev() {
620
        if is_single_node_replacement(&replacement.replacement) ||
621
           replacement.replacement.node_data.as_ref().is_empty() {
622
            apply_single_node_replacement(
623
                styled_dom,
624
                replacement.node_idx,
625
                &replacement.replacement
626
            );
627
        } else {
628
            apply_multi_node_replacement(
629
                styled_dom,
630
                replacement.node_idx,
631
                replacement.replacement
632
            );
633
        }
634
    }
635
}
636

            
637
// FFI Option Types
638

            
639
impl_option!(
640
    IconProviderHandle,
641
    OptionIconProviderHandle,
642
    [Clone]
643
);