1#![doc(
9 html_logo_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/azul_logo_full_min.svg.png",
10 html_favicon_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/favicon.ico"
11)]
12#![deny(unused_must_use)]
23#![warn(clippy::all)]
24#![cfg_attr(not(test), warn(
29 clippy::pedantic,
30 clippy::nursery,
31 clippy::cargo,
32 missing_debug_implementations,
35 missing_copy_implementations,
36 unreachable_pub,
37 unused_qualifications,
38 unused_lifetimes,
39 unused_import_braces,
40 unused_macro_rules,
41 unused_crate_dependencies,
42 meta_variable_misuse,
43 trivial_casts,
44 trivial_numeric_casts,
45 elided_lifetimes_in_paths,
46 single_use_lifetimes,
47 variant_size_differences,
48 non_ascii_idents,
49 unsafe_op_in_unsafe_fn,
50 let_underscore_drop,
51))]
52#![allow(
53 unknown_lints,
57 mismatched_lifetime_syntaxes, function_casts_as_integer, unused_imports,
61 unused_variables,
62 unused_mut,
63 dead_code,
64 clippy::too_many_arguments,
66 clippy::legacy_numeric_constants,
68 unexpected_cfgs, deprecated, clippy::multiple_crate_versions,
74)]
75
76#[macro_use]
77extern crate alloc;
78extern crate core;
79extern crate self as azul_layout;
83
84#[cfg(feature = "icons")]
89use brotli_decompressor as _;
90use lru as _;
92#[cfg(feature = "text_layout")]
95use unicode_normalization as _;
96#[cfg(feature = "xml")]
97use xmlwriter as _;
98
99#[cfg(feature = "web_lift")]
115#[inline]
116pub unsafe fn az_mark(_addr: u32, _val: u32) {
117 core::ptr::write_volatile(_addr as usize as *mut u32, _val);
119}
120#[cfg(not(feature = "web_lift"))]
128#[inline]
129pub const unsafe fn az_mark(_addr: u32, _val: u32) {}
130
131#[cfg(feature = "web_lift")]
140#[inline]
141#[must_use] pub unsafe fn az_mark_read(_addr: u32) -> u32 {
142 core::ptr::read_volatile(_addr as usize as *const u32)
144}
145#[cfg(not(feature = "web_lift"))]
152#[inline]
153#[must_use] pub const unsafe fn az_mark_read(_addr: u32) -> u32 {
154 0
155}
156
157pub mod font_traits;
159pub mod probe;
162#[cfg(feature = "image_decoding")]
164pub mod image;
165#[cfg(feature = "text_layout")]
167#[allow(private_interfaces)]
169pub mod managers;
170#[cfg(feature = "text_layout")]
172#[allow(
176 dropping_references,
177 private_interfaces,
178 unreachable_patterns,
179 unused_parens,
180 unused_doc_comments,
181 unused_assignments
182)]
183pub mod solver3;
184
185#[cfg(feature = "strfmt")]
187pub mod fmt;
188#[cfg(feature = "strfmt")]
189pub use fmt::{FmtArg, FmtArgVec, FmtArgVecDestructor, FmtValue, fmt_string};
190
191#[cfg(feature = "widgets")]
193#[allow(unused_assignments, improper_ctypes_definitions)]
196pub mod widgets;
197
198#[cfg(feature = "extra")]
200pub mod desktop;
201
202#[cfg(any(
204 feature = "icu",
205 all(target_os = "macos", feature = "icu_macos"),
206 all(target_os = "windows", feature = "icu_windows"),
207))]
208pub mod icu;
209#[cfg(any(
210 feature = "icu",
211 all(target_os = "macos", feature = "icu_macos"),
212 all(target_os = "windows", feature = "icu_windows"),
213))]
214pub use icu::{
215 DateTimeFieldSet, FormatLength, IcuDate, IcuDateTime, IcuError,
216 IcuLocalizer, IcuLocalizerHandle, IcuResult, IcuStringVec, IcuTime,
217 LayoutCallbackInfoIcuExt, ListType, PluralCategory,
218};
219
220#[cfg(feature = "fluent")]
222pub mod fluent;
223#[cfg(feature = "fluent")]
224pub use fluent::{
225 check_fluent_syntax, check_fluent_syntax_bytes, create_fluent_zip,
226 create_fluent_zip_from_strings, export_to_zip, FluentError,
227 FluentLanguageInfo, FluentLanguageInfoVec, FluentLoadError, FluentLoadErrorVec,
228 FluentLocalizerHandle, FluentSyntaxCheckResult,
229 FluentZipLoadResult,
230};
231
232pub use azul_core::url;
237pub use azul_core::url::{Url, UrlParseError, ResultUrlUrlParseError};
238
239#[allow(unused_doc_comments, clippy::should_implement_trait)]
244pub mod file;
245pub use file::{
246 dir_create, dir_create_all, dir_list, dir_delete, dir_delete_all,
247 file_append, file_copy, path_exists, file_metadata, file_read, file_read_string,
248 file_delete, file_rename, file_write, file_write_string,
249 path_canonicalize, path_extension, path_file_name, path_is_dir, path_is_file,
250 path_join, path_parent, temp_dir,
251 DirEntry, DirEntryVec, DirEntryVecDestructor, DirEntryVecDestructorType,
252 FileError, FileErrorKind, FileMetadata, FilePath, OptionFilePath,
253};
254
255pub mod http;
259pub use http::{
260 download_bytes, download_bytes_with_config, http_get,
261 http_get_with_config, is_url_reachable, HttpError, HttpHeader,
262 HttpRequestConfig, HttpResponse, HttpResponseTooLargeError, HttpResult,
263 HttpStatusError,
264};
265
266#[cfg(feature = "json")]
268pub mod json;
269#[cfg(feature = "json")]
270pub use json::{
271 json_parse, json_stringify,
272 Json, JsonInternal, JsonKeyValue, JsonKeyValueVec, JsonKeyValueVecDestructor, JsonKeyValueVecDestructorType,
273 JsonParseError, JsonType, JsonVec,
274 ResultJsonJsonParseError, OptionJson, OptionJsonVec, OptionJsonKeyValueVec,
275};
276
277#[cfg(feature = "zip")]
279pub mod zip;
280#[cfg(feature = "zip")]
281pub use zip::{
282 zip_create, zip_create_from_files, zip_extract_all, zip_list_contents,
283 ZipFile, ZipFileEntry, ZipFileEntryVec, ZipPathEntry, ZipPathEntryVec,
284 ZipReadConfig, ZipWriteConfig, ZipReadError, ZipWriteError,
285};
286
287pub mod icon;
289pub use icon::{
290 default_icon_resolver,
292 ImageIconData, FontIconData,
294 register_image_icon,
296 register_font_icon,
297 register_icons_from_zip,
298 create_default_icon_provider,
299 register_material_icons,
300 register_embedded_material_icons,
301};
302pub use azul_core::icon::{
304 IconProviderHandle, IconResolverCallbackType,
305 resolve_icons_in_styled_dom, OptionIconProviderHandle,
306};
307
308#[cfg(feature = "text_layout")]
310pub mod callbacks;
311#[cfg(feature = "cpurender")]
313#[allow(clippy::type_complexity)]
315pub mod cpurender;
316#[cfg(feature = "cpurender")]
318pub mod glyph_cache;
319#[cfg(feature = "text_layout")]
321pub mod default_actions;
322#[cfg(feature = "text_layout")]
324pub mod event_determination;
325#[cfg(feature = "text_layout")]
327#[allow(clippy::type_complexity)]
329pub mod font;
330
331#[cfg(feature = "text_layout")]
335pub mod headless;
336#[cfg(feature = "text_layout")]
338pub use allsorts::subset::CmapTarget;
339#[cfg(feature = "text_layout")]
340pub use font::parsed::{
341 FontParseWarning, FontParseWarningSeverity, FontType, OwnedGlyph, ParsedFont, PdfFontMetrics,
342 SubsetFont,
343};
344#[cfg(feature = "text_layout_hyphenation")]
346pub use hyphenation;
347#[cfg(feature = "text_layout")]
349pub mod hit_test;
350pub use azul_core::paged;
354#[cfg(feature = "text_layout")]
356#[allow(private_interfaces, unused_labels, clippy::type_complexity)]
359pub mod text3;
360#[cfg(feature = "text_layout")]
362pub mod thread;
363#[cfg(feature = "text_layout")]
365#[allow(clippy::non_canonical_partial_ord_impl)]
367pub mod timer;
368#[cfg(feature = "text_layout")]
370pub mod scroll_timer;
371#[cfg(feature = "text_layout")]
373#[allow(unused_parens)]
375pub mod window;
376#[cfg(feature = "text_layout")]
378pub mod window_state;
379#[cfg(feature = "xml")]
381#[allow(unused_assignments)]
383pub mod xml;
384
385#[cfg(feature = "e2e-server")]
388pub mod e2e;
389
390pub use paged::PageMargins;
393#[cfg(feature = "text_layout")]
394pub use hit_test::{CursorTypeHitTest, FullHitTest};
395#[cfg(feature = "text_layout")]
396pub use solver3::cache::LayoutCache as Solver3LayoutCache;
397#[cfg(feature = "text_layout")]
398pub use solver3::display_list::DisplayList as DisplayList3;
399#[cfg(feature = "text_layout")]
400pub use solver3::layout_document;
401#[cfg(feature = "text_layout")]
402pub use solver3::paged_layout::layout_document_paged;
403#[cfg(feature = "text_layout")]
404pub use solver3::{LayoutContext, LayoutError, Result as LayoutResult3};
405#[cfg(feature = "text_layout")]
406pub use text3::cache::{FontContext, FontManager, TextShapingCache};
407#[cfg(feature = "text_layout")]
411pub use text3::cache::TextShapingCache as TextLayoutCache;
412#[cfg(feature = "font_async_registry")]
413pub use rust_fontconfig::registry::FcFontRegistry;
414#[cfg(feature = "text_layout")]
415pub use window::{CursorBlinkTimerAction, LayoutWindow, ScrollbarDragState, TooltipTimerAction};
416#[cfg(feature = "text_layout")]
417pub use managers::text_input::{PendingTextEdit, OptionPendingTextEdit};
418
419#[cfg(feature = "text_layout")]
420#[allow(clippy::needless_pass_by_value)]
425pub fn parse_font_fn(
426 source: azul_core::resources::LoadedFontSource,
427) -> Option<azul_css::props::basic::FontRef> {
428 use crate::font::parsed::ParsedFont;
429
430 ParsedFont::from_bytes(
431 source.data.as_ref(),
432 source.index as usize,
433 &mut Vec::new(), )
435 .map(parsed_font_to_font_ref)
436}
437
438#[cfg(feature = "text_layout")]
439pub fn parsed_font_to_font_ref(
442 parsed_font: ParsedFont,
443) -> azul_css::props::basic::FontRef {
444 use core::ffi::c_void;
445
446 extern "C" fn parsed_font_destructor(ptr: *mut c_void) {
447 unsafe {
448 drop(Box::from_raw(ptr.cast::<ParsedFont>()));
449 }
450 }
451
452 let boxed = Box::new(parsed_font);
453 let raw_ptr = Box::into_raw(boxed) as *const c_void;
454 azul_css::props::basic::FontRef::new(raw_ptr, parsed_font_destructor)
455}
456
457#[cfg(feature = "text_layout")]
458#[must_use] pub const fn font_ref_to_parsed_font(
464 font_ref: &azul_css::props::basic::FontRef,
465) -> &ParsedFont {
466 unsafe { &*font_ref.parsed.cast::<ParsedFont>() }
469}
470
471#[cfg(test)]
472mod autotest_generated {
473 use super::*;
487
488 #[cfg(not(feature = "web_lift"))]
497 #[test]
498 fn az_mark_read_is_zero_for_every_boundary_address() {
499 let addresses = [
500 0u32,
501 1,
502 0x3_FFFF, 0x4_0000, 0x6_0758, 0xF_0000, 0xF_0001, i32::MIN as u32, (-1i32) as u32, u32::MAX - 1,
510 u32::MAX,
511 u32::MAX.wrapping_add(1), ];
513 for addr in addresses {
514 assert_eq!(unsafe { az_mark_read(addr) }, 0, "az_mark_read(0x{addr:x})");
515 }
516 }
517
518 #[cfg(not(feature = "web_lift"))]
521 #[test]
522 fn az_mark_read_sweeps_the_whole_address_space_as_zero() {
523 for addr in (0u32..=u32::MAX).step_by(1 << 24) {
524 assert_eq!(unsafe { az_mark_read(addr) }, 0);
525 }
526 }
527
528 #[cfg(not(feature = "web_lift"))]
531 #[test]
532 fn az_mark_writes_are_unobservable_without_web_lift() {
533 let addresses = [0u32, 0x4_0000, 0x6_0758, 0xF_0000, u32::MAX];
534 let values = [0u32, 1, u32::MAX / 2, u32::MAX - 1, u32::MAX, i32::MIN as u32];
535 for addr in addresses {
536 for val in values {
537 unsafe { az_mark(addr, val) };
538 assert_eq!(
539 unsafe { az_mark_read(addr) },
540 0,
541 "az_mark(0x{addr:x}, {val}) must not be observable"
542 );
543 }
544 }
545 for _ in 0..1_000 {
547 unsafe { az_mark(0x6_0758, u32::MAX) };
548 }
549 assert_eq!(unsafe { az_mark_read(0x6_0758) }, 0);
550 }
551
552 #[cfg(not(feature = "web_lift"))]
556 #[test]
557 fn az_mark_no_op_variants_are_const_evaluable() {
558 const _WRITE_MIN: () = unsafe { az_mark(0, 0) };
559 const _WRITE_MAX: () = unsafe { az_mark(u32::MAX, u32::MAX) };
560 const READ_ZERO: u32 = unsafe { az_mark_read(0) };
561 const READ_MAX: u32 = unsafe { az_mark_read(u32::MAX) };
562 assert_eq!(READ_ZERO, 0);
563 assert_eq!(READ_MAX, 0);
564 }
565
566 #[cfg(feature = "text_layout")]
572 const MOCK_MONO: &[u8] = crate::text3::mock_fonts::MOCK_MONO_TTF;
573
574 #[cfg(feature = "text_layout")]
575 fn loaded_source(bytes: Vec<u8>, index: u32) -> azul_core::resources::LoadedFontSource {
576 azul_core::resources::LoadedFontSource {
577 data: azul_css::U8Vec::from_vec(bytes),
578 index,
579 load_outlines: true,
580 }
581 }
582
583 #[cfg(feature = "text_layout")]
584 fn parse_mock() -> ParsedFont {
585 ParsedFont::from_bytes(MOCK_MONO, 0, &mut Vec::new())
586 .expect("Azul Mock Mono must parse (positive control)")
587 }
588
589 #[cfg(feature = "text_layout")]
596 #[test]
597 fn parse_font_fn_rejects_malformed_input() {
598 let cases: Vec<(&str, Vec<u8>)> = vec![
599 ("empty", Vec::new()),
600 ("single_nul", vec![0u8]),
601 ("whitespace_only", b" \t\n".to_vec()),
602 ("garbage", (0u8..=255).cycle().take(4096).collect()),
603 ("invalid_utf8", vec![0xFF, 0xFE, 0x00]),
604 ("sfnt_magic_only", vec![0x00, 0x01, 0x00, 0x00]),
605 ("header_only", MOCK_MONO[..12].to_vec()),
606 ("truncated_font", MOCK_MONO[..64].to_vec()),
607 ("half_a_font", MOCK_MONO[..MOCK_MONO.len() / 2].to_vec()),
608 ("unicode_emoji", "\u{1F600}\u{1F600}".repeat(1_000).into_bytes()),
609 ("combining_marks", "e\u{0301}".repeat(10_000).into_bytes()),
610 ("nested_brackets", vec![b'['; 10_000]),
611 ("boundary_numbers", b"0 -0 9223372036854775807 NaN inf -inf 1e309".to_vec()),
612 ("leading_junk_then_font", {
613 let mut v = b"garbage".to_vec();
614 v.extend_from_slice(MOCK_MONO);
615 v
616 }),
617 ];
618 for (name, bytes) in cases {
619 assert!(
620 parse_font_fn(loaded_source(bytes, 0)).is_none(),
621 "{name} must not parse into a FontRef"
622 );
623 }
624 }
625
626 #[cfg(feature = "text_layout")]
629 #[test]
630 fn parse_font_fn_survives_extremely_long_input() {
631 assert!(parse_font_fn(loaded_source(vec![0u8; 1_000_000], 0)).is_none());
632 assert!(parse_font_fn(loaded_source(vec![b'a'; 1_000_000], 0)).is_none());
633 let mut ttcf = b"ttcf".to_vec();
635 ttcf.extend_from_slice(&vec![0xABu8; 1_000_000]);
636 assert!(parse_font_fn(loaded_source(ttcf, 0)).is_none());
637 }
638
639 #[cfg(feature = "text_layout")]
643 #[test]
644 fn parse_font_fn_parses_the_positive_control() {
645 let font_ref = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0))
646 .expect("the positive control must parse");
647 let parsed = font_ref_to_parsed_font(&font_ref);
648
649 assert_eq!(parsed.num_glyphs(), 96);
650 assert_eq!(parsed.num_glyphs(), parsed.maxp_table.num_glyphs);
651 assert_eq!(parsed.font_metrics.units_per_em, 1000);
652 assert!(parsed.font_metrics.ascent > 0.0);
653 assert!(parsed.font_metrics.descent <= 0.0);
654 assert!(parsed.font_metrics.ascent.is_finite());
655 assert!(parsed.font_metrics.descent.is_finite());
656 assert!(parsed.font_metrics.line_gap.is_finite());
657 assert_eq!(parsed.font_type, FontType::TrueType);
658 assert_eq!(parsed.original_index, 0);
659 assert!(parsed.cmap_subtable.is_some());
660 assert_eq!(parsed.hash, parse_mock().hash);
661 }
662
663 #[cfg(feature = "text_layout")]
667 #[test]
668 fn parse_font_fn_ignores_the_load_outlines_flag() {
669 let with = azul_core::resources::LoadedFontSource {
670 data: azul_css::U8Vec::from_vec(MOCK_MONO.to_vec()),
671 index: 0,
672 load_outlines: true,
673 };
674 let without = azul_core::resources::LoadedFontSource {
675 data: azul_css::U8Vec::from_vec(MOCK_MONO.to_vec()),
676 index: 0,
677 load_outlines: false,
678 };
679 let a = parse_font_fn(with).expect("parses with outlines");
680 let b = parse_font_fn(without).expect("parses without outlines");
681 let (pa, pb) = (font_ref_to_parsed_font(&a), font_ref_to_parsed_font(&b));
682 assert_eq!(pa.hash, pb.hash);
683 assert_eq!(pa.num_glyphs(), pb.num_glyphs());
684 assert_eq!(pa.pdf_font_metrics, pb.pdf_font_metrics);
685 }
686
687 #[cfg(feature = "text_layout")]
691 #[test]
692 fn parse_font_fn_with_an_out_of_range_index_is_deterministic() {
693 let baseline = parse_mock();
694 for index in [1u32, 2, 0x7FFF_FFFF, u32::MAX - 1, u32::MAX] {
695 if let Some(font_ref) = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), index)) {
696 let parsed = font_ref_to_parsed_font(&font_ref);
697 assert_eq!(
698 parsed.num_glyphs(),
699 baseline.num_glyphs(),
700 "index {index} must not conjure a different face"
701 );
702 assert_eq!(parsed.original_index, index as usize);
703 }
704 }
705 }
706
707 #[cfg(feature = "text_layout")]
710 #[test]
711 fn parse_font_fn_failure_path_is_repeatable() {
712 for _ in 0..200 {
713 assert!(parse_font_fn(loaded_source(Vec::new(), 0)).is_none());
714 assert!(parse_font_fn(loaded_source(vec![0xFF; 3], u32::MAX)).is_none());
715 }
716 assert!(parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).is_some());
718 }
719
720 #[cfg(feature = "text_layout")]
724 #[test]
725 fn parse_font_fn_mints_a_fresh_identity_per_call() {
726 let a = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).expect("parses");
727 let b = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).expect("parses");
728 assert_ne!(a, b, "two parses of the same bytes are two distinct handles");
729 assert_ne!(a.id, b.id);
730 assert!(b > a, "ids are monotonically assigned");
731 assert_eq!(
733 font_ref_to_parsed_font(&a).hash,
734 font_ref_to_parsed_font(&b).hash
735 );
736 }
737
738 #[cfg(feature = "text_layout")]
745 #[test]
746 fn parsed_font_font_ref_round_trip_preserves_the_face() {
747 let original = parse_mock();
748 let expected_hash = original.hash;
749 let expected_glyphs = original.num_glyphs();
750 let expected_metrics = original.pdf_font_metrics;
751 let expected_upem = original.font_metrics.units_per_em;
752 let expected_ascent = original.font_metrics.ascent;
753 let expected_type = original.font_type.clone();
754 let expected_index = original.original_index;
755
756 let font_ref = parsed_font_to_font_ref(original);
757 let decoded = font_ref_to_parsed_font(&font_ref);
758
759 assert_eq!(decoded.hash, expected_hash);
760 assert_eq!(decoded.num_glyphs(), expected_glyphs);
761 assert_eq!(decoded.pdf_font_metrics, expected_metrics);
762 assert_eq!(decoded.font_metrics.units_per_em, expected_upem);
763 assert!((decoded.font_metrics.ascent - expected_ascent).abs() < f32::EPSILON);
764 assert_eq!(decoded.font_type, expected_type);
765 assert_eq!(decoded.original_index, expected_index);
766 }
767
768 #[cfg(feature = "text_layout")]
771 #[test]
772 fn parsed_font_to_font_ref_handle_invariants() {
773 use core::sync::atomic::Ordering as AtomicOrdering;
774
775 let font_ref = parsed_font_to_font_ref(parse_mock());
776 assert!(!font_ref.parsed.is_null());
777 assert!(!font_ref.copies.is_null());
778 assert!(font_ref.run_destructor);
779 assert_ne!(font_ref.id, 0, "id 0 is reserved for un-initialised handles");
780 assert_eq!(unsafe { (*font_ref.copies).load(AtomicOrdering::SeqCst) }, 1);
781 assert_eq!(font_ref.get_parsed(), font_ref.parsed);
782 }
783
784 #[cfg(feature = "text_layout")]
787 #[test]
788 fn font_ref_to_parsed_font_is_a_stable_reborrow() {
789 use core::ffi::c_void;
790
791 let font_ref = parsed_font_to_font_ref(parse_mock());
792 let first: *const ParsedFont = font_ref_to_parsed_font(&font_ref);
793 let second: *const ParsedFont = font_ref_to_parsed_font(&font_ref);
794 assert!(core::ptr::eq(first, second), "reborrow must be stable");
795 assert!(core::ptr::eq(first.cast::<c_void>(), font_ref.get_parsed()));
796 }
797
798 #[cfg(feature = "text_layout")]
802 #[test]
803 fn cloning_a_font_ref_shares_the_face_and_the_drop_is_refcounted() {
804 use core::sync::atomic::Ordering as AtomicOrdering;
805
806 let original = parsed_font_to_font_ref(parse_mock());
807 let expected_hash = font_ref_to_parsed_font(&original).hash;
808
809 let clone = original.clone();
810 assert_eq!(clone.id, original.id, "a clone is the same font");
811 assert_eq!(clone, original);
812 assert!(core::ptr::eq(clone.parsed, original.parsed));
813 assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 2);
814
815 drop(clone);
816 assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1);
817 assert_eq!(
818 font_ref_to_parsed_font(&original).hash,
819 expected_hash,
820 "the face must survive its clone being dropped"
821 );
822 assert_eq!(font_ref_to_parsed_font(&original).num_glyphs(), 96);
823 }
824
825 #[cfg(feature = "text_layout")]
829 #[test]
830 fn font_ref_clone_drop_cycles_do_not_double_free() {
831 use core::sync::atomic::Ordering as AtomicOrdering;
832
833 let original = parsed_font_to_font_ref(parse_mock());
834 let expected_hash = font_ref_to_parsed_font(&original).hash;
835
836 for _ in 0..1_000 {
837 let c = original.clone();
838 assert_eq!(font_ref_to_parsed_font(&c).hash, expected_hash);
839 }
840
841 let batch: Vec<_> = (0..1_000).map(|_| original.clone()).collect();
842 assert_eq!(
843 unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
844 1_001
845 );
846 drop(batch);
847 assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1);
848 assert_eq!(font_ref_to_parsed_font(&original).hash, expected_hash);
849 }
850
851 #[cfg(feature = "text_layout")]
854 #[test]
855 fn font_ref_identity_is_per_handle_not_per_content() {
856 use std::collections::{BTreeSet, HashSet};
857
858 let a = parsed_font_to_font_ref(parse_mock());
859 let b = parsed_font_to_font_ref(parse_mock());
860 assert_ne!(a, b);
861 assert!(a < b, "ids are monotonically assigned, so a precedes b");
862 assert_eq!(
863 font_ref_to_parsed_font(&a).hash,
864 font_ref_to_parsed_font(&b).hash,
865 "…even though the content hash is the same"
866 );
867
868 let set: HashSet<_> = vec![a.clone(), a.clone(), a.clone(), b.clone()]
869 .into_iter()
870 .collect();
871 assert_eq!(set.len(), 2, "clones dedup, distinct handles do not");
872
873 let ordered: BTreeSet<_> = vec![b.clone(), a.clone(), b.clone()].into_iter().collect();
874 assert_eq!(ordered.len(), 2);
875 assert_eq!(ordered.iter().next(), Some(&a));
876 }
877
878 #[cfg(feature = "text_layout")]
881 #[test]
882 fn many_font_refs_do_not_alias_each_other() {
883 let refs: Vec<_> = (0..16).map(|_| parsed_font_to_font_ref(parse_mock())).collect();
884 for (i, a) in refs.iter().enumerate() {
885 assert_eq!(font_ref_to_parsed_font(a).num_glyphs(), 96);
886 for b in refs.iter().skip(i + 1) {
887 assert!(
888 !core::ptr::eq(a.parsed, b.parsed),
889 "independently boxed faces must not share a pointer"
890 );
891 assert_ne!(a.id, b.id);
892 }
893 }
894 }
895}