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
    // ── Numeric conversion. A layout + raster engine crosses
76
    // px ↔ device-int ↔ float on essentially every path: a glyph's subpixel
77
    // origin becomes an integer scanline, a u32 pixel index becomes an i32
78
    // span bound, a node count becomes an f32 extent. ~150 sites, none of
79
    // them a defect; scoping each one buries the signal it is supposed to
80
    // carry. Overflow-checked builds (the dev-profile CI job) are what
81
    // actually catch a bad conversion here, and they do — that is how the
82
    // pass-2b tile-blit wrap was found.
83
    clippy::cast_possible_truncation,
84
    clippy::cast_possible_wrap,
85
    clippy::cast_precision_loss,
86
    clippy::cast_sign_loss,
87
    // Exact float comparison is the POINT in the cache/diff paths: "is this
88
    // the same value last frame computed" is a bit-identity question, and an
89
    // epsilon there would make the incremental path silently disagree with a
90
    // fresh render — the one property the equivalence laws pin.
91
    clippy::float_cmp,
92
    // `mul_add` is FUSED, so it changes f32 results. Layout output must stay
93
    // bit-reproducible across builds and platforms (the e2e corpus pins exact
94
    // frames), so the plain `a + b * c` form is deliberate.
95
    clippy::suboptimal_flops,
96
    // Geometry code is x0/x1/y0/y1, w/h, sx/sy, tx/ty — the similarity IS the
97
    // convention, and renaming to satisfy the lint would make it less readable.
98
    clippy::similar_names,
99
    // The solver, the rasterizer and the shaping walkers are long and branchy
100
    // by design: one pass, one place to read it. Splitting them threads state
101
    // through new signatures without making anything clearer.
102
    clippy::too_many_lines,
103
    clippy::cognitive_complexity,
104
    // `Default::default()` inside a struct literal whose field types are
105
    // obvious from the literal itself. Naming the type adds an import and a
106
    // line of noise per site.
107
    clippy::default_trait_access,
108
    // Four break-token builders filter a child list into a Vec and then
109
    // `extend` from it. Inlining the twelve-line borrow-capturing iterator
110
    // into each `extend` call saves one small allocation on a COLD page-break
111
    // path, in the code whose correctness the pagination suite exists to
112
    // protect. Not a trade worth making; revisit if a profile disagrees.
113
    clippy::needless_collect,
114
    // Nursery lint that wants `map_or_else` for every if-let/match. On the
115
    // multi-arm dispatch chains in this crate the rewrite nests closures
116
    // inside each other's else-branch; see the same call in azul-core.
117
    clippy::option_if_let_else,
118
    // A helper item declared next to the statements that use it, rather than
119
    // hoisted to the top of the function, is a deliberate locality choice.
120
    clippy::items_after_statements,
121
    // Arms that happen to share a body are frequently distinct CASES that
122
    // must stay separately readable (and separately editable) — merging them
123
    // by body is a refactor hazard, not a cleanup.
124
    clippy::match_same_arms,
125
    // By-value parameters are the C-ABI shape: generated bindings hand
126
    // ownership across the boundary.
127
    clippy::needless_pass_by_value,
128
    // Would force a `S: BuildHasher` parameter onto public API for no gain.
129
    clippy::implicit_hasher,
130
    // Types carrying f32 geometry cannot implement `Eq`.
131
    clippy::derive_partial_eq_without_eq,
132

            
133
    // ── Documentation prose. Deferred to the same dedicated docs pass as
134
    // `missing_docs` above (see the TODO there): these are shape-of-the-prose
135
    // lints over existing, accurate doc comments, not missing or wrong docs.
136
    clippy::too_long_first_doc_paragraph,
137
    clippy::doc_lazy_continuation,
138
    clippy::missing_panics_doc,
139
    clippy::missing_errors_doc,
140
)]
141

            
142
#[macro_use]
143
extern crate alloc;
144
extern crate core;
145
// Let this crate refer to itself as `azul_layout::…`. The e2e/debug-server port
146
// (`src/e2e/full.rs`) was written verbatim against the published crate name; the
147
// self-alias makes those ~80 `azul_layout::…` paths resolve without editing them.
148
extern crate self as azul_layout;
149

            
150
// Dependencies kept for downstream/feature-plumbing use but not referenced
151
// directly in this crate's source — marked intentionally linked so
152
// unused_crate_dependencies stays quiet (the lint's own suggested fix).
153
// `brotli-decompressor`: decompresses the codegen material_icons.ttf.br in azul-dll.
154
#[cfg(feature = "icons")]
155
use brotli_decompressor as _;
156
// `lru`: reserved for the slippy-map tile cache (azul-dll widgets).
157
use lru as _;
158
// `unicode-normalization` / `xmlwriter`: pulled by text_layout / xml for the
159
// shaping + SVG-writer paths consumed downstream.
160
#[cfg(feature = "text_layout")]
161
use unicode_normalization as _;
162
#[cfg(feature = "xml")]
163
use xmlwriter as _;
164
// `rustls` / `webpki-roots`: selected through ureq's `rustls-no-provider` +
165
// `rustls-webpki-roots` features and reached only via `ureq::tls::*`, so this
166
// crate never names them — but it must depend on them to pin the versions
167
// ureq resolves against (see the `http` feature).
168
#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
169
use rustls as _;
170
#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
171
use webpki_roots as _;
172

            
173
/// Web-lift diagnostic marker: a volatile store of `val` to the absolute wasm
174
/// linear-memory address `addr` (the 0x40000–0xF0000 free band the e2e harness
175
/// peeks via `AzStartup_peekU32`).
176
///
177
/// Compiles to NOTHING without the `web_lift`
178
/// feature — absolute-address stores would segfault native builds (macOS
179
/// `__PAGEZERO` covers the low 4 GiB). All in-tree diagnostic markers MUST go
180
/// through this helper rather than calling `core::ptr::write_volatile` on a
181
/// literal address directly.
182
///
183
/// # Safety
184
///
185
/// With the `web_lift` feature enabled, `addr` must be a valid, writable wasm
186
/// linear-memory address (within the 0x40000–0xF0000 diagnostic band). Without
187
/// the feature this is a no-op and always safe.
188
#[cfg(feature = "web_lift")]
189
#[inline]
190
pub unsafe fn az_mark(_addr: u32, _val: u32) {
191
    // Volatile isn't const-callable, so this variant is a plain (non-const) fn.
192
    core::ptr::write_volatile(_addr as usize as *mut u32, _val);
193
}
194
/// No-op `const` variant used when the `web_lift` feature is off.
195
///
196
/// # Safety
197
///
198
/// Always safe — this variant does nothing; the `unsafe` marker only exists to
199
/// keep the signature identical to the `web_lift` variant so call sites compile
200
/// unchanged under both features.
201
#[cfg(not(feature = "web_lift"))]
202
#[inline]
203
2470124
pub const unsafe fn az_mark(_addr: u32, _val: u32) {}
204

            
205
/// Read counterpart of [`az_mark`] (marker counters like `0x60758`).
206
/// Returns 0 without the `web_lift` feature.
207
///
208
/// # Safety
209
///
210
/// With the `web_lift` feature enabled, `addr` must be a valid, readable wasm
211
/// linear-memory address (within the 0x40000–0xF0000 diagnostic band). Without
212
/// the feature this is a no-op that returns 0 and is always safe.
213
#[cfg(feature = "web_lift")]
214
#[inline]
215
#[must_use] pub unsafe fn az_mark_read(_addr: u32) -> u32 {
216
    // Volatile isn't const-callable, so this variant is a plain (non-const) fn.
