Skip to main content

azul_layout/
icon.rs

1//! Default icon resolver implementations for Azul
2//!
3//! This module provides the standard callback implementations for icon resolution.
4//! The core types and resolution infrastructure are in `azul_core::icon`.
5//!
6//! # Usage
7//!
8//! ```rust,ignore
9//! use azul_core::icon::IconProviderHandle;
10//! use azul_layout::icon::{default_icon_resolver, ImageIconData, FontIconData};
11//!
12//! // Create provider with the default resolver
13//! let provider = IconProviderHandle::with_resolver(default_icon_resolver);
14//!
15//! // Register an image icon
16//! provider.register_icon("app-images", "logo", RefAny::new(ImageIconData { 
17//!     image: image_ref, width: 32.0, height: 32.0 
18//! }));
19//!
20//! // Register a font icon
21//! provider.register_icon("material-icons", "home", RefAny::new(FontIconData {
22//!     font: font_ref, icon_char: "\u{e88a}".to_string()
23//! }));
24//! ```
25
26use alloc::{
27    string::{String, ToString},
28    vec::Vec,
29};
30
31use azul_css::{
32    system::SystemStyle,
33    props::basic::{FontRef, StyleFontFamily, StyleFontFamilyVec},
34    props::basic::length::FloatValue,
35    props::layout::{LayoutWidth, LayoutHeight},
36    props::property::CssProperty,
37    props::style::filter::{StyleFilter, StyleFilterVec, StyleColorMatrix},
38    props::style::text::StyleTextColor,
39    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
40    css::{Css, CssPropertyValue},
41};
42
43use azul_core::{
44    dom::{Dom, NodeData},
45    icon::IconProviderHandle,
46    refany::{OptionRefAny, RefAny},
47    resources::ImageRef,
48    styled_dom::StyledDom,
49};
50
51// ============================================================================
52// Icon Data Marker Structs (for RefAny::downcast)
53// ============================================================================
54
55/// Image-based icon data stored in `RefAny` for the icon resolver.
56///
57/// Pass to `register_image_icon` or wrap in `RefAny::new(...)` and register
58/// directly via `IconProviderHandle::register_icon`.
59pub struct ImageIconData {
60    pub image: ImageRef,
61    /// Width duplicated from ImageRef at registration time
62    pub width: f32,
63    /// Height duplicated from ImageRef at registration time
64    pub height: f32,
65}
66
67/// Font-based icon data stored in `RefAny` for the icon resolver.
68///
69/// Pass to `register_font_icon` or wrap in `RefAny::new(...)` and register
70/// directly via `IconProviderHandle::register_icon`.
71pub struct FontIconData {
72    pub font: FontRef,
73    /// The character/codepoint for this specific icon (e.g., "\u{e88a}" for home)
74    pub icon_char: String,
75}
76
77// ============================================================================
78// Default Icon Resolver
79// ============================================================================
80
81/// Default icon resolver that handles both image and font icons.
82///
83/// Resolution logic:
84/// 1. If icon_data is None -> return empty div (icon not found)
85/// 2. If icon_data contains ImageIconData -> render as image
86/// 3. If icon_data contains FontIconData -> render as text with font
87/// 4. Unknown data type -> return empty div
88///
89/// Styles from the original icon DOM are copied to the result,
90/// filtered based on SystemStyle preferences.
91pub extern "C" fn default_icon_resolver(
92    icon_data: OptionRefAny,
93    original_icon_dom: &StyledDom,
94    system_style: &SystemStyle,
95) -> StyledDom {
96    // No icon found → empty div
97    let Some(mut data) = icon_data.into_option() else {
98        let mut dom = Dom::create_div();
99        return StyledDom::create(&mut dom, Css::empty());
100    };
101    
102    // Try ImageIconData
103    if let Some(img) = data.downcast_ref::<ImageIconData>() {
104        return create_image_icon_from_original(&*img, original_icon_dom, system_style);
105    }
106    
107    // Try FontIconData
108    if let Some(font_icon) = data.downcast_ref::<FontIconData>() {
109        return create_font_icon_from_original(&*font_icon, original_icon_dom, system_style);
110    }
111    
112    // Unknown data type → empty div
113    let mut dom = Dom::create_div();
114    StyledDom::create(&mut dom, Css::empty())
115}
116
117// Icon DOM Creation (from original)
118
119/// Create a StyledDom for an image-based icon, copying styles from original.
120///
121/// Applies SystemStyle-aware modifications:
122/// - Grayscale filter if `prefer_grayscale` is true
123/// - Tint color overlay if `tint_color` is set
124fn create_image_icon_from_original(
125    img: &ImageIconData,
126    original: &StyledDom,
127    system_style: &SystemStyle,
128) -> StyledDom {
129    let mut dom = Dom::create_image(img.image.clone());
130    
131    // Copy appropriate styles from original
132    if let Some(original_node) = original.node_data.as_ref().first() {
133        let mut props_vec = copy_appropriate_styles_vec(original_node);
134        
135        // Add default dimensions if not specified in original styles
136        let has_width = props_vec.iter().any(|p| matches!(&p.property, CssProperty::Width(_)));
137        let has_height = props_vec.iter().any(|p| matches!(&p.property, CssProperty::Height(_)));
138        
139        if !has_width {
140            props_vec.push(CssPropertyWithConditions::simple(
141                CssProperty::width(LayoutWidth::px(img.width))
142            ));
143        }
144        if !has_height {
145            props_vec.push(CssPropertyWithConditions::simple(
146                CssProperty::height(LayoutHeight::px(img.height))
147            ));
148        }
149        
150        // Apply SystemStyle-aware filters
151        apply_icon_style_filters(&mut props_vec, system_style);
152        
153        dom.root.set_css_props(CssPropertyWithConditionsVec::from_vec(props_vec));
154        
155        // Copy accessibility info
156        if let Some(a11y) = original_node.get_accessibility_info() {
157            dom = dom.with_accessibility_info(*a11y.clone());
158        }
159    } else {
160        // No original node, use default dimensions
161        let mut props_vec = vec![
162            CssPropertyWithConditions::simple(CssProperty::width(LayoutWidth::px(img.width))),
163            CssPropertyWithConditions::simple(CssProperty::height(LayoutHeight::px(img.height))),
164        ];
165        
166        // Apply SystemStyle-aware filters even without original node
167        apply_icon_style_filters(&mut props_vec, system_style);
168        
169        dom.root.set_css_props(CssPropertyWithConditionsVec::from_vec(props_vec));
170    }
171    
172    StyledDom::create(&mut dom, Css::empty())
173}
174
175/// Create a StyledDom for a font-based icon, copying styles from original.
176///
177/// Applies SystemStyle-aware modifications:
178/// - Text color override if `inherit_text_color` is true
179/// - Tint color if `tint_color` is set
180fn create_font_icon_from_original(
181    font_icon: &FontIconData,
182    original: &StyledDom,
183    system_style: &SystemStyle,
184) -> StyledDom {
185    let mut dom = Dom::create_text(font_icon.icon_char.clone());
186    
187    // Add font family
188    let font_prop = CssPropertyWithConditions::simple(
189        CssProperty::font_family(StyleFontFamilyVec::from_vec(vec![
190            StyleFontFamily::Ref(font_icon.font.clone())
191        ]))
192    );
193    
194    if let Some(original_node) = original.node_data.as_ref().first() {
195        let mut props_vec = copy_appropriate_styles_vec(original_node);
196        props_vec.push(font_prop);
197        
198        // Apply SystemStyle-aware color modifications for font icons
199        apply_font_icon_color(&mut props_vec, system_style);
200        
201        dom.root.set_css_props(CssPropertyWithConditionsVec::from_vec(props_vec));
202        
203        // Copy accessibility info
204        if let Some(a11y) = original_node.get_accessibility_info() {
205            dom = dom.with_accessibility_info(*a11y.clone());
206        }
207    } else {
208        // No original node, just set the font
209        let mut props_vec = vec![font_prop];
210        
211        // Apply SystemStyle-aware color modifications
212        apply_font_icon_color(&mut props_vec, system_style);
213        
214        dom.root.set_css_props(CssPropertyWithConditionsVec::from_vec(props_vec));
215    }
216    
217    StyledDom::create(&mut dom, Css::empty())
218}
219
220/// Copy styles from original node
221/// Returns a Vec for easier manipulation
222fn copy_appropriate_styles_vec(
223    original_node: &NodeData,
224) -> Vec<CssPropertyWithConditions> {
225    // Reconstruct the legacy flat list from the unified Css store.
226    original_node
227        .get_style()
228        .iter_inline_properties()
229        .map(|(prop, conds)| CssPropertyWithConditions {
230            property: prop.clone(),
231            apply_if: conds.clone(),
232        })
233        .collect()
234}
235
236/// Apply SystemStyle-aware filters to icon properties.
237///
238/// This adds CSS filters based on accessibility and theming settings:
239/// - Grayscale filter if `prefer_grayscale` is true
240fn apply_icon_style_filters(
241    props_vec: &mut Vec<CssPropertyWithConditions>,
242    system_style: &SystemStyle,
243) {
244    let icon_style = &system_style.icon_style;
245    
246    // Collect filters to apply
247    let mut filters = Vec::new();
248    
249    // Grayscale filter: Uses a color matrix that converts to grayscale
250    // Standard luminance weights: R*0.2126 + G*0.7152 + B*0.0722
251    if icon_style.prefer_grayscale {
252        // Grayscale color matrix (4x5):
253        // [0.2126, 0.7152, 0.0722, 0, 0]  <- R output
254        // [0.2126, 0.7152, 0.0722, 0, 0]  <- G output
255        // [0.2126, 0.7152, 0.0722, 0, 0]  <- B output
256        // [0,      0,      0,      1, 0]  <- A output
257        let grayscale_matrix = StyleColorMatrix {
258            m0: FloatValue::new(0.2126),
259            m1: FloatValue::new(0.7152),
260            m2: FloatValue::new(0.0722),
261            m3: FloatValue::new(0.0),
262            m4: FloatValue::new(0.0),
263            m5: FloatValue::new(0.2126),
264            m6: FloatValue::new(0.7152),
265            m7: FloatValue::new(0.0722),
266            m8: FloatValue::new(0.0),
267            m9: FloatValue::new(0.0),
268            m10: FloatValue::new(0.2126),
269            m11: FloatValue::new(0.7152),
270            m12: FloatValue::new(0.0722),
271            m13: FloatValue::new(0.0),
272            m14: FloatValue::new(0.0),
273            m15: FloatValue::new(0.0),
274            m16: FloatValue::new(0.0),
275            m17: FloatValue::new(0.0),
276            m18: FloatValue::new(1.0),
277            m19: FloatValue::new(0.0),
278        };
279        filters.push(StyleFilter::ColorMatrix(grayscale_matrix));
280    }
281    
282    // Apply tint color as a flood filter if specified
283    if let azul_css::props::basic::color::OptionColorU::Some(tint) = &icon_style.tint_color {
284        filters.push(StyleFilter::Flood(*tint));
285    }
286    
287    // Add filters if any were collected
288    if !filters.is_empty() {
289        props_vec.push(CssPropertyWithConditions::simple(
290            CssProperty::Filter(CssPropertyValue::Exact(StyleFilterVec::from_vec(filters)))
291        ));
292    }
293}
294
295/// Apply SystemStyle-aware color modifications for font icons.
296///
297/// Font icons can use text color directly, so we can:
298/// - Apply tint color as text color
299/// - Inherit text color from parent
300fn apply_font_icon_color(
301    props_vec: &mut Vec<CssPropertyWithConditions>,
302    system_style: &SystemStyle,
303) {
304    let icon_style = &system_style.icon_style;
305    
306    // If tint color is specified, use it as the text color
307    if let azul_css::props::basic::color::OptionColorU::Some(tint) = &icon_style.tint_color {
308        props_vec.push(CssPropertyWithConditions::simple(
309            CssProperty::TextColor(CssPropertyValue::Exact(StyleTextColor { inner: *tint }))
310        ));
311    }
312    // Note: inherit_text_color doesn't need explicit handling - text color
313    // is inherited by default in CSS. We only need to NOT override it.
314}
315
316// IconProviderHandle Helper Functions
317
318/// Register an image icon in a pack
319pub fn register_image_icon(provider: &mut IconProviderHandle, pack_name: &str, icon_name: &str, image: ImageRef) {
320    // Get dimensions from ImageRef
321    let size = image.get_size();
322    let data = ImageIconData { 
323        image, 
324        width: size.width, 
325        height: size.height,
326    };
327    provider.register_icon(pack_name, icon_name, RefAny::new(data));
328}
329
330/// Register icons from a ZIP file (file names become icon names)
331#[cfg(feature = "zip_support")]
332pub fn register_icons_from_zip(provider: &mut IconProviderHandle, pack_name: &str, zip_bytes: &[u8]) {
333    for (icon_name, image, width, height) in load_images_from_zip(zip_bytes) {
334        let data = ImageIconData { image, width, height };
335        provider.register_icon(pack_name, &icon_name, RefAny::new(data));
336    }
337}
338
339#[cfg(not(feature = "zip_support"))]
340pub fn register_icons_from_zip(_provider: &mut IconProviderHandle, _pack_name: &str, _zip_bytes: &[u8]) {
341    // ZIP support not enabled
342}
343
344/// Register a font icon in a pack
345pub fn register_font_icon(provider: &mut IconProviderHandle, pack_name: &str, icon_name: &str, font: FontRef, icon_char: &str) {
346    let data = FontIconData { 
347        font, 
348        icon_char: icon_char.to_string() 
349    };
350    provider.register_icon(pack_name, icon_name, RefAny::new(data));
351}
352
353// ============================================================================
354// ZIP Support
355// ============================================================================
356
357/// Load all images from a ZIP file, returning (icon_name, ImageRef, width, height)
358#[cfg(all(feature = "zip_support", feature = "image_decoding"))]
359fn load_images_from_zip(zip_bytes: &[u8]) -> Vec<(String, ImageRef, f32, f32)> {
360    use crate::zip::{ZipFile, ZipReadConfig};
361    use crate::image::decode::{decode_raw_image_from_any_bytes, ResultRawImageDecodeImageError};
362    use std::path::Path;
363    
364    let mut result = Vec::new();
365    let config = ZipReadConfig::default();
366    let entries = match ZipFile::list(zip_bytes, &config) {
367        Ok(e) => e,
368        Err(_) => return result,
369    };
370    
371    for entry in entries.iter() {
372        if entry.path.ends_with('/') { continue; } // Skip directories
373        
374        let file_bytes = match ZipFile::get_single_file(zip_bytes, entry, &config) {
375            Ok(Some(b)) => b,
376            _ => continue,
377        };
378        
379        // Decode as image
380        if let ResultRawImageDecodeImageError::Ok(raw_image) = decode_raw_image_from_any_bytes(&file_bytes) {
381            // Icon name = filename without extension
382            let path = Path::new(&entry.path);
383            let icon_name = path.file_stem()
384                .and_then(|s| s.to_str())
385                .unwrap_or("")
386                .to_string();
387            
388            let width = raw_image.width as f32;
389            let height = raw_image.height as f32;
390            
391            if let Some(image) = ImageRef::new_rawimage(raw_image) {
392                result.push((icon_name, image, width, height));
393            }
394        }
395    }
396    
397    result
398}
399
400#[cfg(not(all(feature = "zip_support", feature = "image_decoding")))]
401fn load_images_from_zip(_zip_bytes: &[u8]) -> Vec<(String, ImageRef, f32, f32)> {
402    Vec::new()
403}
404
405// ============================================================================
406// Material Icons Registration
407// ============================================================================
408
409/// Register all Material Icons in the provider.
410/// 
411/// This registers all 2234 Material Icons from the `material-icons` crate.
412/// Each icon is registered under the "material-icons" pack with its HTML name
413/// (e.g., "home", "settings", "arrow_back", etc.).
414/// 
415/// Requires the "icons" feature with material-icons crate.
416#[cfg(feature = "icons")]
417pub fn register_material_icons(provider: &mut IconProviderHandle, font: FontRef) {
418    use material_icons::{ALL_ICONS, icon_to_char, icon_to_html_name};
419    
420    // Register all Material Icons with their Unicode codepoints
421    for icon in ALL_ICONS.iter() {
422        let icon_char = icon_to_char(*icon);
423        let name = icon_to_html_name(icon);
424        
425        let data = FontIconData {
426            font: font.clone(),
427            icon_char: icon_char.to_string(),
428        };
429        provider.register_icon("material-icons", name, RefAny::new(data));
430    }
431}
432
433#[cfg(not(feature = "icons"))]
434pub fn register_material_icons(_provider: &mut IconProviderHandle, _font: FontRef) {
435    // Icons feature not enabled
436}
437
438/// Load the embedded Material Icons font and register all standard icons.
439/// 
440/// This uses the `material-icons` crate which embeds the Material Icons TTF font.
441/// The font is Apache 2.0 licensed by Google.
442/// 
443/// Returns true if registration was successful.
444/// Register all Material Icons from caller-supplied TTF bytes.
445///
446/// The font bytes are NOT embedded here. `azul-doc codegen all` generates
447/// `target/codegen/material_icons.ttf.br`, and `azul-doc` builds (depends
448/// on) `azul-layout` — so `include!`ing that generated artifact in this
449/// crate is a build cycle (it bit us on `cargo clean`). The `include!` +
450/// brotli-decompression live in `azul-dll` (downstream of codegen), which
451/// passes the decompressed TTF in here.
452#[cfg(all(feature = "icons", feature = "text_layout"))]
453pub fn register_embedded_material_icons(
454    provider: &mut IconProviderHandle,
455    font_bytes: &[u8],
456) -> bool {
457    use crate::font::parsed::ParsedFont;
458    use crate::parsed_font_to_font_ref;
459
460    let mut warnings = Vec::new();
461    let parsed_font = match ParsedFont::from_bytes(font_bytes, 0, &mut warnings) {
462        Some(f) => f,
463        None => {
464            return false;
465        }
466    };
467
468    let font_ref = parsed_font_to_font_ref(parsed_font);
469    register_material_icons(provider, font_ref);
470
471    true
472}
473
474#[cfg(not(all(feature = "icons", feature = "text_layout")))]
475pub fn register_embedded_material_icons(
476    _provider: &mut IconProviderHandle,
477    _font_bytes: &[u8],
478) -> bool {
479    // Icons or text_layout feature not enabled
480    false
481}
482
483// ============================================================================
484// Convenience Functions
485// ============================================================================
486
487/// Create an IconProviderHandle with the default resolver.
488pub fn create_default_icon_provider() -> IconProviderHandle {
489    IconProviderHandle::with_resolver(default_icon_resolver)
490}
491
492// The embedded Material Icons font bytes (the `include!` of the
493// codegen-generated `target/codegen/material_icons.ttf.br` + brotli
494// decompression) deliberately live in `azul-dll`, not here — see
495// `register_embedded_material_icons` above for why (build-cycle: azul-doc
496// builds azul-layout to generate that artifact).
497
498// ============================================================================
499// Tests
500// ============================================================================
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    #[test]
507    fn test_default_resolver_no_data() {
508        let style = SystemStyle::default();
509        let original = StyledDom::default();
510        
511        let result = default_icon_resolver(OptionRefAny::None, &original, &style);
512        
513        // Without data, should return empty div StyledDom
514        assert_eq!(result.node_data.as_ref().len(), 1);
515    }
516    
517    #[test]
518    fn test_create_default_provider() {
519        let provider = create_default_icon_provider();
520        assert!(provider.list_packs().is_empty());
521    }
522}