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