217
    core::ptr::read_volatile(_addr as usize as *const u32)
218
}
219
/// No-op `const` variant (returns 0) used when the `web_lift` feature is off.
220
///
221
/// # Safety
222
///
223
/// Always safe — returns 0 and touches nothing; the `unsafe` marker only exists
224
/// to keep the signature identical to the `web_lift` variant.
225
#[cfg(not(feature = "web_lift"))]
226
#[inline]
227
270455
#[must_use] pub const unsafe fn az_mark_read(_addr: u32) -> u32 {
228
270455
    0
229
270455
}
230

            
231
/// Font traits available regardless of text layout feature.
232
pub mod font_traits;
233
pub mod resource_handles;
234
/// Optional probe instrumentation. With the `probe` feature off this
235
/// is a tiny module of no-op stubs and pays zero cost.
236
pub mod probe;
237
/// Opt-in telemetry client: consent tiers, OTLP/HTTP JSON encoding, a
238
/// disk-backed ping queue and an uploader. Requires the `telemetry`
239
/// feature (std-only); collection stays *off* at runtime until a consent
240
/// tier is configured, so linking it in does not by itself send anything.
241
#[cfg(feature = "telemetry")]
242
// Scoped allows, per the crate lint policy above:
243
//   * the submodule names (`config`, `metrics`) are the public API grouping,
244
//     while the types are re-exported at `telemetry::*` where the `Telemetry`
245
//     prefix is what disambiguates them from every other `Config` in the tree;
246
//   * the registry and the config cell are global-by-design, so their guards
247
//     are held across the whole read/modify they protect — tightening the
248
//     drop would split an atomic update in half;
249
//   * `missing_const_for_fn` fires on accessors that read a `static` today and
250
//     will read more tomorrow; making them `const` is not a promise this API
251
//     wants to keep.
252
#[allow(
253
    clippy::module_name_repetitions,
254
    clippy::significant_drop_tightening,
255
    clippy::missing_const_for_fn
256
)]
257
pub mod telemetry;
258
/// Version checking + update policy (Phase 4): manifest source, install-kind
259
/// backstops (package-managed binaries never self-update), anti-downgrade,
260
/// cooldown/suspend state. Requires the `updater` feature.
261
#[cfg(feature = "updater")]
262
pub mod updater;
263
/// Process-wide `AppConfig` snapshot (app name/version, update manifest,
264
/// changelog + support-mailbox URLs) published by `App::run`, read by the
265
/// updater and the system dialogs.
266
#[cfg(feature = "std")]
267
pub mod appenv;
268
/// Built-in system dialogs (`SysDialogType`): report-a-problem, update
269
/// checker with Markdown changelog, crash reporter. ALWAYS CPU-rendered.
270
#[cfg(all(feature = "std", feature = "widgets", feature = "text_layout"))]
271
pub mod dialogs;
272
/// The ACTION JOURNAL: a bounded breadcrumb trail of dispatched callbacks,
273
/// for problem reports and crash dumps. Off until enabled.
274
#[cfg(feature = "std")]
275
pub mod journal;
276
/// Image decoding and encoding (wraps the `image` crate).
277
#[cfg(feature = "image_decoding")]
278
pub mod image;
279
/// Scroll, hover, clipboard, cursor, and focus managers.
280
#[cfg(feature = "text_layout")]
281
// Scoped (was crate-wide): internal manager types exposed for tests.
282
#[allow(private_interfaces)]
283
pub mod managers;
284
/// CSS layout solver: block, inline, flex, grid, and table formatting.
285
#[cfg(feature = "text_layout")]
286
// Scoped (was crate-wide): solver internals — intentional `drop(&_)` scope
287
// markers, internal types exposed for tests, incremental-relayout assignments,
288
// generated/parenthesized property code, and exhaustive generated matches.
289
#[allow(
290
    dropping_references,
291
    private_interfaces,
292
    unreachable_patterns,
293
    unused_parens,
294
    unused_doc_comments,
295
    unused_assignments
296
)]
297
pub mod solver3;
298

            
299
/// C-compatible string formatting via `strfmt`.
300
#[cfg(feature = "strfmt")]
301
pub mod fmt;
302
#[cfg(feature = "strfmt")]
303
pub use fmt::{FmtArg, FmtArgVec, FmtArgVecDestructor, FmtValue, fmt_string};
304

            
305
/// Built-in widgets: button, text input, tabs, tree view, node graph, etc.
306
#[cfg(feature = "widgets")]
307
// Scoped (was crate-wide): incremental widget-state assignments and the
308
// node_graph extern "C" fn that returns `()`.
309
#[allow(unused_assignments, improper_ctypes_definitions)]
310
pub mod widgets;
311

            
312
/// Desktop platform helpers (file dialogs, notifications).
313
#[cfg(feature = "extra")]
314
pub mod desktop;
315

            
316
/// ICU internationalization: date/time formatting, plurals, list formatting.
317
#[cfg(any(
318
    feature = "icu",
