1
//! Callback types for the Azul UI framework.
2
//!
3
//! This module defines the callback infrastructure used by the event system,
4
//! layout engine, and virtual view rendering. Key design patterns:
5
//!
6
//! - **Core vs Layout callback split**: `CoreCallbackType` and
7
//!   `CoreRenderImageCallbackType` store function pointers as `usize` to avoid
8
//!   circular dependencies between `azul-core` and `azul-layout`. The actual
9
//!   function pointer types are defined in `azul-layout` and transmuted at
10
//!   invocation time.
11
//!
12
//! - **FFI callable pattern**: Callback structs carry an optional
13
//!   `ctx: OptionRefAny` field that holds a foreign callable (e.g. a Python
14
//!   function object). The `extern "C"` trampoline stored in `cb` extracts
15
//!   both the user data and the foreign callable from `RefAny` and dispatches
16
//!   the call. Native Rust code sets `ctx` to `None`.
17
//!
18
//! - **Info structs**: `LayoutCallbackInfo`, `VirtualViewCallbackInfo`, and
19
//!   the layout-side `CallbackInfo` provide read-only access to framework
20
//!   resources (fonts, images, GL context, window size) during callback
21
//!   invocation.
22

            
23
#[cfg(not(feature = "std"))]
24
use alloc::string::ToString;
25
use alloc::{alloc::Layout, boxed::Box, collections::BTreeMap, sync::Arc, vec::Vec};
26
use core::{
27
    ffi::c_void,
28
    fmt,
29
    sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
30
};
31
#[cfg(feature = "std")]
32
use std::hash::Hash;
33

            
34
use azul_css::{
35
    css::{CssPath, CssPropertyValue},
36
    props::{
37
        basic::{
38
            AnimationInterpolationFunction, FontRef, InterpolateResolver, LayoutRect, LayoutSize,
39
        },
40
        property::{CssProperty, CssPropertyType},
41
    },
42
    system::SystemStyle,
43
    AzString,
44
};
45
use rust_fontconfig::{FcFontCache, OwnedFontSource};
46

            
47
use crate::{
48
    dom::{Dom, DomId, DomNodeId, EventFilter, OptionDom},
49
    geom::{
50
        LogicalPosition, LogicalRect, LogicalRectVec, LogicalSize, OptionLogicalPosition,
51
        PhysicalSize,
52
    },
53
    gl::OptionGlContextPtr,
54
    hit_test::OverflowingScrollNode,
55
    id::{NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeId},
56
    prop_cache::CssPropertyCache,
57
    refany::{OptionRefAny, RefAny},
58
    resources::{
59
        DpiScaleFactor, FontInstanceKey, IdNamespace, ImageCache, ImageMask, ImageRef,
60
        RendererResources,
61
    },
62
    styled_dom::{
63
        NodeHierarchyItemId, NodeHierarchyItemVec, StyledNode,
64
        StyledNodeVec,
65
    },
66
    task::{
67
        Duration as AzDuration, GetSystemTimeCallback, Instant as AzInstant, Instant,
68
        TerminateTimer, ThreadId, ThreadReceiver, ThreadSendMsg, TimerId,
69
    },
70
    window::{
71
        AzStringPair, KeyboardState, MouseState, OptionChar, RawWindowHandle, UpdateFocusWarning,
72
        WindowFlags, WindowSize, WindowTheme,
73
    },
74
    FastBTreeSet, OrderedMap,
75
};
76

            
77
/// Specifies if the screen should be updated after the callback function has returned
78
#[repr(C)]
79
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
80
pub enum Update {
81
    /// The screen does not need to redraw after the callback has been called
82
    DoNothing,
83
    /// After the callback is called, the screen needs to redraw (`layout()` function being called
84
    /// again)
85
    RefreshDom,
86
    /// The layout has to be re-calculated for all windows
87
    RefreshDomAllWindows,
88
}
89

            
90
impl Update {
91
206
    pub fn max_self(&mut self, other: Self) {
92
206
        if (*self == Self::DoNothing && other != Self::DoNothing)
93
148
            || (*self == Self::RefreshDom && other == Self::RefreshDomAllWindows)
94
71
        {
95
71
            *self = other;
96
135
        }
97
206
    }
98
}
99

            
100
// -- layout callback
101

            
102
/// Callback function pointer (has to be a function pointer in
103
/// order to be compatible with C APIs later on).
104
///
105
/// IMPORTANT: The callback needs to deallocate the `RefAnyPtr` and `LayoutCallbackInfoPtr`,
106
/// otherwise that memory is leaked. If you use the official auto-generated
107
/// bindings, this is already done for you.
108
///
109
/// NOTE: The original callback was `fn(&self, LayoutCallbackInfo) -> Dom`
110
/// which then evolved to `fn(&RefAny, LayoutCallbackInfo) -> Dom`.
111
/// The indirection is necessary because of the memory management
112
/// around the C API
113
///
114
/// The memory management across the callback boundary is handled by
115
/// the caller (see `LayoutCallback` and `LayoutCallbackInfo`).
116
pub type LayoutCallbackType = extern "C" fn(RefAny, LayoutCallbackInfo) -> Dom;
117

            
118
1
extern "C" fn default_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
119
1
    Dom::create_body()
120
1
}
121

            
122
/// Wrapper around the layout callback
123
///
124
/// For FFI languages (Python, Java, etc.), the `RefAny` contains both:
125
/// - The user's application data
126
/// - The callback function object from the foreign language
127
///
128
/// The trampoline function (stored in `cb`) knows how to extract both
129
/// from the `RefAny` and invoke the foreign callback with the user data.
130
#[repr(C)]
131
pub struct LayoutCallback {
132
    pub cb: LayoutCallbackType,
133
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
134
    /// Native Rust code sets this to None
135
    pub ctx: OptionRefAny,
136
}
137

            
138
impl_callback!(LayoutCallback, LayoutCallbackType);
139

            
140
impl LayoutCallback {
141
3
    pub fn create<I: Into<Self>>(cb: I) -> Self {
142
3
        cb.into()
143
3
    }
144
}
145

            
146
// Host-invoker plumbing for managed-FFI bindings (Lua, Ruby, Perl, …):
147
// expands to a static `az_layout_callback_thunk` (the `cb` we hand to the
148
// framework when the host calls `LayoutCallback::create_from_host_handle`),
149
// an `AzLayoutCallback_createFromHostHandle` C-ABI export, plus the
150
// `AzApp_setLayoutCallbackInvoker` setter the host calls once at module
151
// load. See `crate::host_invoker` for the design.
152
crate::impl_managed_callback! {
153
    wrapper:        LayoutCallback,
154
    info_ty:        LayoutCallbackInfo,
155
    return_ty:      Dom,
156
    default_ret:    Dom::create_body(),
157
    invoker_static: LAYOUT_CALLBACK_INVOKER,
158
    invoker_ty:     AzLayoutCallbackInvoker,
159
    thunk_fn:       az_layout_callback_thunk,
160
    setter_fn:      AzApp_setLayoutCallbackInvoker,
161
    from_handle_fn: AzLayoutCallback_createFromHostHandle,
162
}
163

            
164
impl Default for LayoutCallback {
165
77886
    fn default() -> Self {
166
77886
        Self {
167
77886
            cb: default_layout_callback,
168
77886
            ctx: OptionRefAny::None,
169
77886
        }
170
77886
    }
171
}
172

            
173
// -- virtualized view callback
174

            
175
pub type VirtualViewCallbackType = extern "C" fn(RefAny, VirtualViewCallbackInfo) -> VirtualViewReturn;
176

            
177
/// Callback that, given a rectangle area on the screen, returns the DOM
178
/// appropriate for that bounds (useful for infinite lists)
179
#[repr(C)]
180
pub struct VirtualViewCallback {
181
    pub cb: VirtualViewCallbackType,
182
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
183
    /// Native Rust code sets this to None
184
    pub ctx: OptionRefAny,
185
}
186
impl_callback!(VirtualViewCallback, VirtualViewCallbackType);
187

            
188
// Host-invoker plumbing for VirtualViewCallback. See `crate::host_invoker`.
189
crate::impl_managed_callback! {
190
    wrapper:        VirtualViewCallback,
191
    info_ty:        VirtualViewCallbackInfo,
192
    return_ty:      VirtualViewReturn,
193
    default_ret:    VirtualViewReturn::default(),
194
    invoker_static: VIRTUAL_VIEW_CALLBACK_INVOKER,
195
    invoker_ty:     AzVirtualViewCallbackInvoker,
196
    thunk_fn:       az_virtual_view_callback_thunk,
197
    setter_fn:      AzApp_setVirtualViewCallbackInvoker,
198
    from_handle_fn: AzVirtualViewCallback_createFromHostHandle,
199
}
200

            
201
impl VirtualViewCallback {
202
511
    pub fn create(cb: VirtualViewCallbackType) -> Self {
203
511
        Self {
204
511
            cb,
205
511
            ctx: OptionRefAny::None,
206
511
        }
207
511
    }
208
}
209

            
210
// -- caret / selection tween callbacks (system text animations)
211
//
212
// The framework animates the caret and the selection highlight between their
213
// previous and current geometry ("tween"). The MATH of the tween is a user-
214
// replaceable C-ABI function set in `AppConfig.system_animations` (defaults
215
// below): the framework drives a short timer, computes the linear progress
216
// `t = elapsed / configured duration`, and calls the function to obtain the
217
// geometry to RENDER this frame. While a tween is in flight the caret blink
218
// is suppressed (the caret stays solid while it moves).
219

            
220
/// Inputs for one caret-tween evaluation.
221
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
222
#[repr(C)]
223
pub struct CaretTweenInfo {
224
    /// Caret rectangle the previous frame RENDERED (mid-flight retargets
225
    /// start from the interpolated position, not the old logical one).
226
    pub past: LogicalRect,
227
    /// Caret rectangle the current layout actually wants.
228
    pub current: LogicalRect,
229
    /// Linear time progress `0.0..=1.0` (elapsed / configured duration).
230
    /// Easing/curves are this function's job.
231
    pub t: f32,
232
}
233

            
234
/// Returns the caret rectangle to render at progress `info.t`.
235
pub type CaretTweenCallbackType = extern "C" fn(RefAny, CaretTweenInfo) -> LogicalRect;
236

            
237
/// User-settable caret tween interpolator (see [`CaretTweenInfo`]).
238
#[repr(C)]
239
pub struct CaretTweenCallback {
240
    pub cb: CaretTweenCallbackType,
241
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
242
    /// Native Rust code sets this to None
243
    pub ctx: OptionRefAny,
244
}
245
impl_callback!(CaretTweenCallback, CaretTweenCallbackType);
246

            
247
impl CaretTweenCallback {
248
482
    pub fn create(cb: CaretTweenCallbackType) -> Self {
249
482
        Self {
250
482
            cb,
251
482
            ctx: OptionRefAny::None,
252
482
        }
253
482
    }
254
}
255

            
256
/// Inputs for one selection-tween evaluation.
257
///
258
/// Carries the full PAST and CURRENT selection band geometry: all rectangles
259
/// of the selection highlight, in display-list order — spanning multiple
260
/// lines and, for a cross-block selection, multiple nodes.
261
#[derive(Debug, Clone, PartialEq, PartialOrd)]
262
#[repr(C)]
263
pub struct SelectionTweenInfo {
264
    /// Selection rectangles the previous frame RENDERED.
265
    pub past: LogicalRectVec,
266
    /// Selection rectangles the current layout actually wants.
267
    pub current: LogicalRectVec,
268
    /// Linear time progress `0.0..=1.0` (elapsed / configured duration).
269
    pub t: f32,
270
}
271

            
272
/// Returns the selection rectangles to render at progress `info.t`.
273
/// MUST return exactly `info.current.len()` rectangles — a mismatched
274
/// length makes the framework fall back to `info.current` unanimated.
275
pub type SelectionTweenCallbackType =
276
    extern "C" fn(RefAny, SelectionTweenInfo) -> LogicalRectVec;
