1use alloc::{
39 boxed::Box,
40 collections::BTreeMap,
41 string::{String, ToString},
42 sync::Arc,
43 vec::Vec,
44};
45use core::fmt;
46
47#[cfg(feature = "std")]
48use std::sync::Mutex;
49
50#[cfg(not(feature = "std"))]
51use spin::Mutex;
52
53use azul_css::{AzString, system::SystemStyle};
54
55use crate::{
56 debug::DebugLog,
57 dom::{Dom, NodeType},
58 refany::{OptionRefAny, RefAny},
59 styled_dom::StyledDom,
60};
61
62pub type IconResolverCallbackType = extern "C" fn(
77 icon_data: OptionRefAny,
78 original_icon_dom: &StyledDom,
79 system_style: &SystemStyle,
80) -> StyledDom;
81
82pub extern "C" fn default_icon_resolver(
84 _icon_data: OptionRefAny,
85 _original_icon_dom: &StyledDom,
86 _system_style: &SystemStyle,
87) -> StyledDom {
88 StyledDom::default()
90}
91
92#[derive(Clone)]
96pub struct IconProviderInner {
97 pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
100 pub resolver: IconResolverCallbackType,
102}
103
104impl Default for IconProviderInner {
105 fn default() -> Self {
106 Self {
107 icons: BTreeMap::new(),
108 resolver: default_icon_resolver,
109 }
110 }
111}
112
113#[repr(C)]
127pub struct IconProviderHandle {
128 pub inner: Box<IconProviderInner>,
130}
131
132impl Clone for IconProviderHandle {
133 fn clone(&self) -> Self {
134 Self { inner: Box::new((*self.inner).clone()) }
135 }
136}
137
138impl fmt::Debug for IconProviderHandle {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 let pack_count = self.inner.icons.len();
141 let icon_count: usize = self.inner.icons.values().map(|p| p.len()).sum();
142
143 f.debug_struct("IconProviderHandle")
144 .field("pack_count", &pack_count)
145 .field("icon_count", &icon_count)
146 .finish()
147 }
148}
149
150impl Default for IconProviderHandle {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl IconProviderHandle {
157 pub fn new() -> Self {
163 Self {
164 inner: Box::new(IconProviderInner {
165 icons: BTreeMap::new(),
166 resolver: default_icon_resolver,
167 })
168 }
169 }
170
171 pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
173 Self {
174 inner: Box::new(IconProviderInner {
175 icons: BTreeMap::new(),
176 resolver,
177 })
178 }
179 }
180
181 pub fn into_shared(self) -> Arc<Mutex<IconProviderInner>> {
186 Arc::new(Mutex::new(*self.inner))
187 }
188
189 pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
191 self.inner.resolver = resolver;
192 }
193
194 pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
196 let pack = self.inner.icons
197 .entry(pack_name.to_string())
198 .or_insert_with(BTreeMap::new);
199 pack.insert(icon_name.to_lowercase(), data);
200 }
201
202 pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
204 if let Some(pack) = self.inner.icons.get_mut(pack_name) {
205 pack.remove(&icon_name.to_lowercase());
206 if pack.is_empty() {
207 self.inner.icons.remove(pack_name);
208 }
209 }
210 }
211
212 pub fn unregister_pack(&mut self, pack_name: &str) {
214 self.inner.icons.remove(pack_name);
215 }
216
217 pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
219 let icon_name_lower = icon_name.to_lowercase();
220 for pack in self.inner.icons.values() {
221 if let Some(data) = pack.get(&icon_name_lower) {
222 return Some(data.clone());
223 }
224 }
225 None
226 }
227
228 pub fn has_icon(&self, icon_name: &str) -> bool {
230 let icon_name_lower = icon_name.to_lowercase();
231 self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
232 }
233
234 pub fn list_packs(&self) -> Vec<String> {
236 self.inner.icons.keys().cloned().collect()
237 }
238
239 pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
241 self.inner.icons.get(pack_name)
242 .map(|pack| pack.keys().cloned().collect())
243 .unwrap_or_default()
244 }
245
246 pub fn debug_lookup(&self, icon_name: &str) -> AzString {
248 let icon_name_lower = icon_name.to_lowercase();
249
250 let mut result = format!("Debug lookup for icon '{}' (normalized: '{}'):\n", icon_name, icon_name_lower);
251
252 result.push_str(&format!(" Total packs: {}\n", self.inner.icons.len()));
254 for (pack_name, pack) in self.inner.icons.iter() {
255 result.push_str(&format!(" Pack '{}': {} icons\n", pack_name, pack.len()));
256 for (name, _) in pack.iter() {
257 result.push_str(&format!(" - {}\n", name));
258 }
259 }
260
261 let mut found_in_pack: Option<&str> = None;
263 let mut refany: Option<&RefAny> = None;
264 for (pack_name, pack) in self.inner.icons.iter() {
265 if let Some(data) = pack.get(&icon_name_lower) {
266 found_in_pack = Some(pack_name);
267 refany = Some(data);
268 break;
269 }
270 }
271
272 match (found_in_pack, refany) {
273 (Some(pack), Some(data)) => {
274 result.push_str(&format!("\n FOUND in pack '{}'\n", pack));
275 let type_name = data.get_type_name();
276 result.push_str(&format!(" RefAny type_name: '{}'\n", type_name.as_str()));
277
278 let debug_info = data.sharing_info.debug_get_refcount_copied();
279 result.push_str(&format!(" RefAny size: {} bytes\n", debug_info._internal_layout_size));
280
281 let type_str = type_name.as_str();
282 if type_str.contains("ImageIconData") {
283 result.push_str(" RefAny type: ImageIconData (image-based icon)\n");
284 } else if type_str.contains("FontIconData") {
285 result.push_str(" RefAny type: FontIconData (font-based icon)\n");
286 } else {
287 result.push_str(&format!(" RefAny type: UNKNOWN ('{}')\n", type_str));
288 }
289 }
290 _ => {
291 result.push_str(&format!("\n NOT FOUND in any pack\n"));
292 }
293 }
294
295 AzString::from(result)
296 }
297}
298
299#[derive(Clone)]
304pub struct SharedIconProvider {
305 inner: Arc<Mutex<IconProviderInner>>,
306}
307
308impl SharedIconProvider {
309 pub fn from_handle(handle: IconProviderHandle) -> Self {
311 Self { inner: handle.into_shared() }
312 }
313
314 pub fn resolve(
316 &self,
317 original_icon_dom: &StyledDom,
318 icon_name: &str,
319 system_style: &SystemStyle,
320 ) -> StyledDom {
321 let (resolver, lookup_result) = {
322 let guard = match self.inner.lock() {
323 Ok(g) => g,
324 Err(_) => return StyledDom::default(),
325 };
326
327 let resolver = guard.resolver;
328 let icon_name_lower = icon_name.to_lowercase();
329
330 let lookup_result = guard.icons.values()
331 .find_map(|pack| pack.get(&icon_name_lower).cloned());
332
333 (resolver, lookup_result)
334 };
335
336 resolver(lookup_result.into(), original_icon_dom, system_style)
337 }
338
339 pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
341 let icon_name_lower = icon_name.to_lowercase();
342 self.inner.lock().ok().and_then(|guard| {
343 for pack in guard.icons.values() {
344 if let Some(data) = pack.get(&icon_name_lower) {
345 return Some(data.clone());
346 }
347 }
348 None
349 })
350 }
351
352 pub fn has_icon(&self, icon_name: &str) -> bool {
354 let icon_name_lower = icon_name.to_lowercase();
355 self.inner.lock()
356 .map(|guard| guard.icons.values().any(|p| p.contains_key(&icon_name_lower)))
357 .unwrap_or(false)
358 }
359}
360
361struct CollectedIcon {
365 node_idx: usize,
367 icon_name: AzString,
369}
370
371struct IconReplacement {
373 node_idx: usize,
375 replacement: StyledDom,
377}
378
379fn collect_icon_nodes(styled_dom: &StyledDom) -> Vec<CollectedIcon> {
381 let mut icons = Vec::new();
382
383 let node_data = styled_dom.node_data.as_ref();
384 for (idx, node) in node_data.iter().enumerate() {
385 if let NodeType::Icon(icon_name) = node.get_node_type() {
386 icons.push(CollectedIcon {
387 node_idx: idx,
388 icon_name: icon_name.clone(),
389 });
390 }
391 }
392
393 icons
394}
395
396fn generate_a11y_label(icon_name: &str) -> AzString {
398 AzString::from(format!("{} icon", icon_name.replace('_', " ").replace('-', " ")))
399}
400
401fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
404 use crate::dom::{NodeDataVec, DomId};
405 use crate::styled_dom::{
406 StyledNodeVec, NodeIdVec, TagIdToNodeIdMappingVec,
407 NodeHierarchyItemVec, NodeHierarchyItemId, ParentWithNodeDepthVec, ParentWithNodeDepth,
408 };
409 use crate::style::{CascadeInfoVec, CascadeInfo};
410 use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
411
412 let node_data = styled_dom.node_data.as_ref();
413 let styled_nodes = styled_dom.styled_nodes.as_ref();
414
415 if node_idx >= node_data.len() {
416 return StyledDom::default();
417 }
418
419 let single_node = node_data[node_idx].clone();
421 let single_styled = if node_idx < styled_nodes.len() {
422 styled_nodes[node_idx].clone()
423 } else {
424 crate::styled_dom::StyledNode::default()
425 };
426
427 StyledDom {
428 root: styled_dom.root.clone(),
429 node_hierarchy: styled_dom.node_hierarchy.clone(),
430 node_data: NodeDataVec::from_vec(vec![single_node]),
431 styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
432 cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
433 nodes_with_window_callbacks: NodeIdVec::from_vec(Vec::new()),
434 nodes_with_not_callbacks: NodeIdVec::from_vec(Vec::new()),
435 nodes_with_datasets: NodeIdVec::from_vec(Vec::new()),
436 tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
437 non_leaf_nodes: ParentWithNodeDepthVec::from_vec(vec![ParentWithNodeDepth {
438 depth: 0,
439 node_id: styled_dom.root.clone(),
440 }]),
441 css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
442 dom_id: DomId::ROOT_ID,
443 }
444}
445
446fn resolve_collected_icons(
448 icons: &[CollectedIcon],
449 styled_dom: &StyledDom,
450 provider: &SharedIconProvider,
451 system_style: &SystemStyle,
452) -> Vec<IconReplacement> {
453 icons.iter().map(|icon| {
454 let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
456 let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
457 IconReplacement {
458 node_idx: icon.node_idx,
459 replacement,
460 }
461 }).collect()
462}
463
464fn is_single_node_replacement(replacement: &StyledDom) -> bool {
466 replacement.node_data.as_ref().len() == 1
467}
468
469fn apply_single_node_replacement(
471 styled_dom: &mut StyledDom,
472 node_idx: usize,
473 replacement: &StyledDom,
474) {
475 if replacement.node_data.as_ref().is_empty() {
476 let node_data = styled_dom.node_data.as_mut();
478 if let Some(node) = node_data.get_mut(node_idx) {
479 node.set_node_type(NodeType::Div);
480 }
481 } else {
482 let replacement_root = &replacement.node_data.as_ref()[0];
484 let replacement_node_type = replacement_root.get_node_type().clone();
485
486 let node_data = styled_dom.node_data.as_mut();
487 if let Some(node) = node_data.get_mut(node_idx) {
488 node.set_node_type(replacement_node_type);
490
491 node.css_props = replacement_root.get_css_props().clone();
493
494 if let Some(a11y) = replacement_root.get_accessibility_info() {
496 node.set_accessibility_info(*a11y.clone());
497 }
498 }
499
500 if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
502 let styled_nodes = styled_dom.styled_nodes.as_mut();
503 if let Some(styled) = styled_nodes.get_mut(node_idx) {
504 *styled = replacement_styled.clone();
505 }
506 }
507 }
508}
509
510fn apply_multi_node_replacement(
512 styled_dom: &mut StyledDom,
513 node_idx: usize,
514 replacement: StyledDom,
515) {
516 let replacement_len = replacement.node_data.as_ref().len();
517 if replacement_len == 0 {
518 let node_data = styled_dom.node_data.as_mut();
519 if let Some(node) = node_data.get_mut(node_idx) {
520 node.set_node_type(NodeType::Div);
521 }
522 return;
523 }
524
525 apply_single_node_replacement(styled_dom, node_idx, &replacement);
527
528 if replacement_len > 1 {
529 #[cfg(debug_assertions)]
531 eprintln!(
532 "Warning: Icon replacement has {} nodes, only root node used.",
533 replacement_len
534 );
535 }
536}
537
538pub fn resolve_icons_in_styled_dom(
547 styled_dom: &mut StyledDom,
548 provider: &SharedIconProvider,
549 system_style: &SystemStyle,
550) {
551 resolve_icons_in_styled_dom_with_log(styled_dom, provider, system_style, None)
552}
553
554pub fn resolve_icons_in_styled_dom_with_log(
556 styled_dom: &mut StyledDom,
557 provider: &SharedIconProvider,
558 system_style: &SystemStyle,
559 mut debug_log: Option<&mut DebugLog>,
560) {
561 use crate::log_debug;
562
563 let icons = collect_icon_nodes(styled_dom);
565
566 if icons.is_empty() {
567 if let Some(ref mut log) = debug_log {
568 log_debug!(log, Icon, "No icon nodes found in StyledDom");
569 }
570 return;
571 }
572
573 if let Some(ref mut log) = debug_log {
574 log_debug!(log, Icon, "Found {} icon nodes to resolve", icons.len());
575 for icon in &icons {
576 let has_icon = provider.has_icon(icon.icon_name.as_str());
577 log_debug!(log, Icon, " - Icon '{}' at node {}: registered={}",
578 icon.icon_name.as_str(), icon.node_idx, has_icon);
579 }
580 }
581
582 let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
585
586 if let Some(ref mut log) = debug_log {
587 for replacement in &replacements {
588 let node_count = replacement.replacement.node_data.as_ref().len();
589 let node_type = replacement.replacement.node_data.as_ref()
590 .first()
591 .map(|n| format!("{:?}", n.get_node_type()))
592 .unwrap_or_else(|| "empty".to_string());
593 log_debug!(log, Icon, " - Replacement at {}: {} nodes, root type: {}",
594 replacement.node_idx, node_count, node_type);
595 }
596 }
597
598 for replacement in replacements.into_iter().rev() {
600 if is_single_node_replacement(&replacement.replacement) ||
601 replacement.replacement.node_data.as_ref().is_empty() {
602 apply_single_node_replacement(
603 styled_dom,
604 replacement.node_idx,
605 &replacement.replacement
606 );
607 } else {
608 apply_multi_node_replacement(
609 styled_dom,
610 replacement.node_idx,
611 replacement.replacement
612 );
613 }
614 }
615}
616
617impl_option!(
620 IconProviderHandle,
621 OptionIconProviderHandle,
622 [Clone]
623);
624
625#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn test_icon_provider_new() {
633 let provider = IconProviderHandle::new();
634 assert!(provider.list_packs().is_empty());
635 }
636
637 extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
639
640 #[test]
641 fn test_icon_registration() {
642 let mut provider = IconProviderHandle::new();
643
644 let dummy_data = crate::refany::RefAny::new_c(
646 core::ptr::null(),
647 0,
648 1,
649 0,
650 "".into(),
651 dummy_destructor,
652 0, 0, );
655
656 provider.register_icon("images", "home", dummy_data.clone());
657 assert!(provider.has_icon("home"));
658 assert!(provider.has_icon("HOME")); provider.unregister_icon("images", "home");
661 assert!(!provider.has_icon("home"));
662 }
663
664 #[test]
665 fn test_icon_provider_lookup() {
666 let mut provider = IconProviderHandle::new();
667
668 let dummy_data = crate::refany::RefAny::new_c(
669 core::ptr::null(),
670 0,
671 1,
672 0,
673 "".into(),
674 dummy_destructor,
675 0, 0, );
678
679 provider.register_icon("images", "logo", dummy_data);
680
681 assert!(provider.has_icon("logo"));
682 assert!(!provider.has_icon("missing"));
683
684 let lookup = provider.lookup("logo");
685 assert!(lookup.is_some());
686 }
687
688 #[test]
689 fn test_pack_operations() {
690 let mut provider = IconProviderHandle::new();
691
692 let dummy_data = crate::refany::RefAny::new_c(
693 core::ptr::null(),
694 0,
695 1,
696 0,
697 "".into(),
698 dummy_destructor,
699 0, 0, );
702
703 provider.register_icon("pack1", "icon1", dummy_data.clone());
705 provider.register_icon("pack2", "icon2", dummy_data);
706
707 assert_eq!(provider.list_packs().len(), 2);
708 assert!(provider.has_icon("icon1"));
709 assert!(provider.has_icon("icon2"));
710
711 provider.unregister_pack("pack1");
713 assert!(!provider.has_icon("icon1"));
714 assert!(provider.has_icon("icon2"));
715 assert_eq!(provider.list_packs().len(), 1);
716 }
717}