319
    all(target_os = "macos", feature = "icu_macos"),
320
    all(target_os = "windows", feature = "icu_windows"),
321
))]
322
pub mod icu;
323
#[cfg(any(
324
    feature = "icu",
325
    all(target_os = "macos", feature = "icu_macos"),
326
    all(target_os = "windows", feature = "icu_windows"),
327
))]
328
pub use icu::{
329
    DateTimeFieldSet, FormatLength, IcuDate, IcuDateTime, IcuError,
330
    IcuLocalizer, IcuLocalizerHandle, IcuResult, IcuStringVec, IcuTime,
331
    LayoutCallbackInfoIcuExt, ListType, PluralCategory,
332
};
333

            
334
/// Project Fluent localization: message bundles, argument formatting, ZIP I/O.
335
#[cfg(feature = "fluent")]
336
pub mod fluent;
337
#[cfg(feature = "fluent")]
338
pub use fluent::{
339
    check_fluent_syntax, check_fluent_syntax_bytes, create_fluent_zip,
340
    create_fluent_zip_from_strings, export_to_zip, FluentError,
341
    FluentLanguageInfo, FluentLanguageInfoVec, FluentLoadError, FluentLoadErrorVec,
342
    FluentLocalizerHandle, FluentSyntaxCheckResult,
343
    FluentZipLoadResult,
344
};
345

            
346
/// URL parsing (RFC 3986 compliant). Pure-Rust, always present (no TLS deps).
347
/// URL types live in `azul_core::url`; re-exported so `azul_layout::url::*`
348
/// keeps resolving. `Url::parse`/`join` are enabled via the `http` feature
349
/// (which turns on `azul-core/url`).
350
pub use azul_core::url;
351
pub use azul_core::url::{Url, UrlParseError, ResultUrlUrlParseError};
352

            
353
/// File system operations (C-compatible wrappers for `std::fs`).
354
// Scoped (was crate-wide): `///` doc comments before `impl_vec!`/`impl_option!`
355
// macro invocations, and an infallible inherent `FilePath::from_str` (returns
356
// `Self`, so it cannot implement the fallible `FromStr` trait).
357
#[allow(unused_doc_comments, clippy::should_implement_trait)]
358
pub mod file;
359
pub use file::{
360
    dir_create, dir_create_all, dir_list, dir_delete, dir_delete_all,
361
    file_append, file_copy, path_exists, file_metadata, file_read, file_read_string,
362
    file_delete, file_rename, file_write, file_write_string,
363
    path_canonicalize, path_extension, path_file_name, path_is_dir, path_is_file,
364
    path_join, path_parent, temp_dir,
365
    DirEntry, DirEntryVec, DirEntryVecDestructor, DirEntryVecDestructorType,
366
    FileError, FileErrorKind, FileMetadata, FilePath, OptionFilePath,
367
};
368

            
369
/// HTTP client: GET/POST requests with pure-Rust TLS.
370
///
371
/// API surface always present (stub when off); ureq/rustls only pulled in with `http`.
372
pub mod http;
373
pub use http::{
374
    download_bytes, download_bytes_with_config, http_get,
375
    http_get_with_config, http_post, http_post_with_config, http_put_with_config,
376
    http_request_with_config, is_url_reachable, HttpError, HttpHeader,
377
    HttpMethod, HttpRequestConfig, HttpResponse, HttpResponseTooLargeError,
378
    HttpResult, HttpStatusError,
379
};
380

            
381
/// JSON parsing and serialization for the C API.
382
#[cfg(feature = "json")]
383
pub mod json;
384
#[cfg(feature = "json")]
385
pub use json::{
386
    json_parse, json_stringify,
387
    Json, JsonInternal, JsonKeyValue, JsonKeyValueVec, JsonKeyValueVecDestructor, JsonKeyValueVecDestructorType,
388
    JsonParseError, JsonType, JsonVec,
389
    ResultJsonJsonParseError, OptionJson, OptionJsonVec, OptionJsonKeyValueVec,
390
};
391

            
392
/// ZIP file creation, extraction, and listing.
393
#[cfg(feature = "zip")]
394
pub mod zip;
395
#[cfg(feature = "zip")]
396
pub use zip::{
397
    zip_create, zip_create_from_files, zip_extract_all, zip_list_contents,
398
    ZipFile, ZipFileEntry, ZipFileEntryVec, ZipPathEntry, ZipPathEntryVec,
399
    ZipReadConfig, ZipWriteConfig, ZipReadError, ZipWriteError,
400
};
401

            
402
/// Icon provider: resolves icons from Material Icons font, images, or ZIP packs.
403
pub mod icon;
404
pub use icon::{
405
    // Resolver
406
    default_icon_resolver,
407
    // Data types for RefAny
408
    ImageIconData, FontIconData,
409
    // Helpers
410
    register_image_icon,
411
    register_font_icon,
412
    register_icons_from_zip,
413
    create_default_icon_provider,
414
    register_material_icons,
415
    register_embedded_material_icons,
416
};
417
// Re-export core icon types
418
pub use azul_core::icon::{
419
    IconProviderHandle, IconResolverCallbackType,
420
    resolve_icons_in_styled_dom, OptionIconProviderHandle,
421
};
422

            
423
/// Callback handling for layout events (invocation, result processing).
424
#[cfg(feature = "text_layout")]
425
pub mod callbacks;
426
/// CPU-based software rendering (no GPU required).
427
#[cfg(feature = "cpurender")]
428
// Scoped (was crate-wide): complex rasterizer signatures.
429
#[allow(clippy::type_complexity)]
430
pub mod cpurender;
431
/// Glyph path and cell cache for CPU text rendering.
432
#[cfg(feature = "cpurender")]
433
pub mod glyph_cache;
434
/// Default keyboard actions (copy, paste, select-all, undo, etc.).
435
#[cfg(feature = "text_layout")]
436
pub mod default_actions;
437
/// Post-layout developer warnings for raw text nodes used without a
438
/// containing block (azul does not auto-wrap them the way browsers do).
439
///
440
/// Reads computed display through `solver3::getters`, so it exists only
441
/// where the layout solver does — same gate as `window` and
442
/// `default_actions`, its only caller being inside `window`.
443
#[cfg(feature = "text_layout")]
444
pub mod dom_lint;
445
/// Apply structural `DocumentOperation`s to a plain XML tree.
446
///
447
/// The Path-2 helper for apps without their own document model (the PDF
448
/// editor).
449
#[cfg(all(feature = "text_layout", feature = "xml"))]
450
pub mod document_edit;
451
/// Event determination: maps raw input to DOM node callbacks.
452
#[cfg(feature = "text_layout")]
453
pub mod event_determination;
454
/// Font parsing, metrics extraction, and subsetting.
455
#[cfg(feature = "text_layout")]
456
// Scoped (was crate-wide): complex font-table signatures.
457
#[allow(clippy::type_complexity)]
458
pub mod font;
459

            
460
/// Headless backend for CPU-only rendering without a display server.
461
///
462
/// Used with `AZUL_HEADLESS=1` for E2E testing, CI, and screenshot capture.
463
#[cfg(feature = "text_layout")]
464
pub mod headless;
465
// Re-export allsorts types needed by printpdf
466
#[cfg(feature = "text_layout")]
467
pub use allsorts::subset::CmapTarget;
468
#[cfg(feature = "text_layout")]
469
pub use font::parsed::{
470
    FontParseWarning, FontParseWarningSeverity, FontType, OwnedGlyph, ParsedFont, PdfFontMetrics,
471
    SubsetFont,
472
};
473
// Re-export hyphenation for external crates (like printpdf)
474
#[cfg(feature = "text_layout_hyphenation")]
475
pub use hyphenation;
476
/// Hit-testing: maps screen coordinates to DOM nodes.
477
#[cfg(feature = "text_layout")]
478
pub mod hit_test;
479
/// Paged media: the `FragmentationContext` (continuous vs. paged) and page margins.
480
/// The primitive types live in `azul_core::paged`; re-exported here so existing
481
/// `azul_layout::paged::*` / `crate::paged::*` paths keep resolving.
482
pub use azul_core::paged;
483
/// Text shaping, line breaking (Knuth-Plass), and inline formatting.
484
#[cfg(feature = "text_layout")]
485
// Scoped (was crate-wide): internal types exposed for tests, a labelled
486
// shaping loop, and complex shaping/cache signatures.
487
#[allow(private_interfaces, unused_labels, clippy::type_complexity)]
488
pub mod text3;
489
/// Thread callback wrappers for the C API.
490
#[cfg(feature = "text_layout")]
491
pub mod thread;
492
/// Timer callback wrappers for the C API.
493
#[cfg(feature = "text_layout")]
494
// Scoped (was crate-wide): hand-written `Ord`/`PartialOrd` on a timer type.
495
#[allow(clippy::non_canonical_partial_ord_impl)]
496
pub mod timer;
497
/// Scroll physics timer for momentum-based smooth scrolling.
498
#[cfg(feature = "text_layout")]
499
pub mod scroll_timer;
500
/// Content overlay + journal for quickly-mutable content.
501
///
502
/// The single write chokepoint and overlay→DOM read order (images now,
503
/// text/structural next).
504
#[cfg(feature = "text_layout")]
505
pub mod overlay;
506
/// Window layout management: relayout, event processing, state sync.
507
#[cfg(feature = "text_layout")]
508
// Scoped (was crate-wide): parenthesized layout expressions.
509
#[allow(unused_parens)]
510
pub mod window;
511
/// Window state types (keyboard, mouse, DPI, focus).
512
#[cfg(feature = "text_layout")]
513
pub mod window_state;
514
/// XML and XHTML parsing for declarative UI definitions.
515
#[cfg(feature = "xml")]
516
// Scoped (was crate-wide): incremental parser-state assignments.
517
#[allow(unused_assignments)]
518
pub mod xml;
519

            
520
/// Debug / E2E server op-dispatch, ported verbatim from the DLL. Gated behind
521
/// the `e2e-server` feature (NOT in `default`), so the lean crate is unaffected.
522
#[cfg(feature = "e2e-server")]
523
pub mod e2e;
524

            
525
// Export the main layout function and window management
526
/// Canonical paged-media page margins (defined in [`paged`]).
527
pub use paged::PageMargins;
528
#[cfg(feature = "text_layout")]
529
pub use hit_test::{CursorTypeHitTest, FullHitTest};
530
#[cfg(feature = "text_layout")]
531
pub use solver3::cache::LayoutCache as Solver3LayoutCache;
532
#[cfg(feature = "text_layout")]
533
pub use solver3::display_list::DisplayList as DisplayList3;
534
#[cfg(feature = "text_layout")]
535
pub use solver3::layout_document;
536
#[cfg(feature = "text_layout")]
537
pub use solver3::paged_layout::layout_document_paged;
538
/// The analysis-returning paged entries + the precalculation-only path
539
/// (document editors: page count / page-of-node WITHOUT materializing pages).
540
#[cfg(feature = "text_layout")]
541
pub use solver3::paged_layout::{
542
    compute_document_pagination, layout_document_paged_v2, pagination_to_dom_breaks,
543
    spine_path_at_y, BreaksDelta, PagedLayoutResult, PaginationSession, StructuralBreak,
544
};
545
/// Standalone page-break analysis (typed breaks, spans, policy, lazy pages).
546
#[cfg(feature = "text_layout")]
547
pub use solver3::{
548
    display_list::paginate_single_page,
549
    page_breaks::{
550
        compute_page_breaks, compute_page_breaks_from_display_list,
551
        compute_page_breaks_with_report, compute_page_breaks_with_sequence,
552
        page_of_y, page_spans, recompute_page_breaks_from, BreakKind,
553
        BreakPolicy, MonolithReason, MonolithWarning, PageBreakPosition,
554
        PageBreakInput, PageConstraints, PaginationInfo,
555
    },
556
    pagination::{PageSequence, PageSetup},
557
};
558
#[cfg(feature = "text_layout")]
559
pub use solver3::{LayoutContext, LayoutError, Result as LayoutResult3};
560
#[cfg(feature = "text_layout")]
561
pub use text3::cache::{FontContext, FontManager, TextShapingCache};
562
/// Backwards-compat alias for the old `TextLayoutCache` name.
563
/// Will be dropped at the next API revision; new code should use
564
/// [`TextShapingCache`] directly.
565
#[cfg(feature = "text_layout")]
566
pub use text3::cache::TextShapingCache as TextLayoutCache;
567
#[cfg(feature = "font_async_registry")]
568
pub use rust_fontconfig::registry::FcFontRegistry;
569
#[cfg(feature = "text_layout")]
570
pub use window::{CursorBlinkTimerAction, LayoutWindow, ScrollbarDragState, TooltipTimerAction};
571
#[cfg(feature = "text_layout")]
572
pub use managers::text_input::{PendingTextEdit, OptionPendingTextEdit};
573

            
574
#[cfg(feature = "text_layout")]
575
/// Parses raw font bytes into a [`FontRef`](azul_css::props::basic::FontRef)
576
/// suitable for use in the layout system.
577
// signature must match the `ParseFontFn = fn(LoadedFontSource) -> ...` callback type
578
// (core/src/resources.rs) and the api.json export, so the owned param cannot become &.
579
#[allow(clippy::needless_pass_by_value)]
580
428
pub fn parse_font_fn(
581
428
    source: azul_core::resources::LoadedFontSource,
582
428
) -> Option<azul_css::props::basic::FontRef> {
583
    use crate::font::parsed::ParsedFont;
584

            
585
428
    ParsedFont::from_bytes(
586
428
        source.data.as_ref(),
587
428
        source.index as usize,
588
428
        &mut Vec::new(), // Ignore warnings for now
589
    )
590
428
    .map(parsed_font_to_font_ref)
591
428
}
592

            
593
#[cfg(feature = "text_layout")]
594
/// Wraps a [`ParsedFont`] in a [`FontRef`](azul_css::props::basic::FontRef),
595
/// transferring ownership to the returned handle.
596
19557
pub fn parsed_font_to_font_ref(
597
19557
    parsed_font: ParsedFont,
598
19557
) -> azul_css::props::basic::FontRef {
599
    use core::ffi::c_void;
600

            
601
19557
    extern "C" fn parsed_font_destructor(ptr: *mut c_void) {
602
19557
        unsafe {
603
19557
            drop(Box::from_raw(ptr.cast::<ParsedFont>()));
604
19557
        }
605
19557
    }
606

            
607
19557
    let boxed = Box::new(parsed_font);
608
19557
    let raw_ptr = Box::into_raw(boxed) as *const c_void;
609
19557
    azul_css::props::basic::FontRef::new(raw_ptr, parsed_font_destructor)
610
19557
}
611

            
612
#[cfg(feature = "text_layout")]
613
/// Recovers a reference to the [`ParsedFont`] stored inside a [`FontRef`](azul_css::props::basic::FontRef).
614
///
615
/// # Safety contract
616
/// The `font_ref` must have been created by [`parsed_font_to_font_ref`],
617
/// so that `font_ref.parsed` points to a valid `ParsedFont`.
618
2535905
#[must_use] pub const fn font_ref_to_parsed_font(
619
2535905
    font_ref: &azul_css::props::basic::FontRef,