277

            
278
/// User-settable selection tween interpolator (see [`SelectionTweenInfo`]).
279
#[repr(C)]
280
pub struct SelectionTweenCallback {
281
    pub cb: SelectionTweenCallbackType,
282
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
283
    /// Native Rust code sets this to None
284
    pub ctx: OptionRefAny,
285
}
286
impl_callback!(SelectionTweenCallback, SelectionTweenCallbackType);
287

            
288
impl SelectionTweenCallback {
289
482
    pub fn create(cb: SelectionTweenCallbackType) -> Self {
290
482
        Self {
291
482
            cb,
292
482
            ctx: OptionRefAny::None,
293
482
        }
294
482
    }
295
}
296

            
297
/// Trapezoidal velocity profile: velocity ramps up HARD over the first
298
/// `RAMP` of the duration, cruises at constant speed, and ramps down hard
299
/// over the last `RAMP` — a `/‾‾‾\` velocity curve. In position terms:
300
/// a brief quadratic ease-in, a LINEAR middle, a brief quadratic ease-out.
301
/// Chosen over ease-out-cubic for the caret/selection defaults: at the
302
/// very short default durations the motion should read as "barely
303
/// noticeable glide", not as a spring (user directive). Analytic integral,
304
/// exact — a cubic bezier cannot express the flat-velocity plateau.
305
#[inline]
306
1309
fn trapezoid_ease(t: f32) -> f32 {
307
    const RAMP: f32 = 0.25;
308
    // Peak velocity so the total distance integrates to exactly 1.
309
    const V: f32 = 1.0 / (1.0 - RAMP);
310
1309
    let t = t.clamp(0.0, 1.0);
311
1309
    if t < RAMP {
312
1188
        V * t * t / (2.0 * RAMP)
313
121
    } else if t <= 1.0 - RAMP {
314
88
        V * (RAMP / 2.0 + (t - RAMP))
315
    } else {
316
33
        let inv = 1.0 - t;
317
33
        1.0 - V * inv * inv / (2.0 * RAMP)
318
    }
319
1309
}
320

            
321
#[inline]
322
// Plain `a + (b - a) * e`, NOT mul_add: fused multiply-add changes f32
323
// results, and tween geometry must be bit-reproducible across builds (the
324
// e2e corpus pins pixel-exact frames).
325
#[allow(clippy::suboptimal_flops)]
326
1397
fn lerp_rect(from: LogicalRect, to: LogicalRect, e: f32) -> LogicalRect {
327
1397
    LogicalRect {
328
1397
        origin: LogicalPosition {
329
1397
            x: from.origin.x + (to.origin.x - from.origin.x) * e,
330
1397
            y: from.origin.y + (to.origin.y - from.origin.y) * e,
331
1397
        },
332
1397
        size: LogicalSize {
333
1397
            width: from.size.width + (to.size.width - from.size.width) * e,
334
1397
            height: from.size.height + (to.size.height - from.size.height) * e,
335
1397
        },
336
1397
    }
337
1397
}
338

            
339
/// Default caret tween: trapezoidal-velocity lerp of origin and size
340
/// (hard rise, linear cruise, hard fall — see [`trapezoid_ease`]).
341
#[must_use]
342
1199
pub extern "C" fn default_caret_tween(_data: RefAny, info: CaretTweenInfo) -> LogicalRect {
343
1199
    lerp_rect(info.past, info.current, trapezoid_ease(info.t))
344
1199
}
345

            
346
/// Default selection tween: trapezoidal-velocity lerp, rectangles paired by
347
/// the LINE they sit on — not by their position in the list.
348
///
349
/// Index pairing broke every UPWARD extension: growing the selection upward
350
/// prepends a rect, which shifts every later rect one slot, so each line lerped
351
/// from the geometry of the line ABOVE it and the whole band visibly slid.
352
/// Geometric pairing is stable under insertion at either end.
353
///
354
/// Rectangles with no counterpart on their line (a line the selection did not
355
/// cover before) appear at their final geometry immediately. Each past
356
/// rectangle is consumed at most once, so a line that bidi splits into several
357
/// rectangles still pairs one-to-one.
358
#[must_use]
359
110
pub extern "C" fn default_selection_tween(
360
110
    _data: RefAny,
361
110
    info: SelectionTweenInfo,
362
110
) -> LogicalRectVec {
363
110
    let e = trapezoid_ease(info.t);
364
110
    let past = info.past.as_ref();
365
110
    let mut taken = alloc::vec![false; past.len()];
366
110
    let out: Vec<LogicalRect> = info
367
110
        .current
368
110
        .as_ref()
369
110
        .iter()
370
407
        .map(|cur| {
371
407
            take_same_line_rect(past, &mut taken, *cur).map_or(*cur, |p| lerp_rect(p, *cur, e))
372
407
        })
373
110
        .collect();
374
110
    out.into()
375
110
}
376

            
377
/// The not-yet-consumed `past` rectangle sitting on the same line as `cur` —
378
/// the closest one vertically, ties to the earlier one — marked consumed.
379
/// `None` when no past rectangle shares that line.
380
///
381
/// "Same line" means the vertical CENTRES are within half the shorter
382
/// rectangle's height of each other: a line that shifted by a fraction of its
383
/// own height is still recognised (and glides there), a different line never
384
/// is. The test is a positive comparison, which NaN fails, so garbage geometry
385
/// pops instead of pairing wrongly.
386
407
fn take_same_line_rect(
387
407
    past: &[LogicalRect],
388
407
    taken: &mut [bool],
389
407
    cur: LogicalRect,
390
407
) -> Option<LogicalRect> {
391
407
    let cur_centre = cur.origin.y + cur.size.height / 2.0;
392
407
    let mut best: Option<(usize, f32)> = None;
393

            
394
1562
    for (i, p) in past.iter().enumerate() {
395
1562
        if taken.get(i).copied().unwrap_or(true) {
396
1122
            continue;
397
440
        }
398
440
        let dy = (p.origin.y + p.size.height / 2.0 - cur_centre).abs();
399
440
        let tolerance = p.size.height.min(cur.size.height) / 2.0;
400
440
        if dy <= tolerance && best.is_none_or(|(_, best_dy)| dy < best_dy) {
401
198
            best = Some((i, dy));
402
242
        }
403
    }
404

            
405
407
    let (idx, _) = best?;
406
198
    if let Some(slot) = taken.get_mut(idx) {
407
198
        *slot = true;
408
198
    }
409
198
    past.get(idx).copied()
410
407
}
411

            
412
/// Reason why a `VirtualView` callback is being invoked.
413
///
414
/// This helps the callback optimize its behavior based on why it's being called.
415
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416
#[repr(C, u8)]
417
pub enum VirtualViewCallbackReason {
418
    /// Initial render - first time the `VirtualView` appears
419
    InitialRender,
420
    /// Parent DOM was recreated (cache invalidated)
421
    DomRecreated,
422
    /// Window/VirtualView bounds expanded beyond current `scroll_size`
423
    BoundsExpanded,
424
    /// Scroll position is near an edge (within `EDGE_THRESHOLD`, currently 200px)
425
    EdgeScrolled(EdgeType),
426
    /// Scroll position extends beyond current `scroll_size`
427
    ScrollBeyondContent,
428
}
429

            
430
/// Which edge triggered a scroll-based re-invocation
431
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432
#[repr(C)]
433
pub enum EdgeType {
434
    Top,
435
    Bottom,
436
    Left,
437
    Right,
438
}
439

            
440
#[derive(Debug)]
441
#[repr(C)]
442
pub struct VirtualViewCallbackInfo {
443
    pub reason: VirtualViewCallbackReason,
444
    pub system_fonts: *const FcFontCache,
445
    pub image_cache: *const ImageCache,
446
    pub window_theme: WindowTheme,
447
    /// RECT 1 - THE CONTAINER: the `VirtualView`'s on-screen box, computed by
448
    /// the framework from the outer DOM. You do not set this; you render into
449
    /// it.
450
    pub bounds: HidpiAdjustedBounds,
451
    /// RECT 2 - WHAT IS CURRENTLY MATERIALIZED, in VIRTUAL space: the window
452
    /// you returned last time (`origin` = where it starts in the document,
453
    /// `size` = its extent). Zero-sized on the first invoke.
454
    pub materialized: LogicalRect,
455
    /// RECT 3 - THE DOCUMENT, in VIRTUAL space: the extent you last declared,
456
    /// which is what the scrollbar currently represents.
457
    pub virtual_rect: LogicalRect,
458
    /// WHERE THE USER IS LOOKING: the live scroll offset in virtual space.
459
    ///
460
    /// This is the input your "which slice do I render?" math keys off. It was
461
    /// previously spelled `virtual_scroll_offset` and the engine hardcoded
462
    /// that to zero, so apps computing a page index from it always rendered
463
    /// the first page — one of the two reasons a `VirtualView` could not
464
    /// scroll.
465
    pub scroll_offset: LogicalPosition,
466
    /// Pointer to the callable (`OptionRefAny`) for FFI language bindings (Python, etc.)
467
    /// Set by the caller before invoking the callback. Native Rust callbacks have this as null.
468
    callable_ptr: *const OptionRefAny,
469
    /// Headless DOM measurement hook (see [`Self::measure_dom`]): a
470
    /// layout-crate trampoline (a [`MeasureDomFn`] stored as an opaque
471
    /// pointer, null = no hook) + its `LayoutWindow` context, injected at
472
    /// invoke time. Null on paths that cannot measure (then `measure_dom`
473
    /// returns zero).
474
    measure_dom_fn: *const c_void,
475
    measure_dom_ctx: *mut c_void,
476
    /// Extension for future ABI stability (mutable data)
477
    _abi_mut: *mut c_void,
478
}
479

            
480
/// Trampoline signature for [`VirtualViewCallbackInfo::measure_dom`]:
481
/// `(layout_window_ctx, dom, available) -> content extent`. The `Dom` is
482
/// passed by pointer and CONSUMED (moved out) by the trampoline.
483
pub type MeasureDomFn = extern "C" fn(*mut c_void, *mut Dom, LogicalSize) -> LogicalSize;
484

            
485
impl Clone for VirtualViewCallbackInfo {
486
    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
487
1
    fn clone(&self) -> Self {
488
1
        Self {
489
1
            reason: self.reason,
490
1
            system_fonts: self.system_fonts,
491
1
            image_cache: self.image_cache,
492
1
            window_theme: self.window_theme,
493
1
            bounds: self.bounds,
494
1
            materialized: self.materialized,
495
1
            virtual_rect: self.virtual_rect,
496
1
            scroll_offset: self.scroll_offset,
497
1
            callable_ptr: self.callable_ptr,
498
1
            measure_dom_fn: self.measure_dom_fn,
499
1
            measure_dom_ctx: self.measure_dom_ctx,
500
1
            _abi_mut: self._abi_mut,
501
1
        }
502
1
    }
503
}
504

            
505
impl VirtualViewCallbackInfo {
506
687
    #[must_use] pub const fn new<'a>(
507
687
        reason: VirtualViewCallbackReason,
508
687
        system_fonts: &'a FcFontCache,
509
687
        image_cache: &'a ImageCache,
510
687
        window_theme: WindowTheme,
511
687
        bounds: HidpiAdjustedBounds,
512
687
        materialized: LogicalRect,
513
687
        virtual_rect: LogicalRect,
514
687
        scroll_offset: LogicalPosition,
515
687
    ) -> Self {
516
687
        Self {
517
687
            reason,
518
687
            system_fonts: core::ptr::from_ref::<FcFontCache>(system_fonts),
519
687
            image_cache: core::ptr::from_ref::<ImageCache>(image_cache),
520
687
            window_theme,
521
687
            bounds,
522
687
            materialized,
523
687
            virtual_rect,
524
687
            scroll_offset,
525
687
            callable_ptr: core::ptr::null(),
526
687
            measure_dom_fn: core::ptr::null(),
527
687
            measure_dom_ctx: core::ptr::null_mut(),
528
687
            _abi_mut: core::ptr::null_mut(),
529
687
        }
530
687
    }
531

            
532
    /// Set the callable pointer for FFI language bindings
533
1
    pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
534
1
        self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
535
1
    }
536

            
537
    /// Inject the headless-measure trampoline (called by the layout crate
538
    /// right before the user callback is invoked).
539
233
    pub fn set_measure_dom_fn(&mut self, f: MeasureDomFn, ctx: *mut c_void) {
540
233
        self.measure_dom_fn = f as *const c_void;
541
233
        self.measure_dom_ctx = ctx;
542
233
    }
543

            
544
    /// Measure a DOM headlessly: style + lay it out against `available`
545
    /// constraints using the host window's fonts and system style, without
546
    /// touching the live layout. Returns the union of all node bounds.
547
    ///
548
    /// Use a very tall `available.height` (e.g. `1_000_000.0`) to obtain a
549
    /// DOM's natural height at a fixed width - the building block for
550
    /// virtual-scroll sizing: measure one (or a few) item template(s), then
551
    /// `virtual_scroll_size.height = item_height * item_count` and render
552
    /// only the visible window of items. Each call is a full cold layout
553
    /// pass, so cache measured sizes per item template.
554
    ///
555
    /// Returns `LogicalSize::zero()` when no measure hook was injected.
556
13
    #[must_use] pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
557
13
        if self.measure_dom_fn.is_null() {
558
7
            return LogicalSize::zero();
559
6
        }
560
        // SAFETY: measure_dom_fn is only ever set via set_measure_dom_fn,
561
        // which stores a valid MeasureDomFn.
562
6
        let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
563
6
        let mut dom = core::mem::ManuallyDrop::new(dom);
564
6
        f(self.measure_dom_ctx, core::ptr::from_mut::<Dom>(&mut dom), available)
565
13
    }
566

            
567
    /// Get the callable for FFI language bindings (Python, etc.)
568
69
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
569
69
        if self.callable_ptr.is_null() {
570
4
            OptionRefAny::None
571
        } else {
572
65
            unsafe { (*self.callable_ptr).clone() }
573
        }
574
69
    }
575

            
576
523
    #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
577
523
        self.bounds
578
523
    }
579

            
580
2
    const fn internal_get_system_fonts(&self) -> &FcFontCache {
581
2
        unsafe { &*self.system_fonts }
582
2
    }
583
1
    const fn internal_get_image_cache(&self) -> &ImageCache {
584
1
        unsafe { &*self.image_cache }
585
1
    }
586
}
587

            
588
/// Return value for a `VirtualView` rendering callback.
589
///
590
/// Contains two size/offset pairs for lazy loading and virtualization:
591
///
592
/// - `scroll_size` / `scroll_offset`: Size and position of actually rendered content
593
/// - `virtual_scroll_size` / `virtual_scroll_offset`: Size for scrollbar representation
594
///
595
/// The callback is re-invoked on: initial render, parent DOM recreation, window expansion
596
/// beyond `scroll_size`, or scrolling near content edges (`EDGE_THRESHOLD`, currently 200px).
597
///
598
/// Return `OptionDom::None` to keep the current DOM and only update scroll bounds.
599
#[derive(Debug, Clone, PartialEq, Eq)]
600
#[repr(C)]
601
pub struct VirtualViewReturn {
602
    /// The DOM with actual rendered content, or None to keep current DOM.
603
    ///
604
    /// - `OptionDom::Some(dom)` - Replace current content with this new DOM
605
    /// - `OptionDom::None` - Keep using the previous DOM, only update scroll bounds
606
    ///
607
    /// Returning `None` is an optimization when the callback determines that the
608
    /// current content is sufficient (e.g., already rendered ahead of scroll position).
609
    pub dom: OptionDom,
610

            
611
    /// WHAT THIS CALLBACK MATERIALIZED, in VIRTUAL space.
612
    ///
613
    /// `origin` = where this window of content begins in the document;
614
    /// `size` = how much of the document it covers.
615
    ///
616
    /// One rect, not a loose offset + size: they are a single fact about a
617
    /// single window, and storing them apart is exactly how the origin came
618
    /// to be dropped on the floor (content could not be placed, so a
619
    /// `VirtualView` could never actually scroll).
620
    ///
621
    /// The engine places the content at
622
    /// `container.origin + (materialized.origin - current_scroll_offset)`.
623
    ///
624
    /// **Example**: a table showing rows 10-30 at 30px each reports
625
    /// `origin.y = 300`, `size.height = 600`.
626
    pub materialized: LogicalRect,
627

            
628
    /// THE WHOLE DOCUMENT, in VIRTUAL space — what the scrollbar represents.
629
    ///
630
    /// `origin` is normally zero; `size` is your current best estimate and MAY
631
    /// change as work completes (e.g. a background pagination pass refining a
632
    /// page count). Refining it is cheap and safe: **only the scrollbar reads
633
    /// this**, so the thumb resizes and no content moves.
634
    ///
635
    /// **Example**: a 1000-row table reports `size.height = 30_000` even
636
    /// though `materialized` covers 600px of it.
637
    pub virtual_rect: LogicalRect,
638
}
639

            
640
impl Default for VirtualViewReturn {
641
1
    fn default() -> Self {
642
1
        Self {
643
1
            dom: OptionDom::None,
644
1
            materialized: LogicalRect::zero(),
645
1
            virtual_rect: LogicalRect::zero(),
646
1
        }
647
1
    }
648
}
649

            
650
impl VirtualViewReturn {
651
    /// Creates a new `VirtualViewReturn` with updated DOM content.
652
    ///
653
    /// Use this when the callback has rendered new content to display.
654
    ///
655
    /// # Arguments
656
    /// - `dom` - The new DOM to render
657
    /// - `materialized` - what you rendered, and where it sits in the document
658
    /// - `virtual_rect` - how big the document is (scrollbar sizing)
659
1
    #[must_use] pub const fn with_dom(
660
1
        dom: Dom,
661
1
        materialized: LogicalRect,
662
1
        virtual_rect: LogicalRect,
663
1
    ) -> Self {
664
1
        Self {
665
1
            dom: OptionDom::Some(dom),
666
1
            materialized,
667
1
            virtual_rect,
668
1
        }
669
1
    }
