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

            
26
use alloc::{
27
    string::{String, ToString},
28
    vec::Vec,
29
};
30

            
31
use 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

            
43
use 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
pub 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`.
71
pub 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.
91
1
pub extern "C" fn default_icon_resolver(
92
1
    icon_data: OptionRefAny,
93
1
    original_icon_dom: &StyledDom,
94
1
    system_style: &SystemStyle,
95
1
) -> StyledDom {
96
    // No icon found → empty div
97
1
    let Some(mut data) = icon_data.into_option() else {
98
1
        let mut dom = Dom::create_div();
99
1
        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
1
}
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
124
fn 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
180
fn 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
222
fn 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
240
fn 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
300
fn 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
319
pub 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")]
332
pub 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"))]
340
pub 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
345
pub 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"))]
359
fn 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")))]
401
fn 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")]
417
pub 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"))]
434
pub 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"))]
453
pub 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")))]
475
pub 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.
488
1
pub fn create_default_icon_provider() -> IconProviderHandle {
489
1
    IconProviderHandle::with_resolver(default_icon_resolver)
490
1
}
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)]
503
mod tests {
504
    use super::*;
505

            
506
    #[test]
507
1
    fn test_default_resolver_no_data() {
508
1
        let style = SystemStyle::default();
509
1
        let original = StyledDom::default();
510
        
511
1
        let result = default_icon_resolver(OptionRefAny::None, &original, &style);
512
        
513
        // Without data, should return empty div StyledDom
514
1
        assert_eq!(result.node_data.as_ref().len(), 1);
515
1
    }
516
    
517
    #[test]
518
1
    fn test_create_default_provider() {
519
1
        let provider = create_default_icon_provider();
520
1
        assert!(provider.list_packs().is_empty());
521
1
    }
522
}