620
2535905
) -> &ParsedFont {
621
    // SAFETY: `font_ref.parsed` was created by `parsed_font_to_font_ref`
622
    // via `Box::into_raw`, so it points to a valid, aligned `ParsedFont`.
623
2535905
    unsafe { &*font_ref.parsed.cast::<ParsedFont>() }
624
2535905
}
625

            
626
#[cfg(test)]
627
mod autotest_generated {
628
    //! Adversarial unit tests generated by the autotest fleet.
629
    //!
630
    //! Covers the four items defined directly in `lib.rs`:
631
    //!   * `az_mark` / `az_mark_read` — the web-lift diagnostic markers. Only the
632
    //!     `#[cfg(not(feature = "web_lift"))]` (no-op `const`) variants are
633
    //!     exercised: the `web_lift` variants store to *absolute* addresses and
634
    //!     would segfault a native test binary, so they are deliberately untested
635
    //!     here (the doc comment says as much).
636
    //!   * `parse_font_fn` — raw bytes → `Option<FontRef>`.
637
    //!   * `parsed_font_to_font_ref` / `font_ref_to_parsed_font` — the
638
    //!     `Box::into_raw` / reborrow round-trip, plus the refcounted
639
    //!     clone/drop contract of the `FontRef` handle those two produce.
640

            
641
    use super::*;
642

            
643
    // ---------------------------------------------------------------
644
    // az_mark / az_mark_read  (numeric — no-op variants)
645
    // ---------------------------------------------------------------
646

            
647
    /// Without `web_lift` the read is documented to return 0 for *every* address,
648
    /// including the ends of the u32 range and the 0x40000–0xF0000 diagnostic band.
649
    /// A non-zero answer here would mean the native build is really dereferencing
650
    /// an absolute address.
651
    #[cfg(not(feature = "web_lift"))]
652
    #[test]
653
    fn az_mark_read_is_zero_for_every_boundary_address() {
654
        let addresses = [
655
            0u32,
656
            1,
657
            0x3_FFFF,          // one below the diagnostic band
658
            0x4_0000,          // band start
659
            0x6_0758,          // a real marker counter from the docs
660
            0xF_0000,          // band end
661
            0xF_0001,          // one past the band
662
            i32::MIN as u32,   // "negative" input, reinterpreted
663
            (-1i32) as u32,    // == u32::MAX
664
            u32::MAX - 1,
665
            u32::MAX,
666
            u32::MAX.wrapping_add(1), // wraps to 0, must not panic
667
        ];
668
        for addr in addresses {
669
            assert_eq!(unsafe { az_mark_read(addr) }, 0, "az_mark_read(0x{addr:x})");
670
        }
671
    }
672

            
673
    /// Sweep the whole u32 address space at a coarse stride: no address may panic
674
    /// or return anything but 0.
675
    #[cfg(not(feature = "web_lift"))]
676
    #[test]
677
    fn az_mark_read_sweeps_the_whole_address_space_as_zero() {
678
        for addr in (0u32..=u32::MAX).step_by(1 << 24) {
679
            assert_eq!(unsafe { az_mark_read(addr) }, 0);
680
        }
681
    }
682

            
683
    /// A write must remain unobservable (the no-op variant stores nothing), for
684
    /// every combination of boundary address and boundary value.
685
    #[cfg(not(feature = "web_lift"))]
686
    #[test]
687
    fn az_mark_writes_are_unobservable_without_web_lift() {
688
        let addresses = [0u32, 0x4_0000, 0x6_0758, 0xF_0000, u32::MAX];
689
        let values = [0u32, 1, u32::MAX / 2, u32::MAX - 1, u32::MAX, i32::MIN as u32];
690
        for addr in addresses {
691
            for val in values {
692
                unsafe { az_mark(addr, val) };
693
                assert_eq!(
694
                    unsafe { az_mark_read(addr) },
695
                    0,
696
                    "az_mark(0x{addr:x}, {val}) must not be observable"
697
                );
698
            }
699
        }
700
        // Repeating a write is still a no-op (idempotent, no accumulating state).
701
        for _ in 0..1_000 {
702
            unsafe { az_mark(0x6_0758, u32::MAX) };
703
        }
704
        assert_eq!(unsafe { az_mark_read(0x6_0758) }, 0);
705
    }
706

            
707
    /// Both no-op variants are `const fn`; this fails to *compile* if that ever
708
    /// regresses (the `web_lift` variants are non-const on purpose, so this test
709
    /// is gated off there).
710
    #[cfg(not(feature = "web_lift"))]
711
    #[test]
712
    fn az_mark_no_op_variants_are_const_evaluable() {
713
        const _WRITE_MIN: () = unsafe { az_mark(0, 0) };
714
        const _WRITE_MAX: () = unsafe { az_mark(u32::MAX, u32::MAX) };
715
        const READ_ZERO: u32 = unsafe { az_mark_read(0) };
716
        const READ_MAX: u32 = unsafe { az_mark_read(u32::MAX) };
717
        assert_eq!(READ_ZERO, 0);
718
        assert_eq!(READ_MAX, 0);
719
    }
720

            
721
    // ---------------------------------------------------------------
722
    // Shared font fixtures (text_layout only)
723
    // ---------------------------------------------------------------
724

            
725
    /// Positive control: the built-in `Azul Mock Mono` font (96 glyphs, upem 1000).
726
    #[cfg(feature = "text_layout")]
727
    const MOCK_MONO: &[u8] = text3::mock_fonts::MOCK_MONO_TTF;
728

            
729
    #[cfg(feature = "text_layout")]
730
    fn loaded_source(bytes: Vec<u8>, index: u32) -> azul_core::resources::LoadedFontSource {
731
        azul_core::resources::LoadedFontSource {
732
            data: azul_css::U8Vec::from_vec(bytes),
733
            index,
734
            load_outlines: true,
735
        }
736
    }
737

            
738
    #[cfg(feature = "text_layout")]
739
    fn parse_mock() -> ParsedFont {
740
        ParsedFont::from_bytes(MOCK_MONO, 0, &mut Vec::new())
741
            .expect("Azul Mock Mono must parse (positive control)")
742
    }
743

            
744
    // ---------------------------------------------------------------
745
    // parse_font_fn  (parser)
746
    // ---------------------------------------------------------------
747

            
748
    /// Malformed / hostile byte soup must come back as `None`, never a panic and
749
    /// never a bogus `FontRef` (which would later be dereferenced as a `ParsedFont`).
750
    #[cfg(feature = "text_layout")]
751
    #[test]
752
    fn parse_font_fn_rejects_malformed_input() {
753
        let cases: Vec<(&str, Vec<u8>)> = vec![
754
            ("empty", Vec::new()),
755
            ("single_nul", vec![0u8]),
756
            ("whitespace_only", b"   \t\n".to_vec()),
757
            ("garbage", (0u8..=255).cycle().take(4096).collect()),
758
            ("invalid_utf8", vec![0xFF, 0xFE, 0x00]),
759
            ("sfnt_magic_only", vec![0x00, 0x01, 0x00, 0x00]),
760
            ("header_only", MOCK_MONO[..12].to_vec()),
761
            ("truncated_font", MOCK_MONO[..64].to_vec()),
762
            ("half_a_font", MOCK_MONO[..MOCK_MONO.len() / 2].to_vec()),
763
            ("unicode_emoji", "\u{1F600}\u{1F600}".repeat(1_000).into_bytes()),
764
            ("combining_marks", "e\u{0301}".repeat(10_000).into_bytes()),
765
            ("nested_brackets", vec![b'['; 10_000]),
766
            ("boundary_numbers", b"0 -0 9223372036854775807 NaN inf -inf 1e309".to_vec()),
767
            ("leading_junk_then_font", {
768
                let mut v = b"garbage".to_vec();
769
                v.extend_from_slice(MOCK_MONO);
770
                v
771
            }),
772
        ];
773
        for (name, bytes) in cases {
774
            assert!(
775
                parse_font_fn(loaded_source(bytes, 0)).is_none(),
776
                "{name} must not parse into a FontRef"
777
            );
778
        }
779
    }
780

            
781
    /// Multi-megabyte junk: must terminate quickly and return `None`, not hang or
782
    /// allocate its way out of memory (the sfnt table directory is attacker-controlled).
783
    #[cfg(feature = "text_layout")]
784
    #[test]
785
    fn parse_font_fn_survives_extremely_long_input() {
786
        assert!(parse_font_fn(loaded_source(vec![0u8; 1_000_000], 0)).is_none());
787
        assert!(parse_font_fn(loaded_source(vec![b'a'; 1_000_000], 0)).is_none());
788
        // "ttcf" collection magic followed by a megabyte of junk offsets.
789
        let mut ttcf = b"ttcf".to_vec();
790
        ttcf.extend_from_slice(&vec![0xABu8; 1_000_000]);
791
        assert!(parse_font_fn(loaded_source(ttcf, 0)).is_none());
792
    }
793

            
794
    /// Positive control: a real font parses, and the handle we get back really does
795
    /// point at the parsed face (this is the only sanctioned way to build the
796
    /// `FontRef` that `font_ref_to_parsed_font` is allowed to reborrow).
797
    #[cfg(feature = "text_layout")]
798
    #[test]
799
    fn parse_font_fn_parses_the_positive_control() {
800
        let font_ref = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0))