670

            
671
    /// Creates a return value that keeps the current DOM unchanged.
672
    ///
673
    /// Use this when the callback determines that the existing content
674
    /// is sufficient (e.g., already rendered ahead of scroll position).
675
    /// This is an optimization to avoid rebuilding the DOM unnecessarily.
676
    ///
677
    /// # Arguments
678
    /// - `materialized` - the window currently rendered, and where it sits
679
    /// - `virtual_rect` - how big the document is (scrollbar sizing)
680
6
    #[must_use] pub const fn keep_current(
681
6
        materialized: LogicalRect,
682
6
        virtual_rect: LogicalRect,
683
6
    ) -> Self {
684
6
        Self {
685
6
            dom: OptionDom::None,
686
6
            materialized,
687
6
            virtual_rect,
688
6
        }
689
6
    }
690

            
691
}
692

            
693
// --  thread callback
694

            
695
// -- timer callback
696

            
697
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
698
#[repr(C)]
699
pub struct TimerCallbackReturn {
700
    pub should_update: Update,
701
    pub should_terminate: TerminateTimer,
702
}
703

            
704
impl TimerCallbackReturn {
705
    /// Creates a new `TimerCallbackReturn` with the given update and terminate flags.
706
9
    #[must_use] pub const fn create(should_update: Update, should_terminate: TerminateTimer) -> Self {
707
9
        Self {
708
9
            should_update,
709
9
            should_terminate,
710
9
        }
711
9
    }
712

            
713
    /// Timer continues running, no DOM update needed.
714
3
    #[must_use] pub const fn continue_unchanged() -> Self {
715
3
        Self {
716
3
            should_update: Update::DoNothing,
717
3
            should_terminate: TerminateTimer::Continue,
718
3
        }
719
3
    }
720

            
721
    /// Timer continues running and DOM should be refreshed.
722
1
    #[must_use] pub const fn continue_and_refresh_dom() -> Self {
723
1
        Self {
724
1
            should_update: Update::RefreshDom,
725
1
            should_terminate: TerminateTimer::Continue,
726
1
        }
727
1
    }
728

            
729
    /// Timer should stop, no DOM update needed.
730
4231
    #[must_use] pub const fn terminate_unchanged() -> Self {
731
4231
        Self {
732
4231
            should_update: Update::DoNothing,
733
4231
            should_terminate: TerminateTimer::Terminate,
734
4231
        }
735
4231
    }
736

            
737
    /// Timer should stop and DOM should be refreshed.
738
2
    #[must_use] pub const fn terminate_and_refresh_dom() -> Self {
739
2
        Self {
740
2
            should_update: Update::RefreshDom,
741
2
            should_terminate: TerminateTimer::Terminate,
742
2
        }
743
2
    }
744
}
745

            
746
impl Default for TimerCallbackReturn {
747
1
    fn default() -> Self {
748
1
        Self::continue_unchanged()
749
1
    }
750
}
751

            
752
/// Gives the `layout()` function access to the `RendererResources` and the `Window`
753
/// (for querying images and fonts, as well as width / height)
754
///
755
#[derive(Debug)]
756
#[repr(C)]
757
/// Reference data container for `LayoutCallbackInfo` (all read-only fields)
758
///
759
/// This struct consolidates all readonly references that layout callbacks need to query state.
760
/// By grouping these into a single struct, we reduce the number of parameters to
761
/// `LayoutCallbackInfo::new()` from 6 to 2, making the API more maintainable and easier to extend.
762
///
763
/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
764
/// reference.
765
pub struct LayoutCallbackInfoRefData<'a> {
766
    /// Allows the `layout()` function to reference image IDs
767
    pub image_cache: &'a ImageCache,
768
    /// OpenGL context so that the `layout()` function can render textures
769
    pub gl_context: &'a OptionGlContextPtr,
770
    /// Reference to the system font cache
771
    pub system_fonts: &'a FcFontCache,
772
    /// Platform-specific system style (colors, spacing, etc.)
773
    /// Used for CSD rendering and menu windows.
774
    pub system_style: Arc<SystemStyle>,
775
    /// Active route match (if routing is configured).
776
    /// Contains the matched pattern and extracted parameters.
777
    pub active_route: Option<&'a crate::resources::RouteMatch>,
778
    /// #28 (d): SNAPSHOT of the system's monitors, taken (locked + cloned)
779
    /// by the caller right before invoking the layout callback. A snapshot —
780
    /// not the live `Arc<Mutex<…>>` handle — because `azul-core` is `no_std`
781
    /// (no Mutex) and the list is read-only during a layout pass anyway.
782
    /// Lets `layout()` bound how much content it builds on first layout
783
    /// (e.g. at most monitor-height lines / monitor-area characters), so
784
    /// opening a huge file can never build an unbounded DOM.
785
    pub monitors: crate::window::MonitorVec,
786
}
787

            
788
/// What triggered the current `layout()` invocation.
789
///
790
/// The framework re-invokes the layout callback for any change that may
791
/// produce a structurally different DOM (resize across a CSS breakpoint,
792
/// theme toggle, route switch, callback returning `Update::RefreshDom`).
793
/// `LayoutCallbackInfo::relayout_reason()` exposes which trigger this
794
/// particular call corresponds to so the callback can branch - for
795
/// example, skip expensive analytics on `Resize` calls.
796
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
797
#[repr(C)]
798
#[derive(Default)]
799
pub enum RelayoutReason {
800
    /// First layout call for this window.
801
    #[default]
802
    Initial,
803
    /// A user callback returned `Update::RefreshDom`.
804
    RefreshDom,
805
    /// Window size changed across a CSS breakpoint or DPI scale change.
806
    /// The callback can branch on `info.window_width_*` to emit a
807
    /// different tree (e.g. hamburger menu vs sidebar).
808
    Resize,
809
    /// System theme changed (light/dark).
810
    ThemeChange,
811
    /// `CallbackInfo::switch_route` or `set_route_param` produced a new
812
    /// route match. The callback should branch on
813
    /// `info.get_active_route()`.
814
    RouteChange,
815
    /// Catch-all for relayouts that don't fit one of the above categories.
816
    Other,
817
}
818

            
819

            
820
#[repr(C)]
821
pub struct LayoutCallbackInfo {
822
    /// Single reference to all readonly reference data
823
    /// This consolidates 4 individual parameters into 1, improving API ergonomics
824
    ref_data: *const LayoutCallbackInfoRefData<'static>,
825
    /// Window size (so that apps can return a different UI depending on
826
    /// the window size - mobile / desktop view). Should be later removed
827
    /// in favor of "resize" handlers and @media queries.
828
    pub window_size: WindowSize,
829
    /// Registers whether the UI is dependent on the window theme
830
    pub theme: WindowTheme,
831
    /// What triggered this `layout()` call. Read via `relayout_reason()`.
832
    pub relayout_reason: RelayoutReason,
833
    /// Pointer to the callable (`OptionRefAny`) for FFI language bindings (Python, etc.)
834
    /// Set by the caller before invoking the callback. Native Rust callbacks have this as null.
835
    callable_ptr: *const OptionRefAny,
836
    /// Extension for future ABI stability (mutable data)
837
    _abi_mut: *mut c_void,
838
}
839

            
840
/// One recorded window-size query made by a `layout()` callback.
841
///
842
/// See [`LayoutCallbackInfo::window_width_less_than`] & co. The engine replays
843
/// these against a
844
/// prospective new size to decide whether a resize could change the DOM at
845
/// all: if no recorded answer flips (and no CSS breakpoint is crossed), the
846
/// callback is provably size-stable across that resize and is not re-invoked.
847
#[repr(C)]
848
#[derive(Debug, Clone, Copy, PartialEq)]
849
pub struct SizeQuery {
850
    pub axis: SizeQueryAxis,
851
    pub op: SizeQueryOp,
852
    pub threshold_px: f32,
853
    /// The answer given at recording time, evaluated against the size the
854
    /// callback actually saw.
855
    pub answer: bool,
856
}
857

            
858
/// Which window dimension a [`SizeQuery`] tested.
859
#[repr(C)]
860
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
861
pub enum SizeQueryAxis {
862
    Width,
863
    Height,
864
}
865

            
866
/// The comparison a [`SizeQuery`] performed.
867
///
868
/// Four variants rather than a greater/smaller bool because the recorded
869
/// operator must REPLAY EXACTLY:
870
/// `window_width_less_than` is a strict `<` while `window_width_between`'s
871
/// lower bound is `>=`, and collapsing either onto the other misjudges a
872
/// resize landing precisely on the queried boundary — the one pixel the app
873
/// explicitly said it cares about.
874
#[repr(C)]
875
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
876
pub enum SizeQueryOp {
877
    /// `dim < threshold` (`window_width_less_than` / `window_height_less_than`)
878
    LessThan,
879
    /// `dim > threshold` (`window_width_greater_than` / `window_height_greater_than`)
880
    GreaterThan,
881
    /// `dim >= threshold` (the lower bound of `window_*_between`)
882
    GreaterOrEqual,
883
    /// `dim <= threshold` (the upper bound of `window_*_between`)
884
    LessOrEqual,
885
}
886

            
887
impl SizeQuery {
888
    /// What this query would answer at `size` — compare with [`Self::answer`]
889
    /// to detect a flip. MUST mirror the operators of the recording methods
890
    /// exactly (see [`SizeQueryOp`]), or the engine would skip a `layout()`
891
    /// re-invocation right at the boundary the app asked about.
892
9
    #[must_use] pub fn answer_at(&self, size: LogicalSize) -> bool {
893
9
        let dim = match self.axis {
894
9
            SizeQueryAxis::Width => size.width,
895
            SizeQueryAxis::Height => size.height,
896
        };
897
9
        match self.op {
898
5
            SizeQueryOp::LessThan => dim < self.threshold_px,
899
            SizeQueryOp::GreaterThan => dim > self.threshold_px,
900
2
            SizeQueryOp::GreaterOrEqual => dim >= self.threshold_px,
901
2
            SizeQueryOp::LessOrEqual => dim <= self.threshold_px,
902
        }
903
9
    }
904

            
905
    /// Would this query's answer differ at `size` from the recorded one?
906
9
    #[must_use] pub fn flips_at(&self, size: LogicalSize) -> bool {
907
9
        self.answer_at(size) != self.answer
908
9
    }
909
}
910

            
911
/// Thread-local recorder backing the responsive helpers
912
/// (`LayoutCallbackInfo::window_width_less_than` & co.).
913
///
914
/// A thread-local (rather than a field on the FFI-frozen `LayoutCallbackInfo`)
915
/// works because the layout callback is invoked SYNCHRONOUSLY on the calling
916
/// thread: the engine drains the recording immediately after the callback
917
/// returns, on the same thread that made the queries.
918
///
919
/// Bounded, and the overflow direction matters: SILENTLY dropping queries
920
/// would drop exactly the flips the engine needs to see — the UNSAFE
921
/// direction, a resize skipping a `layout()` that would have branched. So the
922
/// cap does not drop; it latches an `overflowed` flag that the drain reports,
923
/// and the engine then treats the callback as size-dependent EVERYWHERE
924
/// (every resize re-invokes it — today's behaviour, merely un-optimized).
925
#[cfg(feature = "std")]
926
mod size_query_recorder {
927
    use super::SizeQuery;
928

            
929
    /// More distinct thresholds than any real breakpoint scheme uses; a
930
    /// callback exceeding this is generating them programmatically.
931
    pub(super) const SIZE_QUERY_CAP: usize = 256;
932

            
933
    std::thread_local! {
934
        static RECORDED: core::cell::RefCell<(Vec<SizeQuery>, bool)> =
935
            const { core::cell::RefCell::new((Vec::new(), false)) };
936
    }
937

            
938
8314
    pub(super) fn record(q: SizeQuery) {
939
8314
        RECORDED.with(|r| {
940
8314
            let mut r = r.borrow_mut();
941
8314
            if r.0.len() >= SIZE_QUERY_CAP {
942
7714
                r.1 = true; // overflowed: the drain must report "unbounded"
943
7714
            } else {
944
600
                r.0.push(q);
945
600
            }
946
8314
        });
947
8314
    }
948

            
949
    /// Drain the recording. Returns `(queries, overflowed)`; `overflowed`
950
    /// means the cap was hit and the list is INCOMPLETE — treat every resize
951
    /// as potentially DOM-changing.
952
12
    pub(super) fn take() -> (Vec<SizeQuery>, bool) {
953
12
        RECORDED.with(|r| {
954
12
            let mut r = r.borrow_mut();
955
12
            let overflowed = r.1;
956
12
            r.1 = false;
957
12
            (core::mem::take(&mut r.0), overflowed)
958
12
        })
959
12
    }
960
}
961

            
962
#[cfg(feature = "std")]
963
8314
fn record_size_query(q: SizeQuery) {
964
8314
    size_query_recorder::record(q);
965
8314
}
966

            
967
/// Without `std` there is no thread-local to record into; the queries still
968
/// ANSWER correctly, the engine just cannot prove size-stability and falls
969
/// back to re-invoking `layout()` on breakpoint-relevant resizes (web builds
970
/// are out of scope for the resize fast path).
971
#[cfg(not(feature = "std"))]
972
fn record_size_query(_q: SizeQuery) {}
973

            
974
/// Drain the size queries recorded since the last drain on THIS thread.
975
///
976
/// Call immediately after a `layout()` callback returns, on the same thread.
977
/// `(queries, overflowed)` — on `overflowed == true` the list is incomplete
978
/// and the caller must treat the callback as size-dependent everywhere.
979
#[cfg(feature = "std")]
980
12
#[must_use] pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
981
12
    size_query_recorder::take()
