use alloc::{
boxed::Box,
collections::BTreeMap,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use core::fmt;
use core::mem::ManuallyDrop;
#[cfg(feature = "std")]
use std::sync::Mutex;
#[cfg(not(feature = "std"))]
use self::nostd_lock::Mutex;
#[cfg(not(feature = "std"))]
mod nostd_lock {
use core::cell::UnsafeCell;
use core::ops::{Deref, DerefMut};
use core::sync::atomic::{AtomicBool, Ordering};
pub struct Mutex<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Send for Mutex<T> {}
unsafe impl<T: Send> Sync for Mutex<T> {}
pub struct MutexGuard<'a, T> {
lock: &'a Mutex<T>,
}
impl<T> Mutex<T> {
pub fn new(data: T) -> Self {
Mutex {
locked: AtomicBool::new(false),
data: UnsafeCell::new(data),
}
}
pub fn lock(&self) -> Result<MutexGuard<'_, T>, core::convert::Infallible> {
while self
.locked
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
Ok(MutexGuard { lock: self })
}
}
impl<'a, T> Deref for MutexGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<'a, T> DerefMut for MutexGuard<'a, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
self.lock.locked.store(false, Ordering::Release);
}
}
impl<T> core::fmt::Debug for Mutex<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Mutex").finish_non_exhaustive()
}
}
}
use azul_css::{system::SystemStyle, AzString};
use crate::{
dom::{Dom, NodeData, 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_node: &NodeData,
system_style: &SystemStyle,
) -> Dom;
#[must_use]
pub extern "C" fn default_icon_resolver(
_icon_data: OptionRefAny,
_original_icon_node: &NodeData,
_system_style: &SystemStyle,
) -> Dom {
Dom::create_div()
}
#[derive(Debug, 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: ManuallyDrop<Box<IconProviderInner>>,
pub run_destructor: bool,
}
impl Clone for IconProviderHandle {
fn clone(&self) -> Self {
Self {
inner: ManuallyDrop::new(Box::new((**self.inner).clone())),
run_destructor: true,
}
}
}
impl Drop for IconProviderHandle {
fn drop(&mut self) {
if self.run_destructor {
self.run_destructor = false;
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
}
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(BTreeMap::len).sum();
f.debug_struct("IconProviderHandle")
.field("pack_count", &pack_count)
.field("icon_count", &icon_count)
.finish_non_exhaustive()
}
}
impl Default for IconProviderHandle {
fn default() -> Self {
Self::new()
}
}
impl IconProviderInner {
#[must_use]
pub fn lookup_spec(&self, spec: &str) -> Option<RefAny> {
let verbatim = spec.to_lowercase();
if let Some(data) = self.icons.values().find_map(|pack| pack.get(&verbatim)) {
return Some(data.clone());
}
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (pack, name) = match entry.split_once(':') {
Some((p, n)) => (Some(p.trim()), n.trim()),
None => (None, entry),
};
let name_lower = name.to_lowercase();
let found = pack.map_or_else(
|| self.icons.values().find_map(|pack| pack.get(&name_lower)),
|p| self.icons.get(p).and_then(|pack| pack.get(&name_lower)),
);
if let Some(data) = found {
return Some(data.clone());
}
}
None
}
}
impl IconProviderHandle {
#[must_use]
pub fn new() -> Self {
Self {
inner: ManuallyDrop::new(Box::new(IconProviderInner {
icons: BTreeMap::new(),
resolver: default_icon_resolver,
})),
run_destructor: true,
}
}
pub fn with_resolver(resolver: IconResolverCallbackType) -> Self {
Self {
inner: ManuallyDrop::new(Box::new(IconProviderInner {
icons: BTreeMap::new(),
resolver,
})),
run_destructor: true,
}
}
pub(crate) fn into_shared(mut self) -> Arc<Mutex<IconProviderInner>> {
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
self.run_destructor = false;
Arc::new(Mutex::new(*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 {
if let Some(data) = pack.get(&icon_name_lower) {
return Some((pack_name.as_str(), data));
}
}
None
}
#[must_use]
pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
self.inner.lookup_spec(icon_name)
}
#[must_use]
pub fn has_icon(&self, icon_name: &str) -> bool {
self.inner.lookup_spec(icon_name).is_some()
}
#[must_use]
pub fn list_packs(&self) -> Vec<String> {
self.inner.icons.keys().cloned().collect()
}
#[must_use]
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()
}
#[allow(clippy::used_underscore_binding)] #[must_use]
pub fn debug_lookup(&self, icon_name: &str) -> AzString {
use core::fmt::Write;
let icon_name_lower = icon_name.to_lowercase();
let mut result =
format!("Debug lookup for icon '{icon_name}' (normalized: '{icon_name_lower}'):\n");
let _ = writeln!(result, " Total packs: {}", self.inner.icons.len());
for (pack_name, pack) in &self.inner.icons {
let _ = writeln!(result, " Pack '{}': {} icons", pack_name, pack.len());
for name in pack.keys() {
let _ = writeln!(result, " - {name}");
}
}
match self.lookup_with_pack(icon_name) {
Some((pack, data)) => {
let _ = writeln!(result, "\n FOUND in pack '{pack}'");
let type_name = data.get_type_name();
let _ = writeln!(result, " RefAny type_name: '{}'", type_name.as_str());
let debug_info = data.sharing_info.debug_get_refcount_copied();
let _ = writeln!(
result,
" RefAny size: {} bytes",
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 {
let _ = writeln!(result, " RefAny type: UNKNOWN ('{type_str}')");
}
}
None => {
result.push_str("\n NOT FOUND in any pack\n");
}
}
AzString::from(result)
}
}
#[derive(Debug, Clone)]
pub struct SharedIconProvider {
inner: Arc<Mutex<IconProviderInner>>,
cache: Arc<Mutex<IconResolutionCache>>,
}
const ICON_CACHE_CAP: usize = 512;
#[derive(Debug)]
struct IconCacheEntry {
original: NodeData,
resolution: Dom,
}
#[derive(Debug, Default)]
struct IconResolutionCache {
system_style: Option<SystemStyle>,
entries: BTreeMap<String, Vec<IconCacheEntry>>,
total: usize,
}
impl SharedIconProvider {
#[must_use]
pub fn from_handle(handle: IconProviderHandle) -> Self {
Self {
inner: handle.into_shared(),
cache: Arc::new(Mutex::new(IconResolutionCache::default())),
}
}
pub fn register_icon(&self, pack_name: &str, icon_name: &str, data: RefAny) {
if let Ok(mut inner) = self.inner.lock() {
let pack = inner.icons.entry(pack_name.to_string()).or_default();
pack.insert(icon_name.to_lowercase(), data);
}
if let Ok(mut cache) = self.cache.lock() {
cache.entries.clear();
cache.total = 0;
cache.system_style = None;
}
}
fn validate_cache_for_style(&self, system_style: &SystemStyle) {
let Ok(mut cache) = self.cache.lock() else {
return;
};
match &cache.system_style {
Some(cached) if cached == system_style => {}
_ => {
cache.entries.clear();
cache.total = 0;
cache.system_style = Some(system_style.clone());
}
}
}
fn cached_resolution(&self, spec: &str, node: &NodeData) -> Option<Dom> {
let cache = self.cache.lock().ok()?;
cache
.entries
.get(spec)?
.iter()
.find_map(|e| (e.original == *node).then(|| e.resolution.clone()))
}
fn store_resolution(&self, spec: &str, node: &NodeData, resolution: &Dom) {
let Ok(mut cache) = self.cache.lock() else {
return;
};
if cache.total >= ICON_CACHE_CAP {
cache.entries.clear();
cache.total = 0;
}
cache
.entries
.entry(spec.to_string())
.or_default()
.push(IconCacheEntry {
original: node.clone(),
resolution: resolution.clone(),
});
cache.total += 1;
}
#[must_use]
pub fn resolve(
&self,
original_icon_node: &NodeData,
icon_name: &str,
system_style: &SystemStyle,
) -> Dom {
let (resolver, lookup_result) = {
let Ok(guard) = self.inner.lock() else {
return Dom::create_div();
};
let resolver = guard.resolver;
let lookup_result = guard.lookup_spec(icon_name);
(resolver, lookup_result)
};
resolver(lookup_result.into(), original_icon_node, system_style)
}
#[must_use]
fn resolve_cached(
&self,
original_icon_node: &NodeData,
icon_name: &str,
system_style: &SystemStyle,
) -> Dom {
if let Some(hit) = self.cached_resolution(icon_name, original_icon_node) {
return hit;
}
let resolved = self.resolve(original_icon_node, icon_name, system_style);
self.store_resolution(icon_name, original_icon_node, &resolved);
resolved
}
#[must_use]
pub fn lookup(&self, icon_name: &str) -> Option<RefAny> {
self.inner
.lock()
.ok()
.and_then(|guard| guard.lookup_spec(icon_name))
}
#[must_use]
pub fn has_icon(&self, icon_name: &str) -> bool {
self.inner
.lock()
.map(|guard| guard.lookup_spec(icon_name).is_some())
.unwrap_or(false)
}
}
const MAX_ICON_INDIRECTION: usize = 8;
pub fn resolve_icons_in_dom(
dom: &mut Dom,
provider: &SharedIconProvider,
system_style: &SystemStyle,
) {
provider.validate_cache_for_style(system_style);
resolve_icons_in_dom_inner(dom, provider, system_style);
}
#[must_use]
pub fn styled_dom_resolving_icons(
mut dom: Dom,
provider: &SharedIconProvider,
system_style: &SystemStyle,
) -> StyledDom {
resolve_icons_in_dom(&mut dom, provider, system_style);
StyledDom::create_from_dom(dom)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IconViewState {
pub spec: AzString,
}
#[must_use]
pub(crate) fn icon_view(spec: impl Into<AzString>) -> Dom {
let dataset = RefAny::new(IconViewState { spec: spec.into() });
Dom::create_virtual_view(
dataset.clone(),
crate::callbacks::VirtualViewCallback::create(render_icon_view),
)
.with_dataset(OptionRefAny::Some(dataset))
.with_css("display: inline-block; overflow: hidden;")
}
extern "C" fn render_icon_view(
mut data: RefAny,
info: crate::callbacks::VirtualViewCallbackInfo,
) -> crate::callbacks::VirtualViewReturn {
use crate::geom::{LogicalPosition, LogicalRect, LogicalSize};
let spec = match data.downcast_ref::<IconViewState>() {
None => return crate::callbacks::VirtualViewReturn::default(),
Some(state) => state.spec.clone(),
};
let dom = Dom::create_icon(spec);
let bounds = info.bounds.get_logical_size();
let available = if bounds.width > 0.0 && bounds.height > 0.0 {
bounds
} else {
LogicalSize::new(UNCONSTRAINED, UNCONSTRAINED)
};
let measured = info.measure_dom(dom.clone(), available);
let size = if measured.width > 0.0 && measured.height > 0.0 {
measured
} else {
bounds
};
let rect = LogicalRect::new(LogicalPosition::zero(), size);
crate::callbacks::VirtualViewReturn::with_dom(dom, rect, rect)
}
const UNCONSTRAINED: f32 = 4096.0;
fn resolve_icons_in_dom_inner(
dom: &mut Dom,
provider: &SharedIconProvider,
system_style: &SystemStyle,
) {
let mut seen = 0;
while let Some(spec) = icon_spec_of(dom) {
if seen >= MAX_ICON_INDIRECTION {
break;
}
let replacement = provider.resolve_cached(&dom.root, spec.as_str(), system_style);
if icon_spec_of(&replacement).as_ref().map(AzString::as_str) == Some(spec.as_str()) {
break;
}
let mut css = dom.css.clone().into_library_owned_vec();
let mut replacement = replacement;
css.extend(replacement.css.clone().into_library_owned_vec());
replacement.css = css.into();
*dom = replacement;
seen += 1;
}
for child in dom.children.as_mut() {
resolve_icons_in_dom_inner(child, provider, system_style);
}
}
fn icon_spec_of(dom: &Dom) -> Option<AzString> {
let NodeType::Icon(name) = dom.root.get_node_type() else {
return None;
};
let name = name.as_str();
if !name.is_empty() {
return Some(AzString::from(name));
}
let mut derived = alloc::string::String::new();
for child in dom.children.as_ref() {
if let NodeType::Text(t) = child.root.get_node_type() {
derived.push_str(t.as_str());
}
}
let derived = derived.trim();
if derived.is_empty() {
None
} else {
Some(AzString::from(derived))
}
}
impl_option!(IconProviderHandle, OptionIconProviderHandle, [Clone]);
#[cfg(test)]
#[path = "icon_test.rs"]
mod icon_test;