801
            .expect("the positive control must parse");
802
        let parsed = font_ref_to_parsed_font(&font_ref);
803

            
804
        assert_eq!(parsed.num_glyphs(), 96);
805
        assert_eq!(parsed.num_glyphs(), parsed.maxp_table.num_glyphs);
806
        assert_eq!(parsed.font_metrics.units_per_em, 1000);
807
        assert!(parsed.font_metrics.ascent > 0.0);
808
        assert!(parsed.font_metrics.descent <= 0.0);
809
        assert!(parsed.font_metrics.ascent.is_finite());
810
        assert!(parsed.font_metrics.descent.is_finite());
811
        assert!(parsed.font_metrics.line_gap.is_finite());
812
        assert_eq!(parsed.font_type, FontType::TrueType);
813
        assert_eq!(parsed.original_index, 0);
814
        assert!(parsed.cmap_subtable.is_some());
815
        assert_eq!(parsed.hash, parse_mock().hash);
816
    }
817

            
818
    /// `load_outlines` is not consulted by `parse_font_fn` (only `data` + `index`
819
    /// are). Both settings must therefore yield the same face — if this ever
820
    /// diverges, callers that flip the flag silently get a different font.
821
    #[cfg(feature = "text_layout")]
822
    #[test]
823
    fn parse_font_fn_ignores_the_load_outlines_flag() {
824
        let with = azul_core::resources::LoadedFontSource {
825
            data: azul_css::U8Vec::from_vec(MOCK_MONO.to_vec()),
826
            index: 0,
827
            load_outlines: true,
828
        };
829
        let without = azul_core::resources::LoadedFontSource {
830
            data: azul_css::U8Vec::from_vec(MOCK_MONO.to_vec()),
831
            index: 0,
832
            load_outlines: false,
833
        };
834
        let a = parse_font_fn(with).expect("parses with outlines");
835
        let b = parse_font_fn(without).expect("parses without outlines");
836
        let (pa, pb) = (font_ref_to_parsed_font(&a), font_ref_to_parsed_font(&b));
837
        assert_eq!(pa.hash, pb.hash);
838
        assert_eq!(pa.num_glyphs(), pb.num_glyphs());
839
        assert_eq!(pa.pdf_font_metrics, pb.pdf_font_metrics);
840
    }