982
12
}
983

            
984
#[cfg(not(feature = "std"))]
985
#[must_use] pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
986
    (alloc::vec::Vec::new(), false)
987
}
988

            
989
impl Clone for LayoutCallbackInfo {
990
    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
991
7
    fn clone(&self) -> Self {
992
7
        Self {
993
7
            ref_data: self.ref_data,
994
7
            window_size: self.window_size,
995
7
            theme: self.theme,
996
7
            relayout_reason: self.relayout_reason,
997
7
            callable_ptr: self.callable_ptr,
998
7
            _abi_mut: self._abi_mut,
999
7
        }
7
    }
}
impl core::fmt::Debug for LayoutCallbackInfo {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("LayoutCallbackInfo")
            .field("window_size", &self.window_size)
            .field("theme", &self.theme)
            .field("relayout_reason", &self.relayout_reason)
            .finish_non_exhaustive()
    }
}
impl LayoutCallbackInfo {
38
    #[must_use] pub const fn new<'a>(
38
        ref_data: &'a LayoutCallbackInfoRefData<'a>,
38
        window_size: WindowSize,
38
        theme: WindowTheme,
38
    ) -> Self {
38
        Self::new_with_reason(ref_data, window_size, theme, RelayoutReason::Initial)
38
    }
    // the `as *const ...<'static>` is a deliberate 'a -> 'static lifetime launder
    // on the raw pointer (see SAFETY note below), not a redundant cast.
    #[allow(clippy::unnecessary_cast)]
44
    #[must_use] pub const fn new_with_reason<'a>(
44
        ref_data: &'a LayoutCallbackInfoRefData<'a>,
44
        window_size: WindowSize,
44
        theme: WindowTheme,
44
        relayout_reason: RelayoutReason,
44
    ) -> Self {
44
        Self {
44
            // SAFETY: We cast away the lifetime 'a to 'static because LayoutCallbackInfo
44
            // only lives for the duration of the callback, which is shorter than 'a
44
            ref_data: core::ptr::from_ref::<LayoutCallbackInfoRefData<'a>>(ref_data)
44
                as *const LayoutCallbackInfoRefData<'static>,
44
            window_size,
44
            theme,
44
            relayout_reason,
44
            callable_ptr: core::ptr::null(),
44
            _abi_mut: core::ptr::null_mut(),
44
        }
44
    }
    /// Returns what triggered the current `layout()` invocation.
13
    #[must_use] pub const fn relayout_reason(&self) -> RelayoutReason {
13
        self.relayout_reason
13
    }
    /// Is the window's LOGICAL viewport wider than `width_px`?
    ///
    /// The structural-breakpoint helper: branch on this in `layout()` to
    /// return an entirely different DOM per form factor
    /// (`ribbon.dom_desktop()` vs `ribbon.dom_mobile()`), instead of
    /// emitting both trees and toggling visibility with `@media` rules.
    ///
    /// CONTRACT: the framework re-invokes `layout()` on every window resize
    /// (`RelayoutReason::Resize` - the regenerate path never takes the
    /// layout-equivalence shortcut when the window size changed), so the
    /// answer cannot go stale: crossing the breakpoint in either direction
    /// re-runs `layout()` and the callback returns the other tree. If a
    /// future optimization ever skips DOM regeneration on resize, it must
    /// register the thresholds queried here and force a rebuild when one is
    /// crossed - grep for this comment.
    #[must_use] pub fn viewport_bigger_than(&self, width_px: f32) -> bool {
        self.window_size.dimensions.width > width_px
    }
    /// Set the callable pointer for FFI language bindings
1
    pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
1
        self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
1
    }
    /// Get the callable for FFI language bindings (Python, etc.)
68
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
68
        if self.callable_ptr.is_null() {
2
            OptionRefAny::None
        } else {
66
            unsafe { (*self.callable_ptr).clone() }
        }
68
    }
    /// Get a clone of the system style Arc
130
    #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
130
        unsafe { (*self.ref_data).system_style.clone() }
130
    }
    /// #28 (d): snapshot of the system's monitors, taken by the caller right
    /// before this layout pass. Empty when the platform hasn't populated
    /// monitor info (headless, web, very early startup).
1
    #[must_use] pub fn get_monitors(&self) -> crate::window::MonitorVec {
1
        unsafe { (*self.ref_data).monitors.clone() }
1
    }
    /// #28 (d): the LARGEST monitor size in physical px — the safe upper
    /// bound for "how much content could possibly be visible at once" when
    /// the window's own monitor is not yet known at first layout. Apps use
    /// it to bound how much content the first `layout()` builds (e.g. at
    /// most monitor-height text lines, or monitor-width × monitor-height
    /// characters for a single unbroken line), so opening a huge file never
    /// builds an unbounded DOM. `None` when no monitor info is available.
2
    #[must_use] pub fn get_max_monitor_size(&self) -> azul_css::props::basic::OptionLayoutSize {
2
        let monitors = unsafe { &(*self.ref_data).monitors };
2
        let mut best: Option<LayoutSize> = None;
3
        for m in monitors.as_ref() {
3
            let s = m.size;
3
            let better = best.is_none_or(|b| (s.width * s.height) > (b.width * b.height));
3
            if better {
2
                best = Some(s);
2
            }
        }
2
        best.into()
2
    }
13
    const fn internal_get_image_cache(&self) -> &ImageCache {
13
        unsafe { (*self.ref_data).image_cache }
13
    }
3
    const fn internal_get_system_fonts(&self) -> &FcFontCache {
3
        unsafe { (*self.ref_data).system_fonts }
3
    }
2
    const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
2
        unsafe { (*self.ref_data).gl_context }
2
    }
1
    #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
1
        self.internal_get_gl_context().clone()
1
    }
2
    #[must_use] pub fn get_system_fonts(&self) -> Vec<AzStringPair> {
2
        let fc_cache = self.internal_get_system_fonts();
2
        fc_cache
2
            .list()
2
            .into_iter()
2
            .filter_map(|(pattern, font_id)| {
                let source = fc_cache.get_font_by_id(&font_id)?;
                match source {
                    OwnedFontSource::Memory(_) => None,
                    OwnedFontSource::Disk(d) => Some((pattern.name.as_ref()?.clone(), d.path)),
                }
            })
2
            .map(|(k, v)| AzStringPair {
                key: k.into(),
                value: v.into(),
            })
2
            .collect()
2
    }
    /// The window's ALREADY-BUILT system font cache.
    ///
    /// `get_system_fonts` only hands back stringified name/path pairs, which
    /// is useless to a layout callback that wants to run engine layout of
    /// its own (paginating a document, measuring for an export). Such an app
    /// had to call `build_font_cache()` and re-scan every font on the
    /// machine — measured at ~5 SECONDS on the first frame, during which the
    /// client cannot answer the compositor's configure/ping handshake and
    /// loses its surface.
    ///
    /// The cache is internally `Arc<RwLock<_>>` (rust-fontconfig 4.1+), so
    /// this clone is a handle, not a copy: the caller sees the same fonts
    /// the window already resolved, including builder-thread additions.
    #[must_use] pub fn get_font_cache(&self) -> FcFontCache {
        self.internal_get_system_fonts().clone()
    }
12
    #[must_use] pub fn get_image(&self, image_id: &AzString) -> Option<ImageRef> {
12
        self.internal_get_image_cache()
12
            .get_css_image_id(image_id)
12
            .cloned()
12
    }
    /// Get the active route match (pattern + extracted parameters).
    ///
    /// Returns `None` if no routes are configured or no route is active.
41
    #[must_use] pub const fn get_active_route(&self) -> Option<&crate::resources::RouteMatch> {
41
        unsafe { (*self.ref_data).active_route }
41
    }
    /// Get a route parameter by key (e.g. `get_route_param("id")` for `/user/:id`).
    ///
    /// Returns `None` if no route is active or the parameter doesn't exist.
39
    #[must_use] pub fn get_route_param(&self, key: &str) -> Option<&AzString> {
39
        self.get_active_route()?.get_param(key)
39
    }
    /// The pattern of the route this layout callback is rendering, e.g.
    /// `"/user/:id"`.
    ///
    /// `"/"` when the app configured no routes: an app without routing is on
    /// the default route, so a callback that branches on the pattern always
    /// has one string to branch on rather than an empty one.
    ///
    /// # C API
    /// ```c
    /// AzString pattern = AzLayoutCallbackInfo_getRoutePattern(&info);
    /// ```
    #[must_use] pub fn get_route_pattern(&self) -> AzString {
        self.get_active_route()
            .map_or_else(|| AzString::from_const_str("/"), |route| route.pattern.clone())
    }
    /// A route parameter by key, empty when the parameter or the route is
    /// absent. The owned-key, owned-return form the FFI needs;
    /// [`Self::get_route_param`] is the borrowing Rust one.
    ///
    /// # C API
    /// ```c
    /// AzString id = AzLayoutCallbackInfo_getRouteParamOrEmpty(&info,
    ///     AzString_fromConstStr("id"));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[must_use] pub fn get_route_param_or_empty(&self, key: AzString) -> AzString {
        self.get_route_param(key.as_str())
            .cloned()
            .unwrap_or_else(|| AzString::from_const_str(""))
    }
    // Responsive layout helper methods.
    //
    // These are THE sanctioned way for `layout()` to branch on window size
    // (mobile vs desktop DOM shapes, instead of `display:none` stacks). Every
    // call is RECORDED, and the recording is what makes resize cheap: a resize
    // that flips none of the recorded answers (and crosses no CSS breakpoint)
    // provably cannot change what the callback returns through this channel,
    // so the engine re-flows the existing DOM instead of re-invoking it
    // (`LayoutWindow::resize_needs_full_regeneration`). Reading the size
    // imperatively (`get_window_width()`, `info.window_size`) to branch the
    // DOM is a bug in the app: the engine cannot see that read, so the DOM
    // goes stale across exactly the resizes the app cared about.
    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
6078
    fn record_width_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
6078
        record_size_query(SizeQuery {
6078
            axis: SizeQueryAxis::Width,
6078
            op,
6078
            threshold_px,
6078
            answer,
6078
        });
6078
        answer
6078
    }
    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
2236
    fn record_height_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
2236
        record_size_query(SizeQuery {
2236
            axis: SizeQueryAxis::Height,
2236
            op,
2236
            threshold_px,
2236
            answer,
2236
        });
2236
        answer
2236
    }
    /// Returns true if the window width is less than the given pixel value.
    /// Recorded — see the note above these helpers.
1110
    #[must_use] pub fn window_width_less_than(&self, px: f32) -> bool {
1110
        let answer = self.window_size.dimensions.width < px;
1110
        self.record_width_query(SizeQueryOp::LessThan, px, answer)
1110
    }
    /// Returns true if the window width is greater than the given pixel value.
    /// Recorded — see the note above these helpers.
936
    #[must_use] pub fn window_width_greater_than(&self, px: f32) -> bool {
936
        let answer = self.window_size.dimensions.width > px;
936
        self.record_width_query(SizeQueryOp::GreaterThan, px, answer)
936
    }
    /// Returns true if the window width is between min and max (inclusive).
    /// Recorded as its two bounds — see the note above these helpers.
2016
    #[must_use] pub fn window_width_between(&self, min_px: f32, max_px: f32) -> bool {
2016
        let width = self.window_size.dimensions.width;
2016
        self.record_width_query(SizeQueryOp::GreaterOrEqual, min_px, width >= min_px)
2016
            & self.record_width_query(SizeQueryOp::LessOrEqual, max_px, width <= max_px)
2016
    }
    /// Returns true if the window height is less than the given pixel value.
    /// Recorded — see the note above these helpers.
107
    #[must_use] pub fn window_height_less_than(&self, px: f32) -> bool {
107
        let answer = self.window_size.dimensions.height < px;
107
        self.record_height_query(SizeQueryOp::LessThan, px, answer)
107
    }
    /// Returns true if the window height is greater than the given pixel value.
    /// Recorded — see the note above these helpers.
107
    #[must_use] pub fn window_height_greater_than(&self, px: f32) -> bool {
107
        let answer = self.window_size.dimensions.height > px;
107
        self.record_height_query(SizeQueryOp::GreaterThan, px, answer)
107
    }
    /// Returns true if the window height is between min and max (inclusive).
    /// Recorded as its two bounds — see the note above these helpers.
1011
    #[must_use] pub fn window_height_between(&self, min_px: f32, max_px: f32) -> bool {
1011
        let height = self.window_size.dimensions.height;
1011
        self.record_height_query(SizeQueryOp::GreaterOrEqual, min_px, height >= min_px)
1011
            & self.record_height_query(SizeQueryOp::LessOrEqual, max_px, height <= max_px)
1011
    }
    /// Returns the current window width in pixels
102
    #[must_use] pub const fn get_window_width(&self) -> f32 {
102
        self.window_size.dimensions.width
102
    }
    /// Returns the current window height in pixels
2
    #[must_use] pub const fn get_window_height(&self) -> f32 {
2
        self.window_size.dimensions.height
