1use 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#[derive(Debug)]
60pub struct ImageIconData {
61 pub image: ImageRef,
62 pub width: f32,
64 pub height: f32,
66}
67
68#[derive(Debug)]
73pub struct FontIconData {
74 pub font: FontRef,
75 pub icon_char: String,
77}
78
79#[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 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 if let Some(img) = data.downcast_ref::<ImageIconData>() {
106 return create_image_icon_from_original(&img, original_icon_dom, system_style);
107 }
108
109 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 let mut dom = Dom::create_div();
116 StyledDom::create(&mut dom, Css::empty())
117}
118
119fn 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 if let Some(original_node) = original.node_data.as_ref().first() {
135 let mut props_vec = copy_appropriate_styles_vec(original_node);
136
137 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_icon_style_filters(&mut props_vec, system_style);
154
155 dom.root.set_css_props(CssPropertyWithConditionsVec::from_vec(props_vec));
156
157 if let Some(a11y) = original_node.get_accessibility_info() {
159 dom = dom.with_accessibility_info(a11y.clone());
160 }
161 } else {
162 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_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
177fn 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 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_font_icon_color(&mut props_vec, system_style);
202
203 dom.root.set_css_props(CssPropertyWithConditionsVec::from_vec(props_vec));
204
205 if let Some(a11y) = original_node.get_accessibility_info() {
207 dom = dom.with_accessibility_info(a11y.clone());
208 }
209 } else {
210 let mut props_vec = vec![font_prop];
212
213 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
222fn copy_appropriate_styles_vec(
225 original_node: &NodeData,
226) -> Vec<CssPropertyWithConditions> {
227 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
238fn 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 let mut filters = Vec::new();
250
251 if icon_style.prefer_grayscale {
254 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 if let azul_css::props::basic::color::OptionColorU::Some(tint) = &icon_style.tint_color {
286 filters.push(StyleFilter::Flood(*tint));
287 }
288
289 if !filters.is_empty() {
291 props_vec.push(CssPropertyWithConditions::simple(
292 CssProperty::Filter(CssPropertyValue::Exact(StyleFilterVec::from_vec(filters)))
293 ));
294 }
295}
296
297fn 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 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 }
317
318pub fn register_image_icon(provider: &mut IconProviderHandle, pack_name: &str, icon_name: &str, image: ImageRef) {
322 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#[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 }
345
346pub 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#[cfg(all(feature = "zip", feature = "image_decoding"))]
361#[allow(clippy::cast_precision_loss)] fn 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; } let Ok(Some(file_bytes)) = ZipFile::get_single_file(zip_bytes, entry, &config) else {
377 continue;
378 };
379
380 if let ResultRawImageDecodeImageError::Ok(raw_image) = decode_raw_image_from_any_bytes(&file_bytes) {
382 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#[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 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 }
438
439#[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 false
479}
480
481pub fn create_default_icon_provider() -> IconProviderHandle {
487 IconProviderHandle::with_resolver(default_icon_resolver)
488}
489
490#[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 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}
521
522#[cfg(test)]
523#[allow(
524 clippy::float_cmp,
525 clippy::items_after_statements,
526 clippy::redundant_clone,
527 clippy::cast_possible_truncation,
528 clippy::cast_precision_loss,
529 clippy::cast_sign_loss,
530 clippy::cast_lossless,
531 clippy::unreadable_literal,
532 clippy::too_many_lines,
533 clippy::many_single_char_names,
534 clippy::similar_names,
535 unused_qualifications,
536 unreachable_pub,
537 private_interfaces
538)] mod autotest_generated {
540 use azul_core::{
541 a11y::SmallAriaInfo,
542 dom::NodeType,
543 resources::RawImageFormat,
544 };
545 use azul_css::props::basic::color::{ColorU, OptionColorU};
546
547 use super::*;
548
549 fn dummy_font_ref() -> FontRef {
558 static DUMMY_FONT_DATA: u8 = 0;
559 extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
560 FontRef::new(
561 core::ptr::addr_of!(DUMMY_FONT_DATA).cast::<core::ffi::c_void>(),
562 dummy_destructor,
563 )
564 }
565
566 fn null_img(width: usize, height: usize) -> ImageRef {
569 ImageRef::null_image(width, height, RawImageFormat::RGBA8, Vec::new())
570 }
571
572 fn image_icon(width: f32, height: f32) -> ImageIconData {
574 ImageIconData {
575 image: null_img(1, 1),
576 width,
577 height,
578 }
579 }
580
581 fn font_icon(icon_char: &str) -> FontIconData {
582 FontIconData {
583 font: dummy_font_ref(),
584 icon_char: icon_char.to_string(),
585 }
586 }
587
588 fn grayscale_style() -> SystemStyle {
589 let mut s = SystemStyle::default();
590 s.icon_style.prefer_grayscale = true;
591 s
592 }
593
594 fn tint_style(color: ColorU) -> SystemStyle {
595 let mut s = SystemStyle::default();
596 s.icon_style.tint_color = OptionColorU::Some(color);
597 s
598 }
599
600 fn original_with(props: Vec<CssPropertyWithConditions>) -> StyledDom {
602 let mut dom = Dom::create_div();
603 dom.root
604 .set_css_props(CssPropertyWithConditionsVec::from_vec(props));
605 StyledDom::create(&mut dom, Css::empty())
606 }
607
608 fn original_without_nodes() -> StyledDom {
612 StyledDom {
613 node_data: Vec::new().into(),
614 ..StyledDom::default()
615 }
616 }
617
618 fn all_props(dom: &StyledDom) -> Vec<CssPropertyWithConditions> {
622 dom.node_data
623 .as_ref()
624 .iter()
625 .flat_map(|nd| {
626 nd.get_style()
627 .iter_inline_properties()
628 .map(|(property, apply_if)| CssPropertyWithConditions {
629 property: property.clone(),
630 apply_if: apply_if.clone(),
631 })
632 .collect::<Vec<_>>()
633 })
634 .collect()
635 }
636
637 fn width_px(dom: &StyledDom) -> Option<f32> {
638 all_props(dom).into_iter().find_map(|p| match p.property {
639 CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(px))) => Some(px.number.get()),
640 _ => None,
641 })
642 }
643
644 fn height_px(dom: &StyledDom) -> Option<f32> {
645 all_props(dom).into_iter().find_map(|p| match p.property {
646 CssProperty::Height(CssPropertyValue::Exact(LayoutHeight::Px(px))) => {
647 Some(px.number.get())
648 }
649 _ => None,
650 })
651 }
652
653 fn count_widths(dom: &StyledDom) -> usize {
654 all_props(dom)
655 .iter()
656 .filter(|p| matches!(p.property, CssProperty::Width(_)))
657 .count()
658 }
659
660 fn text_of(dom: &StyledDom) -> Option<String> {
661 dom.node_data
662 .as_ref()
663 .iter()
664 .find_map(|nd| match nd.get_node_type() {
665 NodeType::Text(t) => Some(t.as_str().to_string()),
666 _ => None,
667 })
668 }
669
670 fn has_image_node(dom: &StyledDom) -> bool {
671 dom.node_data
672 .as_ref()
673 .iter()
674 .any(|nd| matches!(nd.get_node_type(), NodeType::Image(_)))
675 }
676
677 fn filters_of(props: &[CssPropertyWithConditions]) -> Vec<StyleFilter> {
679 props
680 .iter()
681 .filter_map(|p| match &p.property {
682 CssProperty::Filter(CssPropertyValue::Exact(v)) => Some(v.as_ref().to_vec()),
683 _ => None,
684 })
685 .flatten()
686 .collect()
687 }
688
689 fn text_color_of(props: &[CssPropertyWithConditions]) -> Option<ColorU> {
690 props.iter().find_map(|p| match &p.property {
691 CssProperty::TextColor(CssPropertyValue::Exact(c)) => Some(c.inner),
692 _ => None,
693 })
694 }
695
696 fn resolve(data: RefAny, original: &StyledDom, style: &SystemStyle) -> StyledDom {
697 default_icon_resolver(OptionRefAny::Some(data), original, style)
698 }
699
700 #[test]
705 fn resolver_none_yields_single_unstyled_div() {
706 let out = default_icon_resolver(
707 OptionRefAny::None,
708 &StyledDom::default(),
709 &SystemStyle::default(),
710 );
711 assert_eq!(out.node_data.as_ref().len(), 1);
712 assert!(matches!(
713 out.node_data.as_ref()[0].get_node_type(),
714 NodeType::Div
715 ));
716 assert!(all_props(&out).is_empty());
719 }
720
721 #[test]
722 fn resolver_unknown_refany_type_yields_empty_div() {
723 struct NotAnIconAtAll {
726 _payload: [u64; 4],
727 }
728 let data = RefAny::new(NotAnIconAtAll { _payload: [7; 4] });
729 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
730
731 assert_eq!(out.node_data.as_ref().len(), 1);
732 assert!(matches!(
733 out.node_data.as_ref()[0].get_node_type(),
734 NodeType::Div
735 ));
736 assert!(!has_image_node(&out));
737 assert!(text_of(&out).is_none());
738 }
739
740 #[test]
741 fn resolver_image_icon_yields_image_node_with_default_dimensions() {
742 let out = resolve(
743 RefAny::new(image_icon(32.0, 24.0)),
744 &StyledDom::default(),
745 &SystemStyle::default(),
746 );
747 assert!(has_image_node(&out));
748 assert_eq!(width_px(&out), Some(32.0));
749 assert_eq!(height_px(&out), Some(24.0));
750 }
751
752 #[test]
753 fn resolver_font_icon_yields_text_node_with_font_family() {
754 let font = dummy_font_ref();
755 let data = RefAny::new(FontIconData {
756 font: font.clone(),
757 icon_char: "\u{e88a}".to_string(),
758 });
759 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
760
761 assert_eq!(out.node_data.as_ref().len(), 1);
762 assert_eq!(text_of(&out).as_deref(), Some("\u{e88a}"));
763
764 let has_font = all_props(&out).iter().any(|p| match &p.property {
766 CssProperty::FontFamily(CssPropertyValue::Exact(families)) => families
767 .as_ref()
768 .iter()
769 .any(|f| matches!(f, StyleFontFamily::Ref(fr) if *fr == font)),
770 _ => false,
771 });
772 assert!(has_font, "font-family with the icon's FontRef must be set");
773 }
774
775 #[test]
780 fn image_icon_with_node_less_original_still_gets_dimensions() {
781 let original = original_without_nodes();
784 let out = resolve(
785 RefAny::new(image_icon(16.0, 16.0)),
786 &original,
787 &SystemStyle::default(),
788 );
789 assert!(has_image_node(&out));
790 assert_eq!(width_px(&out), Some(16.0));
791 assert_eq!(height_px(&out), Some(16.0));
792 }
793
794 #[test]
795 fn font_icon_with_node_less_original_still_gets_font() {
796 let original = original_without_nodes();
797 let out = resolve(RefAny::new(font_icon("A")), &original, &SystemStyle::default());
798 assert_eq!(text_of(&out).as_deref(), Some("A"));
799 assert!(all_props(&out)
800 .iter()
801 .any(|p| matches!(p.property, CssProperty::FontFamily(_))));
802 }
803
804 #[test]
809 fn image_icon_nan_dimensions_saturate_to_zero_without_panicking() {
810 let out = resolve(
813 RefAny::new(image_icon(f32::NAN, f32::NAN)),
814 &StyledDom::default(),
815 &SystemStyle::default(),
816 );
817 let (w, h) = (
818 width_px(&out).expect("width emitted"),
819 height_px(&out).expect("height emitted"),
820 );
821 assert!(w.is_finite() && h.is_finite(), "NaN must not survive into CSS");
822 assert_eq!(w, 0.0);
823 assert_eq!(h, 0.0);
824 }
825
826 #[test]
827 fn image_icon_infinite_dimensions_saturate_to_finite_values() {
828 let out = resolve(
829 RefAny::new(image_icon(f32::INFINITY, f32::NEG_INFINITY)),
830 &StyledDom::default(),
831 &SystemStyle::default(),
832 );
833 let w = width_px(&out).expect("width emitted");
834 let h = height_px(&out).expect("height emitted");
835 assert!(w.is_finite(), "+inf must saturate, got {w}");
836 assert!(h.is_finite(), "-inf must saturate, got {h}");
837 assert!(w > 0.0 && h < 0.0, "saturation must keep the sign");
838 }
839
840 #[test]
841 fn image_icon_negative_dimensions_are_passed_through_unclamped() {
842 let out = resolve(
845 RefAny::new(image_icon(-32.0, -1.5)),
846 &StyledDom::default(),
847 &SystemStyle::default(),
848 );
849 assert_eq!(width_px(&out), Some(-32.0));
850 assert_eq!(height_px(&out), Some(-1.5));
851 }
852
853 #[test]
854 fn register_image_icon_with_usize_max_size_saturates() {
855 let mut provider = create_default_icon_provider();
858 register_image_icon(
859 &mut provider,
860 "huge",
861 "big",
862 null_img(usize::MAX, usize::MAX),
863 );
864 let data = provider.lookup("big").expect("icon registered");
865 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
866
867 let w = width_px(&out).expect("width emitted");
868 assert!(w.is_finite() && w > 0.0, "usize::MAX size must saturate finitely, got {w}");
869 }
870
871 #[test]
872 fn image_icon_zero_size_is_preserved() {
873 let out = resolve(
874 RefAny::new(image_icon(0.0, 0.0)),
875 &StyledDom::default(),
876 &SystemStyle::default(),
877 );
878 assert_eq!(width_px(&out), Some(0.0));
879 assert_eq!(height_px(&out), Some(0.0));
880 }
881
882 #[test]
887 fn original_dimensions_win_over_image_defaults() {
888 let original = original_with(vec![
889 CssPropertyWithConditions::simple(CssProperty::width(LayoutWidth::px(999.0))),
890 CssPropertyWithConditions::simple(CssProperty::height(LayoutHeight::px(888.0))),
891 ]);
892 let out = resolve(
893 RefAny::new(image_icon(32.0, 32.0)),
894 &original,
895 &SystemStyle::default(),
896 );
897
898 assert_eq!(width_px(&out), Some(999.0));
899 assert_eq!(height_px(&out), Some(888.0));
900 assert_eq!(count_widths(&out), 1);
902 }
903
904 #[test]
905 fn copy_appropriate_styles_vec_round_trips_exactly() {
906 let props = vec![
908 CssPropertyWithConditions::simple(CssProperty::width(LayoutWidth::px(12.5))),
909 CssPropertyWithConditions::simple(CssProperty::height(LayoutHeight::px(7.0))),
910 ];
911 let mut nd = NodeData::create_div();
912 nd.set_css_props(CssPropertyWithConditionsVec::from_vec(props.clone()));
913
914 assert_eq!(copy_appropriate_styles_vec(&nd), props);
915 }
916
917 #[test]
918 fn copy_appropriate_styles_vec_of_unstyled_node_is_empty() {
919 let nd = NodeData::create_div();
920 assert!(copy_appropriate_styles_vec(&nd).is_empty());
921 }
922
923 #[test]
924 fn copy_appropriate_styles_vec_preserves_order_of_many_props() {
925 let props: Vec<CssPropertyWithConditions> = (0..512u32)
928 .map(|i| {
929 CssPropertyWithConditions::simple(CssProperty::width(LayoutWidth::px(i as f32)))
930 })
931 .collect();
932 let mut nd = NodeData::create_div();
933 nd.set_css_props(CssPropertyWithConditionsVec::from_vec(props.clone()));
934
935 let copied = copy_appropriate_styles_vec(&nd);
936 assert_eq!(copied.len(), 512);
937 assert_eq!(copied, props);
938 }
939
940 #[test]
941 fn accessibility_info_is_copied_onto_the_resolved_icon() {
942 let mut dom = Dom::create_div().with_accessibility_info(SmallAriaInfo::label("Save").to_full_info());
943 let original = StyledDom::create(&mut dom, Css::empty());
944
945 let out = resolve(
946 RefAny::new(image_icon(8.0, 8.0)),
947 &original,
948 &SystemStyle::default(),
949 );
950
951 let a11y = out
952 .node_data
953 .as_ref()
954 .iter()
955 .find_map(azul_core::dom::NodeData::get_accessibility_info)
956 .expect("a11y info must survive icon resolution");
957 assert_eq!(
958 a11y.accessibility_name.as_ref().map(|s| s.as_str()),
959 Some("Save")
960 );
961 }
962
963 #[test]
968 fn icon_filters_default_style_adds_nothing() {
969 let mut props = Vec::new();
970 apply_icon_style_filters(&mut props, &SystemStyle::default());
971 assert!(props.is_empty(), "default SystemStyle must not synthesise a filter");
972 }
973
974 #[test]
975 fn icon_filters_grayscale_uses_quantised_luminance_matrix() {
976 let mut props = Vec::new();
977 apply_icon_style_filters(&mut props, &grayscale_style());
978
979 let filters = filters_of(&props);
980 assert_eq!(filters.len(), 1);
981 let StyleFilter::ColorMatrix(m) = &filters[0] else {
982 panic!("prefer_grayscale must emit a ColorMatrix filter, got {:?}", filters[0]);
983 };
984
985 for r in [m.m0, m.m5, m.m10] {
987 assert!((r.get() - 0.2126).abs() < 0.001, "R weight {}", r.get());
988 }
989 for g in [m.m1, m.m6, m.m11] {
990 assert!((g.get() - 0.7152).abs() < 0.001, "G weight {}", g.get());
991 }
992 for b in [m.m2, m.m7, m.m12] {
993 assert!((b.get() - 0.0722).abs() < 0.001, "B weight {}", b.get());
994 }
995 assert_eq!(m.m18.get(), 1.0);
997 assert_eq!(m.m15.get(), 0.0);
998 assert_eq!(m.m19.get(), 0.0);
999
1000 assert_ne!(m.m0.get(), 0.2126);
1002 }
1003
1004 #[test]
1005 fn icon_filters_tint_emits_flood_even_when_fully_transparent() {
1006 let transparent = ColorU { r: 1, g: 2, b: 3, a: 0 };
1008 let mut props = Vec::new();
1009 apply_icon_style_filters(&mut props, &tint_style(transparent));
1010
1011 let filters = filters_of(&props);
1012 assert_eq!(filters.len(), 1);
1013 assert!(matches!(filters[0], StyleFilter::Flood(c) if c == transparent));
1014 }
1015
1016 #[test]
1017 fn icon_filters_grayscale_and_tint_are_ordered_matrix_then_flood() {
1018 let tint = ColorU { r: 255, g: 0, b: 128, a: 255 };
1019 let mut style = grayscale_style();
1020 style.icon_style.tint_color = OptionColorU::Some(tint);
1021
1022 let mut props = Vec::new();
1023 apply_icon_style_filters(&mut props, &style);
1024
1025 let filter_decls = props
1028 .iter()
1029 .filter(|p| matches!(p.property, CssProperty::Filter(_)))
1030 .count();
1031 assert_eq!(filter_decls, 1);
1032
1033 let filters = filters_of(&props);
1034 assert_eq!(filters.len(), 2);
1035 assert!(matches!(filters[0], StyleFilter::ColorMatrix(_)));
1036 assert!(matches!(filters[1], StyleFilter::Flood(c) if c == tint));
1037 }
1038
1039 #[test]
1040 fn icon_filters_preserve_pre_existing_properties() {
1041 let mut props = vec![CssPropertyWithConditions::simple(CssProperty::width(
1042 LayoutWidth::px(4.0),
1043 ))];
1044 apply_icon_style_filters(&mut props, &grayscale_style());
1045
1046 assert_eq!(props.len(), 2);
1047 assert!(matches!(props[0].property, CssProperty::Width(_)), "existing props must not be clobbered");
1048 assert!(matches!(props[1].property, CssProperty::Filter(_)));
1049 }
1050
1051 #[test]
1052 fn image_icon_grayscale_reaches_the_resolved_dom() {
1053 let out = resolve(
1054 RefAny::new(image_icon(10.0, 10.0)),
1055 &StyledDom::default(),
1056 &grayscale_style(),
1057 );
1058 let filters = filters_of(&all_props(&out));
1059 assert_eq!(filters.len(), 1);
1060 assert!(matches!(filters[0], StyleFilter::ColorMatrix(_)));
1061 }
1062
1063 #[test]
1068 fn font_icon_color_default_style_adds_nothing() {
1069 let mut props = Vec::new();
1070 apply_font_icon_color(&mut props, &SystemStyle::default());
1071 assert!(props.is_empty());
1072 }
1073
1074 #[test]
1075 fn font_icon_color_inherit_text_color_alone_is_a_noop() {
1076 let mut style = SystemStyle::default();
1079 style.icon_style.inherit_text_color = true;
1080 let mut props = Vec::new();
1081 apply_font_icon_color(&mut props, &style);
1082 assert!(props.is_empty());
1083 }
1084
1085 #[test]
1086 fn font_icon_color_tint_becomes_text_color() {
1087 let tint = ColorU { r: 9, g: 8, b: 7, a: 6 };
1088 let mut props = Vec::new();
1089 apply_font_icon_color(&mut props, &tint_style(tint));
1090
1091 assert_eq!(props.len(), 1);
1092 assert_eq!(text_color_of(&props), Some(tint));
1093 }
1094
1095 #[test]
1096 fn font_icon_color_tint_wins_over_inherit_text_color() {
1097 let tint = ColorU { r: 1, g: 1, b: 1, a: 255 };
1098 let mut style = tint_style(tint);
1099 style.icon_style.inherit_text_color = true;
1100
1101 let mut props = Vec::new();
1102 apply_font_icon_color(&mut props, &style);
1103 assert_eq!(text_color_of(&props), Some(tint));
1104 }
1105
1106 #[test]
1107 fn font_icons_never_get_a_grayscale_filter() {
1108 let out = resolve(
1111 RefAny::new(font_icon("\u{e88a}")),
1112 &StyledDom::default(),
1113 &grayscale_style(),
1114 );
1115 assert!(filters_of(&all_props(&out)).is_empty());
1116 }
1117
1118 #[test]
1123 fn font_icon_empty_char_yields_empty_text_node() {
1124 let out = resolve(
1125 RefAny::new(font_icon("")),
1126 &StyledDom::default(),
1127 &SystemStyle::default(),
1128 );
1129 assert_eq!(text_of(&out).as_deref(), Some(""));
1130 }
1131
1132 #[test]
1133 fn font_icon_hostile_unicode_round_trips_verbatim() {
1134 for s in [
1137 "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}",
1138 "\u{202E}gnippilf\u{202C}",
1139 "e\u{0301}\u{0327}\u{0328}",
1140 "a\0b",
1141 "\u{F8FF}",
1142 "\u{FFFD}",
1143 ] {
1144 let out = resolve(
1145 RefAny::new(font_icon(s)),
1146 &StyledDom::default(),
1147 &SystemStyle::default(),
1148 );
1149 assert_eq!(text_of(&out).as_deref(), Some(s), "icon_char {s:?} was altered");
1150 }
1151 }
1152
1153 #[test]
1154 fn font_icon_huge_char_string_does_not_panic() {
1155 let huge = "\u{e88a}".repeat(65_536);
1156 let out = resolve(
1157 RefAny::new(font_icon(&huge)),
1158 &StyledDom::default(),
1159 &SystemStyle::default(),
1160 );
1161 assert_eq!(text_of(&out).map(|s| s.chars().count()), Some(65_536));
1162 }
1163
1164 #[test]
1169 fn register_image_icon_lowercases_the_name_and_reads_size_from_the_imageref() {
1170 let mut provider = create_default_icon_provider();
1171 register_image_icon(&mut provider, "App-Images", "HOME", null_img(64, 32));
1172
1173 assert_eq!(provider.list_packs(), vec![String::from("App-Images")]);
1175 assert_eq!(
1176 provider.list_icons_in_pack("App-Images"),
1177 vec![String::from("home")]
1178 );
1179 assert!(provider.list_icons_in_pack("app-images").is_empty());
1180 assert!(provider.has_icon("hOmE"));
1181
1182 let data = provider.lookup("HOME").expect("case-insensitive lookup");
1183 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
1184 assert_eq!(width_px(&out), Some(64.0));
1185 assert_eq!(height_px(&out), Some(32.0));
1186 }
1187
1188 #[test]
1189 fn register_font_icon_accepts_empty_pack_and_icon_names() {
1190 let mut provider = create_default_icon_provider();
1191 register_font_icon(&mut provider, "", "", dummy_font_ref(), "");
1192
1193 assert_eq!(provider.list_packs(), vec![String::new()]);
1194 assert!(provider.has_icon(""));
1195 let data = provider.lookup("").expect("empty-named icon is still addressable");
1196 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
1197 assert_eq!(text_of(&out).as_deref(), Some(""));
1198 }
1199
1200 #[test]
1201 fn register_icon_handles_oversized_and_unicode_names() {
1202 let mut provider = create_default_icon_provider();
1203 let long_name = "n".repeat(10_000);
1204 register_font_icon(&mut provider, "p", &long_name, dummy_font_ref(), "x");
1205 assert!(provider.has_icon(&long_name));
1206
1207 register_font_icon(&mut provider, "p", "\u{130}", dummy_font_ref(), "y");
1210 let folded = "\u{130}".to_lowercase();
1211 assert!(provider.has_icon("\u{130}"));
1212 assert!(provider.has_icon(&folded));
1213 assert!(!provider.has_icon("i"));
1214 }
1215
1216 #[test]
1217 fn duplicate_icon_across_packs_resolves_to_the_alphabetically_first_pack() {
1218 let mut provider = create_default_icon_provider();
1221 register_image_icon(&mut provider, "zzz", "dup", null_img(1, 1));
1222 register_image_icon(&mut provider, "aaa", "dup", null_img(2, 2));
1223
1224 let data = provider.lookup("dup").expect("icon registered");
1225 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
1226 assert_eq!(
1227 width_px(&out),
1228 Some(2.0),
1229 "lookup must return the alphabetically-first pack's icon"
1230 );
1231 }
1232
1233 #[test]
1234 fn re_registering_an_icon_replaces_it_and_unregistering_drops_the_empty_pack() {
1235 let mut provider = create_default_icon_provider();
1236 register_image_icon(&mut provider, "p", "icon", null_img(1, 1));
1237 register_image_icon(&mut provider, "p", "ICON", null_img(5, 5));
1238
1239 assert_eq!(provider.list_icons_in_pack("p").len(), 1);
1240 let data = provider.lookup("icon").expect("icon registered");
1241 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
1242 assert_eq!(width_px(&out), Some(5.0));
1243
1244 provider.unregister_icon("p", "IcOn");
1245 assert!(!provider.has_icon("icon"));
1246 assert!(provider.list_packs().is_empty(), "empty pack must be removed");
1247 }
1248
1249 #[test]
1250 fn create_default_icon_provider_starts_empty_and_misses_resolve_to_a_placeholder() {
1251 let provider = create_default_icon_provider();
1252 assert!(provider.list_packs().is_empty());
1253 assert!(provider.lookup("nope").is_none());
1254 assert!(!provider.has_icon("nope"));
1255
1256 let out = default_icon_resolver(
1257 OptionRefAny::from(provider.lookup("nope")),
1258 &StyledDom::default(),
1259 &SystemStyle::default(),
1260 );
1261 assert_eq!(out.node_data.as_ref().len(), 1);
1262 assert!(all_props(&out).is_empty());
1263 }
1264
1265 #[test]
1270 fn load_images_from_zip_rejects_malformed_archives() {
1271 assert!(load_images_from_zip(&[]).is_empty());
1272 assert!(load_images_from_zip(b"definitely not a zip file").is_empty());
1273 assert!(load_images_from_zip(b"PK\x03\x04\x00\x00\x00\x00").is_empty());
1275 assert!(load_images_from_zip(b"PK\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF").is_empty());
1277 assert!(load_images_from_zip(&[0xFFu8; 4096]).is_empty());
1278 }
1279
1280 #[test]
1281 fn register_icons_from_zip_registers_nothing_for_garbage_bytes() {
1282 for bytes in [
1283 &b""[..],
1284 &b"not a zip"[..],
1285 &b"PK\x03\x04\x00\x00\x00\x00"[..],
1286 &[0x00u8; 512][..],
1287 ] {
1288 let mut provider = create_default_icon_provider();
1289 register_icons_from_zip(&mut provider, "pack", bytes);
1290 assert!(
1291 provider.list_packs().is_empty(),
1292 "a malformed ZIP must not create a pack"
1293 );
1294 }
1295 }
1296
1297 #[test]
1298 fn register_embedded_material_icons_rejects_non_font_bytes() {
1299 for bytes in [
1300 &b""[..],
1301 &b"this is not a TTF"[..],
1302 &b"\x00\x01\x00\x00"[..],
1304 &[0xFFu8; 256][..],
1305 ] {
1306 let mut provider = create_default_icon_provider();
1307 let ok = register_embedded_material_icons(&mut provider, bytes);
1308 assert!(!ok, "corrupt font bytes must not report success");
1309 assert!(provider.list_packs().is_empty());
1310 }
1311 }
1312
1313 #[cfg(feature = "icons")]
1314 #[test]
1315 fn register_material_icons_fills_a_single_lowercase_pack() {
1316 let mut provider = create_default_icon_provider();
1317 let font = dummy_font_ref();
1318 register_material_icons(&mut provider, &font);
1319
1320 assert_eq!(provider.list_packs(), vec![String::from("material-icons")]);
1321 let names = provider.list_icons_in_pack("material-icons");
1322 assert!(names.len() > 1000, "expected the full icon set, got {}", names.len());
1323 assert!(
1324 names.iter().all(|n| *n == n.to_lowercase()),
1325 "every registered icon name must be normalised to lowercase"
1326 );
1327 assert!(provider.has_icon("home"));
1328 assert!(provider.has_icon("HOME"));
1329
1330 let data = provider.lookup("home").expect("material 'home' icon");
1331 let out = resolve(data, &StyledDom::default(), &SystemStyle::default());
1332 assert!(text_of(&out).is_some(), "a material icon must resolve to a text node");
1333 }
1334}