841

            
842
    /// `index` is cast `u32 as usize` and fed to the table provider. Out-of-range
843
    /// face indices on a single-face font must be deterministic — no panic, no
844
    /// `12 + index * 4` overflow, and no face with a different glyph count.
845
    #[cfg(feature = "text_layout")]
846
    #[test]
847
    fn parse_font_fn_with_an_out_of_range_index_is_deterministic() {
848
        let baseline = parse_mock();
849
        for index in [1u32, 2, 0x7FFF_FFFF, u32::MAX - 1, u32::MAX] {
850
            if let Some(font_ref) = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), index)) {
851
                let parsed = font_ref_to_parsed_font(&font_ref);
852
                assert_eq!(
853
                    parsed.num_glyphs(),
854
                    baseline.num_glyphs(),
855
                    "index {index} must not conjure a different face"
856
                );
857
                assert_eq!(parsed.original_index, index as usize);
858
            }
859
        }
860
    }
861

            
862
    /// Empty / garbage input on the failing path must not leak or corrupt state
863
    /// across repeated calls (the destructor is only installed on the `Some` path).
864
    #[cfg(feature = "text_layout")]
865
    #[test]
866
    fn parse_font_fn_failure_path_is_repeatable() {
867
        for _ in 0..200 {
868
            assert!(parse_font_fn(loaded_source(Vec::new(), 0)).is_none());
869
            assert!(parse_font_fn(loaded_source(vec![0xFF; 3], u32::MAX)).is_none());
870
        }
871
        // …and a good parse still works afterwards.
872
        assert!(parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).is_some());