2
    }
    /// Returns the current window DPI scale factor (1.0 = 96 DPI, 2.0 = 192 DPI)
    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
6
    #[must_use] pub fn get_dpi_factor(&self) -> f32 {
6
        self.window_size.dpi as f32 / 96.0
6
    }
}
/// Information about the bounds of a laid-out div rectangle.
///
/// Necessary when invoking `VirtualViewCallbacks` and `RenderImageCallbacks`, so
/// that they can change what their content is based on their size.
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct HidpiAdjustedBounds {
    pub logical_size: LogicalSize,
    pub hidpi_factor: DpiScaleFactor,
}
impl HidpiAdjustedBounds {
    #[inline]
    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
19
    #[must_use] pub const fn from_bounds(bounds: LayoutSize, hidpi_factor: DpiScaleFactor) -> Self {
19
        let logical_size = LogicalSize::new(bounds.width as f32, bounds.height as f32);
19
        Self {
19
            logical_size,
19
            hidpi_factor,
19
        }
19
    }
13
    #[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
13
        self.get_logical_size()
13
            .to_physical(self.get_hidpi_factor().inner.get())
13
    }
734
    #[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
734
        self.logical_size
734
    }
18
    #[must_use] pub const fn get_hidpi_factor(&self) -> DpiScaleFactor {
18
        self.hidpi_factor
