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