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