18
    }
}
/// Defines the `focus_targeted` node ID for the next frame
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum FocusTarget {
    Id(DomNodeId),
    Path(FocusTargetPath),
    Previous,
    Next,
    First,
    Last,
    NoFocus,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct FocusTargetPath {
    pub dom: DomId,
    pub css_path: CssPath,
}
// -- normal callback
// core callback types (usize-based placeholders)
//
// These types use `usize` instead of function pointers to avoid creating
// a circular dependency between azul-core and azul-layout.
//
// The actual function pointers will be stored in azul-layout, which will
// use unsafe code to transmute between usize and the real function pointers.
//
// IMPORTANT: The memory layout must be identical to the real types!
//
// Naming convention: "Core" prefix indicates these are the low-level types
/// Core callback type - uses usize instead of function pointer to avoid circular dependencies.
///
/// **IMPORTANT**: This is NOT actually a usize at runtime - it's a function pointer that is
/// cast to usize for storage in the data model. When invoking the callback, this usize is
/// unsafely cast back to the actual function pointer type:
/// `extern "C" fn(RefAny, CallbackInfo) -> Update`
///
/// This design allows azul-core to store callbacks without depending on azul-layout's `CallbackInfo`
/// type. The actual function pointer type is defined in azul-layout as `CallbackType`.
pub type CoreCallbackType = usize;
/// Stores a callback as usize (actually a function pointer cast to usize)
///
/// **IMPORTANT**: The `cb` field stores a function pointer disguised as usize to avoid
/// circular dependencies between azul-core and azul-layout. When creating a `CoreCallback`,
/// you can directly assign a function pointer - Rust will implicitly cast it to usize.
/// When invoking, the usize must be unsafely cast back to the function pointer type.
///
/// Must return an `Update` that denotes if the screen should be redrawn.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreCallback {
    pub cb: CoreCallbackType,
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
    /// Native Rust code sets this to None
    pub ctx: OptionRefAny,
}
/// Allow creating `CoreCallback` from a raw function pointer (as usize)
/// Sets callable to None (for native Rust/C usage)
impl From<CoreCallbackType> for CoreCallback {
1251
    fn from(cb: CoreCallbackType) -> Self {
1251
        Self {
1251
            cb,
1251
            ctx: OptionRefAny::None,
1251
        }
1251
    }
}
impl_option!(
    CoreCallback,
    OptionCoreCallback,
    [Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash]
);
/// Data associated with a callback (event filter, callback, and user data)
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreCallbackData {
    pub event: EventFilter,
    pub callback: CoreCallback,
    pub refany: RefAny,
}
impl_option!(
    CoreCallbackData,
    OptionCoreCallbackData,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(CoreCallbackData, CoreCallbackDataVec, CoreCallbackDataVecDestructor, CoreCallbackDataVecDestructorType, CoreCallbackDataVecSlice, OptionCoreCallbackData);
impl_vec_clone!(
    CoreCallbackData,
    CoreCallbackDataVec,
    CoreCallbackDataVecDestructor
);
impl_vec_mut!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_debug!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_partialord!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_ord!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_partialeq!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_eq!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_hash!(CoreCallbackData, CoreCallbackDataVec);
impl CoreCallbackDataVec {
    #[inline]
5
    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CoreCallbackData> {
5
        NodeDataContainerRef {
5
            internal: self.as_ref(),
5
        }
5
    }
    #[inline]
2
    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, CoreCallbackData> {
2
        NodeDataContainerRefMut {
2
            internal: self.as_mut(),
2
        }
2
    }
}
// -- image rendering callback
/// Image rendering callback type - uses usize instead of function pointer
pub type CoreRenderImageCallbackType = usize;
/// Callback that returns a rendered OpenGL texture (usize placeholder)
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreRenderImageCallback {
    pub cb: CoreRenderImageCallbackType,
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
    /// Native Rust code sets this to None
    pub ctx: OptionRefAny,
}
/// Allow creating `CoreRenderImageCallback` from a raw function pointer (as usize)
/// Sets callable to None (for native Rust/C usage)
impl From<CoreRenderImageCallbackType> for CoreRenderImageCallback {
153
    fn from(cb: CoreRenderImageCallbackType) -> Self {
153
        Self {
153
            cb,
153
            ctx: OptionRefAny::None,
153
        }
153
    }
}
/// Image callback with associated data
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreImageCallback {
    pub refany: RefAny,
    pub callback: CoreRenderImageCallback,
}
impl_option!(
    CoreImageCallback,
    OptionCoreImageCallback,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
#[cfg(test)]
#[allow(
    clippy::float_cmp,
    clippy::too_many_lines,
    clippy::cast_precision_loss,
    clippy::unusual_byte_groupings
)]
mod autotest_generated {
    use alloc::string::String;
    use super::*;
    use crate::{
        events::HoverEventFilter,
        resources::{RawImageFormat, RouteMatch},
        window::StringPairVec,
    };
    // ---- helpers -----------------------------------------------------------
    fn s(v: &str) -> AzString {
        AzString::from(String::from(v))
    }
    fn win(width: f32, height: f32, dpi: u32) -> WindowSize {
        WindowSize {
            dimensions: LogicalSize::new(width, height),
            dpi,
            min_dimensions: None.into(),
            max_dimensions: None.into(),
        }
    }
    /// Owns everything a `LayoutCallbackInfoRefData` borrows, so that the raw
    /// pointer `LayoutCallbackInfo` launders to `'static` always points at
    /// live memory for the duration of a test.
    struct Fixture {
        fonts: FcFontCache,
        images: ImageCache,
        style: Arc<SystemStyle>,
        gl: OptionGlContextPtr,
        route: Option<RouteMatch>,
    }
    impl Fixture {
        fn new() -> Self {
            Self {
                fonts: FcFontCache::default(),
                images: ImageCache::default(),
                style: Arc::new(SystemStyle::default()),
                gl: OptionGlContextPtr::None,
                route: None,
            }
        }
        fn with_route(route: RouteMatch) -> Self {
            let mut f = Self::new();
            f.route = Some(route);
            f
        }
        fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
            LayoutCallbackInfoRefData {
                image_cache: &self.images,
                gl_context: &self.gl,
                system_fonts: &self.fonts,
                system_style: self.style.clone(),
                active_route: self.route.as_ref(),
                monitors: crate::window::MonitorVec::from_const_slice(&[]),
            }
        }
    }
    /// #28 (d): `get_max_monitor_size` returns the LARGEST monitor by area
    /// (the safe "how much could possibly be visible" bound for first
    /// layout) and `None` on an empty snapshot (headless/web).
    #[test]
    fn max_monitor_size_is_largest_by_area_or_none() {
        use azul_css::props::basic::LayoutSize;
        use crate::window::{Monitor, MonitorVec};
        let fixture = Fixture::new();
        let mut rd = fixture.ref_data();
        rd.monitors = MonitorVec::from_vec(Vec::from([
            Monitor {
                size: LayoutSize::new(1920, 1080),
                ..Monitor::default()
            },
            Monitor {
                size: LayoutSize::new(2560, 1440),
                ..Monitor::default()
            },
            Monitor {
                size: LayoutSize::new(800, 600),
                ..Monitor::default()
            },
        ]));
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        let max: Option<LayoutSize> = info.get_max_monitor_size().into();
        assert_eq!(max, Some(LayoutSize::new(2560, 1440)));
        assert_eq!(info.get_monitors().len(), 3);
        let rd2 = fixture.ref_data(); // empty snapshot
        let info2 = LayoutCallbackInfo::new(&rd2, WindowSize::default(), WindowTheme::LightMode);
        let none: Option<LayoutSize> = info2.get_max_monitor_size().into();
        assert_eq!(none, None);
    }
    /// `/user/:id` with a plain and a non-ASCII parameter.
    fn user_route() -> RouteMatch {
        RouteMatch {
            pattern: s("/user/:id"),
            params: StringPairVec::from_vec(Vec::from([
                AzStringPair {
                    key: s("id"),
                    value: s("42"),
                },
                AzStringPair {
                    key: s("\u{1F600}"),
                    value: s("emoji"),
                },
            ])),
        }
    }
    fn vv_info<'a>(
        fonts: &'a FcFontCache,
        images: &'a ImageCache,
        bounds: HidpiAdjustedBounds,
    ) -> VirtualViewCallbackInfo {
        VirtualViewCallbackInfo::new(
            VirtualViewCallbackReason::InitialRender,
            fonts,
            images,
            WindowTheme::LightMode,
            bounds,
            // materialized: a window at y=2 covering 100x200 of the document
            LogicalRect::new(LogicalPosition::new(1.0, 2.0), LogicalSize::new(100.0, 200.0)),
            // virtual_rect: the whole document
            LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(1000.0, 2000.0)),
            // where the user is looking
            LogicalPosition::new(3.0, 4.0),
        )
    }
    fn bounds_1x1() -> HidpiAdjustedBounds {
        HidpiAdjustedBounds::from_bounds(LayoutSize::new(1, 1), DpiScaleFactor::new(1.0))
    }
    // ---- Update::max_self --------------------------------------------------
    const ALL_UPDATES: [Update; 3] = [
        Update::DoNothing,
        Update::RefreshDom,
        Update::RefreshDomAllWindows,
    ];
    /// `max_self` must be exactly the `Ord`-max of the lattice, for every one
    /// of the 3x3 combinations (this is the whole contract, so check it
    /// exhaustively rather than sampling).
    #[test]
    fn update_max_self_is_exhaustively_ord_max() {
        for a in ALL_UPDATES {
            for b in ALL_UPDATES {
                let mut got = a;
                got.max_self(b);
                assert_eq!(
                    got,
                    core::cmp::max(a, b),
                    "max_self({a:?}, {b:?}) disagrees with Ord::max"
                );
            }
        }
    }
    #[test]
    fn update_max_self_is_idempotent_and_monotone() {
        for a in ALL_UPDATES {
            // idempotent: x.max(x) == x
            let mut got = a;
            got.max_self(a);
            assert_eq!(got, a);
            // absorbing top element: nothing can lower RefreshDomAllWindows
            let mut top = Update::RefreshDomAllWindows;
            top.max_self(a);
            assert_eq!(top, Update::RefreshDomAllWindows);
            // monotone: max_self never decreases self
            let mut m = a;
            m.max_self(Update::DoNothing);
            assert!(m >= a);
        }
    }
    /// Applying the same set of updates in any order must converge to the same
    /// value (commutativity/associativity of the fold), since callbacks fold
    /// their `Update`s in nondeterministic order.
    #[test]
    fn update_max_self_fold_is_order_independent() {
        for a in ALL_UPDATES {
            for b in ALL_UPDATES {
                for c in ALL_UPDATES {
                    let mut fwd = a;
                    fwd.max_self(b);
                    fwd.max_self(c);
                    let mut rev = c;
                    rev.max_self(b);
                    rev.max_self(a);
                    assert_eq!(fwd, rev, "fold of {a:?},{b:?},{c:?} is order-dependent");
                }
            }
        }
    }
    // ---- LayoutCallback / default_layout_callback ---------------------------
    static ALT_LAYOUT_CALLS: AtomicUsize = AtomicUsize::new(0);
    // NOTE: the body must differ from `default_layout_callback`'s, otherwise
    // identical-code-folding may merge the two symbols and the pointer
    // inequality assertion below would compare equal addresses.
    extern "C" fn alt_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
        ALT_LAYOUT_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
        Dom::create_body()
    }
    #[test]
    fn default_layout_callback_returns_body_and_does_not_panic() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, win(0.0, 0.0, 0), WindowTheme::DarkMode);
        // extreme arg: zero-sized window, zero DPI, empty caches
        let dom = default_layout_callback(RefAny::new(0u32), info);
        assert_eq!(dom, Dom::create_body());
    }
    #[test]
    fn layout_callback_create_stores_the_given_fn_and_null_ctx() {
        let from_default = LayoutCallback::create(default_layout_callback as LayoutCallbackType);
        assert!(
            from_default.ctx.is_none(),
            "native-Rust create() must leave the FFI ctx empty"
        );
        assert_eq!(from_default, LayoutCallback::default());
        // create() must actually store its argument, not silently fall back
        // to the default callback.
        let from_alt = LayoutCallback::create(alt_layout_callback as LayoutCallbackType);
        assert!(from_alt.ctx.is_none());
        assert_ne!(
            from_alt, from_default,
            "create() ignored its argument (or the two fns were ICF-folded)"
        );
        // the stored pointer is callable and is the one we passed in
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let before = ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst);
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        let _ = (from_alt.cb)(RefAny::new(()), info);
        assert_eq!(ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst), before + 1);
    }
    // ---- VirtualViewCallback ------------------------------------------------
    extern "C" fn vv_keep_current_cb(_: RefAny, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
        VirtualViewReturn::keep_current(info.materialized, info.virtual_rect)
    }
    #[test]
    fn virtual_view_callback_create_round_trips_through_the_fn_ptr() {
        let cb = VirtualViewCallback::create(vv_keep_current_cb as VirtualViewCallbackType);
        assert!(cb.ctx.is_none());
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let info = vv_info(&fonts, &images, bounds_1x1());
        let ret = (cb.cb)(RefAny::new(0u8), info);
        assert!(ret.dom.is_none());
        assert_eq!(ret.materialized.size, LogicalSize::new(100.0, 200.0));
        assert_eq!(ret.materialized.origin, LogicalPosition::new(1.0, 2.0));
        assert_eq!(ret.virtual_rect.size, LogicalSize::new(1000.0, 2000.0));
        assert_eq!(ret.virtual_rect.origin, LogicalPosition::zero());
    }
    // ---- VirtualViewCallbackInfo -------------------------------------------
    #[test]
    fn virtual_view_callback_info_new_holds_its_fields() {
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let bounds = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(800, 600),
            DpiScaleFactor::new(2.0),
        );
        let info = vv_info(&fonts, &images, bounds);
        assert_eq!(info.reason, VirtualViewCallbackReason::InitialRender);
        assert_eq!(info.window_theme, WindowTheme::LightMode);
        assert_eq!(info.get_bounds().get_logical_size(), LogicalSize::new(800.0, 600.0));
        assert_eq!(info.get_bounds().get_hidpi_factor(), DpiScaleFactor::new(2.0));
        assert_eq!(info.materialized.size, LogicalSize::new(100.0, 200.0));
        // the raw pointers must alias the borrows we handed in
        assert!(core::ptr::eq(info.internal_get_system_fonts(), &fonts));
        assert!(core::ptr::eq(info.internal_get_image_cache(), &images));
        // FFI ctx starts empty and the measure hook starts absent
        assert!(info.get_ctx().is_none());
        assert_eq!(
            info.measure_dom(Dom::create_body(), LogicalSize::new(10.0, 10.0)),
            LogicalSize::zero()
        );
        // clone must not disturb any of that
        let cloned = info.clone();
        assert_eq!(cloned.reason, info.reason);
        assert!(core::ptr::eq(cloned.internal_get_system_fonts(), &fonts));
        assert!(cloned.get_ctx().is_none());
    }
    #[test]
    fn virtual_view_callback_info_new_survives_nan_and_infinite_geometry() {
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let info = VirtualViewCallbackInfo::new(
            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
            &fonts,
            &images,
            WindowTheme::DarkMode,
            HidpiAdjustedBounds::from_bounds(
                LayoutSize::new(isize::MAX, isize::MIN),
                DpiScaleFactor::new(f32::NAN),
            ),
            LogicalRect::new(
                LogicalPosition::new(f32::NEG_INFINITY, f32::MAX),
                LogicalSize::new(f32::NAN, f32::INFINITY),
            ),
            LogicalRect::new(
                LogicalPosition::new(-0.0, f32::EPSILON),
                LogicalSize::new(f32::MIN, 0.0),
            ),
            LogicalPosition::new(-0.0, f32::EPSILON),
        );
        // extreme values are stored verbatim, not silently clamped
        assert!(info.materialized.size.width.is_nan());
        assert!(info.materialized.size.height.is_infinite());
        assert!(
            info.materialized.origin.x.is_infinite()
                && info.materialized.origin.x.is_sign_negative()
        );
        assert_eq!(info.virtual_rect.size.width, f32::MIN);
        assert_eq!(info.reason, VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
        // and none of the getters panic on that instance
        assert!(info.get_ctx().is_none());
        assert!(info.get_bounds().get_logical_size().width > 0.0);
    }
    #[test]
    fn virtual_view_callback_info_get_ctx_clones_without_double_free() {
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let mut info = vv_info(&fonts, &images, bounds_1x1());
        // null callable_ptr -> None (the native-Rust path)
        assert!(info.get_ctx().is_none());
        let callable = OptionRefAny::Some(RefAny::new(0xDEAD_BEEF_u32));
        info.set_callable_ptr(&callable);
        // repeated get_ctx() must hand out independent clones; dropping them
        // all must not corrupt the original RefAny's refcount.
        for _ in 0..64 {
            let got = info.get_ctx();
            assert!(got.is_some());
            drop(got);
        }
        let mut got = info.get_ctx();
        match got {
            OptionRefAny::Some(ref mut r) => {
                let inner = r.downcast_ref::<u32>().expect("ctx should hold a u32");
                assert_eq!(*inner, 0xDEAD_BEEF_u32);
            }
            OptionRefAny::None => panic!("callable_ptr was set, get_ctx() returned None"),
        }
        drop(got);
        // the original is still alive and intact after all those clones dropped
        let mut orig = callable;
        match orig {
            OptionRefAny::Some(ref mut r) => {
                assert_eq!(*r.downcast_ref::<u32>().unwrap(), 0xDEAD_BEEF_u32);
            }
            OptionRefAny::None => panic!("original callable was consumed"),
        }
    }
    // ---- measure_dom --------------------------------------------------------
    static MEASURE_CALLS: AtomicUsize = AtomicUsize::new(0);
    /// Test trampoline. Per the `MeasureDomFn` contract the `Dom` is passed by
    /// pointer and **consumed** (moved out) here.
    extern "C" fn test_measure_dom_fn(
        ctx: *mut c_void,
        dom: *mut Dom,
        available: LogicalSize,
    ) -> LogicalSize {
        MEASURE_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
        // SAFETY: `measure_dom` always passes a valid, owned-but-ManuallyDrop
        // Dom; taking it by value here is exactly the documented contract.
        let dom = unsafe { core::ptr::read(dom) };
        drop(dom);
        if !ctx.is_null() {
            // SAFETY: the only caller below passes a `&mut u32`.
            unsafe {
                *ctx.cast::<u32>() = 0xABCD;
            }
        }
        LogicalSize::new(available.width * 2.0, available.height / 2.0)
    }
    #[test]
    fn measure_dom_without_hook_returns_zero_for_every_input() {
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let info = vv_info(&fonts, &images, bounds_1x1());
        // zero / negative / NaN / infinite / huge available sizes must all take
        // the null-hook early-out without panicking (and must drop the Dom).
        for available in [
            LogicalSize::zero(),
            LogicalSize::new(-1.0, -1.0),
            LogicalSize::new(f32::NAN, f32::NAN),
            LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
            LogicalSize::new(f32::MAX, f32::MIN),
            LogicalSize::new(1.0, 1_000_000.0),
        ] {
            assert_eq!(
                info.measure_dom(Dom::create_body(), available),
                LogicalSize::zero()
            );
        }
    }
    #[test]
    fn measure_dom_with_hook_forwards_ctx_and_available_and_consumes_the_dom() {
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let mut info = vv_info(&fonts, &images, bounds_1x1());
        let mut ctx_val: u32 = 0;
        info.set_measure_dom_fn(
            test_measure_dom_fn,
            core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
        );
        // NOTE: `>` not `== before + 1` - other tests share this static and
        // run in parallel, so only monotonicity is safe to assert here.
        let before = MEASURE_CALLS.load(AtomicOrdering::SeqCst);
        let out = info.measure_dom(Dom::create_body(), LogicalSize::new(100.0, 40.0));
        assert!(MEASURE_CALLS.load(AtomicOrdering::SeqCst) > before);
        assert_eq!(out, LogicalSize::new(200.0, 20.0));
        assert_eq!(ctx_val, 0xABCD, "measure ctx pointer was not forwarded");
        // the documented virtual-scroll sizing idiom: measure at a huge height
        let natural = info.measure_dom(Dom::create_body(), LogicalSize::new(320.0, 1_000_000.0));
        assert_eq!(natural, LogicalSize::new(640.0, 500_000.0));
        // NaN / infinite constraints reach the hook unmodified and come back
        // as NaN/inf rather than panicking or being clamped
        let nan = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::NAN, 4.0));
        assert!(nan.width.is_nan());
        assert_eq!(nan.height, 2.0);
        let inf = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::INFINITY, 4.0));
        assert!(inf.width.is_infinite());
    }
    #[test]
    fn measure_dom_hook_can_be_replaced_and_last_writer_wins() {
        let fonts = FcFontCache::default();
        let images = ImageCache::default();
        let mut info = vv_info(&fonts, &images, bounds_1x1());
        info.set_measure_dom_fn(test_measure_dom_fn, core::ptr::null_mut());
        // null ctx must be tolerated by the trampoline contract
        let first = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
        assert_eq!(first, LogicalSize::new(4.0, 4.0));
        let mut ctx_val: u32 = 0;
        info.set_measure_dom_fn(
            test_measure_dom_fn,
            core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
        );
        let second = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
        assert_eq!(second, first);
        assert_eq!(ctx_val, 0xABCD);
    }
    // ---- VirtualViewReturn --------------------------------------------------
    #[test]
    fn virtual_view_return_with_dom_and_keep_current_hold_their_fields() {
        // the window the callback rendered: 30px tall, sitting 300px down the document
        let mat = LogicalRect::new(
            LogicalPosition::new(0.0, 300.0),
            LogicalSize::new(600.0, 30.0),
        );
        // the whole document estimate
        let virt = LogicalRect::new(
            LogicalPosition::zero(),
            LogicalSize::new(600.0, 30_000.0),
        );
        let with = VirtualViewReturn::with_dom(Dom::create_body(), mat, virt);
        assert!(with.dom.is_some(), "with_dom must produce OptionDom::Some");
        assert_eq!(with.materialized, mat);
        assert_eq!(with.virtual_rect, virt);
        assert_eq!(with.dom, OptionDom::Some(Dom::create_body()));
        let keep = VirtualViewReturn::keep_current(mat, virt);
        assert!(keep.dom.is_none(), "keep_current must produce OptionDom::None");
        assert_eq!(keep.materialized, mat);
        assert_eq!(keep.virtual_rect, virt);
        // the two constructors differ *only* in the dom field
        assert_ne!(with, keep);
        // default is the "keep everything, render nothing" zero value
        let d = VirtualViewReturn::default();
        assert_eq!(
            d,
            VirtualViewReturn::keep_current(LogicalRect::zero(), LogicalRect::zero())
        );
    }
    #[test]
    fn virtual_view_return_keep_current_passes_extreme_values_through_unclamped() {
        // zero
        let z = VirtualViewReturn::keep_current(LogicalRect::zero(), LogicalRect::zero());
        assert_eq!(z.materialized.size, LogicalSize::zero());
        assert_eq!(z.virtual_rect.size, LogicalSize::zero());
        // negative + f32 limits: stored verbatim (no saturation, no panic)
        let n = VirtualViewReturn::keep_current(
            LogicalRect::new(
                LogicalPosition::new(f32::MIN, f32::MAX),
                LogicalSize::new(-1.0, -0.0),
            ),
            LogicalRect::new(
                LogicalPosition::new(-f32::EPSILON, 0.0),
                LogicalSize::new(f32::MAX, f32::MIN_POSITIVE),
            ),
        );
        assert_eq!(n.materialized.size.width, -1.0);
        assert_eq!(n.materialized.origin.x, f32::MIN);
        assert_eq!(n.materialized.origin.y, f32::MAX);
        assert_eq!(n.virtual_rect.size.width, f32::MAX);
        assert_eq!(n.virtual_rect.size.height, f32::MIN_POSITIVE);
        // NaN / inf: stored verbatim; NaN makes the struct unequal to itself
        // under PartialEq, so probe the fields directly.
        let x = VirtualViewReturn::keep_current(
            LogicalRect::new(
                LogicalPosition::new(f32::NEG_INFINITY, f32::NAN),
                LogicalSize::new(f32::NAN, f32::INFINITY),
            ),
            LogicalRect::new(
                LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
                LogicalSize::new(f32::INFINITY, f32::NAN),
            ),
        );
        assert!(x.materialized.size.width.is_nan());
        assert!(
            x.materialized.size.height.is_infinite()
                && x.materialized.size.height.is_sign_positive()
        );
        assert!(
            x.materialized.origin.x.is_infinite()
                && x.materialized.origin.x.is_sign_negative()
        );
        assert!(x.materialized.origin.y.is_nan());
        assert!(x.virtual_rect.origin.y.is_infinite());
        assert!(x.dom.is_none());
    }
    // ---- TimerCallbackReturn ------------------------------------------------
    #[test]
    fn timer_callback_return_constructors_match_their_documented_flags() {
        let c = TimerCallbackReturn::continue_unchanged();
        assert_eq!(c.should_update, Update::DoNothing);
        assert_eq!(c.should_terminate, TerminateTimer::Continue);
        let cr = TimerCallbackReturn::continue_and_refresh_dom();
        assert_eq!(cr.should_update, Update::RefreshDom);
        assert_eq!(cr.should_terminate, TerminateTimer::Continue);
        let t = TimerCallbackReturn::terminate_unchanged();
        assert_eq!(t.should_update, Update::DoNothing);
        assert_eq!(t.should_terminate, TerminateTimer::Terminate);
        let tr = TimerCallbackReturn::terminate_and_refresh_dom();
        assert_eq!(tr.should_update, Update::RefreshDom);
        assert_eq!(tr.should_terminate, TerminateTimer::Terminate);
        // all four are distinct - no constructor is a copy-paste of another
        let all = [c, cr, t, tr];
        for (i, a) in all.iter().enumerate() {
            for (j, b) in all.iter().enumerate() {
                assert_eq!(i == j, a == b, "constructors {i} and {j} collide");
            }
        }
        // Default is documented as "continue, no update"
        assert_eq!(TimerCallbackReturn::default(), c);
    }
    #[test]
    fn timer_callback_return_create_round_trips_every_flag_combination() {
        for u in ALL_UPDATES {
            for t in [TerminateTimer::Continue, TerminateTimer::Terminate] {
                let r = TimerCallbackReturn::create(u, t);
                assert_eq!(r.should_update, u);
                assert_eq!(r.should_terminate, t);
            }
        }
        // the named constructors agree with the generic one
        assert_eq!(
            TimerCallbackReturn::create(Update::DoNothing, TerminateTimer::Continue),
            TimerCallbackReturn::continue_unchanged()
        );
        assert_eq!(
            TimerCallbackReturn::create(Update::RefreshDom, TerminateTimer::Terminate),
            TimerCallbackReturn::terminate_and_refresh_dom()
        );
        // RefreshDomAllWindows is reachable through create() even though no
        // named constructor exposes it
        let all_windows =
            TimerCallbackReturn::create(Update::RefreshDomAllWindows, TerminateTimer::Terminate);
        assert_eq!(all_windows.should_update, Update::RefreshDomAllWindows);
    }
    // ---- LayoutCallbackInfo: construction + getters --------------------------
    #[test]
    fn layout_callback_info_new_defaults_to_initial_reason_and_holds_fields() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, win(1280.0, 720.0, 192), WindowTheme::DarkMode);
        assert_eq!(info.relayout_reason(), RelayoutReason::Initial);
        assert_eq!(info.theme, WindowTheme::DarkMode);
        assert_eq!(info.get_window_width(), 1280.0);
        assert_eq!(info.get_window_height(), 720.0);
        assert_eq!(info.get_dpi_factor(), 2.0);
        assert!(info.get_ctx().is_none());
        // the borrowed resources are reachable through the laundered pointer
        assert!(core::ptr::eq(info.internal_get_image_cache(), &fx.images));
        assert!(core::ptr::eq(info.internal_get_system_fonts(), &fx.fonts));
        assert!(core::ptr::eq(info.internal_get_gl_context(), &fx.gl));
        assert!(info.get_gl_context().is_none());
    }
    #[test]
    fn layout_callback_info_new_with_reason_round_trips_every_reason() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        for reason in [
            RelayoutReason::Initial,
            RelayoutReason::RefreshDom,
            RelayoutReason::Resize,
            RelayoutReason::ThemeChange,
            RelayoutReason::RouteChange,
            RelayoutReason::Other,
        ] {
            let info = LayoutCallbackInfo::new_with_reason(
                &rd,
                WindowSize::default(),
                WindowTheme::LightMode,
                reason,
            );
            assert_eq!(info.relayout_reason(), reason);
            // clone must preserve it
            assert_eq!(info.clone().relayout_reason(), reason);
        }
        assert_eq!(RelayoutReason::default(), RelayoutReason::Initial);
    }
    #[test]
    fn layout_callback_info_get_system_style_shares_the_arc() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        let a = info.get_system_style();
        let b = info.get_system_style();
        // it is a clone of the *same* Arc, not a fresh deep copy
        assert!(Arc::ptr_eq(&a, &b));
        assert!(Arc::ptr_eq(&a, &fx.style));
        // repeated cloning must not leak/underflow the refcount
        let before = Arc::strong_count(&fx.style);
        for _ in 0..128 {
            drop(info.get_system_style());
        }
        assert_eq!(Arc::strong_count(&fx.style), before);
    }
    #[test]
    fn layout_callback_info_get_ctx_is_none_until_set_then_clones_safely() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let mut info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        assert!(info.get_ctx().is_none(), "native path must have a null ctx");
        let callable = OptionRefAny::Some(RefAny::new(7u64));
        info.set_callable_ptr(&callable);
        for _ in 0..64 {
            assert!(info.get_ctx().is_some());
        }
        let mut got = info.get_ctx();
        match got {
            OptionRefAny::Some(ref mut r) => assert_eq!(*r.downcast_ref::<u64>().unwrap(), 7),
            OptionRefAny::None => panic!("get_ctx() lost the callable"),
        }
        drop(got);
        // a clone of the info keeps pointing at the same callable
        let cloned = info.clone();
        assert!(cloned.get_ctx().is_some());
    }
    #[test]
    fn layout_callback_info_get_system_fonts_is_empty_for_an_empty_cache() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        // an empty FcFontCache must yield an empty list, not panic
        let fonts: Vec<AzStringPair> = info.get_system_fonts();
        assert!(fonts.is_empty());
        // and be stable across calls
        assert_eq!(info.get_system_fonts().len(), fonts.len());
    }
    // ---- LayoutCallbackInfo::get_image --------------------------------------
    #[test]
    fn get_image_returns_none_for_missing_empty_and_hostile_ids() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        assert!(info.get_image(&s("")).is_none());
        assert!(info.get_image(&s("   ")).is_none());
        assert!(info.get_image(&s("nope")).is_none());
        assert!(info.get_image(&s("\u{1F600}\u{0301}")).is_none());
        assert!(info.get_image(&s("\0")).is_none());
        assert!(info.get_image(&s(&"x".repeat(100_000))).is_none());
    }
    #[test]
    fn get_image_finds_an_inserted_id_and_is_exact_match() {
        let mut fx = Fixture::new();
        fx.images.add_css_image_id(
            s("logo"),
            ImageRef::null_image(2, 2, RawImageFormat::RGBA8, Vec::new()),
        );
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        assert!(info.get_image(&s("logo")).is_some(), "positive control");
        // lookup is exact: no trimming, no case folding, no prefix matching
        assert!(info.get_image(&s("Logo")).is_none());
        assert!(info.get_image(&s(" logo")).is_none());
        assert!(info.get_image(&s("logo ")).is_none());
        assert!(info.get_image(&s("log")).is_none());
        assert!(info.get_image(&s("logos")).is_none());
    }
    // ---- LayoutCallbackInfo::get_active_route / get_route_param -------------
    #[test]
    fn get_route_param_returns_none_when_no_route_is_active() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        assert!(info.get_active_route().is_none());
        // every hostile key must take the `?` early-out, never panic
        for key in ["", " ", "\t\n", "id", "\u{1F600}", "\0", "../../etc/passwd"] {
            assert!(info.get_route_param(key).is_none(), "key {key:?}");
        }
    }
    #[test]
    fn get_route_param_valid_minimal_and_unicode_positive_controls() {
        let fx = Fixture::with_route(user_route());
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        let route = info.get_active_route().expect("route was configured");
        assert_eq!(route.pattern.as_str(), "/user/:id");
        // positive control
        assert_eq!(info.get_route_param("id").map(AzString::as_str), Some("42"));
        // multibyte key round-trips
        assert_eq!(
            info.get_route_param("\u{1F600}").map(AzString::as_str),
            Some("emoji")
        );
    }
    #[test]
    fn get_route_param_rejects_malformed_keys_without_trimming_or_folding() {
        let fx = Fixture::with_route(user_route());
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        // empty / whitespace-only
        assert!(info.get_route_param("").is_none());
        assert!(info.get_route_param("   ").is_none());
        assert!(info.get_route_param("\t\n").is_none());
        // leading/trailing junk is NOT trimmed, and lookup is case-sensitive
        assert!(info.get_route_param(" id").is_none());
        assert!(info.get_route_param("id ").is_none());
        assert!(info.get_route_param("  id  ").is_none());
        assert!(info.get_route_param("id;garbage").is_none());
        assert!(info.get_route_param("ID").is_none());
        assert!(info.get_route_param("Id").is_none());
        // no prefix / substring matching
        assert!(info.get_route_param("i").is_none());
        assert!(info.get_route_param("idd").is_none());
        // garbage bytes, NUL, control chars
        assert!(info.get_route_param("\0").is_none());
        assert!(info.get_route_param("id\0").is_none());
        assert!(info.get_route_param("\u{7F}\u{1}\u{2}").is_none());
        // boundary numeric strings
        for key in [
            "0",
            "-0",
            "9223372036854775807",
            "-9223372036854775808",
            "18446744073709551616",
            "NaN",
            "inf",
            "-inf",
            "1e400",
            "0.0000000000000000001",
        ] {
            assert!(info.get_route_param(key).is_none(), "key {key:?}");
        }
        // non-ASCII that is *not* a param, incl. combining marks
        assert!(info.get_route_param("i\u{0301}d").is_none());
        assert!(info.get_route_param("\u{1F600}\u{1F600}").is_none());
    }
    #[test]
    fn get_route_param_handles_pathological_key_sizes_and_nesting() {
        let fx = Fixture::with_route(user_route());
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        // extremely long key: must return None quickly, not hang or overflow
        let huge = "x".repeat(1_000_000);
        assert!(info.get_route_param(&huge).is_none());
        // a long key that *shares a prefix* with a real param
        let long_id = alloc::format!("id{}", "0".repeat(1_000_000));
        assert!(info.get_route_param(&long_id).is_none());
        // deeply nested brackets: the lookup is a flat scan, so this must not
        // recurse or stack-overflow
        let nested = "[".repeat(10_000) + &"]".repeat(10_000);
        assert!(info.get_route_param(&nested).is_none());
    }
    #[test]
    fn get_route_param_preserves_huge_and_unicode_values() {
        let big = "v".repeat(200_000);
        let route = RouteMatch {
            pattern: s("/blob/:data"),
            params: StringPairVec::from_vec(Vec::from([AzStringPair {
                key: s("data"),
                value: s(&big),
            }])),
        };
        let fx = Fixture::with_route(route);
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
        let got = info.get_route_param("data").expect("param exists");
        assert_eq!(got.as_str().len(), 200_000);
    }
    // ---- LayoutCallbackInfo: responsive predicates ---------------------------
    #[test]
    fn window_predicates_obey_trichotomy_and_the_between_identity() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let probes = [
            0.0f32,
            -0.0,
            1.0,
            -1.0,
            640.0,
            f32::MIN,
            f32::MAX,
            f32::MIN_POSITIVE,
            f32::INFINITY,
            f32::NEG_INFINITY,
        ];
        for &dim in &probes {
            let info = LayoutCallbackInfo::new(&rd, win(dim, dim, 96), WindowTheme::LightMode);
            for &px in &probes {
                let lt = info.window_width_less_than(px);
                let gt = info.window_width_greater_than(px);
                let eq = info.get_window_width() == px;
                // exactly one of <, >, == holds for non-NaN operands
                assert_eq!(
                    u8::from(lt) + u8::from(gt) + u8::from(eq),
                    1,
                    "trichotomy broken for width {dim} vs {px}"
                );
                // height predicates mirror the width ones on a square window
                assert_eq!(info.window_height_less_than(px), lt);
                assert_eq!(info.window_height_greater_than(px), gt);
                for &px2 in &probes {
                    // between(a, b) == !(w < a) && !(w > b)
                    assert_eq!(
                        info.window_width_between(px, px2),
                        !info.window_width_less_than(px) && !info.window_width_greater_than(px2),
                        "between identity broken for width {dim} in [{px}, {px2}]"
                    );
                    assert_eq!(
                        info.window_height_between(px, px2),
                        info.window_width_between(px, px2)
                    );
                }
            }
        }
    }
    #[test]
    fn window_predicates_with_inverted_and_degenerate_ranges() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
        // inverted range is always empty
        assert!(!info.window_width_between(1000.0, 100.0));
        assert!(!info.window_height_between(1000.0, 100.0));
        // degenerate (min == max) range is inclusive on both ends
        assert!(info.window_width_between(640.0, 640.0));
        assert!(info.window_height_between(480.0, 480.0));
        assert!(!info.window_width_between(639.9, 639.95));
        // inclusive boundaries
        assert!(info.window_width_between(640.0, 1000.0));
        assert!(info.window_width_between(0.0, 640.0));
        // strictness at the exact boundary
        assert!(!info.window_width_less_than(640.0));
        assert!(!info.window_width_greater_than(640.0));
        assert!(info.window_width_less_than(640.001));
        assert!(info.window_width_greater_than(639.999));
        // the widest possible range contains a finite width
        assert!(info.window_width_between(f32::NEG_INFINITY, f32::INFINITY));
    }
    #[test]
    fn window_predicates_are_all_false_for_nan_probes() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
        // every comparison against NaN is false - no panic, no accidental `true`
        assert!(!info.window_width_less_than(f32::NAN));
        assert!(!info.window_width_greater_than(f32::NAN));
        assert!(!info.window_width_between(f32::NAN, f32::NAN));
        assert!(!info.window_width_between(f32::NAN, 10_000.0));
        assert!(!info.window_width_between(0.0, f32::NAN));
        assert!(!info.window_height_less_than(f32::NAN));
        assert!(!info.window_height_greater_than(f32::NAN));
        assert!(!info.window_height_between(f32::NAN, f32::NAN));
        assert!(!info.window_height_between(f32::NAN, 10_000.0));
        assert!(!info.window_height_between(0.0, f32::NAN));
    }
    #[test]
    fn window_predicates_are_all_false_for_a_nan_sized_window() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        let info = LayoutCallbackInfo::new(
            &rd,
            win(f32::NAN, f32::NAN, 96),
            WindowTheme::LightMode,
        );
        assert!(info.get_window_width().is_nan());
        assert!(info.get_window_height().is_nan());
        // a NaN window is neither smaller, larger, nor within any range
        for px in [0.0f32, 640.0, f32::MAX, f32::INFINITY, f32::NEG_INFINITY] {
            assert!(!info.window_width_less_than(px));
            assert!(!info.window_width_greater_than(px));
            assert!(!info.window_height_less_than(px));
            assert!(!info.window_height_greater_than(px));
            assert!(!info.window_width_between(f32::NEG_INFINITY, px));
            assert!(!info.window_height_between(px, f32::INFINITY));
        }
    }
    #[test]
    fn get_dpi_factor_at_zero_and_u32_limits() {
        let fx = Fixture::new();
        let rd = fx.ref_data();
        // 96 DPI is the 1.0 baseline
        let base = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 96), WindowTheme::LightMode);
        assert_eq!(base.get_dpi_factor(), 1.0);
        let hidpi = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 192), WindowTheme::LightMode);
        assert_eq!(hidpi.get_dpi_factor(), 2.0);
        // dpi = 0 must not divide-by-zero-panic; it yields 0.0
        let zero = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 0), WindowTheme::LightMode);
        assert_eq!(zero.get_dpi_factor(), 0.0);
        // u32::MAX must not overflow the f32 cast - it stays finite
        let max = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, u32::MAX), WindowTheme::LightMode);
        let f = max.get_dpi_factor();
        assert!(f.is_finite() && f > 0.0, "dpi factor {f} is not finite");
        assert_eq!(f, (u32::MAX as f32) / 96.0);
        // dpi = 1 rounds to a tiny-but-positive factor rather than 0
        let one = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 1), WindowTheme::LightMode);
        assert!(one.get_dpi_factor() > 0.0);
    }
    // ---- HidpiAdjustedBounds -------------------------------------------------
    #[test]
    fn hidpi_adjusted_bounds_from_bounds_holds_its_fields() {
        let b = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(800, 600),
            DpiScaleFactor::new(1.5),
        );
        assert_eq!(b.get_logical_size(), LogicalSize::new(800.0, 600.0));
        assert_eq!(b.get_hidpi_factor(), DpiScaleFactor::new(1.5));
        assert_eq!(b.logical_size, b.get_logical_size());
        assert_eq!(b.hidpi_factor, b.get_hidpi_factor());
        let p = b.get_physical_size();
        assert_eq!(p.width, 1200);
        assert_eq!(p.height, 900);
    }
    #[test]
    fn hidpi_adjusted_bounds_at_zero() {
        let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(0, 0), DpiScaleFactor::new(1.0));
        assert_eq!(b.get_logical_size(), LogicalSize::zero());
        let p = b.get_physical_size();
        assert_eq!(p.width, 0);
        assert_eq!(p.height, 0);
        // a zero scale factor collapses any size to 0x0 without panicking
        let z = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(1920, 1080),
            DpiScaleFactor::new(0.0),
        );
        let zp = z.get_physical_size();
        assert_eq!(zp.width, 0);
        assert_eq!(zp.height, 0);
    }
    /// `get_physical_size` funnels through `roundf(x) as u32`, which is a
    /// *saturating* float->int cast in Rust: negatives clamp to 0, huge values
    /// clamp to u32::MAX, NaN becomes 0. Pin that down so a future refactor to
    /// an unchecked cast (UB) or a panicking one is caught.
    #[test]
    fn hidpi_adjusted_bounds_physical_size_saturates_on_negative_input() {
        let b = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(-100, -50),
            DpiScaleFactor::new(1.0),
        );
        assert_eq!(b.get_logical_size(), LogicalSize::new(-100.0, -50.0));
        let p = b.get_physical_size();
        assert_eq!(p.width, 0, "negative logical width must clamp to 0, not wrap");
        assert_eq!(p.height, 0, "negative logical height must clamp to 0, not wrap");
        // negative scale factor on a positive size clamps the same way
        let neg_scale = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(100, 100),
            DpiScaleFactor::new(-2.0),
        );
        let np = neg_scale.get_physical_size();
        assert_eq!(np.width, 0);
        assert_eq!(np.height, 0);
    }
    #[test]
    fn hidpi_adjusted_bounds_physical_size_saturates_at_the_upper_limit() {
        // isize::MAX logical px * 1.0 overflows u32 -> must saturate, not wrap
        let b = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(isize::MAX, isize::MAX),
            DpiScaleFactor::new(1.0),
        );
        let p = b.get_physical_size();
        assert_eq!(p.width, u32::MAX);
        assert_eq!(p.height, u32::MAX);
        // isize::MIN saturates downwards to 0
        let min = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(isize::MIN, isize::MIN),
            DpiScaleFactor::new(1.0),
        );
        let mp = min.get_physical_size();
        assert_eq!(mp.width, 0);
        assert_eq!(mp.height, 0);
        // a huge scale factor on a modest size also saturates
        let huge_scale = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(1000, 1000),
            DpiScaleFactor::new(f32::MAX),
        );
        let hp = huge_scale.get_physical_size();
        assert_eq!(hp.width, u32::MAX);
        assert_eq!(hp.height, u32::MAX);
    }
    /// `DpiScaleFactor` stores its f32 in a fixed-point `isize` (x1000), so
    /// NaN quantizes to 0 and +/-inf quantize to the isize limits. Assert the
    /// *observable* consequence rather than a panic.
    #[test]
    fn hidpi_adjusted_bounds_physical_size_with_nan_and_infinite_scale() {
        let nan = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(100, 100),
            DpiScaleFactor::new(f32::NAN),
        );
        // NaN -> fixed-point 0 -> 0.0 scale -> 0x0 physical
        assert_eq!(nan.get_hidpi_factor().inner.get(), 0.0);
        let np = nan.get_physical_size();
        assert_eq!(np.width, 0);
        assert_eq!(np.height, 0);
        let inf = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(100, 100),
            DpiScaleFactor::new(f32::INFINITY),
        );
        // +inf -> saturated fixed-point -> huge (but finite) scale
        assert!(inf.get_hidpi_factor().inner.get().is_finite());
        let ip = inf.get_physical_size();
        assert_eq!(ip.width, u32::MAX);
        assert_eq!(ip.height, u32::MAX);
        let neg_inf = HidpiAdjustedBounds::from_bounds(
            LayoutSize::new(100, 100),
            DpiScaleFactor::new(f32::NEG_INFINITY),
        );
        let nip = neg_inf.get_physical_size();
        assert_eq!(nip.width, 0);
        assert_eq!(nip.height, 0);
    }
    #[test]
    fn hidpi_adjusted_bounds_physical_size_rounds_to_nearest() {
        // 0.5px rounds away from zero (libm::roundf), not truncates
        let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(3, 3), DpiScaleFactor::new(1.5));
        let p = b.get_physical_size();
        assert_eq!(p.width, 5, "3 * 1.5 = 4.5 must round to 5");
        assert_eq!(p.height, 5);
        // idempotent: repeated calls give the same answer
        let p2 = b.get_physical_size();
        assert_eq!(p.width, p2.width);
        assert_eq!(p.height, p2.height);
    }
    // ---- CoreCallbackDataVec -------------------------------------------------
    fn cb_data(cb: usize) -> CoreCallbackData {
        CoreCallbackData {
            event: EventFilter::Hover(HoverEventFilter::MouseOver),
            callback: CoreCallback::from(cb),
            refany: RefAny::new(cb),
        }
    }
    #[test]
    fn core_callback_data_vec_as_container_on_empty_vecs_does_not_panic() {
        // both the const-empty and the heap-empty representation must produce
        // a valid (length-0) container - a null-ptr slice here would be UB
        let empty = CoreCallbackDataVec::new();
        assert_eq!(empty.as_container().len(), 0);
        assert!(empty.as_container().internal.is_empty());
        let from_empty_vec = CoreCallbackDataVec::from_vec(Vec::new());
        assert_eq!(from_empty_vec.as_container().len(), 0);
        let mut mut_empty = CoreCallbackDataVec::from_vec(Vec::new());
        assert!(mut_empty.as_container_mut().internal.is_empty());
    }
    #[test]
    fn core_callback_data_vec_as_container_matches_the_backing_vec() {
        let v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2), cb_data(3)]));
        let c = v.as_container();
        assert_eq!(c.len(), 3);
        assert_eq!(c.len(), v.len());
        assert_eq!(c.internal[0].callback.cb, 1);
        assert_eq!(c.internal[2].callback.cb, 3);
        // the container borrows - it does not copy
        assert!(core::ptr::eq(c.internal.as_ptr(), v.as_slice().as_ptr()));
    }
    #[test]
    fn core_callback_data_vec_as_container_mut_writes_through() {
        let mut v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2)]));
        {
            let mut c = v.as_container_mut();
            assert_eq!(c.internal.len(), 2);
            c.internal[0].callback.cb = 99;
            c.internal[1].event = EventFilter::Hover(HoverEventFilter::MouseDown);
        }
        // mutations are visible through the immutable container
        let c = v.as_container();
        assert_eq!(c.internal[0].callback.cb, 99);
        assert_eq!(
            c.internal[1].event,
            EventFilter::Hover(HoverEventFilter::MouseDown)
        );
        assert_eq!(c.len(), 2);
    }
}
/// Tests for the recorded window-size queries — the responsive helpers
/// (`window_width_less_than` & co.) every `layout()` should branch on, and the
/// mechanism that lets a resize skip `layout()` when no recorded answer flips.
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod size_query_tests {
    use super::*;
    use crate::geom::LogicalSize;
5
    fn win(width: f32, height: f32) -> WindowSize {
5
        WindowSize {
5
            dimensions: LogicalSize::new(width, height),
5
            ..WindowSize::default()
5
        }
5
    }
5
    fn info_at(rd: &LayoutCallbackInfoRefData<'_>, w: f32, h: f32) -> LayoutCallbackInfo {
5
        LayoutCallbackInfo::new(rd, win(w, h), WindowTheme::LightMode)
5
    }
12
    fn drain() -> (alloc::vec::Vec<SizeQuery>, bool) {
12
        take_recorded_size_queries()
12
    }
    /// Build the minimal ref-data a `LayoutCallbackInfo` needs. The queries
    /// only read `window_size`, so everything else can be empty.
    struct Rd {
        image_cache: crate::resources::ImageCache,
        gl: crate::gl::OptionGlContextPtr,
        fonts: rust_fontconfig::FcFontCache,
        style: alloc::sync::Arc<azul_css::system::SystemStyle>,
    }
    impl Rd {
5
        fn new() -> Self {
5
            Self {
5
                image_cache: crate::resources::ImageCache::default(),
5
                gl: crate::gl::OptionGlContextPtr::None,
5
                fonts: rust_fontconfig::FcFontCache::default(),
5
                style: alloc::sync::Arc::new(azul_css::system::SystemStyle::default()),
5
            }
5
        }
5
        fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
5
            LayoutCallbackInfoRefData {
5
                image_cache: &self.image_cache,
5
                gl_context: &self.gl,
5
                system_fonts: &self.fonts,
5
                system_style: self.style.clone(),
5
                active_route: None,
5
                monitors: crate::window::MonitorVec::from_const_slice(&[]),
5
            }
5
        }
    }
    #[test]