873
    }
874

            
875
    /// Every successful parse mints a *fresh* identity, even for byte-identical
876
    /// input: `FontRef` equality is the never-reused `id`, not the heap pointer
877
    /// (freeing a font and reusing its address must not forge identity).
878
    #[cfg(feature = "text_layout")]
879
    #[test]
880
    fn parse_font_fn_mints_a_fresh_identity_per_call() {
881
        let a = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).expect("parses");
882
        let b = parse_font_fn(loaded_source(MOCK_MONO.to_vec(), 0)).expect("parses");
883
        assert_ne!(a, b, "two parses of the same bytes are two distinct handles");
884
        assert_ne!(a.id, b.id);
885
        assert!(b > a, "ids are monotonically assigned");
886
        // …but the *content* is identical.
887
        assert_eq!(
888
            font_ref_to_parsed_font(&a).hash,
889
            font_ref_to_parsed_font(&b).hash
890
        );
891
    }
892

            
893
    // ---------------------------------------------------------------
894
    // parsed_font_to_font_ref / font_ref_to_parsed_font  (round-trip)
895
    // ---------------------------------------------------------------
896

            
897
    /// encode == decode: wrapping a `ParsedFont` and reborrowing it must hand back
898
    /// the very same face, field for field.
899
    #[cfg(feature = "text_layout")]
900
    #[test]
901
    fn parsed_font_font_ref_round_trip_preserves_the_face() {
902
        let original = parse_mock();
903
        let expected_hash = original.hash;
904
        let expected_glyphs = original.num_glyphs();
905
        let expected_metrics = original.pdf_font_metrics;
906
        let expected_upem = original.font_metrics.units_per_em;
907
        let expected_ascent = original.font_metrics.ascent;
908
        let expected_type = original.font_type.clone();
909
        let expected_index = original.original_index;
910

            
911
        let font_ref = parsed_font_to_font_ref(original);
912
        let decoded = font_ref_to_parsed_font(&font_ref);
913

            
914
        assert_eq!(decoded.hash, expected_hash);
915
        assert_eq!(decoded.num_glyphs(), expected_glyphs);
916
        assert_eq!(decoded.pdf_font_metrics, expected_metrics);
917
        assert_eq!(decoded.font_metrics.units_per_em, expected_upem);
918
        assert!((decoded.font_metrics.ascent - expected_ascent).abs() < f32::EPSILON);
919
        assert_eq!(decoded.font_type, expected_type);
920
        assert_eq!(decoded.original_index, expected_index);
921
    }
