use alloc::{
boxed::Box,
collections::BTreeMap,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use core::fmt;
#[cfg(feature = "std")]
use std::sync::Mutex;
#[cfg(not(feature = "std"))]
use spin::Mutex;
use azul_css::{AzString, system::SystemStyle};
use crate::{
debug::DebugLog,
dom::{Dom, NodeType},
refany::{OptionRefAny, RefAny},
styled_dom::StyledDom,
};
pub type IconResolverCallbackType = extern "C" fn(
icon_data: OptionRefAny,
original_icon_dom: &StyledDom,
system_style: &SystemStyle,
) -> StyledDom;
pub extern "C" fn default_icon_resolver(
_icon_data: OptionRefAny,
_original_icon_dom: &StyledDom,
_system_style: &SystemStyle,
) -> StyledDom {
StyledDom::default()
}
#[derive(Clone)]
pub struct IconProviderInner {
pub icons: BTreeMap<String, BTreeMap<String, RefAny>>,
pub resolver: IconResolverCallbackType,
}
impl Default for IconProviderInner {
fn default() -> Self {
Self {
icons: BTreeMap::new(),
resolver: default_icon_resolver,
}
}
}
#[repr(C)]
pub struct IconProviderHandle {
pub inner: Box<IconProviderInner>,
}
impl Clone for IconProviderHandle {
fn clone(&self) -> Self {
Self { inner: Box::new((*self.inner).clone()) }
}
}
impl fmt::Debug for IconProviderHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let pack_count = self.inner.icons.len();
let icon_count: usize = self.inner.icons.values().map(|p| p.len()).sum();
f.debug_struct("IconProviderHandle")
.field("pack_count", &pack_count)
.field("icon_count", &icon_count)
.finish()
}
}
impl Default for IconProviderHandle {
fn default() -> Self {
Self::new()
}
}
impl IconProviderHandle {
pub fn new() -> Self {
Self {
inner: Box::new(IconProviderInner {
icons: BTreeMap::new(),
resolver: default_icon_resolver,
})
}
}
pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
Self {
inner: Box::new(IconProviderInner {
icons: BTreeMap::new(),
resolver,
})
}
}
pub fn into_shared(self) -> Arc<Mutex<IconProviderInner>> {
Arc::new(Mutex::new(*self.inner))
}
pub fn set_resolver(&mut self, resolver: IconResolverCallbackType) {
self.inner.resolver = resolver;
}
pub fn register_icon(&mut self, pack_name: &str, icon_name: &str, data: RefAny) {
let pack = self.inner.icons
.entry(pack_name.to_string())
.or_insert_with(BTreeMap::new);
pack.insert(icon_name.to_lowercase(), data);
}
pub fn unregister_icon(&mut self, pack_name: &str, icon_name: &str) {
if let Some(pack) = self.inner.icons.get_mut(pack_name) {
pack.remove(&icon_name.to_lowercase());
if pack.is_empty() {
self.inner.icons.remove(pack_name);
}
}
}
pub fn unregister_pack(&mut self, pack_name: &str) {
self.inner.icons.remove(pack_name);
}
pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
let icon_name_lower = icon_name.to_lowercase();
for pack in self.inner.icons.values() {
if let Some(data) = pack.get(&icon_name_lower) {
return Some(data.clone());
}
}
None
}
pub fn has_icon(&self, icon_name: &str) -> bool {
let icon_name_lower = icon_name.to_lowercase();
self.inner.icons.values().any(|p| p.contains_key(&icon_name_lower))
}
pub fn list_packs(&self) -> Vec<String> {
self.inner.icons.keys().cloned().collect()
}
pub fn list_icons_in_pack(&self, pack_name: &str) -> Vec<String> {
self.inner.icons.get(pack_name)
.map(|pack| pack.keys().cloned().collect())
.unwrap_or_default()
}
pub fn debug_lookup(&self, icon_name: &str) -> AzString {
let icon_name_lower = icon_name.to_lowercase();
let mut result = format!("Debug lookup for icon '{}' (normalized: '{}'):\n", icon_name, icon_name_lower);
result.push_str(&format!(" Total packs: {}\n", self.inner.icons.len()));
for (pack_name, pack) in self.inner.icons.iter() {
result.push_str(&format!(" Pack '{}': {} icons\n", pack_name, pack.len()));
for (name, _) in pack.iter() {
result.push_str(&format!(" - {}\n", name));
}
}
let mut found_in_pack: Option<&str> = None;
let mut refany: Option<&RefAny> = None;
for (pack_name, pack) in self.inner.icons.iter() {
if let Some(data) = pack.get(&icon_name_lower) {
found_in_pack = Some(pack_name);
refany = Some(data);
break;
}
}
match (found_in_pack, refany) {
(Some(pack), Some(data)) => {
result.push_str(&format!("\n FOUND in pack '{}'\n", pack));
let type_name = data.get_type_name();
result.push_str(&format!(" RefAny type_name: '{}'\n", type_name.as_str()));
let debug_info = data.sharing_info.debug_get_refcount_copied();
result.push_str(&format!(" RefAny size: {} bytes\n", debug_info._internal_layout_size));
let type_str = type_name.as_str();
if type_str.contains("ImageIconData") {
result.push_str(" RefAny type: ImageIconData (image-based icon)\n");
} else if type_str.contains("FontIconData") {
result.push_str(" RefAny type: FontIconData (font-based icon)\n");
} else {
result.push_str(&format!(" RefAny type: UNKNOWN ('{}')\n", type_str));
}
}
_ => {
result.push_str(&format!("\n NOT FOUND in any pack\n"));
}
}
AzString::from(result)
}
}
#[derive(Clone)]
pub struct SharedIconProvider {
inner: Arc<Mutex<IconProviderInner>>,
}
impl SharedIconProvider {
pub fn from_handle(handle: IconProviderHandle) -> Self {
Self { inner: handle.into_shared() }
}
pub fn resolve(
&self,
original_icon_dom: &StyledDom,
icon_name: &str,
system_style: &SystemStyle,
) -> StyledDom {
let (resolver, lookup_result) = {
let guard = match self.inner.lock() {
Ok(g) => g,
Err(_) => return StyledDom::default(),
};
let resolver = guard.resolver;
let icon_name_lower = icon_name.to_lowercase();
let lookup_result = guard.icons.values()
.find_map(|pack| pack.get(&icon_name_lower).cloned());
(resolver, lookup_result)
};
resolver(lookup_result.into(), original_icon_dom, system_style)
}
pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
let icon_name_lower = icon_name.to_lowercase();
self.inner.lock().ok().and_then(|guard| {
for pack in guard.icons.values() {
if let Some(data) = pack.get(&icon_name_lower) {
return Some(data.clone());
}
}
None
})
}
pub fn has_icon(&self, icon_name: &str) -> bool {
let icon_name_lower = icon_name.to_lowercase();
self.inner.lock()
.map(|guard| guard.icons.values().any(|p| p.contains_key(&icon_name_lower)))
.unwrap_or(false)
}
}
struct CollectedIcon {
node_idx: usize,
icon_name: AzString,
}
struct IconReplacement {
node_idx: usize,
replacement: StyledDom,
}
fn collect_icon_nodes(styled_dom: &StyledDom) -> Vec<CollectedIcon> {
let mut icons = Vec::new();
let node_data = styled_dom.node_data.as_ref();
for (idx, node) in node_data.iter().enumerate() {
if let NodeType::Icon(icon_name) = node.get_node_type() {
icons.push(CollectedIcon {
node_idx: idx,
icon_name: icon_name.clone(),
});
}
}
icons
}
fn generate_a11y_label(icon_name: &str) -> AzString {
AzString::from(format!("{} icon", icon_name.replace('_', " ").replace('-', " ")))
}
fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
use crate::dom::{NodeDataVec, DomId};
use crate::styled_dom::{
StyledNodeVec, NodeHierarchyItemIdVec, TagIdToNodeIdMappingVec,
NodeHierarchyItemVec, NodeHierarchyItemId, ParentWithNodeDepthVec, ParentWithNodeDepth,
};
use crate::style::{CascadeInfoVec, CascadeInfo};
use crate::prop_cache::{CssPropertyCachePtr, CssPropertyCache};
let node_data = styled_dom.node_data.as_ref();
let styled_nodes = styled_dom.styled_nodes.as_ref();
if node_idx >= node_data.len() {
return StyledDom::default();
}
let single_node = node_data[node_idx].clone();
let single_styled = if node_idx < styled_nodes.len() {
styled_nodes[node_idx].clone()
} else {
crate::styled_dom::StyledNode::default()
};
StyledDom {
root: styled_dom.root.clone(),
node_hierarchy: styled_dom.node_hierarchy.clone(),
node_data: NodeDataVec::from_vec(vec![single_node]),
styled_nodes: StyledNodeVec::from_vec(vec![single_styled]),
cascade_info: CascadeInfoVec::from_vec(vec![CascadeInfo { index_in_parent: 0, is_last_child: true }]),
nodes_with_window_callbacks: NodeHierarchyItemIdVec::from_vec(Vec::new()),
nodes_with_not_callbacks: NodeHierarchyItemIdVec::from_vec(Vec::new()),
nodes_with_datasets: NodeHierarchyItemIdVec::from_vec(Vec::new()),
tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
non_leaf_nodes: ParentWithNodeDepthVec::from_vec(vec![ParentWithNodeDepth {
depth: 0,
node_id: styled_dom.root.clone(),
}]),
css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
dom_id: DomId::ROOT_ID,
}
}
fn resolve_collected_icons(
icons: &[CollectedIcon],
styled_dom: &StyledDom,
provider: &SharedIconProvider,
system_style: &SystemStyle,
) -> Vec<IconReplacement> {
icons.iter().map(|icon| {
let original_icon_dom = extract_single_node_styled_dom(styled_dom, icon.node_idx);
let replacement = provider.resolve(&original_icon_dom, icon.icon_name.as_str(), system_style);
IconReplacement {
node_idx: icon.node_idx,
replacement,
}
}).collect()
}
fn is_single_node_replacement(replacement: &StyledDom) -> bool {
replacement.node_data.as_ref().len() == 1
}
fn apply_single_node_replacement(
styled_dom: &mut StyledDom,
node_idx: usize,
replacement: &StyledDom,
) {
if replacement.node_data.as_ref().is_empty() {
let node_data = styled_dom.node_data.as_mut();
if let Some(node) = node_data.get_mut(node_idx) {
node.set_node_type(NodeType::Div);
}
} else {
let replacement_root = &replacement.node_data.as_ref()[0];
let replacement_node_type = replacement_root.get_node_type().clone();
let node_data = styled_dom.node_data.as_mut();
if let Some(node) = node_data.get_mut(node_idx) {
node.set_node_type(replacement_node_type);
node.css_props = replacement_root.get_css_props().clone();
if let Some(a11y) = replacement_root.get_accessibility_info() {
node.set_accessibility_info(*a11y.clone());
}
}
if let Some(replacement_styled) = replacement.styled_nodes.as_ref().first() {
let styled_nodes = styled_dom.styled_nodes.as_mut();
if let Some(styled) = styled_nodes.get_mut(node_idx) {
*styled = replacement_styled.clone();
}
}
}
}
fn apply_multi_node_replacement(
styled_dom: &mut StyledDom,
node_idx: usize,
replacement: StyledDom,
) {
let replacement_len = replacement.node_data.as_ref().len();
if replacement_len == 0 {
let node_data = styled_dom.node_data.as_mut();
if let Some(node) = node_data.get_mut(node_idx) {
node.set_node_type(NodeType::Div);
}
return;
}
apply_single_node_replacement(styled_dom, node_idx, &replacement);
if replacement_len > 1 {
#[cfg(debug_assertions)]
eprintln!(
"Warning: Icon replacement has {} nodes, only root node used.",
replacement_len
);
}
}
pub fn resolve_icons_in_styled_dom(
styled_dom: &mut StyledDom,
provider: &SharedIconProvider,
system_style: &SystemStyle,
) {
resolve_icons_in_styled_dom_with_log(styled_dom, provider, system_style, None)
}
pub fn resolve_icons_in_styled_dom_with_log(
styled_dom: &mut StyledDom,
provider: &SharedIconProvider,
system_style: &SystemStyle,
mut debug_log: Option<&mut DebugLog>,
) {
use crate::log_debug;
let icons = collect_icon_nodes(styled_dom);
if icons.is_empty() {
if let Some(ref mut log) = debug_log {
log_debug!(log, Icon, "No icon nodes found in StyledDom");
}
return;
}
if let Some(ref mut log) = debug_log {
log_debug!(log, Icon, "Found {} icon nodes to resolve", icons.len());
for icon in &icons {
let has_icon = provider.has_icon(icon.icon_name.as_str());
log_debug!(log, Icon, " - Icon '{}' at node {}: registered={}",
icon.icon_name.as_str(), icon.node_idx, has_icon);
}
}
let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
if let Some(ref mut log) = debug_log {
for replacement in &replacements {
let node_count = replacement.replacement.node_data.as_ref().len();
let node_type = replacement.replacement.node_data.as_ref()
.first()
.map(|n| format!("{:?}", n.get_node_type()))
.unwrap_or_else(|| "empty".to_string());
log_debug!(log, Icon, " - Replacement at {}: {} nodes, root type: {}",
replacement.node_idx, node_count, node_type);
}
}
for replacement in replacements.into_iter().rev() {
if is_single_node_replacement(&replacement.replacement) ||
replacement.replacement.node_data.as_ref().is_empty() {
apply_single_node_replacement(
styled_dom,
replacement.node_idx,
&replacement.replacement
);
} else {
apply_multi_node_replacement(
styled_dom,
replacement.node_idx,
replacement.replacement
);
}
}
}
impl_option!(
IconProviderHandle,
OptionIconProviderHandle,
[Clone]
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_icon_provider_new() {
let provider = IconProviderHandle::new();
assert!(provider.list_packs().is_empty());
}
extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
#[test]
fn test_icon_registration() {
let mut provider = IconProviderHandle::new();
let dummy_data = crate::refany::RefAny::new_c(
core::ptr::null(),
0,
1,
0,
"".into(),
dummy_destructor,
0, 0, );
provider.register_icon("images", "home", dummy_data.clone());
assert!(provider.has_icon("home"));
assert!(provider.has_icon("HOME"));
provider.unregister_icon("images", "home");
assert!(!provider.has_icon("home"));
}
#[test]
fn test_icon_provider_lookup() {
let mut provider = IconProviderHandle::new();
let dummy_data = crate::refany::RefAny::new_c(
core::ptr::null(),
0,
1,
0,
"".into(),
dummy_destructor,
0, 0, );
provider.register_icon("images", "logo", dummy_data);
assert!(provider.has_icon("logo"));
assert!(!provider.has_icon("missing"));
let lookup = provider.lookup("logo");
assert!(lookup.is_some());
}
#[test]
fn test_pack_operations() {
let mut provider = IconProviderHandle::new();
let dummy_data = crate::refany::RefAny::new_c(
core::ptr::null(),
0,
1,
0,
"".into(),
dummy_destructor,
0, 0, );
provider.register_icon("pack1", "icon1", dummy_data.clone());
provider.register_icon("pack2", "icon2", dummy_data);
assert_eq!(provider.list_packs().len(), 2);
assert!(provider.has_icon("icon1"));
assert!(provider.has_icon("icon2"));
provider.unregister_pack("pack1");
assert!(!provider.has_icon("icon1"));
assert!(provider.has_icon("icon2"));
assert_eq!(provider.list_packs().len(), 1);
}
}