1
    fn every_responsive_helper_records_with_its_exact_operator() {
1
        let rd = Rd::new();
1
        let rd = rd.ref_data();
1
        let _ = drain();
1
        let info = info_at(&rd, 800.0, 600.0);
1
        assert!(!info.window_width_less_than(800.0), "strict <: boundary is false");
1
        assert!(!info.window_width_greater_than(800.0), "strict >: boundary is false");
1
        assert!(info.window_width_between(800.0, 1024.0), "between is inclusive");
1
        assert!(info.window_height_less_than(601.0));
1
        assert!(!info.window_height_greater_than(600.0));
1
        assert!(info.window_height_between(0.0, 600.0));
1
        let (recorded, overflowed) = drain();
        // between records BOTH of its bounds, so 4 single-bound calls + 2
        // between calls = 8 queries.
1
        assert_eq!(recorded.len(), 8, "every call recorded; between records two bounds");
1
        assert!(!overflowed);
1
        assert_eq!(recorded[0].op, SizeQueryOp::LessThan);
1
        assert_eq!(recorded[1].op, SizeQueryOp::GreaterThan);
1
        assert_eq!(recorded[2].op, SizeQueryOp::GreaterOrEqual);
1
        assert_eq!(recorded[3].op, SizeQueryOp::LessOrEqual);
1
    }
    #[test]
