Skip to main content

azul_layout/
lib.rs

1//! Layout crate for the Azul GUI framework.
2//!
3//! Provides the layout solver (`solver3`), text shaping (`text3`), font
4//! management (`font`), hit testing, page fragmentation, and widget support.
5//! Integrates with `azul-core` for DOM types and `azul-css` for style
6//! properties.
7
8#![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// Lint policy: deny correctness/safety issues, warn on style (`clippy::all`).
13//
14// Crate-wide allows are intentionally limited to lints that are either
15//   (a) pervasive AND feature-sensitive — an import/binding/field that is unused
16//       under one feature set is live under another, so a per-site fix would
17//       break a different feature build — or
18//   (b) churny / newer-toolchain lints with little value in scoping.
19// Lints that fire in only a few, well-localized places are scoped with
20// `#[allow(...)]` on the specific `pub mod` declarations further down, so the
21// rest of the (hand-written) crate is actually checked.
22#![deny(unused_must_use)]
23#![warn(clippy::all)]
24// Extreme-lint lockdown: all clippy groups plus opt-in rustc lints, enforced as
25// -D warnings on library code by the CI clippy job. Test builds are exempt via
26// cfg(not(test)) below since the set is high-noise and low-value on unit and
27// generated tests; clippy::all correctness still applies to test code.
28#![cfg_attr(not(test), warn(
29    clippy::pedantic,
30    clippy::nursery,
31    clippy::cargo,
32    // missing_docs,  // TODO(docs): re-enable as a dedicated final docs pass; disabled
33    //                // for now so the cleanup focuses on code-quality lints, not doc debt.
34    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` lets the two forward-compat lints below be listed even on
54    // the CI toolchain (1.88), where they are not yet known, without emitting an
55    // "unknown lint" warning of their own. They still apply on newer rustc.
56    unknown_lints,
57    mismatched_lifetime_syntaxes,          // newer rustc; fires in macro-generated code
58    function_casts_as_integer,             // newer rustc; widget callback pointer identity
59    // pervasive + feature-sensitive (unused under one feature, live under another):
60    unused_imports,
61    unused_variables,
62    unused_mut,
63    dead_code,
64    // design lint, pervasive across the layout solver / renderer:
65    clippy::too_many_arguments,
66    // churny / 3rd-party, low value to scope:
67    clippy::legacy_numeric_constants,
68    unexpected_cfgs,                        // web-lift diagnostic cfgs
69    deprecated,                             // image crate tiff encoder (only under `tiff`)
70    // transitive dependency-version dups not resolvable in azul's source —
71    // syn 1↔2 (proc-macro migration), heck/jni-sys/rustc-hash/rustls-webpki;
72    // re-audit when the dep tree aligns.
73    clippy::multiple_crate_versions,
74)]
75
76#[macro_use]
77extern crate alloc;
78extern crate core;
79// Let this crate refer to itself as `azul_layout::…`. The e2e/debug-server port
80// (`src/e2e/full.rs`) was written verbatim against the published crate name; the
81// self-alias makes those ~80 `azul_layout::…` paths resolve without editing them.
82extern crate self as azul_layout;
83
84// Dependencies kept for downstream/feature-plumbing use but not referenced
85// directly in this crate's source — marked intentionally linked so
86// unused_crate_dependencies stays quiet (the lint's own suggested fix).
87// `brotli-decompressor`: decompresses the codegen material_icons.ttf.br in azul-dll.
88#[cfg(feature = "icons")]
89use brotli_decompressor as _;
90// `lru`: reserved for the slippy-map tile cache (azul-dll widgets).
91use lru as _;
92// `unicode-normalization` / `xmlwriter`: pulled by text_layout / xml for the
93// shaping + SVG-writer paths consumed downstream.
94#[cfg(feature = "text_layout")]
95use unicode_normalization as _;
96#[cfg(feature = "xml")]
97use xmlwriter as _;
98
99/// Web-lift diagnostic marker: a volatile store of `val` to the absolute wasm
100/// linear-memory address `addr` (the 0x40000–0xF0000 free band the e2e harness
101/// peeks via `AzStartup_peekU32`).
102///
103/// Compiles to NOTHING without the `web_lift`
104/// feature — absolute-address stores would segfault native builds (macOS
105/// `__PAGEZERO` covers the low 4 GiB). All in-tree diagnostic markers MUST go
106/// through this helper rather than calling `core::ptr::write_volatile` on a
107/// literal address directly.
108///
109/// # Safety
110///
111/// With the `web_lift` feature enabled, `addr` must be a valid, writable wasm
112/// linear-memory address (within the 0x40000–0xF0000 diagnostic band). Without
113/// the feature this is a no-op and always safe.
114#[cfg(feature = "web_lift")]
115#[inline]
116pub unsafe fn az_mark(_addr: u32, _val: u32) {
117    // Volatile isn't const-callable, so this variant is a plain (non-const) fn.
118    core::ptr::write_volatile(_addr as usize as *mut u32, _val);
119}
120/// No-op `const` variant used when the `web_lift` feature is off.
121///
122/// # Safety
123///
124/// Always safe — this variant does nothing; the `unsafe` marker only exists to
125/// keep the signature identical to the `web_lift` variant so call sites compile
126/// unchanged under both features.
127#[cfg(not(feature = "web_lift"))]
128#[inline]
129pub const unsafe fn az_mark(_addr: u32, _val: u32) {}
130
131/// Read counterpart of [`az_mark`] (marker counters like `0x60758`).
132/// Returns 0 without the `web_lift` feature.
133///
134/// # Safety
135///
136/// With the `web_lift` feature enabled, `addr` must be a valid, readable wasm
137/// linear-memory address (within the 0x40000–0xF0000 diagnostic band). Without
138/// the feature this is a no-op that returns 0 and is always safe.
139#[cfg(feature = "web_lift")]
140#[inline]
141#[must_use] pub unsafe fn az_mark_read(_addr: u32) -> u32 {
142    // Volatile isn't const-callable, so this variant is a plain (non-const) fn.
143    core::ptr::read_volatile(_addr as usize as *const u32)
144}
145/// No-op `const` variant (returns 0) used when the `web_lift` feature is off.
146///
147/// # Safety
148///
149/// Always safe — returns 0 and touches nothing; the `unsafe` marker only exists
150/// to keep the signature identical to the `web_lift` variant.
151#[cfg(not(feature = "web_lift"))]
152#[inline]
153#[must_use] pub const unsafe fn az_mark_read(_addr: u32) -> u32 {
154    0
155}
156
157/// Font traits available regardless of text layout feature.
158pub mod font_traits;
159/// Optional probe instrumentation. With the `probe` feature off this
160/// is a tiny module of no-op stubs and pays zero cost.
161pub mod probe;
162/// Image decoding and encoding (wraps the `image` crate).
163#[cfg(feature = "image_decoding")]
164pub mod image;
165/// Scroll, hover, clipboard, cursor, and focus managers.
166#[cfg(feature = "text_layout")]
167// Scoped (was crate-wide): internal manager types exposed for tests.
168#[allow(private_interfaces)]
169pub mod managers;
170/// CSS layout solver: block, inline, flex, grid, and table formatting.
171#[cfg(feature = "text_layout")]
172// Scoped (was crate-wide): solver internals — intentional `drop(&_)` scope
173// markers, internal types exposed for tests, incremental-relayout assignments,
174// generated/parenthesized property code, and exhaustive generated matches.
175#[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/// C-compatible string formatting via `strfmt`.
186#[cfg(feature = "strfmt")]
187pub mod fmt;
188#[cfg(feature = "strfmt")]
189pub use fmt::{FmtArg, FmtArgVec, FmtArgVecDestructor, FmtValue, fmt_string};
190
191/// Built-in widgets: button, text input, tabs, tree view, node graph, etc.
192#[cfg(feature = "widgets")]
193// Scoped (was crate-wide): incremental widget-state assignments and the
194// node_graph extern "C" fn that returns `()`.
195#[allow(unused_assignments, improper_ctypes_definitions)]
196pub mod widgets;
197
198/// Desktop platform helpers (file dialogs, notifications).
199#[cfg(feature = "extra")]
200pub mod desktop;
201
202/// ICU internationalization: date/time formatting, plurals, list formatting.
203#[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/// Project Fluent localization: message bundles, argument formatting, ZIP I/O.
221#[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
232/// URL parsing (RFC 3986 compliant). Pure-Rust, always present (no TLS deps).
233/// URL types live in `azul_core::url`; re-exported so `azul_layout::url::*`
234/// keeps resolving. `Url::parse`/`join` are enabled via the `http` feature
235/// (which turns on `azul-core/url`).
236pub use azul_core::url;
237pub use azul_core::url::{Url, UrlParseError, ResultUrlUrlParseError};
238
239/// File system operations (C-compatible wrappers for `std::fs`).
240// Scoped (was crate-wide): `///` doc comments before `impl_vec!`/`impl_option!`
241// macro invocations, and an infallible inherent `FilePath::from_str` (returns
242// `Self`, so it cannot implement the fallible `FromStr` trait).
243#[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
255/// HTTP client: GET/POST requests with pure-Rust TLS.
256///
257/// API surface always present (stub when off); ureq/rustls only pulled in with `http`.
258pub 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/// JSON parsing and serialization for the C API.
267#[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/// ZIP file creation, extraction, and listing.
278#[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
287/// Icon provider: resolves icons from Material Icons font, images, or ZIP packs.
288pub mod icon;
289pub use icon::{
290    // Resolver
291    default_icon_resolver,
292    // Data types for RefAny
293    ImageIconData, FontIconData,
294    // Helpers
295    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};
302// Re-export core icon types
303pub use azul_core::icon::{
304    IconProviderHandle, IconResolverCallbackType,
305    resolve_icons_in_styled_dom, OptionIconProviderHandle,
306};
307
308/// Callback handling for layout events (invocation, result processing).
309#[cfg(feature = "text_layout")]
310pub mod callbacks;
311/// CPU-based software rendering (no GPU required).
312#[cfg(feature = "cpurender")]
313// Scoped (was crate-wide): complex rasterizer signatures.
314#[allow(clippy::type_complexity)]
315pub mod cpurender;
316/// Glyph path and cell cache for CPU text rendering.
317#[cfg(feature = "cpurender")]
318pub mod glyph_cache;
319/// Default keyboard actions (copy, paste, select-all, undo, etc.).
320#[cfg(feature = "text_layout")]
321pub mod default_actions;
322/// Event determination: maps raw input to DOM node callbacks.
323#[cfg(feature = "text_layout")]
324pub mod event_determination;
325/// Font parsing, metrics extraction, and subsetting.
326#[cfg(feature = "text_layout")]
327// Scoped (was crate-wide): complex font-table signatures.
328#[allow(clippy::type_complexity)]
329pub mod font;
330
331/// Headless backend for CPU-only rendering without a display server.
332///
333/// Used with `AZUL_HEADLESS=1` for E2E testing, CI, and screenshot capture.
334#[cfg(feature = "text_layout")]
335pub mod headless;
336// Re-export allsorts types needed by printpdf
337#[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// Re-export hyphenation for external crates (like printpdf)
345#[cfg(feature = "text_layout_hyphenation")]
346pub use hyphenation;
347/// Hit-testing: maps screen coordinates to DOM nodes.
348#[cfg(feature = "text_layout")]
349pub mod hit_test;
350/// Paged media: the `FragmentationContext` (continuous vs. paged) and page margins.
351/// The primitive types live in `azul_core::paged`; re-exported here so existing
352/// `azul_layout::paged::*` / `crate::paged::*` paths keep resolving.
353pub use azul_core::paged;
354/// Text shaping, line breaking (Knuth-Plass), and inline formatting.
355#[cfg(feature = "text_layout")]
356// Scoped (was crate-wide): internal types exposed for tests, a labelled
357// shaping loop, and complex shaping/cache signatures.
358#[allow(private_interfaces, unused_labels, clippy::type_complexity)]
359pub mod text3;
360/// Thread callback wrappers for the C API.
361#[cfg(feature = "text_layout")]
362pub mod thread;
363/// Timer callback wrappers for the C API.
364#[cfg(feature = "text_layout")]
365// Scoped (was crate-wide): hand-written `Ord`/`PartialOrd` on a timer type.
366#[allow(clippy::non_canonical_partial_ord_impl)]
367pub mod timer;
368/// Scroll physics timer for momentum-based smooth scrolling.
369#[cfg(feature = "text_layout")]
370pub mod scroll_timer;
371/// Window layout management: relayout, event processing, state sync.
372#[cfg(feature = "text_layout")]
373// Scoped (was crate-wide): parenthesized layout expressions.
374#[allow(unused_parens)]
375pub mod window;
376/// Window state types (keyboard, mouse, DPI, focus).
377#[cfg(feature = "text_layout")]
378pub mod window_state;
379/// XML and XHTML parsing for declarative UI definitions.
380#[cfg(feature = "xml")]
381// Scoped (was crate-wide): incremental parser-state assignments.
382#[allow(unused_assignments)]
383pub mod xml;
384
385/// Debug / E2E server op-dispatch, ported verbatim from the DLL. Gated behind
386/// the `e2e-server` feature (NOT in `default`), so the lean crate is unaffected.
387#[cfg(feature = "e2e-server")]
388pub mod e2e;
389
390// Export the main layout function and window management
391/// Canonical paged-media page margins (defined in [`paged`]).
392pub 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/// Backwards-compat alias for the old `TextLayoutCache` name.
408/// Will be dropped at the next API revision; new code should use
409/// [`TextShapingCache`] directly.
410#[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/// Parses raw font bytes into a [`FontRef`](azul_css::props::basic::FontRef)
421/// suitable for use in the layout system.
422// signature must match the `ParseFontFn = fn(LoadedFontSource) -> ...` callback type
423// (core/src/resources.rs) and the api.json export, so the owned param cannot become &.
424#[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(), // Ignore warnings for now
434    )
435    .map(parsed_font_to_font_ref)
436}
437
438#[cfg(feature = "text_layout")]
439/// Wraps a [`ParsedFont`] in a [`FontRef`](azul_css::props::basic::FontRef),
440/// transferring ownership to the returned handle.
441pub 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/// Recovers a reference to the [`ParsedFont`] stored inside a [`FontRef`](azul_css::props::basic::FontRef).
459///
460/// # Safety contract
461/// The `font_ref` must have been created by [`parsed_font_to_font_ref`],
462/// so that `font_ref.parsed` points to a valid `ParsedFont`.
463#[must_use] pub const fn font_ref_to_parsed_font(
464    font_ref: &azul_css::props::basic::FontRef,
465) -> &ParsedFont {
466    // SAFETY: `font_ref.parsed` was created by `parsed_font_to_font_ref`
467    // via `Box::into_raw`, so it points to a valid, aligned `ParsedFont`.
468    unsafe { &*font_ref.parsed.cast::<ParsedFont>() }
469}
470
471#[cfg(test)]
472mod autotest_generated {
473    //! Adversarial unit tests generated by the autotest fleet.
474    //!
475    //! Covers the four items defined directly in `lib.rs`:
476    //!   * `az_mark` / `az_mark_read` — the web-lift diagnostic markers. Only the
477    //!     `#[cfg(not(feature = "web_lift"))]` (no-op `const`) variants are
478    //!     exercised: the `web_lift` variants store to *absolute* addresses and
479    //!     would segfault a native test binary, so they are deliberately untested
480    //!     here (the doc comment says as much).
481    //!   * `parse_font_fn` — raw bytes → `Option<FontRef>`.
482    //!   * `parsed_font_to_font_ref` / `font_ref_to_parsed_font` — the
483    //!     `Box::into_raw` / reborrow round-trip, plus the refcounted
484    //!     clone/drop contract of the `FontRef` handle those two produce.
485
486    use super::*;
487
488    // ---------------------------------------------------------------
489    // az_mark / az_mark_read  (numeric — no-op variants)
490    // ---------------------------------------------------------------
491
492    /// Without `web_lift` the read is documented to return 0 for *every* address,
493    /// including the ends of the u32 range and the 0x40000–0xF0000 diagnostic band.
494    /// A non-zero answer here would mean the native build is really dereferencing
495    /// an absolute address.
496    #[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,          // one below the diagnostic band
503            0x4_0000,          // band start
504            0x6_0758,          // a real marker counter from the docs
505            0xF_0000,          // band end
506            0xF_0001,          // one past the band
507            i32::MIN as u32,   // "negative" input, reinterpreted
508            (-1i32) as u32,    // == u32::MAX
509            u32::MAX - 1,
510            u32::MAX,
511            u32::MAX.wrapping_add(1), // wraps to 0, must not panic
512        ];
513        for addr in addresses {
514            assert_eq!(unsafe { az_mark_read(addr) }, 0, "az_mark_read(0x{addr:x})");
515        }
516    }
517
518    /// Sweep the whole u32 address space at a coarse stride: no address may panic
519    /// or return anything but 0.
520    #[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    /// A write must remain unobservable (the no-op variant stores nothing), for
529    /// every combination of boundary address and boundary value.
530    #[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        // Repeating a write is still a no-op (idempotent, no accumulating state).
546        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    /// Both no-op variants are `const fn`; this fails to *compile* if that ever
553    /// regresses (the `web_lift` variants are non-const on purpose, so this test
554    /// is gated off there).
555    #[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    // ---------------------------------------------------------------
567    // Shared font fixtures (text_layout only)
568    // ---------------------------------------------------------------
569
570    /// Positive control: the built-in `Azul Mock Mono` font (96 glyphs, upem 1000).
571    #[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    // ---------------------------------------------------------------
590    // parse_font_fn  (parser)
591    // ---------------------------------------------------------------
592
593    /// Malformed / hostile byte soup must come back as `None`, never a panic and
594    /// never a bogus `FontRef` (which would later be dereferenced as a `ParsedFont`).
595    #[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    /// Multi-megabyte junk: must terminate quickly and return `None`, not hang or
627    /// allocate its way out of memory (the sfnt table directory is attacker-controlled).
628    #[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        // "ttcf" collection magic followed by a megabyte of junk offsets.
634        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    /// Positive control: a real font parses, and the handle we get back really does
640    /// point at the parsed face (this is the only sanctioned way to build the
641    /// `FontRef` that `font_ref_to_parsed_font` is allowed to reborrow).
642    #[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    /// `load_outlines` is not consulted by `parse_font_fn` (only `data` + `index`
664    /// are). Both settings must therefore yield the same face — if this ever
665    /// diverges, callers that flip the flag silently get a different font.
666    #[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    /// `index` is cast `u32 as usize` and fed to the table provider. Out-of-range
688    /// face indices on a single-face font must be deterministic — no panic, no
689    /// `12 + index * 4` overflow, and no face with a different glyph count.
690    #[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    /// Empty / garbage input on the failing path must not leak or corrupt state
708    /// across repeated calls (the destructor is only installed on the `Some` path).
709    #[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        // …and a good parse still works afterwards.
717        assert!(parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).is_some());
718    }
719
720    /// Every successful parse mints a *fresh* identity, even for byte-identical
721    /// input: `FontRef` equality is the never-reused `id`, not the heap pointer
722    /// (freeing a font and reusing its address must not forge identity).
723    #[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        // …but the *content* is identical.
732        assert_eq!(
733            font_ref_to_parsed_font(&a).hash,
734            font_ref_to_parsed_font(&b).hash
735        );
736    }
737
738    // ---------------------------------------------------------------
739    // parsed_font_to_font_ref / font_ref_to_parsed_font  (round-trip)
740    // ---------------------------------------------------------------
741
742    /// encode == decode: wrapping a `ParsedFont` and reborrowing it must hand back
743    /// the very same face, field for field.
744    #[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    /// The freshly-minted handle's invariants: live pointer, refcount of exactly 1,
769    /// destructor armed, non-zero id (id 0 flags a raw-reconstructed handle).
770    #[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    /// `font_ref_to_parsed_font` is a pure reborrow: repeated calls must yield the
785    /// same address, and that address must be the handle's `parsed` pointer.
786    #[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    /// A clone shares the face (same pointer, same id) and bumps the refcount;
799    /// dropping the clone must NOT free the face out from under the original.
800    /// Reading through the survivor after the drop is the use-after-free probe.
801    #[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    /// Hammer the refcount: 1_000 clone/drop cycles (plus a batch held live at once)
826    /// must leave the face readable and the count back at 1 — a double-decrement
827    /// would free the `ParsedFont` early and turn the next reborrow into a UAF.
828    #[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    /// Identity semantics as a hash/ordering key: clones collapse, independently
852    /// wrapped faces don't — even when they hold byte-identical font data.
853    #[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    /// Wrapping many faces in a row must keep every handle pointing at its *own*
879    /// face — a shared/stale `Box::into_raw` would make them alias.
880    #[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}