922

            
923
    /// The freshly-minted handle's invariants: live pointer, refcount of exactly 1,
924
    /// destructor armed, non-zero id (id 0 flags a raw-reconstructed handle).
925
    #[cfg(feature = "text_layout")]
926
    #[test]
927
    fn parsed_font_to_font_ref_handle_invariants() {
928
        use core::sync::atomic::Ordering as AtomicOrdering;
929

            
930
        let font_ref = parsed_font_to_font_ref(parse_mock());
931
        assert!(!font_ref.parsed.is_null());
932
        assert!(!font_ref.copies.is_null());
933
        assert!(font_ref.run_destructor);
934
        assert_ne!(font_ref.id, 0, "id 0 is reserved for un-initialised handles");
935
        assert_eq!(unsafe { (*font_ref.copies).load(AtomicOrdering::SeqCst) }, 1);
936
        assert_eq!(font_ref.get_parsed(), font_ref.parsed);
937
    }
938

            
939
    /// `font_ref_to_parsed_font` is a pure reborrow: repeated calls must yield the
940
    /// same address, and that address must be the handle's `parsed` pointer.
941
    #[cfg(feature = "text_layout")]
942
    #[test]
943
    fn font_ref_to_parsed_font_is_a_stable_reborrow() {
944
        use core::ffi::c_void;
945

            
946
        let font_ref = parsed_font_to_font_ref(parse_mock());
947
        let first: *const ParsedFont = font_ref_to_parsed_font(&font_ref);
948
        let second: *const ParsedFont = font_ref_to_parsed_font(&font_ref);
949
        assert!(core::ptr::eq(first, second), "reborrow must be stable");
950
        assert!(core::ptr::eq(first.cast::<c_void>(), font_ref.get_parsed()));
951
    }
952

            
953
    /// A clone shares the face (same pointer, same id) and bumps the refcount;
954
    /// dropping the clone must NOT free the face out from under the original.
955
    /// Reading through the survivor after the drop is the use-after-free probe.
956
    #[cfg(feature = "text_layout")]
957
    #[test]
958
    fn cloning_a_font_ref_shares_the_face_and_the_drop_is_refcounted() {
959
        use core::sync::atomic::Ordering as AtomicOrdering;
960

            
961
        let original = parsed_font_to_font_ref(parse_mock());
962
        let expected_hash = font_ref_to_parsed_font(&original).hash;
963

            
964
        let clone = original.clone();
965
        assert_eq!(clone.id, original.id, "a clone is the same font");
966
        assert_eq!(clone, original);
967
        assert!(core::ptr::eq(clone.parsed, original.parsed));
968
        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 2);
969

            
970
        drop(clone);
971
        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1);
972
        assert_eq!(
973
            font_ref_to_parsed_font(&original).hash,
974
            expected_hash,
975
            "the face must survive its clone being dropped"
976
        );
977
        assert_eq!(font_ref_to_parsed_font(&original).num_glyphs(), 96);
978
    }
979

            
980
    /// Hammer the refcount: 1_000 clone/drop cycles (plus a batch held live at once)
981
    /// must leave the face readable and the count back at 1 — a double-decrement
982
    /// would free the `ParsedFont` early and turn the next reborrow into a UAF.
983
    #[cfg(feature = "text_layout")]
984
    #[test]
985
    fn font_ref_clone_drop_cycles_do_not_double_free() {
986
        use core::sync::atomic::Ordering as AtomicOrdering;
987

            
988
        let original = parsed_font_to_font_ref(parse_mock());
989
        let expected_hash = font_ref_to_parsed_font(&original).hash;
990

            
991
        for _ in 0..1_000 {
992
            let c = original.clone();
993
            assert_eq!(font_ref_to_parsed_font(&c).hash, expected_hash);
994
        }
995

            
996
        let batch: Vec<_> = (0..1_000).map(|_| original.clone()).collect();
997
        assert_eq!(
998
            unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
999
            1_001
        );
        drop(batch);
        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1);
        assert_eq!(font_ref_to_parsed_font(&original).hash, expected_hash);
    }
    /// Identity semantics as a hash/ordering key: clones collapse, independently
    /// wrapped faces don't — even when they hold byte-identical font data.
    #[cfg(feature = "text_layout")]
    #[test]
    fn font_ref_identity_is_per_handle_not_per_content() {
        use std::collections::{BTreeSet, HashSet};
        let a = parsed_font_to_font_ref(parse_mock());
        let b = parsed_font_to_font_ref(parse_mock());
        assert_ne!(a, b);
        assert!(a < b, "ids are monotonically assigned, so a precedes b");
        assert_eq!(
            font_ref_to_parsed_font(&a).hash,
            font_ref_to_parsed_font(&b).hash,
            "…even though the content hash is the same"
        );
        let set: HashSet<_> = vec![a.clone(), a.clone(), a.clone(), b.clone()]
            .into_iter()
            .collect();
        assert_eq!(set.len(), 2, "clones dedup, distinct handles do not");
        let ordered: BTreeSet<_> = vec![b.clone(), a.clone(), b.clone()].into_iter().collect();
        assert_eq!(ordered.len(), 2);
        assert_eq!(ordered.iter().next(), Some(&a));
    }
    /// Wrapping many faces in a row must keep every handle pointing at its *own*
    /// face — a shared/stale `Box::into_raw` would make them alias.
    #[cfg(feature = "text_layout")]
    #[test]
    fn many_font_refs_do_not_alias_each_other() {
        let refs: Vec<_> = (0..16).map(|_| parsed_font_to_font_ref(parse_mock())).collect();
        for (i, a) in refs.iter().enumerate() {
            assert_eq!(font_ref_to_parsed_font(a).num_glyphs(), 96);
            for b in refs.iter().skip(i + 1) {
                assert!(
                    !core::ptr::eq(a.parsed, b.parsed),
                    "independently boxed faces must not share a pointer"
                );
                assert_ne!(a.id, b.id);
            }
        }
    }
}