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::{
dom::{Dom, NodeType},
refany::{OptionRefAny, RefAny},
styled_dom::StyledDom,
};
const IMAGE_ICON_DATA_TYPE_NAME: &str = "ImageIconData";
const FONT_ICON_DATA_TYPE_NAME: &str = "FontIconData";
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(crate) 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_default();
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);
}
fn lookup_with_pack(&self, icon_name: &str) -> Option<(&str, &RefAny)> {
let icon_name_lower = icon_name.to_lowercase();
for (pack_name, pack) in self.inner.icons.iter() {
if let Some(data) = pack.get(&icon_name_lower) {
return Some((pack_name.as_str(), data));
}
}
None
}
pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
self.lookup_with_pack(icon_name).map(|(_, data)| data.clone())
}
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));
}
}
match self.lookup_with_pack(icon_name) {
Some((pack, 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(IMAGE_ICON_DATA_TYPE_NAME) {
result.push_str(" RefAny type: ImageIconData (image-based icon)\n");
} else if type_str.contains(FONT_ICON_DATA_TYPE_NAME) {
result.push_str(" RefAny type: FontIconData (font-based icon)\n");
} else {
result.push_str(&format!(" RefAny type: UNKNOWN ('{}')\n", type_str));
}
}
None => {
result.push_str("\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_self(),
});
}
}
icons
}
fn extract_single_node_styled_dom(styled_dom: &StyledDom, node_idx: usize) -> StyledDom {
use crate::dom::{NodeDataVec, DomId};
use crate::id::NodeId;
use crate::styled_dom::{
StyledNodeVec, NodeHierarchyItemIdVec, TagIdToNodeIdMappingVec,
NodeHierarchyItemVec, NodeHierarchyItem, 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: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
node_hierarchy: NodeHierarchyItemVec::from_vec(vec![NodeHierarchyItem {
parent: 0,
previous_sibling: 0,
next_sibling: 0,
last_child: 0,
}]),
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_datasets: NodeHierarchyItemIdVec::from_vec(Vec::new()),
tag_ids_to_node_ids: TagIdToNodeIdMappingVec::from_vec(Vec::new()),
non_leaf_nodes: ParentWithNodeDepthVec::from_vec(Vec::new()),
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.set_style(replacement_root.get_style().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,
) {
let icons = collect_icon_nodes(styled_dom);
if icons.is_empty() {
return;
}
let replacements = resolve_collected_icons(&icons, styled_dom, provider, system_style);
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]
);