1use 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#[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 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 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
128const IMAGE_ICON_DATA_TYPE_NAME: &str = "ImageIconData";
130const FONT_ICON_DATA_TYPE_NAME: &str = "FontIconData";
131
132pub type IconResolverCallbackType = extern "C" fn(
147 icon_data: OptionRefAny,
148 original_icon_dom: &StyledDom,
149 system_style: &SystemStyle,
150) -> StyledDom;
151
152#[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 StyledDom::default()
160}
161
162#[derive(Debug, Clone)]
166pub struct IconProviderInner {
167 pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
170 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#[repr(C)]
197pub struct IconProviderHandle {
198 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 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 #[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 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 pub(crate) fn into_shared(mut self) -> Arc<Mutex<IconProviderInner>> {
281 let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
284 self.run_destructor = false;
285 Arc::new(Mutex::new(*inner))
286 }
287
288 pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
290 self.inner.resolver = resolver;
291 }
292
293 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 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 pub fn unregister_pack(&mut self, pack_name: &str) {
315 self.inner.icons.remove(pack_name);
316 }
317
318 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 #[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 #[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 #[must_use] pub fn list_packs(&self) -> Vec<String> {
342 self.inner.icons.keys().cloned().collect()
343 }
344
345 #[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 #[allow(clippy::used_underscore_binding)] #[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 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 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#[derive(Debug, Clone)]
403pub struct SharedIconProvider {
404 inner: Arc<Mutex<IconProviderInner>>,
405}
406
407impl SharedIconProvider {
408 #[must_use] pub fn from_handle(handle: IconProviderHandle) -> Self {
410 Self { inner: handle.into_shared() }
411 }
412
413 #[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 #[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 #[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
459struct CollectedIcon {
463 node_idx: usize,
465 icon_name: AzString,
467}
468
469struct IconReplacement {
471 node_idx: usize,
473 replacement: StyledDom,
475}
476
477fn 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
494fn 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 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
542fn 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 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
560fn is_single_node_replacement(replacement: &StyledDom) -> bool {
562 replacement.node_data.as_ref().len() == 1
563}
564
565fn 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 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 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 node.set_node_type(replacement_node_type);
586
587 node.set_style(replacement_root.get_style().clone());
589
590 if let Some(a11y) = replacement_root.get_accessibility_info() {
592 node.set_accessibility_info(a11y.clone());
593 }
594 }
595
596 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
606fn 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 apply_single_node_replacement(styled_dom, node_idx, replacement);
623
624 if replacement_len > 1 {
625 #[cfg(all(debug_assertions, feature = "std"))]
627 eprintln!(
628 "Warning: Icon replacement has {replacement_len} nodes, only root node used."
629 );
630 }
631}
632
633pub fn resolve_icons_in_styled_dom(
642 styled_dom: &mut StyledDom,
643 provider: &SharedIconProvider,
644 system_style: &SystemStyle,
645) {
646 let icons = collect_icon_nodes(styled_dom);
648
649 if icons.is_empty() {
650 return;
651 }
652
653 let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
656
657 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
676impl_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 #[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 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}"), String::from("e\u{0301}\u{0301}"), String::from("\u{202e}RTL\u{202d}"), String::from("\u{130}"), String::from("\u{FFFD}\u{10FFFF}"), "[".repeat(10_000), "x".repeat(100_000), ]
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 fn zero_node_styled_dom() -> StyledDom {
747 let mut sd = StyledDom::default();
748 sd.node_data = NodeDataVec::from_vec(Vec::new());
749 sd.styled_nodes = StyledNodeVec::from_vec(Vec::new());
750 sd
751 }
752
753 fn node_type_at(sd: &StyledDom, idx: usize) -> NodeType {
754 sd.node_data.as_ref()[idx].get_node_type().clone()
755 }
756
757 fn icon_indices(sd: &StyledDom) -> Vec<usize> {
758 collect_icon_nodes(sd).iter().map(|i| i.node_idx).collect()
759 }
760
761 extern "C" fn div_resolver(
764 _icon_data: OptionRefAny,
765 _original_icon_dom: &StyledDom,
766 _system_style: &SystemStyle,
767 ) -> StyledDom {
768 StyledDom::create_from_dom(Dom::create_div())
769 }
770
771 extern "C" fn zero_node_resolver(
772 _icon_data: OptionRefAny,
773 _original_icon_dom: &StyledDom,
774 _system_style: &SystemStyle,
775 ) -> StyledDom {
776 zero_node_styled_dom()
777 }
778
779 static REC_CALLS: AtomicUsize = AtomicUsize::new(0);
783 static REC_SAW_DATA: AtomicBool = AtomicBool::new(false);
784 static REC_SAW_ICON_NODE: AtomicBool = AtomicBool::new(false);
785 static REC_NAME_LEN: AtomicUsize = AtomicUsize::new(0);
786
787 extern "C" fn recording_resolver(
788 icon_data: OptionRefAny,
789 original_icon_dom: &StyledDom,
790 _system_style: &SystemStyle,
791 ) -> StyledDom {
792 REC_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
793 if matches!(icon_data, OptionRefAny::Some(_)) {
794 REC_SAW_DATA.store(true, AtomicOrdering::SeqCst);
795 }
796 if let Some(node) = original_icon_dom.node_data.as_ref().first() {
797 if let NodeType::Icon(name) = node.get_node_type() {
798 REC_SAW_ICON_NODE.store(true, AtomicOrdering::SeqCst);
799 REC_NAME_LEN.store(name.as_ref().as_str().len(), AtomicOrdering::SeqCst);
800 }
801 }
802 StyledDom::create_from_dom(Dom::create_div())
803 }
804
805 #[test]
808 fn mutex_new_then_lock_roundtrips_the_value() {
809 let m = Mutex::new(42u32);
810 assert_eq!(*m.lock().unwrap(), 42);
811 *m.lock().unwrap() = u32::MAX;
812 assert_eq!(*m.lock().unwrap(), u32::MAX);
813 }
814
815 #[test]
816 fn mutex_lock_on_empty_and_large_payloads() {
817 let empty: Mutex<Vec<u8>> = Mutex::new(Vec::new());
818 assert!(empty.lock().unwrap().is_empty());
819
820 let big = Mutex::new(vec![0u8; 1_000_000]);
821 assert_eq!(big.lock().unwrap().len(), 1_000_000);
822
823 for _ in 0..1_000 {
825 assert!(big.lock().is_ok());
826 }
827 }
828
829 #[test]
832 fn default_resolver_returns_one_body_node_for_none_and_some() {
833 let orig = StyledDom::default();
834 let style = SystemStyle::default();
835
836 let none = default_icon_resolver(OptionRefAny::None, &orig, &style);
837 assert_eq!(none.node_data.as_ref().len(), 1);
840 assert!(is_single_node_replacement(&none));
841
842 let some = default_icon_resolver(
843 OptionRefAny::Some(RefAny::new(TestIconData { id: 1 })),
844 &orig,
845 &style,
846 );
847 assert_eq!(some.node_data.as_ref().len(), 1);
848 }
849
850 #[test]
851 fn default_resolver_no_panic_on_zero_node_original_dom() {
852 let orig = zero_node_styled_dom();
853 let style = SystemStyle::default();
854 let out = default_icon_resolver(OptionRefAny::None, &orig, &style);
855 assert_eq!(out.node_data.as_ref().len(), 1);
856 }
857
858 #[test]
861 fn new_handle_is_empty_and_all_queries_are_negative() {
862 let h = IconProviderHandle::new();
863 assert!(h.list_packs().is_empty());
864 assert!(h.list_icons_in_pack("anything").is_empty());
865 assert!(!h.has_icon("home"));
866 assert!(h.lookup("home").is_none());
867 assert!(h.lookup_with_pack("home").is_none());
868 assert!(h.debug_lookup("home").as_str().contains("Total packs: 0"));
869 }
870
871 #[test]
872 fn default_handle_matches_new_handle() {
873 let a = IconProviderHandle::default();
874 let b = IconProviderHandle::new();
875 assert_eq!(a.list_packs(), b.list_packs());
876 assert_eq!(a.has_icon(""), b.has_icon(""));
877 }
878
879 #[test]
880 fn with_resolver_installs_the_callback() {
881 let mut h = IconProviderHandle::with_resolver(div_resolver);
882 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
883 let shared = SharedIconProvider::from_handle(h);
884 let out = shared.resolve(&StyledDom::default(), "home", &SystemStyle::default());
885 assert!(matches!(node_type_at(&out, 0), NodeType::Div));
886 }
887
888 #[test]
889 fn set_resolver_overrides_the_default_resolver() {
890 let mut h = IconProviderHandle::new();
891 h.set_resolver(div_resolver);
892 let shared = SharedIconProvider::from_handle(h);
893 let out = shared.resolve(&StyledDom::default(), "missing", &SystemStyle::default());
895 assert!(matches!(node_type_at(&out, 0), NodeType::Div));
896 }
897
898 #[test]
899 fn clone_of_handle_is_deep() {
900 let mut a = IconProviderHandle::new();
901 a.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
902
903 let mut b = a.clone();
904 b.register_icon("p", "settings", RefAny::new(TestIconData { id: 2 }));
905 b.unregister_icon("p", "home");
906
907 assert!(a.has_icon("home"));
908 assert!(!a.has_icon("settings"));
909 assert!(b.has_icon("settings"));
910 assert!(!b.has_icon("home"));
911 }
912
913 #[test]
914 fn drop_of_clones_and_originals_is_safe() {
915 let mut a = IconProviderHandle::new();
917 a.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
918 for _ in 0..100 {
919 let c = a.clone();
920 drop(c);
921 }
922 assert!(a.has_icon("home"));
923 drop(a);
924 }
925
926 #[test]
929 fn register_icon_lowercases_icon_name_but_not_pack_name() {
930 let mut h = IconProviderHandle::new();
931 h.register_icon("MyPack", "HoMe", RefAny::new(TestIconData { id: 1 }));
932
933 assert_eq!(h.list_packs(), vec![String::from("MyPack")]);
934 assert!(h.list_icons_in_pack("MyPack").contains(&String::from("home")));
935 assert!(h.list_icons_in_pack("mypack").is_empty());
937 assert!(h.has_icon("HOME"));
939 assert!(h.has_icon("home"));
940 assert!(h.has_icon("hOmE"));
941 }
942
943 #[test]
944 fn registering_the_same_icon_twice_overwrites_instead_of_duplicating() {
945 let mut h = IconProviderHandle::new();
946 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
947 h.register_icon("p", "HOME", RefAny::new(TestIconData { id: 2 }));
948
949 assert_eq!(h.list_icons_in_pack("p").len(), 1);
950 let mut data = h.lookup("home").expect("icon must exist");
951 assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 2);
952 }
953
954 #[test]
955 fn unregister_icon_drops_the_pack_once_it_is_empty() {
956 let mut h = IconProviderHandle::new();
957 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
958 h.register_icon("p", "settings", RefAny::new(TestIconData { id: 2 }));
959
960 h.unregister_icon("p", "HOME"); assert_eq!(h.list_packs(), vec![String::from("p")]);
962 assert!(!h.has_icon("home"));
963
964 h.unregister_icon("p", "settings");
965 assert!(h.list_packs().is_empty(), "pack must be pruned when empty");
966 }
967
968 #[test]
969 fn unregister_of_unknown_pack_or_icon_is_a_no_op() {
970 let mut h = IconProviderHandle::new();
971 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
972
973 h.unregister_icon("nonexistent-pack", "home");
974 h.unregister_icon("p", "nonexistent-icon");
975 h.unregister_pack("nonexistent-pack");
976 h.unregister_pack("");
977 h.unregister_icon("", "");
978
979 assert!(h.has_icon("home"));
980 assert_eq!(h.list_packs().len(), 1);
981 }
982
983 #[test]
984 fn unregister_pack_removes_all_of_its_icons() {
985 let mut h = IconProviderHandle::new();
986 h.register_icon("p", "a", RefAny::new(TestIconData { id: 1 }));
987 h.register_icon("p", "b", RefAny::new(TestIconData { id: 2 }));
988 h.register_icon("q", "a", RefAny::new(TestIconData { id: 3 }));
989
990 h.unregister_pack("p");
991 assert_eq!(h.list_packs(), vec![String::from("q")]);
992 assert!(h.has_icon("a"));
994 assert!(!h.has_icon("b"));
995 }
996
997 #[test]
998 fn adversarial_names_roundtrip_through_register_lookup_unregister() {
999 for (i, name) in adversarial_names().iter().enumerate() {
1000 let mut h = IconProviderHandle::new();
1001 h.register_icon("p", name, RefAny::new(TestIconData { id: i as u32 }));
1002
1003 assert!(h.has_icon(name), "has_icon failed for name #{i}");
1004 let mut data = h.lookup(name).unwrap_or_else(|| panic!("lookup failed for name #{i}"));
1005 assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i as u32);
1006
1007 h.unregister_icon("p", name);
1008 assert!(!h.has_icon(name), "unregister failed for name #{i}");
1009 assert!(h.list_packs().is_empty());
1010 }
1011 }
1012
1013 #[test]
1014 fn empty_pack_name_and_empty_icon_name_are_legal_keys() {
1015 let mut h = IconProviderHandle::new();
1016 h.register_icon("", "", RefAny::new(TestIconData { id: 9 }));
1017
1018 assert_eq!(h.list_packs(), vec![String::new()]);
1019 assert_eq!(h.list_icons_in_pack(""), vec![String::new()]);
1020 assert!(h.has_icon(""));
1021 let (pack, _) = h.lookup_with_pack("").expect("empty key must be found");
1022 assert_eq!(pack, "");
1023 }
1024
1025 #[test]
1028 fn lookup_empty_input_returns_none() {
1029 let mut h = IconProviderHandle::new();
1030 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1031 assert!(h.lookup("").is_none());
1032 assert!(h.lookup_with_pack("").is_none());
1033 assert!(!h.has_icon(""));
1034 }
1035
1036 #[test]
1037 fn lookup_whitespace_only_is_not_trimmed() {
1038 let mut h = IconProviderHandle::new();
1039 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1040 for ws in [" ", "\t\n", "\r", "\u{a0}"] {
1041 assert!(h.lookup(ws).is_none(), "{ws:?} must not match");
1042 }
1043 h.register_icon("p", " ", RefAny::new(TestIconData { id: 2 }));
1045 assert!(h.lookup(" ").is_some());
1046 assert!(h.lookup(" ").is_none());
1047 assert!(h.lookup("").is_none());
1048 }
1049
1050 #[test]
1051 fn lookup_garbage_and_leading_trailing_junk_returns_none() {
1052 let mut h = IconProviderHandle::new();
1053 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1054
1055 for junk in [
1056 " home ",
1057 "home ",
1058 " home",
1059 "home;garbage",
1060 "home\0",
1061 "\0home",
1062 "ho\nme",
1063 "home/../../etc/passwd",
1064 "\u{1b}[31mhome\u{1b}[0m",
1065 "{\"icon\":\"home\"}",
1066 ] {
1067 assert!(h.lookup(junk).is_none(), "{junk:?} must not match 'home'");
1068 assert!(!h.has_icon(junk));
1069 }
1070 assert!(h.lookup("home").is_some(), "positive control");
1071 }
1072
1073 #[test]
1074 fn lookup_of_extremely_long_name_terminates_and_matches_exactly() {
1075 let mut h = IconProviderHandle::new();
1076 let long = "x".repeat(1_000_000);
1077 h.register_icon("p", &long, RefAny::new(TestIconData { id: 7 }));
1078
1079 assert!(h.lookup(&long).is_some());
1080 assert!(h.has_icon(&long));
1081 assert!(h.lookup(&"x".repeat(999_999)).is_none());
1083 let mut h2 = IconProviderHandle::new();
1085 h2.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1086 assert!(h2.lookup(&long).is_none());
1087 }
1088
1089 #[test]
1090 fn lookup_of_boundary_numeric_strings_is_deterministic() {
1091 let mut h = IconProviderHandle::new();
1092 for (i, n) in ["0", "-0", "9223372036854775807", "-9223372036854775808", "nan", "inf"]
1093 .iter()
1094 .enumerate()
1095 {
1096 h.register_icon("p", n, RefAny::new(TestIconData { id: i as u32 }));
1097 }
1098 assert!(h.lookup("0").is_some());
1100 assert!(h.lookup("-0").is_some());
1101 assert!(h.lookup("0.0").is_none());
1102 assert!(h.lookup("00").is_none());
1103 assert!(h.lookup("+0").is_none());
1104 assert!(h.lookup("9223372036854775808").is_none()); assert!(h.lookup("NaN").is_some());
1107 assert!(h.lookup("INF").is_some());
1108 }
1109
1110 #[test]
1111 fn lookup_of_unicode_names_folds_case_without_panicking() {
1112 let mut h = IconProviderHandle::new();
1113 h.register_icon("p", "\u{1F600}", RefAny::new(TestIconData { id: 1 }));
1114 h.register_icon("p", "\u{C4}", RefAny::new(TestIconData { id: 2 })); h.register_icon("p", "I", RefAny::new(TestIconData { id: 3 }));
1116
1117 assert!(h.lookup("\u{1F600}").is_some(), "emoji key must round-trip");
1118 assert!(h.lookup("\u{E4}").is_some(), "ä must match registered Ä");
1119 assert!(h.lookup("i").is_some(), "I folds to i");
1120
1121 let mut h2 = IconProviderHandle::new();
1124 h2.register_icon("p", "\u{130}", RefAny::new(TestIconData { id: 4 }));
1125 assert!(h2.lookup("\u{130}").is_some(), "self-lookup must still work");
1126 let keys = h2.list_icons_in_pack("p");
1127 assert_eq!(keys, vec![String::from("\u{130}").to_lowercase()]);
1128 assert!(!keys.contains(&String::from("\u{130}")), "key is stored folded, not verbatim");
1129 assert!(h2.lookup("i").is_none(), "the bare ASCII 'i' must not match İ");
1130 }
1131
1132 #[test]
1133 fn lookup_of_deeply_nested_input_does_not_stack_overflow() {
1134 let h = IconProviderHandle::new();
1135 for depth in [1_000usize, 10_000, 100_000] {
1138 let nested = "[".repeat(depth);
1139 assert!(h.lookup(&nested).is_none());
1140 assert!(!h.has_icon(&nested));
1141 assert!(h.debug_lookup(&nested).as_str().contains("NOT FOUND"));
1142 }
1143 }
1144
1145 #[test]
1146 fn lookup_valid_minimal_positive_control_roundtrips_the_payload() {
1147 let mut h = IconProviderHandle::new();
1148 h.register_icon("p", "a", RefAny::new(TestIconData { id: 123 }));
1149
1150 let mut data = h.lookup("a").expect("registered icon must be found");
1151 assert_eq!(*data.downcast_ref::<TestIconData>().unwrap(), TestIconData { id: 123 });
1152 assert!(data.downcast_ref::<u64>().is_none());
1154 }
1155
1156 #[test]
1157 fn lookup_with_pack_first_match_is_the_lexicographically_first_pack() {
1158 let mut h = IconProviderHandle::new();
1159 h.register_icon("zzz", "home", RefAny::new(TestIconData { id: 26 }));
1161 h.register_icon("mmm", "home", RefAny::new(TestIconData { id: 13 }));
1162 h.register_icon("aaa", "home", RefAny::new(TestIconData { id: 1 }));
1163
1164 let (pack, _) = h.lookup_with_pack("HOME").expect("must be found");
1165 assert_eq!(pack, "aaa", "BTreeMap order => first match is the first pack by name");
1166
1167 let mut data = h.lookup("home").unwrap();
1168 assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 1);
1169
1170 h.unregister_pack("aaa");
1172 let (pack, _) = h.lookup_with_pack("home").unwrap();
1173 assert_eq!(pack, "mmm");
1174 }
1175
1176 #[test]
1179 fn has_icon_true_false_and_edge_inputs() {
1180 let mut h = IconProviderHandle::new();
1181 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1182
1183 assert!(h.has_icon("home"));
1184 assert!(!h.has_icon("definitely-not-registered"));
1185
1186 for name in adversarial_names() {
1187 assert!(!h.has_icon(&name));
1189 }
1190 assert!(h.has_icon("home"), "state unchanged by the queries above");
1191 }
1192
1193 #[test]
1196 fn list_packs_is_sorted_and_case_sensitive() {
1197 let mut h = IconProviderHandle::new();
1198 for p in ["zeta", "alpha", "Alpha", "mid", ""] {
1199 h.register_icon(p, "home", RefAny::new(TestIconData { id: 0 }));
1200 }
1201 assert_eq!(
1203 h.list_packs(),
1204 vec![
1205 String::new(),
1206 String::from("Alpha"),
1207 String::from("alpha"),
1208 String::from("mid"),
1209 String::from("zeta"),
1210 ]
1211 );
1212 }
1213
1214 #[test]
1215 fn list_icons_in_pack_returns_folded_keys_and_empty_for_unknown_packs() {
1216 let mut h = IconProviderHandle::new();
1217 h.register_icon("p", "Zoom", RefAny::new(TestIconData { id: 1 }));
1218 h.register_icon("p", "HOME", RefAny::new(TestIconData { id: 2 }));
1219
1220 assert_eq!(h.list_icons_in_pack("p"), vec![String::from("home"), String::from("zoom")]);
1221 assert!(h.list_icons_in_pack("P").is_empty());
1222 assert!(h.list_icons_in_pack("").is_empty());
1223 assert!(h.list_icons_in_pack(&"x".repeat(100_000)).is_empty());
1224 }
1225
1226 #[test]
1229 fn debug_lookup_reports_not_found_for_missing_icons() {
1230 let mut h = IconProviderHandle::new();
1231 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1232
1233 let out = h.debug_lookup("settings");
1234 let s = out.as_str();
1235 assert!(s.contains("NOT FOUND in any pack"));
1236 assert!(s.contains("Total packs: 1"));
1237 assert!(s.contains("Pack 'p': 1 icons"));
1238 }
1239
1240 #[test]
1241 fn debug_lookup_classifies_image_font_and_unknown_refany_types() {
1242 let mut h = IconProviderHandle::new();
1243 h.register_icon("p", "img", RefAny::new(ImageIconData { _w: 16 }));
1244 h.register_icon("p", "fnt", RefAny::new(FontIconData { _codepoint: 0xF015 }));
1245 h.register_icon("p", "other", RefAny::new(TestIconData { id: 1 }));
1246
1247 let img = h.debug_lookup("img");
1248 assert!(img.as_str().contains("FOUND in pack 'p'"));
1249 assert!(img.as_str().contains("RefAny type: ImageIconData"));
1250
1251 let fnt = h.debug_lookup("FNT"); assert!(fnt.as_str().contains("RefAny type: FontIconData"));
1253
1254 let other = h.debug_lookup("other");
1255 assert!(other.as_str().contains("RefAny type: UNKNOWN"));
1256 }
1257
1258 #[test]
1259 fn debug_lookup_survives_adversarial_names() {
1260 let mut h = IconProviderHandle::new();
1261 for (i, name) in adversarial_names().iter().enumerate() {
1262 h.register_icon("p", name, RefAny::new(TestIconData { id: i as u32 }));
1263 }
1264 for name in adversarial_names() {
1265 let out = h.debug_lookup(&name);
1266 assert!(out.as_str().contains("FOUND in pack 'p'"), "must find {name:?}");
1267 }
1268 assert!(h.debug_lookup("never-registered").as_str().contains("NOT FOUND"));
1269 }
1270
1271 #[test]
1274 fn from_handle_preserves_every_registered_icon() {
1275 let mut h = IconProviderHandle::new();
1276 for i in 0..64u32 {
1277 h.register_icon("p", &format!("icon{i}"), RefAny::new(TestIconData { id: i }));
1278 }
1279 let shared = SharedIconProvider::from_handle(h);
1280
1281 for i in 0..64u32 {
1282 let name = format!("ICON{i}");
1283 assert!(shared.has_icon(&name));
1284 let mut data = shared.lookup(&name).expect("must survive into_shared()");
1285 assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i);
1286 }
1287 assert!(!shared.has_icon("icon64"));
1288 assert!(shared.lookup("").is_none());
1289 }
1290
1291 #[test]
1292 fn shared_provider_lookup_and_has_icon_agree_on_adversarial_input() {
1293 let mut h = IconProviderHandle::new();
1294 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1295 let shared = SharedIconProvider::from_handle(h);
1296
1297 for name in adversarial_names() {
1298 assert_eq!(
1299 shared.has_icon(&name),
1300 shared.lookup(&name).is_some(),
1301 "has_icon/lookup disagree for {name:?}"
1302 );
1303 }
1304 assert!(shared.has_icon("HoMe") && shared.lookup("HoMe").is_some());
1305 }
1306
1307 #[test]
1308 fn shared_provider_clone_shares_the_same_icon_table() {
1309 let mut h = IconProviderHandle::new();
1310 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1311 let a = SharedIconProvider::from_handle(h);
1312 let b = a.clone();
1313
1314 assert!(b.has_icon("home"));
1315 drop(a);
1316 assert!(b.has_icon("home"), "clone must keep the Arc alive");
1317 let mut data = b.lookup("home").unwrap();
1318 assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, 1);
1319 }
1320
1321 #[test]
1322 fn shared_resolve_receives_icon_data_and_original_dom() {
1323 let mut h = IconProviderHandle::with_resolver(recording_resolver);
1324 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1325 let shared = SharedIconProvider::from_handle(h);
1326
1327 let original = styled_dom_with_icons(&["home"]);
1328 let icon_idx = icon_indices(&original)[0];
1329 let single = extract_single_node_styled_dom(&original, icon_idx);
1330
1331 let out = shared.resolve(&single, "HOME", &SystemStyle::default());
1332
1333 assert!(REC_CALLS.load(AtomicOrdering::SeqCst) >= 1);
1334 assert!(REC_SAW_DATA.load(AtomicOrdering::SeqCst), "case-folded lookup must pass Some(data)");
1335 assert!(REC_SAW_ICON_NODE.load(AtomicOrdering::SeqCst), "node 0 of the original dom is the Icon");
1336 assert_eq!(REC_NAME_LEN.load(AtomicOrdering::SeqCst), "home".len());
1337 assert!(matches!(node_type_at(&out, 0), NodeType::Div));
1338 }
1339
1340 #[test]
1341 fn shared_resolve_runs_the_resolver_even_when_the_icon_is_missing() {
1342 let h = IconProviderHandle::with_resolver(div_resolver);
1343 let shared = SharedIconProvider::from_handle(h);
1344 let huge = "x".repeat(100_000);
1346 for name in ["", "\u{1F600}", huge.as_str()] {
1347 let out = shared.resolve(&StyledDom::default(), name, &SystemStyle::default());
1348 assert!(matches!(node_type_at(&out, 0), NodeType::Div));
1349 }
1350 }
1351
1352 #[cfg(feature = "std")]
1353 #[test]
1354 fn shared_provider_survives_concurrent_lookups() {
1355 let mut h = IconProviderHandle::new();
1356 for i in 0..16u32 {
1357 h.register_icon("p", &format!("icon{i}"), RefAny::new(TestIconData { id: i }));
1358 }
1359 let shared = SharedIconProvider::from_handle(h);
1360
1361 let mut threads = Vec::new();
1362 for _ in 0..4 {
1363 let s = shared.clone();
1364 threads.push(std::thread::spawn(move || {
1365 let mut hits = 0usize;
1366 for i in 0..200u32 {
1367 let name = format!("icon{}", i % 16);
1368 if s.has_icon(&name) {
1369 hits += 1;
1370 }
1371 let mut data = s.lookup(&name).expect("registered icon");
1372 assert_eq!(data.downcast_ref::<TestIconData>().unwrap().id, i % 16);
1373 }
1374 hits
1375 }));
1376 }
1377 for t in threads {
1378 assert_eq!(t.join().unwrap(), 200);
1379 }
1380 assert!(shared.has_icon("icon0"), "table intact after contention");
1381 }
1382
1383 #[test]
1386 fn collect_icon_nodes_is_empty_when_there_are_no_icons() {
1387 assert!(collect_icon_nodes(&StyledDom::default()).is_empty());
1388 assert!(collect_icon_nodes(&zero_node_styled_dom()).is_empty());
1389 assert!(collect_icon_nodes(&StyledDom::create_from_dom(Dom::create_div())).is_empty());
1390 }
1391
1392 #[test]
1393 fn collect_icon_nodes_finds_every_icon_in_ascending_index_order_with_verbatim_names() {
1394 let names = ["HOME", "\u{1F600}", ""];
1395 let sd = styled_dom_with_icons(&names);
1396 let collected = collect_icon_nodes(&sd);
1397
1398 assert_eq!(collected.len(), names.len());
1399 for (i, c) in collected.iter().enumerate() {
1400 assert_eq!(c.icon_name.as_str(), names[i]);
1402 if i > 0 {
1403 assert!(c.node_idx > collected[i - 1].node_idx, "indices must ascend");
1404 }
1405 }
1406 }
1407
1408 #[test]
1409 fn collect_icon_nodes_handles_a_very_long_icon_name() {
1410 let long = "x".repeat(100_000);
1411 let sd = styled_dom_with_icons(&[&long]);
1412 let collected = collect_icon_nodes(&sd);
1413 assert_eq!(collected.len(), 1);
1414 assert_eq!(collected[0].icon_name.as_str().len(), 100_000);
1415 }
1416
1417 #[test]
1420 fn extract_single_node_at_index_zero() {
1421 let sd = styled_dom_with_icons(&["home"]);
1422 let out = extract_single_node_styled_dom(&sd, 0);
1423 assert_eq!(out.node_data.as_ref().len(), 1);
1424 assert_eq!(out.styled_nodes.as_ref().len(), 1);
1425 assert_eq!(node_type_at(&out, 0), node_type_at(&sd, 0));
1426 }
1427
1428 #[test]
1429 fn extract_single_node_of_the_icon_keeps_the_icon_node_type() {
1430 let sd = styled_dom_with_icons(&["home"]);
1431 let idx = icon_indices(&sd)[0];
1432 let out = extract_single_node_styled_dom(&sd, idx);
1433 assert_eq!(out.node_data.as_ref().len(), 1);
1434 assert!(matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1435 }
1436
1437 #[test]
1438 fn extract_single_node_out_of_bounds_falls_back_to_default_without_panicking() {
1439 let sd = styled_dom_with_icons(&["home"]);
1440 let len = sd.node_data.as_ref().len();
1441
1442 for idx in [len, len + 1, usize::MAX / 2, usize::MAX - 1, usize::MAX] {
1443 let out = extract_single_node_styled_dom(&sd, idx);
1444 assert_eq!(out.node_data.as_ref().len(), 1, "idx {idx} must not panic");
1446 assert!(!matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1447 }
1448 let empty = zero_node_styled_dom();
1450 assert_eq!(extract_single_node_styled_dom(&empty, 0).node_data.as_ref().len(), 1);
1451 }
1452
1453 #[test]
1454 fn extract_single_node_tolerates_styled_nodes_shorter_than_node_data() {
1455 let sd_full = styled_dom_with_icons(&["home"]);
1456 let idx = icon_indices(&sd_full)[0];
1457
1458 let mut sd = sd_full;
1459 sd.styled_nodes = StyledNodeVec::from_vec(Vec::new()); let out = extract_single_node_styled_dom(&sd, idx);
1462 assert_eq!(out.node_data.as_ref().len(), 1);
1463 assert_eq!(out.styled_nodes.as_ref().len(), 1, "must synthesize a default StyledNode");
1464 assert!(matches!(node_type_at(&out, 0), NodeType::Icon(_)));
1465 }
1466
1467 #[test]
1470 fn is_single_node_replacement_true_false_and_edges() {
1471 assert!(is_single_node_replacement(&StyledDom::default()));
1472 assert!(is_single_node_replacement(&StyledDom::create_from_dom(Dom::create_div())));
1473
1474 assert!(!is_single_node_replacement(&zero_node_styled_dom()));
1476
1477 let multi = StyledDom::create_from_dom(
1478 Dom::create_div().with_child(Dom::create_div()).with_child(Dom::create_div()),
1479 );
1480 assert!(multi.node_data.as_ref().len() > 1);
1481 assert!(!is_single_node_replacement(&multi));
1482 }
1483
1484 #[test]
1487 fn apply_single_node_replacement_with_zero_node_dom_turns_the_icon_into_a_div() {
1488 let mut sd = styled_dom_with_icons(&["home"]);
1489 let idx = icon_indices(&sd)[0];
1490 let empty = zero_node_styled_dom();
1491
1492 apply_single_node_replacement(&mut sd, idx, &empty);
1493 assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1494 assert!(collect_icon_nodes(&sd).is_empty());
1495 }
1496
1497 #[test]
1498 fn apply_single_node_replacement_copies_the_replacement_root_node_type() {
1499 let mut sd = styled_dom_with_icons(&["home"]);
1500 let idx = icon_indices(&sd)[0];
1501 let before_len = sd.node_data.as_ref().len();
1502 let repl = StyledDom::create_from_dom(Dom::create_div());
1503
1504 apply_single_node_replacement(&mut sd, idx, &repl);
1505 assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1506 assert_eq!(sd.node_data.as_ref().len(), before_len, "node count must not change");
1507 }
1508
1509 #[test]
1510 fn apply_single_node_replacement_out_of_bounds_index_is_a_no_op() {
1511 let repl = StyledDom::create_from_dom(Dom::create_div());
1512 let empty = zero_node_styled_dom();
1513
1514 let base = styled_dom_with_icons(&["home"]);
1515 let icon_idx = icon_indices(&base)[0];
1516 let len = base.node_data.as_ref().len();
1517
1518 for idx in [len, len + 1, usize::MAX / 2, usize::MAX] {
1519 let mut sd = styled_dom_with_icons(&["home"]);
1520 apply_single_node_replacement(&mut sd, idx, &repl);
1521 apply_single_node_replacement(&mut sd, idx, &empty);
1522 assert_eq!(sd.node_data.as_ref().len(), len, "idx {idx} must not resize");
1523 assert!(
1524 matches!(node_type_at(&sd, icon_idx), NodeType::Icon(_)),
1525 "idx {idx} must leave the icon untouched"
1526 );
1527 }
1528 }
1529
1530 #[test]
1533 fn apply_multi_node_replacement_with_zero_node_dom_turns_the_icon_into_a_div() {
1534 let mut sd = styled_dom_with_icons(&["home"]);
1535 let idx = icon_indices(&sd)[0];
1536
1537 apply_multi_node_replacement(&mut sd, idx, &zero_node_styled_dom());
1538 assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1539 }
1540
1541 #[test]
1542 fn apply_multi_node_replacement_applies_only_the_root_and_does_not_splice() {
1543 let mut sd = styled_dom_with_icons(&["home"]);
1544 let idx = icon_indices(&sd)[0];
1545 let before_len = sd.node_data.as_ref().len();
1546
1547 let repl = StyledDom::create_from_dom(
1548 Dom::create_div().with_child(Dom::create_div()).with_child(Dom::create_div()),
1549 );
1550 assert!(repl.node_data.as_ref().len() > 1);
1551
1552 apply_multi_node_replacement(&mut sd, idx, &repl);
1553
1554 assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1556 assert_eq!(sd.node_data.as_ref().len(), before_len, "children are dropped, not spliced");
1557 }
1558
1559 #[test]
1560 fn apply_multi_node_replacement_out_of_bounds_index_is_a_no_op() {
1561 let repl = StyledDom::create_from_dom(Dom::create_div().with_child(Dom::create_div()));
1562 let base = styled_dom_with_icons(&["home"]);
1563 let icon_idx = icon_indices(&base)[0];
1564 let len = base.node_data.as_ref().len();
1565
1566 for idx in [len, usize::MAX] {
1567 let mut sd = styled_dom_with_icons(&["home"]);
1568 apply_multi_node_replacement(&mut sd, idx, &repl);
1569 apply_multi_node_replacement(&mut sd, idx, &zero_node_styled_dom());
1570 assert_eq!(sd.node_data.as_ref().len(), len);
1571 assert!(matches!(node_type_at(&sd, icon_idx), NodeType::Icon(_)));
1572 }
1573 }
1574
1575 #[test]
1578 fn resolve_collected_icons_preserves_indices_and_resolves_each_icon() {
1579 let mut h = IconProviderHandle::with_resolver(div_resolver);
1580 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1581 let shared = SharedIconProvider::from_handle(h);
1582
1583 let sd = styled_dom_with_icons(&["home", "missing", "\u{1F600}"]);
1584 let icons = collect_icon_nodes(&sd);
1585 let replacements =
1586 resolve_collected_icons(&icons, &sd, &shared, &SystemStyle::default());
1587
1588 assert_eq!(replacements.len(), icons.len());
1589 for (r, i) in replacements.iter().zip(icons.iter()) {
1590 assert_eq!(r.node_idx, i.node_idx);
1591 assert!(matches!(node_type_at(&r.replacement, 0), NodeType::Div));
1593 }
1594 }
1595
1596 #[test]
1597 fn resolve_collected_icons_with_no_icons_returns_no_replacements() {
1598 let shared = SharedIconProvider::from_handle(IconProviderHandle::new());
1599 let sd = StyledDom::default();
1600 let out = resolve_collected_icons(&[], &sd, &shared, &SystemStyle::default());
1601 assert!(out.is_empty());
1602 }
1603
1604 #[test]
1607 fn resolve_icons_in_styled_dom_is_a_no_op_without_icons() {
1608 let shared = SharedIconProvider::from_handle(IconProviderHandle::with_resolver(div_resolver));
1609 let mut sd = StyledDom::create_from_dom(Dom::create_body().with_child(Dom::create_text("hi")));
1610 let before_len = sd.node_data.as_ref().len();
1611 let before_root = node_type_at(&sd, 0);
1612
1613 resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1614
1615 assert_eq!(sd.node_data.as_ref().len(), before_len);
1616 assert_eq!(node_type_at(&sd, 0), before_root);
1617 }
1618
1619 #[test]
1620 fn resolve_icons_in_styled_dom_replaces_every_icon_case_insensitively() {
1621 let mut h = IconProviderHandle::with_resolver(div_resolver);
1622 h.register_icon("p", "home", RefAny::new(TestIconData { id: 1 }));
1623 let shared = SharedIconProvider::from_handle(h);
1624
1625 let mut sd = styled_dom_with_icons(&["HOME", "unregistered", "HoMe"]);
1627 let idxs = icon_indices(&sd);
1628 let before_len = sd.node_data.as_ref().len();
1629
1630 resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1631
1632 assert_eq!(sd.node_data.as_ref().len(), before_len);
1633 assert!(collect_icon_nodes(&sd).is_empty(), "no Icon node may survive resolution");
1634 for idx in idxs {
1635 assert!(matches!(node_type_at(&sd, idx), NodeType::Div));
1636 }
1637 }
1638
1639 #[test]
1640 fn resolve_icons_in_styled_dom_with_the_default_resolver_removes_the_icon_nodes() {
1641 let shared = SharedIconProvider::from_handle(IconProviderHandle::new());
1644 let mut sd = styled_dom_with_icons(&["home"]);
1645 let idx = icon_indices(&sd)[0];
1646
1647 resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1648
1649 assert!(!matches!(node_type_at(&sd, idx), NodeType::Icon(_)));
1650 assert!(collect_icon_nodes(&sd).is_empty());
1651 }
1652
1653 #[test]
1654 fn resolve_icons_in_styled_dom_handles_a_zero_node_replacement() {
1655 let shared =
1656 SharedIconProvider::from_handle(IconProviderHandle::with_resolver(zero_node_resolver));
1657 let mut sd = styled_dom_with_icons(&["home", "other"]);
1658 let idxs = icon_indices(&sd);
1659 let before_len = sd.node_data.as_ref().len();
1660
1661 resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1662
1663 assert_eq!(sd.node_data.as_ref().len(), before_len);
1664 for idx in idxs {
1665 assert!(matches!(node_type_at(&sd, idx), NodeType::Div), "empty => Div placeholder");
1666 }
1667 }
1668
1669 #[test]
1670 fn resolve_icons_in_styled_dom_scales_to_many_icons() {
1671 let shared = SharedIconProvider::from_handle(IconProviderHandle::with_resolver(div_resolver));
1672 let names: Vec<String> = (0..500).map(|i| format!("icon{i}")).collect();
1673 let refs: Vec<&str> = names.iter().map(String::as_str).collect();
1674 let mut sd = styled_dom_with_icons(&refs);
1675 let before_len = sd.node_data.as_ref().len();
1676
1677 resolve_icons_in_styled_dom(&mut sd, &shared, &SystemStyle::default());
1678
1679 assert_eq!(sd.node_data.as_ref().len(), before_len);
1680 assert!(collect_icon_nodes(&sd).is_empty());
1681 }
1682}