1
    fn flips_at_detects_exactly_the_crossings() {
1
        let rd = Rd::new();
1
        let rd = rd.ref_data();
1
        let _ = drain();
1
        let info = info_at(&rd, 800.0, 600.0);
1
        let mobile = info.window_width_less_than(640.0); // false at 800
1
        assert!(!mobile);
1
        let (recorded, _) = drain();
1
        let q = recorded[0];
        // Shrinking within the desktop range does not flip…
1
        assert!(!q.flips_at(LogicalSize::new(700.0, 600.0)));
1
        assert!(!q.flips_at(LogicalSize::new(640.0, 600.0)), "strict <: 640 is still false");
        // …crossing the queried threshold does…
1
        assert!(q.flips_at(LogicalSize::new(639.9, 600.0)));
1
        assert!(q.flips_at(LogicalSize::new(320.0, 600.0)));
        // …and the other axis is irrelevant to a width query.
1
        assert!(!q.flips_at(LogicalSize::new(700.0, 10.0)));
1
    }
    /// `between` must flip on BOTH of its bounds, inclusively — the reason
    /// [`SizeQueryOp`] has four exact operators instead of a bool.
    #[test]
1
    fn between_flips_on_either_bound_with_inclusive_semantics() {
1
        let rd = Rd::new();
1
        let rd = rd.ref_data();
1
        let _ = drain();
1
        let info = info_at(&rd, 800.0, 600.0);
1
        assert!(info.window_width_between(768.0, 1024.0));
1
        let (recorded, _) = drain();
1
        let lower = recorded[0];
1
        let upper = recorded[1];
1
        assert!(!lower.flips_at(LogicalSize::new(768.0, 600.0)), ">= 768: boundary holds");
1
        assert!(lower.flips_at(LogicalSize::new(767.9, 600.0)));
1
        assert!(!upper.flips_at(LogicalSize::new(1024.0, 600.0)), "<= 1024: boundary holds");
1
        assert!(upper.flips_at(LogicalSize::new(1024.1, 600.0)));
1
    }
    #[test]
1
    fn drain_resets_the_recording() {
1
        let rd = Rd::new();
1
        let rd = rd.ref_data();
1
        let _ = drain();
1
        let info = info_at(&rd, 1024.0, 768.0);
1
        let _ = info.window_width_greater_than(640.0);
1
        let (first, _) = drain();
1
        assert_eq!(first.len(), 1);
1
        let (second, overflowed) = drain();
1
        assert!(second.is_empty(), "drain must reset");
1
        assert!(!overflowed);
1
    }
    #[test]
1
    fn overflow_latches_and_reports_rather_than_dropping_silently() {
1
        let rd = Rd::new();
1
        let rd = rd.ref_data();
1
        let _ = drain();
1
        let info = info_at(&rd, 1024.0, 768.0);
266
        for i in 0..(size_query_recorder::SIZE_QUERY_CAP + 10) {
266
            let _ = info.window_width_greater_than(i as f32);
266
        }
1
        let (recorded, overflowed) = drain();
1
        assert_eq!(recorded.len(), size_query_recorder::SIZE_QUERY_CAP);
1
        assert!(
1
            overflowed,
            "past the cap the drain MUST say the list is incomplete — silence \
             here is a resize skipping a layout() that would have branched"
        );
        // And the latch itself resets with the drain.
1
        let (_, overflowed2) = drain();
1
        assert!(!overflowed2);
1
    }
}