Skip to main content

azul_core/
callbacks.rs

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"))]
24use alloc::string::ToString;
25use alloc::{alloc::Layout, boxed::Box, collections::BTreeMap, sync::Arc, vec::Vec};
26use core::{
27    ffi::c_void,
28    fmt,
29    sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
30};
31#[cfg(feature = "std")]
32use std::hash::Hash;
33
34use 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};
45use rust_fontconfig::{FcFontCache, OwnedFontSource};
46
47use 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)]
80pub 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
90impl Update {
91    pub fn max_self(&mut self, other: Self) {
92        if (*self == Self::DoNothing && other != Self::DoNothing)
93            || (*self == Self::RefreshDom && other == Self::RefreshDomAllWindows)
94        {
95            *self = other;
96        }
97    }
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`).
116pub type LayoutCallbackType = extern "C" fn(RefAny, LayoutCallbackInfo) -> Dom;
117
118extern "C" fn default_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
119    Dom::create_body()
120}
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)]
131pub 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
138impl_callback!(LayoutCallback, LayoutCallbackType);
139
140impl LayoutCallback {
141    pub fn create<I: Into<Self>>(cb: I) -> Self {
142        cb.into()
143    }
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.
152crate::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
164impl Default for LayoutCallback {
165    fn default() -> Self {
166        Self {
167            cb: default_layout_callback,
168            ctx: OptionRefAny::None,
169        }
170    }
171}
172
173// -- virtualized view callback
174
175pub 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)]
180pub 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}
186impl_callback!(VirtualViewCallback, VirtualViewCallbackType);
187
188// Host-invoker plumbing for VirtualViewCallback. See `crate::host_invoker`.
189crate::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
201impl VirtualViewCallback {
202    pub fn create(cb: VirtualViewCallbackType) -> Self {
203        Self {
204            cb,
205            ctx: OptionRefAny::None,
206        }
207    }
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)]
223pub 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`.
235pub type CaretTweenCallbackType = extern "C" fn(RefAny, CaretTweenInfo) -> LogicalRect;
236
237/// User-settable caret tween interpolator (see [`CaretTweenInfo`]).
238#[repr(C)]
239pub 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}
245impl_callback!(CaretTweenCallback, CaretTweenCallbackType);
246
247impl CaretTweenCallback {
248    pub fn create(cb: CaretTweenCallbackType) -> Self {
249        Self {
250            cb,
251            ctx: OptionRefAny::None,
252        }
253    }
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)]
263pub 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.
275pub type SelectionTweenCallbackType =
276    extern "C" fn(RefAny, SelectionTweenInfo) -> LogicalRectVec;
277
278/// User-settable selection tween interpolator (see [`SelectionTweenInfo`]).
279#[repr(C)]
280pub 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}
286impl_callback!(SelectionTweenCallback, SelectionTweenCallbackType);
287
288impl SelectionTweenCallback {
289    pub fn create(cb: SelectionTweenCallbackType) -> Self {
290        Self {
291            cb,
292            ctx: OptionRefAny::None,
293        }
294    }
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]
306fn 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    let t = t.clamp(0.0, 1.0);
311    if t < RAMP {
312        V * t * t / (2.0 * RAMP)
313    } else if t <= 1.0 - RAMP {
314        V * (RAMP / 2.0 + (t - RAMP))
315    } else {
316        let inv = 1.0 - t;
317        1.0 - V * inv * inv / (2.0 * RAMP)
318    }
319}
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)]
326fn lerp_rect(from: LogicalRect, to: LogicalRect, e: f32) -> LogicalRect {
327    LogicalRect {
328        origin: LogicalPosition {
329            x: from.origin.x + (to.origin.x - from.origin.x) * e,
330            y: from.origin.y + (to.origin.y - from.origin.y) * e,
331        },
332        size: LogicalSize {
333            width: from.size.width + (to.size.width - from.size.width) * e,
334            height: from.size.height + (to.size.height - from.size.height) * e,
335        },
336    }
337}
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]
342pub extern "C" fn default_caret_tween(_data: RefAny, info: CaretTweenInfo) -> LogicalRect {
343    lerp_rect(info.past, info.current, trapezoid_ease(info.t))
344}
345
346/// Default selection tween: trapezoidal-velocity lerp, rectangles paired by
347/// index. Rectangles with no past counterpart (selection grew) appear at
348/// their final geometry immediately.
349#[must_use]
350pub extern "C" fn default_selection_tween(
351    _data: RefAny,
352    info: SelectionTweenInfo,
353) -> LogicalRectVec {
354    let e = trapezoid_ease(info.t);
355    let past = info.past.as_ref();
356    let out: Vec<LogicalRect> = info
357        .current
358        .as_ref()
359        .iter()
360        .enumerate()
361        .map(|(i, cur)| past.get(i).map_or(*cur, |p| lerp_rect(*p, *cur, e)))
362        .collect();
363    out.into()
364}
365
366/// Reason why a `VirtualView` callback is being invoked.
367///
368/// This helps the callback optimize its behavior based on why it's being called.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370#[repr(C, u8)]
371pub enum VirtualViewCallbackReason {
372    /// Initial render - first time the `VirtualView` appears
373    InitialRender,
374    /// Parent DOM was recreated (cache invalidated)
375    DomRecreated,
376    /// Window/VirtualView bounds expanded beyond current `scroll_size`
377    BoundsExpanded,
378    /// Scroll position is near an edge (within `EDGE_THRESHOLD`, currently 200px)
379    EdgeScrolled(EdgeType),
380    /// Scroll position extends beyond current `scroll_size`
381    ScrollBeyondContent,
382}
383
384/// Which edge triggered a scroll-based re-invocation
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386#[repr(C)]
387pub enum EdgeType {
388    Top,
389    Bottom,
390    Left,
391    Right,
392}
393
394#[derive(Debug)]
395#[repr(C)]
396pub struct VirtualViewCallbackInfo {
397    pub reason: VirtualViewCallbackReason,
398    pub system_fonts: *const FcFontCache,
399    pub image_cache: *const ImageCache,
400    pub window_theme: WindowTheme,
401    pub bounds: HidpiAdjustedBounds,
402    pub scroll_size: LogicalSize,
403    pub scroll_offset: LogicalPosition,
404    pub virtual_scroll_size: LogicalSize,
405    pub virtual_scroll_offset: LogicalPosition,
406    /// Pointer to the callable (`OptionRefAny`) for FFI language bindings (Python, etc.)
407    /// Set by the caller before invoking the callback. Native Rust callbacks have this as null.
408    callable_ptr: *const OptionRefAny,
409    /// Headless DOM measurement hook (see [`Self::measure_dom`]): a
410    /// layout-crate trampoline (a [`MeasureDomFn`] stored as an opaque
411    /// pointer, null = no hook) + its `LayoutWindow` context, injected at
412    /// invoke time. Null on paths that cannot measure (then `measure_dom`
413    /// returns zero).
414    measure_dom_fn: *const c_void,
415    measure_dom_ctx: *mut c_void,
416    /// Extension for future ABI stability (mutable data)
417    _abi_mut: *mut c_void,
418}
419
420/// Trampoline signature for [`VirtualViewCallbackInfo::measure_dom`]:
421/// `(layout_window_ctx, dom, available) -> content extent`. The `Dom` is
422/// passed by pointer and CONSUMED (moved out) by the trampoline.
423pub type MeasureDomFn = extern "C" fn(*mut c_void, *mut Dom, LogicalSize) -> LogicalSize;
424
425impl Clone for VirtualViewCallbackInfo {
426    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
427    fn clone(&self) -> Self {
428        Self {
429            reason: self.reason,
430            system_fonts: self.system_fonts,
431            image_cache: self.image_cache,
432            window_theme: self.window_theme,
433            bounds: self.bounds,
434            scroll_size: self.scroll_size,
435            scroll_offset: self.scroll_offset,
436            virtual_scroll_size: self.virtual_scroll_size,
437            virtual_scroll_offset: self.virtual_scroll_offset,
438            callable_ptr: self.callable_ptr,
439            measure_dom_fn: self.measure_dom_fn,
440            measure_dom_ctx: self.measure_dom_ctx,
441            _abi_mut: self._abi_mut,
442        }
443    }
444}
445
446impl VirtualViewCallbackInfo {
447    #[must_use] pub const fn new<'a>(
448        reason: VirtualViewCallbackReason,
449        system_fonts: &'a FcFontCache,
450        image_cache: &'a ImageCache,
451        window_theme: WindowTheme,
452        bounds: HidpiAdjustedBounds,
453        scroll_size: LogicalSize,
454        scroll_offset: LogicalPosition,
455        virtual_scroll_size: LogicalSize,
456        virtual_scroll_offset: LogicalPosition,
457    ) -> Self {
458        Self {
459            reason,
460            system_fonts: core::ptr::from_ref::<FcFontCache>(system_fonts),
461            image_cache: core::ptr::from_ref::<ImageCache>(image_cache),
462            window_theme,
463            bounds,
464            scroll_size,
465            scroll_offset,
466            virtual_scroll_size,
467            virtual_scroll_offset,
468            callable_ptr: core::ptr::null(),
469            measure_dom_fn: core::ptr::null(),
470            measure_dom_ctx: core::ptr::null_mut(),
471            _abi_mut: core::ptr::null_mut(),
472        }
473    }
474
475    /// Set the callable pointer for FFI language bindings
476    pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
477        self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
478    }
479
480    /// Inject the headless-measure trampoline (called by the layout crate
481    /// right before the user callback is invoked).
482    pub fn set_measure_dom_fn(&mut self, f: MeasureDomFn, ctx: *mut c_void) {
483        self.measure_dom_fn = f as *const c_void;
484        self.measure_dom_ctx = ctx;
485    }
486
487    /// Measure a DOM headlessly: style + lay it out against `available`
488    /// constraints using the host window's fonts and system style, without
489    /// touching the live layout. Returns the union of all node bounds.
490    ///
491    /// Use a very tall `available.height` (e.g. `1_000_000.0`) to obtain a
492    /// DOM's natural height at a fixed width - the building block for
493    /// virtual-scroll sizing: measure one (or a few) item template(s), then
494    /// `virtual_scroll_size.height = item_height * item_count` and render
495    /// only the visible window of items. Each call is a full cold layout
496    /// pass, so cache measured sizes per item template.
497    ///
498    /// Returns `LogicalSize::zero()` when no measure hook was injected.
499    #[must_use] pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
500        if self.measure_dom_fn.is_null() {
501            return LogicalSize::zero();
502        }
503        // SAFETY: measure_dom_fn is only ever set via set_measure_dom_fn,
504        // which stores a valid MeasureDomFn.
505        let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
506        let mut dom = core::mem::ManuallyDrop::new(dom);
507        f(self.measure_dom_ctx, core::ptr::from_mut::<Dom>(&mut dom), available)
508    }
509
510    /// Get the callable for FFI language bindings (Python, etc.)
511    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
512        if self.callable_ptr.is_null() {
513            OptionRefAny::None
514        } else {
515            unsafe { (*self.callable_ptr).clone() }
516        }
517    }
518
519    #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
520        self.bounds
521    }
522
523    const fn internal_get_system_fonts(&self) -> &FcFontCache {
524        unsafe { &*self.system_fonts }
525    }
526    const fn internal_get_image_cache(&self) -> &ImageCache {
527        unsafe { &*self.image_cache }
528    }
529}
530
531/// Return value for a `VirtualView` rendering callback.
532///
533/// Contains two size/offset pairs for lazy loading and virtualization:
534///
535/// - `scroll_size` / `scroll_offset`: Size and position of actually rendered content
536/// - `virtual_scroll_size` / `virtual_scroll_offset`: Size for scrollbar representation
537///
538/// The callback is re-invoked on: initial render, parent DOM recreation, window expansion
539/// beyond `scroll_size`, or scrolling near content edges (`EDGE_THRESHOLD`, currently 200px).
540///
541/// Return `OptionDom::None` to keep the current DOM and only update scroll bounds.
542#[derive(Debug, Clone, PartialEq, Eq)]
543#[repr(C)]
544pub struct VirtualViewReturn {
545    /// The DOM with actual rendered content, or None to keep current DOM.
546    ///
547    /// - `OptionDom::Some(dom)` - Replace current content with this new DOM
548    /// - `OptionDom::None` - Keep using the previous DOM, only update scroll bounds
549    ///
550    /// Returning `None` is an optimization when the callback determines that the
551    /// current content is sufficient (e.g., already rendered ahead of scroll position).
552    pub dom: OptionDom,
553
554    /// Size of the actual rendered content rectangle.
555    ///
556    /// This is the size of the content in the `dom` field (if Some). It may be smaller than
557    /// `virtual_scroll_size` if only a subset of content is rendered (virtualization).
558    ///
559    /// **Example**: For a table showing rows 10-30, this might be 600px tall
560    /// (20 rows x 30px each).
561    pub scroll_size: LogicalSize,
562
563    /// Offset of the actual rendered content within the virtual coordinate space.
564    ///
565    /// This positions the rendered content within the larger virtual space. For
566    /// virtualized content, this will be non-zero to indicate where the rendered
567    /// "window" starts.
568    ///
569    /// **Example**: For a table showing rows 10-30, this might be y=300
570    /// (row 10 starts 300px from the top).
571    pub scroll_offset: LogicalPosition,
572
573    /// Size of the virtual content rectangle (for scrollbar sizing).
574    ///
575    /// This is the size the scrollbar will represent. It can be much larger than
576    /// `scroll_size` to enable lazy loading and virtualization.
577    ///
578    /// **Example**: For a 1000-row table, this might be 30,000px tall
579    /// (1000 rows x 30px each), even though only 20 rows are actually rendered.
580    pub virtual_scroll_size: LogicalSize,
581
582    /// Offset of the virtual content (usually zero).
583    ///
584    /// This is typically `(0, 0)` since the virtual space usually starts at the origin.
585    /// Advanced use cases might use this for complex virtualization scenarios.
586    pub virtual_scroll_offset: LogicalPosition,
587}
588
589impl Default for VirtualViewReturn {
590    fn default() -> Self {
591        Self {
592            dom: OptionDom::None,
593            scroll_size: LogicalSize::zero(),
594            scroll_offset: LogicalPosition::zero(),
595            virtual_scroll_size: LogicalSize::zero(),
596            virtual_scroll_offset: LogicalPosition::zero(),
597        }
598    }
599}
600
601impl VirtualViewReturn {
602    /// Creates a new `VirtualViewReturn` with updated DOM content.
603    ///
604    /// Use this when the callback has rendered new content to display.
605    ///
606    /// # Arguments
607    /// - `dom` - The new DOM to render
608    /// - `scroll_size` - Size of the actual rendered content
609    /// - `scroll_offset` - Position of rendered content in virtual space
610    /// - `virtual_scroll_size` - Size for scrollbar representation
611    /// - `virtual_scroll_offset` - Usually `LogicalPosition::zero()`
612    #[must_use] pub const fn with_dom(
613        dom: Dom,
614        scroll_size: LogicalSize,
615        scroll_offset: LogicalPosition,
616        virtual_scroll_size: LogicalSize,
617        virtual_scroll_offset: LogicalPosition,
618    ) -> Self {
619        Self {
620            dom: OptionDom::Some(dom),
621            scroll_size,
622            scroll_offset,
623            virtual_scroll_size,
624            virtual_scroll_offset,
625        }
626    }
627
628    /// Creates a return value that keeps the current DOM unchanged.
629    ///
630    /// Use this when the callback determines that the existing content
631    /// is sufficient (e.g., already rendered ahead of scroll position).
632    /// This is an optimization to avoid rebuilding the DOM unnecessarily.
633    ///
634    /// # Arguments
635    /// - `scroll_size` - Size of the current rendered content
636    /// - `scroll_offset` - Position of current content in virtual space
637    /// - `virtual_scroll_size` - Size for scrollbar representation
638    /// - `virtual_scroll_offset` - Usually `LogicalPosition::zero()`
639    #[must_use] pub const fn keep_current(
640        scroll_size: LogicalSize,
641        scroll_offset: LogicalPosition,
642        virtual_scroll_size: LogicalSize,
643        virtual_scroll_offset: LogicalPosition,
644    ) -> Self {
645        Self {
646            dom: OptionDom::None,
647            scroll_size,
648            scroll_offset,
649            virtual_scroll_size,
650            virtual_scroll_offset,
651        }
652    }
653
654}
655
656// --  thread callback
657
658// -- timer callback
659
660#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
661#[repr(C)]
662pub struct TimerCallbackReturn {
663    pub should_update: Update,
664    pub should_terminate: TerminateTimer,
665}
666
667impl TimerCallbackReturn {
668    /// Creates a new `TimerCallbackReturn` with the given update and terminate flags.
669    #[must_use] pub const fn create(should_update: Update, should_terminate: TerminateTimer) -> Self {
670        Self {
671            should_update,
672            should_terminate,
673        }
674    }
675
676    /// Timer continues running, no DOM update needed.
677    #[must_use] pub const fn continue_unchanged() -> Self {
678        Self {
679            should_update: Update::DoNothing,
680            should_terminate: TerminateTimer::Continue,
681        }
682    }
683
684    /// Timer continues running and DOM should be refreshed.
685    #[must_use] pub const fn continue_and_refresh_dom() -> Self {
686        Self {
687            should_update: Update::RefreshDom,
688            should_terminate: TerminateTimer::Continue,
689        }
690    }
691
692    /// Timer should stop, no DOM update needed.
693    #[must_use] pub const fn terminate_unchanged() -> Self {
694        Self {
695            should_update: Update::DoNothing,
696            should_terminate: TerminateTimer::Terminate,
697        }
698    }
699
700    /// Timer should stop and DOM should be refreshed.
701    #[must_use] pub const fn terminate_and_refresh_dom() -> Self {
702        Self {
703            should_update: Update::RefreshDom,
704            should_terminate: TerminateTimer::Terminate,
705        }
706    }
707}
708
709impl Default for TimerCallbackReturn {
710    fn default() -> Self {
711        Self::continue_unchanged()
712    }
713}
714
715/// Gives the `layout()` function access to the `RendererResources` and the `Window`
716/// (for querying images and fonts, as well as width / height)
717///
718#[derive(Debug)]
719#[repr(C)]
720/// Reference data container for `LayoutCallbackInfo` (all read-only fields)
721///
722/// This struct consolidates all readonly references that layout callbacks need to query state.
723/// By grouping these into a single struct, we reduce the number of parameters to
724/// `LayoutCallbackInfo::new()` from 6 to 2, making the API more maintainable and easier to extend.
725///
726/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
727/// reference.
728pub struct LayoutCallbackInfoRefData<'a> {
729    /// Allows the `layout()` function to reference image IDs
730    pub image_cache: &'a ImageCache,
731    /// OpenGL context so that the `layout()` function can render textures
732    pub gl_context: &'a OptionGlContextPtr,
733    /// Reference to the system font cache
734    pub system_fonts: &'a FcFontCache,
735    /// Platform-specific system style (colors, spacing, etc.)
736    /// Used for CSD rendering and menu windows.
737    pub system_style: Arc<SystemStyle>,
738    /// Active route match (if routing is configured).
739    /// Contains the matched pattern and extracted parameters.
740    pub active_route: Option<&'a crate::resources::RouteMatch>,
741    /// #28 (d): SNAPSHOT of the system's monitors, taken (locked + cloned)
742    /// by the caller right before invoking the layout callback. A snapshot —
743    /// not the live `Arc<Mutex<…>>` handle — because `azul-core` is `no_std`
744    /// (no Mutex) and the list is read-only during a layout pass anyway.
745    /// Lets `layout()` bound how much content it builds on first layout
746    /// (e.g. at most monitor-height lines / monitor-area characters), so
747    /// opening a huge file can never build an unbounded DOM.
748    pub monitors: crate::window::MonitorVec,
749}
750
751/// What triggered the current `layout()` invocation.
752///
753/// The framework re-invokes the layout callback for any change that may
754/// produce a structurally different DOM (resize across a CSS breakpoint,
755/// theme toggle, route switch, callback returning `Update::RefreshDom`).
756/// `LayoutCallbackInfo::relayout_reason()` exposes which trigger this
757/// particular call corresponds to so the callback can branch - for
758/// example, skip expensive analytics on `Resize` calls.
759#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
760#[repr(C)]
761#[derive(Default)]
762pub enum RelayoutReason {
763    /// First layout call for this window.
764    #[default]
765    Initial,
766    /// A user callback returned `Update::RefreshDom`.
767    RefreshDom,
768    /// Window size changed across a CSS breakpoint or DPI scale change.
769    /// The callback can branch on `info.window_width_*` to emit a
770    /// different tree (e.g. hamburger menu vs sidebar).
771    Resize,
772    /// System theme changed (light/dark).
773    ThemeChange,
774    /// `CallbackInfo::switch_route` or `set_route_param` produced a new
775    /// route match. The callback should branch on
776    /// `info.get_active_route()`.
777    RouteChange,
778    /// Catch-all for relayouts that don't fit one of the above categories.
779    Other,
780}
781
782
783#[repr(C)]
784pub struct LayoutCallbackInfo {
785    /// Single reference to all readonly reference data
786    /// This consolidates 4 individual parameters into 1, improving API ergonomics
787    ref_data: *const LayoutCallbackInfoRefData<'static>,
788    /// Window size (so that apps can return a different UI depending on
789    /// the window size - mobile / desktop view). Should be later removed
790    /// in favor of "resize" handlers and @media queries.
791    pub window_size: WindowSize,
792    /// Registers whether the UI is dependent on the window theme
793    pub theme: WindowTheme,
794    /// What triggered this `layout()` call. Read via `relayout_reason()`.
795    pub relayout_reason: RelayoutReason,
796    /// Pointer to the callable (`OptionRefAny`) for FFI language bindings (Python, etc.)
797    /// Set by the caller before invoking the callback. Native Rust callbacks have this as null.
798    callable_ptr: *const OptionRefAny,
799    /// Extension for future ABI stability (mutable data)
800    _abi_mut: *mut c_void,
801}
802
803/// One recorded window-size query made by a `layout()` callback.
804///
805/// See [`LayoutCallbackInfo::window_width_less_than`] & co. The engine replays
806/// these against a
807/// prospective new size to decide whether a resize could change the DOM at
808/// all: if no recorded answer flips (and no CSS breakpoint is crossed), the
809/// callback is provably size-stable across that resize and is not re-invoked.
810#[repr(C)]
811#[derive(Debug, Clone, Copy, PartialEq)]
812pub struct SizeQuery {
813    pub axis: SizeQueryAxis,
814    pub op: SizeQueryOp,
815    pub threshold_px: f32,
816    /// The answer given at recording time, evaluated against the size the
817    /// callback actually saw.
818    pub answer: bool,
819}
820
821/// Which window dimension a [`SizeQuery`] tested.
822#[repr(C)]
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
824pub enum SizeQueryAxis {
825    Width,
826    Height,
827}
828
829/// The comparison a [`SizeQuery`] performed.
830///
831/// Four variants rather than a greater/smaller bool because the recorded
832/// operator must REPLAY EXACTLY:
833/// `window_width_less_than` is a strict `<` while `window_width_between`'s
834/// lower bound is `>=`, and collapsing either onto the other misjudges a
835/// resize landing precisely on the queried boundary — the one pixel the app
836/// explicitly said it cares about.
837#[repr(C)]
838#[derive(Debug, Clone, Copy, PartialEq, Eq)]
839pub enum SizeQueryOp {
840    /// `dim < threshold` (`window_width_less_than` / `window_height_less_than`)
841    LessThan,
842    /// `dim > threshold` (`window_width_greater_than` / `window_height_greater_than`)
843    GreaterThan,
844    /// `dim >= threshold` (the lower bound of `window_*_between`)
845    GreaterOrEqual,
846    /// `dim <= threshold` (the upper bound of `window_*_between`)
847    LessOrEqual,
848}
849
850impl SizeQuery {
851    /// What this query would answer at `size` — compare with [`Self::answer`]
852    /// to detect a flip. MUST mirror the operators of the recording methods
853    /// exactly (see [`SizeQueryOp`]), or the engine would skip a `layout()`
854    /// re-invocation right at the boundary the app asked about.
855    #[must_use] pub fn answer_at(&self, size: LogicalSize) -> bool {
856        let dim = match self.axis {
857            SizeQueryAxis::Width => size.width,
858            SizeQueryAxis::Height => size.height,
859        };
860        match self.op {
861            SizeQueryOp::LessThan => dim < self.threshold_px,
862            SizeQueryOp::GreaterThan => dim > self.threshold_px,
863            SizeQueryOp::GreaterOrEqual => dim >= self.threshold_px,
864            SizeQueryOp::LessOrEqual => dim <= self.threshold_px,
865        }
866    }
867
868    /// Would this query's answer differ at `size` from the recorded one?
869    #[must_use] pub fn flips_at(&self, size: LogicalSize) -> bool {
870        self.answer_at(size) != self.answer
871    }
872}
873
874/// Thread-local recorder backing the responsive helpers
875/// (`LayoutCallbackInfo::window_width_less_than` & co.).
876///
877/// A thread-local (rather than a field on the FFI-frozen `LayoutCallbackInfo`)
878/// works because the layout callback is invoked SYNCHRONOUSLY on the calling
879/// thread: the engine drains the recording immediately after the callback
880/// returns, on the same thread that made the queries.
881///
882/// Bounded, and the overflow direction matters: SILENTLY dropping queries
883/// would drop exactly the flips the engine needs to see — the UNSAFE
884/// direction, a resize skipping a `layout()` that would have branched. So the
885/// cap does not drop; it latches an `overflowed` flag that the drain reports,
886/// and the engine then treats the callback as size-dependent EVERYWHERE
887/// (every resize re-invokes it — today's behaviour, merely un-optimized).
888#[cfg(feature = "std")]
889mod size_query_recorder {
890    use super::SizeQuery;
891
892    /// More distinct thresholds than any real breakpoint scheme uses; a
893    /// callback exceeding this is generating them programmatically.
894    pub(super) const SIZE_QUERY_CAP: usize = 256;
895
896    std::thread_local! {
897        static RECORDED: core::cell::RefCell<(Vec<SizeQuery>, bool)> =
898            const { core::cell::RefCell::new((Vec::new(), false)) };
899    }
900
901    pub(super) fn record(q: SizeQuery) {
902        RECORDED.with(|r| {
903            let mut r = r.borrow_mut();
904            if r.0.len() >= SIZE_QUERY_CAP {
905                r.1 = true; // overflowed: the drain must report "unbounded"
906            } else {
907                r.0.push(q);
908            }
909        });
910    }
911
912    /// Drain the recording. Returns `(queries, overflowed)`; `overflowed`
913    /// means the cap was hit and the list is INCOMPLETE — treat every resize
914    /// as potentially DOM-changing.
915    pub(super) fn take() -> (Vec<SizeQuery>, bool) {
916        RECORDED.with(|r| {
917            let mut r = r.borrow_mut();
918            let overflowed = r.1;
919            r.1 = false;
920            (core::mem::take(&mut r.0), overflowed)
921        })
922    }
923}
924
925#[cfg(feature = "std")]
926fn record_size_query(q: SizeQuery) {
927    size_query_recorder::record(q);
928}
929
930/// Without `std` there is no thread-local to record into; the queries still
931/// ANSWER correctly, the engine just cannot prove size-stability and falls
932/// back to re-invoking `layout()` on breakpoint-relevant resizes (web builds
933/// are out of scope for the resize fast path).
934#[cfg(not(feature = "std"))]
935fn record_size_query(_q: SizeQuery) {}
936
937/// Drain the size queries recorded since the last drain on THIS thread.
938///
939/// Call immediately after a `layout()` callback returns, on the same thread.
940/// `(queries, overflowed)` — on `overflowed == true` the list is incomplete
941/// and the caller must treat the callback as size-dependent everywhere.
942#[cfg(feature = "std")]
943#[must_use] pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
944    size_query_recorder::take()
945}
946
947#[cfg(not(feature = "std"))]
948#[must_use] pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
949    (alloc::vec::Vec::new(), false)
950}
951
952impl Clone for LayoutCallbackInfo {
953    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
954    fn clone(&self) -> Self {
955        Self {
956            ref_data: self.ref_data,
957            window_size: self.window_size,
958            theme: self.theme,
959            relayout_reason: self.relayout_reason,
960            callable_ptr: self.callable_ptr,
961            _abi_mut: self._abi_mut,
962        }
963    }
964}
965
966impl core::fmt::Debug for LayoutCallbackInfo {
967    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
968        f.debug_struct("LayoutCallbackInfo")
969            .field("window_size", &self.window_size)
970            .field("theme", &self.theme)
971            .field("relayout_reason", &self.relayout_reason)
972            .finish_non_exhaustive()
973    }
974}
975
976impl LayoutCallbackInfo {
977    #[must_use] pub const fn new<'a>(
978        ref_data: &'a LayoutCallbackInfoRefData<'a>,
979        window_size: WindowSize,
980        theme: WindowTheme,
981    ) -> Self {
982        Self::new_with_reason(ref_data, window_size, theme, RelayoutReason::Initial)
983    }
984
985    // the `as *const ...<'static>` is a deliberate 'a -> 'static lifetime launder
986    // on the raw pointer (see SAFETY note below), not a redundant cast.
987    #[allow(clippy::unnecessary_cast)]
988    #[must_use] pub const fn new_with_reason<'a>(
989        ref_data: &'a LayoutCallbackInfoRefData<'a>,
990        window_size: WindowSize,
991        theme: WindowTheme,
992        relayout_reason: RelayoutReason,
993    ) -> Self {
994        Self {
995            // SAFETY: We cast away the lifetime 'a to 'static because LayoutCallbackInfo
996            // only lives for the duration of the callback, which is shorter than 'a
997            ref_data: core::ptr::from_ref::<LayoutCallbackInfoRefData<'a>>(ref_data)
998                as *const LayoutCallbackInfoRefData<'static>,
999            window_size,
1000            theme,
1001            relayout_reason,
1002            callable_ptr: core::ptr::null(),
1003            _abi_mut: core::ptr::null_mut(),
1004        }
1005    }
1006
1007    /// Returns what triggered the current `layout()` invocation.
1008    #[must_use] pub const fn relayout_reason(&self) -> RelayoutReason {
1009        self.relayout_reason
1010    }
1011
1012
1013    /// Is the window's LOGICAL viewport wider than `width_px`?
1014    ///
1015    /// The structural-breakpoint helper: branch on this in `layout()` to
1016    /// return an entirely different DOM per form factor
1017    /// (`ribbon.dom_desktop()` vs `ribbon.dom_mobile()`), instead of
1018    /// emitting both trees and toggling visibility with `@media` rules.
1019    ///
1020    /// CONTRACT: the framework re-invokes `layout()` on every window resize
1021    /// (`RelayoutReason::Resize` - the regenerate path never takes the
1022    /// layout-equivalence shortcut when the window size changed), so the
1023    /// answer cannot go stale: crossing the breakpoint in either direction
1024    /// re-runs `layout()` and the callback returns the other tree. If a
1025    /// future optimization ever skips DOM regeneration on resize, it must
1026    /// register the thresholds queried here and force a rebuild when one is
1027    /// crossed - grep for this comment.
1028    #[must_use] pub fn viewport_bigger_than(&self, width_px: f32) -> bool {
1029        self.window_size.dimensions.width > width_px
1030    }
1031
1032    /// Set the callable pointer for FFI language bindings
1033    pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
1034        self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
1035    }
1036
1037    /// Get the callable for FFI language bindings (Python, etc.)
1038    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
1039        if self.callable_ptr.is_null() {
1040            OptionRefAny::None
1041        } else {
1042            unsafe { (*self.callable_ptr).clone() }
1043        }
1044    }
1045
1046    /// Get a clone of the system style Arc
1047    #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
1048        unsafe { (*self.ref_data).system_style.clone() }
1049    }
1050
1051    /// #28 (d): snapshot of the system's monitors, taken by the caller right
1052    /// before this layout pass. Empty when the platform hasn't populated
1053    /// monitor info (headless, web, very early startup).
1054    #[must_use] pub fn get_monitors(&self) -> crate::window::MonitorVec {
1055        unsafe { (*self.ref_data).monitors.clone() }
1056    }
1057
1058    /// #28 (d): the LARGEST monitor size in physical px — the safe upper
1059    /// bound for "how much content could possibly be visible at once" when
1060    /// the window's own monitor is not yet known at first layout. Apps use
1061    /// it to bound how much content the first `layout()` builds (e.g. at
1062    /// most monitor-height text lines, or monitor-width × monitor-height
1063    /// characters for a single unbroken line), so opening a huge file never
1064    /// builds an unbounded DOM. `None` when no monitor info is available.
1065    #[must_use] pub fn get_max_monitor_size(&self) -> azul_css::props::basic::OptionLayoutSize {
1066        let monitors = unsafe { &(*self.ref_data).monitors };
1067        let mut best: Option<LayoutSize> = None;
1068        for m in monitors.as_ref() {
1069            let s = m.size;
1070            let better = best.is_none_or(|b| (s.width * s.height) > (b.width * b.height));
1071            if better {
1072                best = Some(s);
1073            }
1074        }
1075        best.into()
1076    }
1077
1078    const fn internal_get_image_cache(&self) -> &ImageCache {
1079        unsafe { (*self.ref_data).image_cache }
1080    }
1081    const fn internal_get_system_fonts(&self) -> &FcFontCache {
1082        unsafe { (*self.ref_data).system_fonts }
1083    }
1084    const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
1085        unsafe { (*self.ref_data).gl_context }
1086    }
1087
1088    #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
1089        self.internal_get_gl_context().clone()
1090    }
1091
1092    #[must_use] pub fn get_system_fonts(&self) -> Vec<AzStringPair> {
1093        let fc_cache = self.internal_get_system_fonts();
1094
1095        fc_cache
1096            .list()
1097            .into_iter()
1098            .filter_map(|(pattern, font_id)| {
1099                let source = fc_cache.get_font_by_id(&font_id)?;
1100                match source {
1101                    OwnedFontSource::Memory(_) => None,
1102                    OwnedFontSource::Disk(d) => Some((pattern.name.as_ref()?.clone(), d.path)),
1103                }
1104            })
1105            .map(|(k, v)| AzStringPair {
1106                key: k.into(),
1107                value: v.into(),
1108            })
1109            .collect()
1110    }
1111
1112    /// The window's ALREADY-BUILT system font cache.
1113    ///
1114    /// `get_system_fonts` only hands back stringified name/path pairs, which
1115    /// is useless to a layout callback that wants to run engine layout of
1116    /// its own (paginating a document, measuring for an export). Such an app
1117    /// had to call `build_font_cache()` and re-scan every font on the
1118    /// machine — measured at ~5 SECONDS on the first frame, during which the
1119    /// client cannot answer the compositor's configure/ping handshake and
1120    /// loses its surface.
1121    ///
1122    /// The cache is internally `Arc<RwLock<_>>` (rust-fontconfig 4.1+), so
1123    /// this clone is a handle, not a copy: the caller sees the same fonts
1124    /// the window already resolved, including builder-thread additions.
1125    #[must_use] pub fn get_font_cache(&self) -> FcFontCache {
1126        self.internal_get_system_fonts().clone()
1127    }
1128
1129    #[must_use] pub fn get_image(&self, image_id: &AzString) -> Option<ImageRef> {
1130        self.internal_get_image_cache()
1131            .get_css_image_id(image_id)
1132            .cloned()
1133    }
1134
1135    /// Get the active route match (pattern + extracted parameters).
1136    ///
1137    /// Returns `None` if no routes are configured or no route is active.
1138    #[must_use] pub const fn get_active_route(&self) -> Option<&crate::resources::RouteMatch> {
1139        unsafe { (*self.ref_data).active_route }
1140    }
1141
1142    /// Get a route parameter by key (e.g. `get_route_param("id")` for `/user/:id`).
1143    ///
1144    /// Returns `None` if no route is active or the parameter doesn't exist.
1145    #[must_use] pub fn get_route_param(&self, key: &str) -> Option<&AzString> {
1146        self.get_active_route()?.get_param(key)
1147    }
1148
1149    // Responsive layout helper methods.
1150    //
1151    // These are THE sanctioned way for `layout()` to branch on window size
1152    // (mobile vs desktop DOM shapes, instead of `display:none` stacks). Every
1153    // call is RECORDED, and the recording is what makes resize cheap: a resize
1154    // that flips none of the recorded answers (and crosses no CSS breakpoint)
1155    // provably cannot change what the callback returns through this channel,
1156    // so the engine re-flows the existing DOM instead of re-invoking it
1157    // (`LayoutWindow::resize_needs_full_regeneration`). Reading the size
1158    // imperatively (`get_window_width()`, `info.window_size`) to branch the
1159    // DOM is a bug in the app: the engine cannot see that read, so the DOM
1160    // goes stale across exactly the resizes the app cared about.
1161
1162    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
1163    fn record_width_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
1164        record_size_query(SizeQuery {
1165            axis: SizeQueryAxis::Width,
1166            op,
1167            threshold_px,
1168            answer,
1169        });
1170        answer
1171    }
1172
1173    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
1174    fn record_height_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
1175        record_size_query(SizeQuery {
1176            axis: SizeQueryAxis::Height,
1177            op,
1178            threshold_px,
1179            answer,
1180        });
1181        answer
1182    }
1183
1184    /// Returns true if the window width is less than the given pixel value.
1185    /// Recorded — see the note above these helpers.
1186    #[must_use] pub fn window_width_less_than(&self, px: f32) -> bool {
1187        let answer = self.window_size.dimensions.width < px;
1188        self.record_width_query(SizeQueryOp::LessThan, px, answer)
1189    }
1190
1191    /// Returns true if the window width is greater than the given pixel value.
1192    /// Recorded — see the note above these helpers.
1193    #[must_use] pub fn window_width_greater_than(&self, px: f32) -> bool {
1194        let answer = self.window_size.dimensions.width > px;
1195        self.record_width_query(SizeQueryOp::GreaterThan, px, answer)
1196    }
1197
1198    /// Returns true if the window width is between min and max (inclusive).
1199    /// Recorded as its two bounds — see the note above these helpers.
1200    #[must_use] pub fn window_width_between(&self, min_px: f32, max_px: f32) -> bool {
1201        let width = self.window_size.dimensions.width;
1202        self.record_width_query(SizeQueryOp::GreaterOrEqual, min_px, width >= min_px)
1203            & self.record_width_query(SizeQueryOp::LessOrEqual, max_px, width <= max_px)
1204    }
1205
1206    /// Returns true if the window height is less than the given pixel value.
1207    /// Recorded — see the note above these helpers.
1208    #[must_use] pub fn window_height_less_than(&self, px: f32) -> bool {
1209        let answer = self.window_size.dimensions.height < px;
1210        self.record_height_query(SizeQueryOp::LessThan, px, answer)
1211    }
1212
1213    /// Returns true if the window height is greater than the given pixel value.
1214    /// Recorded — see the note above these helpers.
1215    #[must_use] pub fn window_height_greater_than(&self, px: f32) -> bool {
1216        let answer = self.window_size.dimensions.height > px;
1217        self.record_height_query(SizeQueryOp::GreaterThan, px, answer)
1218    }
1219
1220    /// Returns true if the window height is between min and max (inclusive).
1221    /// Recorded as its two bounds — see the note above these helpers.
1222    #[must_use] pub fn window_height_between(&self, min_px: f32, max_px: f32) -> bool {
1223        let height = self.window_size.dimensions.height;
1224        self.record_height_query(SizeQueryOp::GreaterOrEqual, min_px, height >= min_px)
1225            & self.record_height_query(SizeQueryOp::LessOrEqual, max_px, height <= max_px)
1226    }
1227
1228    /// Returns the current window width in pixels
1229    #[must_use] pub const fn get_window_width(&self) -> f32 {
1230        self.window_size.dimensions.width
1231    }
1232
1233    /// Returns the current window height in pixels
1234    #[must_use] pub const fn get_window_height(&self) -> f32 {
1235        self.window_size.dimensions.height
1236    }
1237
1238    /// Returns the current window DPI scale factor (1.0 = 96 DPI, 2.0 = 192 DPI)
1239    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
1240    #[must_use] pub fn get_dpi_factor(&self) -> f32 {
1241        self.window_size.dpi as f32 / 96.0
1242    }
1243}
1244
1245/// Information about the bounds of a laid-out div rectangle.
1246///
1247/// Necessary when invoking `VirtualViewCallbacks` and `RenderImageCallbacks`, so
1248/// that they can change what their content is based on their size.
1249#[derive(Debug, Copy, Clone)]
1250#[repr(C)]
1251pub struct HidpiAdjustedBounds {
1252    pub logical_size: LogicalSize,
1253    pub hidpi_factor: DpiScaleFactor,
1254}
1255
1256impl HidpiAdjustedBounds {
1257    #[inline]
1258    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
1259    #[must_use] pub const fn from_bounds(bounds: LayoutSize, hidpi_factor: DpiScaleFactor) -> Self {
1260        let logical_size = LogicalSize::new(bounds.width as f32, bounds.height as f32);
1261        Self {
1262            logical_size,
1263            hidpi_factor,
1264        }
1265    }
1266
1267    #[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
1268        self.get_logical_size()
1269            .to_physical(self.get_hidpi_factor().inner.get())
1270    }
1271
1272    #[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
1273        self.logical_size
1274    }
1275
1276    #[must_use] pub const fn get_hidpi_factor(&self) -> DpiScaleFactor {
1277        self.hidpi_factor
1278    }
1279}
1280
1281/// Defines the `focus_targeted` node ID for the next frame
1282#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1283#[repr(C, u8)]
1284pub enum FocusTarget {
1285    Id(DomNodeId),
1286    Path(FocusTargetPath),
1287    Previous,
1288    Next,
1289    First,
1290    Last,
1291    NoFocus,
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1295#[repr(C)]
1296pub struct FocusTargetPath {
1297    pub dom: DomId,
1298    pub css_path: CssPath,
1299}
1300
1301// -- normal callback
1302
1303// core callback types (usize-based placeholders)
1304//
1305// These types use `usize` instead of function pointers to avoid creating
1306// a circular dependency between azul-core and azul-layout.
1307//
1308// The actual function pointers will be stored in azul-layout, which will
1309// use unsafe code to transmute between usize and the real function pointers.
1310//
1311// IMPORTANT: The memory layout must be identical to the real types!
1312//
1313// Naming convention: "Core" prefix indicates these are the low-level types
1314
1315/// Core callback type - uses usize instead of function pointer to avoid circular dependencies.
1316///
1317/// **IMPORTANT**: This is NOT actually a usize at runtime - it's a function pointer that is
1318/// cast to usize for storage in the data model. When invoking the callback, this usize is
1319/// unsafely cast back to the actual function pointer type:
1320/// `extern "C" fn(RefAny, CallbackInfo) -> Update`
1321///
1322/// This design allows azul-core to store callbacks without depending on azul-layout's `CallbackInfo`
1323/// type. The actual function pointer type is defined in azul-layout as `CallbackType`.
1324pub type CoreCallbackType = usize;
1325
1326/// Stores a callback as usize (actually a function pointer cast to usize)
1327///
1328/// **IMPORTANT**: The `cb` field stores a function pointer disguised as usize to avoid
1329/// circular dependencies between azul-core and azul-layout. When creating a `CoreCallback`,
1330/// you can directly assign a function pointer - Rust will implicitly cast it to usize.
1331/// When invoking, the usize must be unsafely cast back to the function pointer type.
1332///
1333/// Must return an `Update` that denotes if the screen should be redrawn.
1334#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1335#[repr(C)]
1336pub struct CoreCallback {
1337    pub cb: CoreCallbackType,
1338    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1339    /// Native Rust code sets this to None
1340    pub ctx: OptionRefAny,
1341}
1342
1343/// Allow creating `CoreCallback` from a raw function pointer (as usize)
1344/// Sets callable to None (for native Rust/C usage)
1345impl From<CoreCallbackType> for CoreCallback {
1346    fn from(cb: CoreCallbackType) -> Self {
1347        Self {
1348            cb,
1349            ctx: OptionRefAny::None,
1350        }
1351    }
1352}
1353
1354impl_option!(
1355    CoreCallback,
1356    OptionCoreCallback,
1357    [Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash]
1358);
1359
1360/// Data associated with a callback (event filter, callback, and user data)
1361#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1362#[repr(C)]
1363pub struct CoreCallbackData {
1364    pub event: EventFilter,
1365    pub callback: CoreCallback,
1366    pub refany: RefAny,
1367}
1368
1369impl_option!(
1370    CoreCallbackData,
1371    OptionCoreCallbackData,
1372    copy = false,
1373    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1374);
1375
1376impl_vec!(CoreCallbackData, CoreCallbackDataVec, CoreCallbackDataVecDestructor, CoreCallbackDataVecDestructorType, CoreCallbackDataVecSlice, OptionCoreCallbackData);
1377impl_vec_clone!(
1378    CoreCallbackData,
1379    CoreCallbackDataVec,
1380    CoreCallbackDataVecDestructor
1381);
1382impl_vec_mut!(CoreCallbackData, CoreCallbackDataVec);
1383impl_vec_debug!(CoreCallbackData, CoreCallbackDataVec);
1384impl_vec_partialord!(CoreCallbackData, CoreCallbackDataVec);
1385impl_vec_ord!(CoreCallbackData, CoreCallbackDataVec);
1386impl_vec_partialeq!(CoreCallbackData, CoreCallbackDataVec);
1387impl_vec_eq!(CoreCallbackData, CoreCallbackDataVec);
1388impl_vec_hash!(CoreCallbackData, CoreCallbackDataVec);
1389
1390impl CoreCallbackDataVec {
1391    #[inline]
1392    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CoreCallbackData> {
1393        NodeDataContainerRef {
1394            internal: self.as_ref(),
1395        }
1396    }
1397    #[inline]
1398    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, CoreCallbackData> {
1399        NodeDataContainerRefMut {
1400            internal: self.as_mut(),
1401        }
1402    }
1403}
1404
1405// -- image rendering callback
1406
1407/// Image rendering callback type - uses usize instead of function pointer
1408pub type CoreRenderImageCallbackType = usize;
1409
1410/// Callback that returns a rendered OpenGL texture (usize placeholder)
1411#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1412#[repr(C)]
1413pub struct CoreRenderImageCallback {
1414    pub cb: CoreRenderImageCallbackType,
1415    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1416    /// Native Rust code sets this to None
1417    pub ctx: OptionRefAny,
1418}
1419
1420/// Allow creating `CoreRenderImageCallback` from a raw function pointer (as usize)
1421/// Sets callable to None (for native Rust/C usage)
1422impl From<CoreRenderImageCallbackType> for CoreRenderImageCallback {
1423    fn from(cb: CoreRenderImageCallbackType) -> Self {
1424        Self {
1425            cb,
1426            ctx: OptionRefAny::None,
1427        }
1428    }
1429}
1430
1431/// Image callback with associated data
1432#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1433#[repr(C)]
1434pub struct CoreImageCallback {
1435    pub refany: RefAny,
1436    pub callback: CoreRenderImageCallback,
1437}
1438
1439impl_option!(
1440    CoreImageCallback,
1441    OptionCoreImageCallback,
1442    copy = false,
1443    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1444);
1445
1446#[cfg(test)]
1447#[allow(
1448    clippy::float_cmp,
1449    clippy::too_many_lines,
1450    clippy::cast_precision_loss,
1451    clippy::unusual_byte_groupings
1452)]
1453mod autotest_generated {
1454    use alloc::string::String;
1455
1456    use super::*;
1457    use crate::{
1458        events::HoverEventFilter,
1459        resources::{RawImageFormat, RouteMatch},
1460        window::StringPairVec,
1461    };
1462
1463    // ---- helpers -----------------------------------------------------------
1464
1465    fn s(v: &str) -> AzString {
1466        AzString::from(String::from(v))
1467    }
1468
1469    fn win(width: f32, height: f32, dpi: u32) -> WindowSize {
1470        WindowSize {
1471            dimensions: LogicalSize::new(width, height),
1472            dpi,
1473            min_dimensions: None.into(),
1474            max_dimensions: None.into(),
1475        }
1476    }
1477
1478    /// Owns everything a `LayoutCallbackInfoRefData` borrows, so that the raw
1479    /// pointer `LayoutCallbackInfo` launders to `'static` always points at
1480    /// live memory for the duration of a test.
1481    struct Fixture {
1482        fonts: FcFontCache,
1483        images: ImageCache,
1484        style: Arc<SystemStyle>,
1485        gl: OptionGlContextPtr,
1486        route: Option<RouteMatch>,
1487    }
1488
1489    impl Fixture {
1490        fn new() -> Self {
1491            Self {
1492                fonts: FcFontCache::default(),
1493                images: ImageCache::default(),
1494                style: Arc::new(SystemStyle::default()),
1495                gl: OptionGlContextPtr::None,
1496                route: None,
1497            }
1498        }
1499
1500        fn with_route(route: RouteMatch) -> Self {
1501            let mut f = Self::new();
1502            f.route = Some(route);
1503            f
1504        }
1505
1506        fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
1507            LayoutCallbackInfoRefData {
1508                image_cache: &self.images,
1509                gl_context: &self.gl,
1510                system_fonts: &self.fonts,
1511                system_style: self.style.clone(),
1512                active_route: self.route.as_ref(),
1513                monitors: crate::window::MonitorVec::from_const_slice(&[]),
1514            }
1515        }
1516    }
1517
1518    /// #28 (d): `get_max_monitor_size` returns the LARGEST monitor by area
1519    /// (the safe "how much could possibly be visible" bound for first
1520    /// layout) and `None` on an empty snapshot (headless/web).
1521    #[test]
1522    fn max_monitor_size_is_largest_by_area_or_none() {
1523        use azul_css::props::basic::LayoutSize;
1524
1525        use crate::window::{Monitor, MonitorVec};
1526
1527        let fixture = Fixture::new();
1528        let mut rd = fixture.ref_data();
1529        rd.monitors = MonitorVec::from_vec(Vec::from([
1530            Monitor {
1531                size: LayoutSize::new(1920, 1080),
1532                ..Monitor::default()
1533            },
1534            Monitor {
1535                size: LayoutSize::new(2560, 1440),
1536                ..Monitor::default()
1537            },
1538            Monitor {
1539                size: LayoutSize::new(800, 600),
1540                ..Monitor::default()
1541            },
1542        ]));
1543        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1544        let max: Option<LayoutSize> = info.get_max_monitor_size().into();
1545        assert_eq!(max, Some(LayoutSize::new(2560, 1440)));
1546        assert_eq!(info.get_monitors().len(), 3);
1547
1548        let rd2 = fixture.ref_data(); // empty snapshot
1549        let info2 = LayoutCallbackInfo::new(&rd2, WindowSize::default(), WindowTheme::LightMode);
1550        let none: Option<LayoutSize> = info2.get_max_monitor_size().into();
1551        assert_eq!(none, None);
1552    }
1553
1554    /// `/user/:id` with a plain and a non-ASCII parameter.
1555    fn user_route() -> RouteMatch {
1556        RouteMatch {
1557            pattern: s("/user/:id"),
1558            params: StringPairVec::from_vec(Vec::from([
1559                AzStringPair {
1560                    key: s("id"),
1561                    value: s("42"),
1562                },
1563                AzStringPair {
1564                    key: s("\u{1F600}"),
1565                    value: s("emoji"),
1566                },
1567            ])),
1568        }
1569    }
1570
1571    fn vv_info<'a>(
1572        fonts: &'a FcFontCache,
1573        images: &'a ImageCache,
1574        bounds: HidpiAdjustedBounds,
1575    ) -> VirtualViewCallbackInfo {
1576        VirtualViewCallbackInfo::new(
1577            VirtualViewCallbackReason::InitialRender,
1578            fonts,
1579            images,
1580            WindowTheme::LightMode,
1581            bounds,
1582            LogicalSize::new(100.0, 200.0),
1583            LogicalPosition::new(1.0, 2.0),
1584            LogicalSize::new(1000.0, 2000.0),
1585            LogicalPosition::new(3.0, 4.0),
1586        )
1587    }
1588
1589    fn bounds_1x1() -> HidpiAdjustedBounds {
1590        HidpiAdjustedBounds::from_bounds(LayoutSize::new(1, 1), DpiScaleFactor::new(1.0))
1591    }
1592
1593    // ---- Update::max_self --------------------------------------------------
1594
1595    const ALL_UPDATES: [Update; 3] = [
1596        Update::DoNothing,
1597        Update::RefreshDom,
1598        Update::RefreshDomAllWindows,
1599    ];
1600
1601    /// `max_self` must be exactly the `Ord`-max of the lattice, for every one
1602    /// of the 3x3 combinations (this is the whole contract, so check it
1603    /// exhaustively rather than sampling).
1604    #[test]
1605    fn update_max_self_is_exhaustively_ord_max() {
1606        for a in ALL_UPDATES {
1607            for b in ALL_UPDATES {
1608                let mut got = a;
1609                got.max_self(b);
1610                assert_eq!(
1611                    got,
1612                    core::cmp::max(a, b),
1613                    "max_self({a:?}, {b:?}) disagrees with Ord::max"
1614                );
1615            }
1616        }
1617    }
1618
1619    #[test]
1620    fn update_max_self_is_idempotent_and_monotone() {
1621        for a in ALL_UPDATES {
1622            // idempotent: x.max(x) == x
1623            let mut got = a;
1624            got.max_self(a);
1625            assert_eq!(got, a);
1626
1627            // absorbing top element: nothing can lower RefreshDomAllWindows
1628            let mut top = Update::RefreshDomAllWindows;
1629            top.max_self(a);
1630            assert_eq!(top, Update::RefreshDomAllWindows);
1631
1632            // monotone: max_self never decreases self
1633            let mut m = a;
1634            m.max_self(Update::DoNothing);
1635            assert!(m >= a);
1636        }
1637    }
1638
1639    /// Applying the same set of updates in any order must converge to the same
1640    /// value (commutativity/associativity of the fold), since callbacks fold
1641    /// their `Update`s in nondeterministic order.
1642    #[test]
1643    fn update_max_self_fold_is_order_independent() {
1644        for a in ALL_UPDATES {
1645            for b in ALL_UPDATES {
1646                for c in ALL_UPDATES {
1647                    let mut fwd = a;
1648                    fwd.max_self(b);
1649                    fwd.max_self(c);
1650
1651                    let mut rev = c;
1652                    rev.max_self(b);
1653                    rev.max_self(a);
1654
1655                    assert_eq!(fwd, rev, "fold of {a:?},{b:?},{c:?} is order-dependent");
1656                }
1657            }
1658        }
1659    }
1660
1661    // ---- LayoutCallback / default_layout_callback ---------------------------
1662
1663    static ALT_LAYOUT_CALLS: AtomicUsize = AtomicUsize::new(0);
1664
1665    // NOTE: the body must differ from `default_layout_callback`'s, otherwise
1666    // identical-code-folding may merge the two symbols and the pointer
1667    // inequality assertion below would compare equal addresses.
1668    extern "C" fn alt_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
1669        ALT_LAYOUT_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1670        Dom::create_body()
1671    }
1672
1673    #[test]
1674    fn default_layout_callback_returns_body_and_does_not_panic() {
1675        let fx = Fixture::new();
1676        let rd = fx.ref_data();
1677        let info = LayoutCallbackInfo::new(&rd, win(0.0, 0.0, 0), WindowTheme::DarkMode);
1678
1679        // extreme arg: zero-sized window, zero DPI, empty caches
1680        let dom = default_layout_callback(RefAny::new(0u32), info);
1681        assert_eq!(dom, Dom::create_body());
1682    }
1683
1684    #[test]
1685    fn layout_callback_create_stores_the_given_fn_and_null_ctx() {
1686        let from_default = LayoutCallback::create(default_layout_callback as LayoutCallbackType);
1687        assert!(
1688            from_default.ctx.is_none(),
1689            "native-Rust create() must leave the FFI ctx empty"
1690        );
1691        assert_eq!(from_default, LayoutCallback::default());
1692
1693        // create() must actually store its argument, not silently fall back
1694        // to the default callback.
1695        let from_alt = LayoutCallback::create(alt_layout_callback as LayoutCallbackType);
1696        assert!(from_alt.ctx.is_none());
1697        assert_ne!(
1698            from_alt, from_default,
1699            "create() ignored its argument (or the two fns were ICF-folded)"
1700        );
1701
1702        // the stored pointer is callable and is the one we passed in
1703        let fx = Fixture::new();
1704        let rd = fx.ref_data();
1705        let before = ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst);
1706        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1707        let _ = (from_alt.cb)(RefAny::new(()), info);
1708        assert_eq!(ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst), before + 1);
1709    }
1710
1711    // ---- VirtualViewCallback ------------------------------------------------
1712
1713    extern "C" fn vv_keep_current_cb(_: RefAny, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
1714        VirtualViewReturn::keep_current(
1715            info.scroll_size,
1716            info.scroll_offset,
1717            info.virtual_scroll_size,
1718            info.virtual_scroll_offset,
1719        )
1720    }
1721
1722    #[test]
1723    fn virtual_view_callback_create_round_trips_through_the_fn_ptr() {
1724        let cb = VirtualViewCallback::create(vv_keep_current_cb as VirtualViewCallbackType);
1725        assert!(cb.ctx.is_none());
1726
1727        let fonts = FcFontCache::default();
1728        let images = ImageCache::default();
1729        let info = vv_info(&fonts, &images, bounds_1x1());
1730
1731        let ret = (cb.cb)(RefAny::new(0u8), info);
1732        assert!(ret.dom.is_none());
1733        assert_eq!(ret.scroll_size, LogicalSize::new(100.0, 200.0));
1734        assert_eq!(ret.scroll_offset, LogicalPosition::new(1.0, 2.0));
1735        assert_eq!(ret.virtual_scroll_size, LogicalSize::new(1000.0, 2000.0));
1736        assert_eq!(ret.virtual_scroll_offset, LogicalPosition::new(3.0, 4.0));
1737    }
1738
1739    // ---- VirtualViewCallbackInfo -------------------------------------------
1740
1741    #[test]
1742    fn virtual_view_callback_info_new_holds_its_fields() {
1743        let fonts = FcFontCache::default();
1744        let images = ImageCache::default();
1745        let bounds = HidpiAdjustedBounds::from_bounds(
1746            LayoutSize::new(800, 600),
1747            DpiScaleFactor::new(2.0),
1748        );
1749        let info = vv_info(&fonts, &images, bounds);
1750
1751        assert_eq!(info.reason, VirtualViewCallbackReason::InitialRender);
1752        assert_eq!(info.window_theme, WindowTheme::LightMode);
1753        assert_eq!(info.get_bounds().get_logical_size(), LogicalSize::new(800.0, 600.0));
1754        assert_eq!(info.get_bounds().get_hidpi_factor(), DpiScaleFactor::new(2.0));
1755        assert_eq!(info.scroll_size, LogicalSize::new(100.0, 200.0));
1756
1757        // the raw pointers must alias the borrows we handed in
1758        assert!(core::ptr::eq(info.internal_get_system_fonts(), &fonts));
1759        assert!(core::ptr::eq(info.internal_get_image_cache(), &images));
1760
1761        // FFI ctx starts empty and the measure hook starts absent
1762        assert!(info.get_ctx().is_none());
1763        assert_eq!(
1764            info.measure_dom(Dom::create_body(), LogicalSize::new(10.0, 10.0)),
1765            LogicalSize::zero()
1766        );
1767
1768        // clone must not disturb any of that
1769        let cloned = info.clone();
1770        assert_eq!(cloned.reason, info.reason);
1771        assert!(core::ptr::eq(cloned.internal_get_system_fonts(), &fonts));
1772        assert!(cloned.get_ctx().is_none());
1773    }
1774
1775    #[test]
1776    fn virtual_view_callback_info_new_survives_nan_and_infinite_geometry() {
1777        let fonts = FcFontCache::default();
1778        let images = ImageCache::default();
1779        let info = VirtualViewCallbackInfo::new(
1780            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
1781            &fonts,
1782            &images,
1783            WindowTheme::DarkMode,
1784            HidpiAdjustedBounds::from_bounds(
1785                LayoutSize::new(isize::MAX, isize::MIN),
1786                DpiScaleFactor::new(f32::NAN),
1787            ),
1788            LogicalSize::new(f32::NAN, f32::INFINITY),
1789            LogicalPosition::new(f32::NEG_INFINITY, f32::MAX),
1790            LogicalSize::new(f32::MIN, 0.0),
1791            LogicalPosition::new(-0.0, f32::EPSILON),
1792        );
1793
1794        // extreme values are stored verbatim, not silently clamped
1795        assert!(info.scroll_size.width.is_nan());
1796        assert!(info.scroll_size.height.is_infinite());
1797        assert!(info.scroll_offset.x.is_infinite() && info.scroll_offset.x.is_sign_negative());
1798        assert_eq!(info.virtual_scroll_size.width, f32::MIN);
1799        assert_eq!(info.reason, VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
1800
1801        // and none of the getters panic on that instance
1802        assert!(info.get_ctx().is_none());
1803        assert!(info.get_bounds().get_logical_size().width > 0.0);
1804    }
1805
1806    #[test]
1807    fn virtual_view_callback_info_get_ctx_clones_without_double_free() {
1808        let fonts = FcFontCache::default();
1809        let images = ImageCache::default();
1810        let mut info = vv_info(&fonts, &images, bounds_1x1());
1811
1812        // null callable_ptr -> None (the native-Rust path)
1813        assert!(info.get_ctx().is_none());
1814
1815        let callable = OptionRefAny::Some(RefAny::new(0xDEAD_BEEF_u32));
1816        info.set_callable_ptr(&callable);
1817
1818        // repeated get_ctx() must hand out independent clones; dropping them
1819        // all must not corrupt the original RefAny's refcount.
1820        for _ in 0..64 {
1821            let got = info.get_ctx();
1822            assert!(got.is_some());
1823            drop(got);
1824        }
1825
1826        let mut got = info.get_ctx();
1827        match got {
1828            OptionRefAny::Some(ref mut r) => {
1829                let inner = r.downcast_ref::<u32>().expect("ctx should hold a u32");
1830                assert_eq!(*inner, 0xDEAD_BEEF_u32);
1831            }
1832            OptionRefAny::None => panic!("callable_ptr was set, get_ctx() returned None"),
1833        }
1834        drop(got);
1835
1836        // the original is still alive and intact after all those clones dropped
1837        let mut orig = callable;
1838        match orig {
1839            OptionRefAny::Some(ref mut r) => {
1840                assert_eq!(*r.downcast_ref::<u32>().unwrap(), 0xDEAD_BEEF_u32);
1841            }
1842            OptionRefAny::None => panic!("original callable was consumed"),
1843        }
1844    }
1845
1846    // ---- measure_dom --------------------------------------------------------
1847
1848    static MEASURE_CALLS: AtomicUsize = AtomicUsize::new(0);
1849
1850    /// Test trampoline. Per the `MeasureDomFn` contract the `Dom` is passed by
1851    /// pointer and **consumed** (moved out) here.
1852    extern "C" fn test_measure_dom_fn(
1853        ctx: *mut c_void,
1854        dom: *mut Dom,
1855        available: LogicalSize,
1856    ) -> LogicalSize {
1857        MEASURE_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1858        // SAFETY: `measure_dom` always passes a valid, owned-but-ManuallyDrop
1859        // Dom; taking it by value here is exactly the documented contract.
1860        let dom = unsafe { core::ptr::read(dom) };
1861        drop(dom);
1862        if !ctx.is_null() {
1863            // SAFETY: the only caller below passes a `&mut u32`.
1864            unsafe {
1865                *ctx.cast::<u32>() = 0xABCD;
1866            }
1867        }
1868        LogicalSize::new(available.width * 2.0, available.height / 2.0)
1869    }
1870
1871    #[test]
1872    fn measure_dom_without_hook_returns_zero_for_every_input() {
1873        let fonts = FcFontCache::default();
1874        let images = ImageCache::default();
1875        let info = vv_info(&fonts, &images, bounds_1x1());
1876
1877        // zero / negative / NaN / infinite / huge available sizes must all take
1878        // the null-hook early-out without panicking (and must drop the Dom).
1879        for available in [
1880            LogicalSize::zero(),
1881            LogicalSize::new(-1.0, -1.0),
1882            LogicalSize::new(f32::NAN, f32::NAN),
1883            LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
1884            LogicalSize::new(f32::MAX, f32::MIN),
1885            LogicalSize::new(1.0, 1_000_000.0),
1886        ] {
1887            assert_eq!(
1888                info.measure_dom(Dom::create_body(), available),
1889                LogicalSize::zero()
1890            );
1891        }
1892    }
1893
1894    #[test]
1895    fn measure_dom_with_hook_forwards_ctx_and_available_and_consumes_the_dom() {
1896        let fonts = FcFontCache::default();
1897        let images = ImageCache::default();
1898        let mut info = vv_info(&fonts, &images, bounds_1x1());
1899
1900        let mut ctx_val: u32 = 0;
1901        info.set_measure_dom_fn(
1902            test_measure_dom_fn,
1903            core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
1904        );
1905
1906        // NOTE: `>` not `== before + 1` - other tests share this static and
1907        // run in parallel, so only monotonicity is safe to assert here.
1908        let before = MEASURE_CALLS.load(AtomicOrdering::SeqCst);
1909        let out = info.measure_dom(Dom::create_body(), LogicalSize::new(100.0, 40.0));
1910
1911        assert!(MEASURE_CALLS.load(AtomicOrdering::SeqCst) > before);
1912        assert_eq!(out, LogicalSize::new(200.0, 20.0));
1913        assert_eq!(ctx_val, 0xABCD, "measure ctx pointer was not forwarded");
1914
1915        // the documented virtual-scroll sizing idiom: measure at a huge height
1916        let natural = info.measure_dom(Dom::create_body(), LogicalSize::new(320.0, 1_000_000.0));
1917        assert_eq!(natural, LogicalSize::new(640.0, 500_000.0));
1918
1919        // NaN / infinite constraints reach the hook unmodified and come back
1920        // as NaN/inf rather than panicking or being clamped
1921        let nan = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::NAN, 4.0));
1922        assert!(nan.width.is_nan());
1923        assert_eq!(nan.height, 2.0);
1924
1925        let inf = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::INFINITY, 4.0));
1926        assert!(inf.width.is_infinite());
1927    }
1928
1929    #[test]
1930    fn measure_dom_hook_can_be_replaced_and_last_writer_wins() {
1931        let fonts = FcFontCache::default();
1932        let images = ImageCache::default();
1933        let mut info = vv_info(&fonts, &images, bounds_1x1());
1934
1935        info.set_measure_dom_fn(test_measure_dom_fn, core::ptr::null_mut());
1936        // null ctx must be tolerated by the trampoline contract
1937        let first = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
1938        assert_eq!(first, LogicalSize::new(4.0, 4.0));
1939
1940        let mut ctx_val: u32 = 0;
1941        info.set_measure_dom_fn(
1942            test_measure_dom_fn,
1943            core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
1944        );
1945        let second = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
1946        assert_eq!(second, first);
1947        assert_eq!(ctx_val, 0xABCD);
1948    }
1949
1950    // ---- VirtualViewReturn --------------------------------------------------
1951
1952    #[test]
1953    fn virtual_view_return_with_dom_and_keep_current_hold_their_fields() {
1954        let ss = LogicalSize::new(600.0, 30.0);
1955        let so = LogicalPosition::new(0.0, 300.0);
1956        let vss = LogicalSize::new(600.0, 30_000.0);
1957        let vso = LogicalPosition::zero();
1958
1959        let with = VirtualViewReturn::with_dom(Dom::create_body(), ss, so, vss, vso);
1960        assert!(with.dom.is_some(), "with_dom must produce OptionDom::Some");
1961        assert_eq!(with.scroll_size, ss);
1962        assert_eq!(with.scroll_offset, so);
1963        assert_eq!(with.virtual_scroll_size, vss);
1964        assert_eq!(with.virtual_scroll_offset, vso);
1965        assert_eq!(with.dom, OptionDom::Some(Dom::create_body()));
1966
1967        let keep = VirtualViewReturn::keep_current(ss, so, vss, vso);
1968        assert!(keep.dom.is_none(), "keep_current must produce OptionDom::None");
1969        assert_eq!(keep.scroll_size, ss);
1970        assert_eq!(keep.scroll_offset, so);
1971        assert_eq!(keep.virtual_scroll_size, vss);
1972        assert_eq!(keep.virtual_scroll_offset, vso);
1973
1974        // the two constructors differ *only* in the dom field
1975        assert_ne!(with, keep);
1976
1977        // default is the "keep everything, render nothing" zero value
1978        let d = VirtualViewReturn::default();
1979        assert_eq!(
1980            d,
1981            VirtualViewReturn::keep_current(
1982                LogicalSize::zero(),
1983                LogicalPosition::zero(),
1984                LogicalSize::zero(),
1985                LogicalPosition::zero()
1986            )
1987        );
1988    }
1989
1990    #[test]
1991    fn virtual_view_return_keep_current_passes_extreme_values_through_unclamped() {
1992        // zero
1993        let z = VirtualViewReturn::keep_current(
1994            LogicalSize::zero(),
1995            LogicalPosition::zero(),
1996            LogicalSize::zero(),
1997            LogicalPosition::zero(),
1998        );
1999        assert_eq!(z.scroll_size, LogicalSize::zero());
2000        assert_eq!(z.virtual_scroll_size, LogicalSize::zero());
2001
2002        // negative + f32 limits: stored verbatim (no saturation, no panic)
2003        let n = VirtualViewReturn::keep_current(
2004            LogicalSize::new(-1.0, -0.0),
2005            LogicalPosition::new(f32::MIN, f32::MAX),
2006            LogicalSize::new(f32::MAX, f32::MIN_POSITIVE),
2007            LogicalPosition::new(-f32::EPSILON, 0.0),
2008        );
2009        assert_eq!(n.scroll_size.width, -1.0);
2010        assert_eq!(n.scroll_offset.x, f32::MIN);
2011        assert_eq!(n.scroll_offset.y, f32::MAX);
2012        assert_eq!(n.virtual_scroll_size.width, f32::MAX);
2013        assert_eq!(n.virtual_scroll_size.height, f32::MIN_POSITIVE);
2014
2015        // NaN / inf: stored verbatim; NaN makes the struct unequal to itself
2016        // under PartialEq, so probe the fields directly.
2017        let x = VirtualViewReturn::keep_current(
2018            LogicalSize::new(f32::NAN, f32::INFINITY),
2019            LogicalPosition::new(f32::NEG_INFINITY, f32::NAN),
2020            LogicalSize::new(f32::INFINITY, f32::NAN),
2021            LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
2022        );
2023        assert!(x.scroll_size.width.is_nan());
2024        assert!(x.scroll_size.height.is_infinite() && x.scroll_size.height.is_sign_positive());
2025        assert!(x.scroll_offset.x.is_infinite() && x.scroll_offset.x.is_sign_negative());
2026        assert!(x.scroll_offset.y.is_nan());
2027        assert!(x.virtual_scroll_offset.y.is_infinite());
2028        assert!(x.dom.is_none());
2029    }
2030
2031    // ---- TimerCallbackReturn ------------------------------------------------
2032
2033    #[test]
2034    fn timer_callback_return_constructors_match_their_documented_flags() {
2035        let c = TimerCallbackReturn::continue_unchanged();
2036        assert_eq!(c.should_update, Update::DoNothing);
2037        assert_eq!(c.should_terminate, TerminateTimer::Continue);
2038
2039        let cr = TimerCallbackReturn::continue_and_refresh_dom();
2040        assert_eq!(cr.should_update, Update::RefreshDom);
2041        assert_eq!(cr.should_terminate, TerminateTimer::Continue);
2042
2043        let t = TimerCallbackReturn::terminate_unchanged();
2044        assert_eq!(t.should_update, Update::DoNothing);
2045        assert_eq!(t.should_terminate, TerminateTimer::Terminate);
2046
2047        let tr = TimerCallbackReturn::terminate_and_refresh_dom();
2048        assert_eq!(tr.should_update, Update::RefreshDom);
2049        assert_eq!(tr.should_terminate, TerminateTimer::Terminate);
2050
2051        // all four are distinct - no constructor is a copy-paste of another
2052        let all = [c, cr, t, tr];
2053        for (i, a) in all.iter().enumerate() {
2054            for (j, b) in all.iter().enumerate() {
2055                assert_eq!(i == j, a == b, "constructors {i} and {j} collide");
2056            }
2057        }
2058
2059        // Default is documented as "continue, no update"
2060        assert_eq!(TimerCallbackReturn::default(), c);
2061    }
2062
2063    #[test]
2064    fn timer_callback_return_create_round_trips_every_flag_combination() {
2065        for u in ALL_UPDATES {
2066            for t in [TerminateTimer::Continue, TerminateTimer::Terminate] {
2067                let r = TimerCallbackReturn::create(u, t);
2068                assert_eq!(r.should_update, u);
2069                assert_eq!(r.should_terminate, t);
2070            }
2071        }
2072
2073        // the named constructors agree with the generic one
2074        assert_eq!(
2075            TimerCallbackReturn::create(Update::DoNothing, TerminateTimer::Continue),
2076            TimerCallbackReturn::continue_unchanged()
2077        );
2078        assert_eq!(
2079            TimerCallbackReturn::create(Update::RefreshDom, TerminateTimer::Terminate),
2080            TimerCallbackReturn::terminate_and_refresh_dom()
2081        );
2082
2083        // RefreshDomAllWindows is reachable through create() even though no
2084        // named constructor exposes it
2085        let all_windows =
2086            TimerCallbackReturn::create(Update::RefreshDomAllWindows, TerminateTimer::Terminate);
2087        assert_eq!(all_windows.should_update, Update::RefreshDomAllWindows);
2088    }
2089
2090    // ---- LayoutCallbackInfo: construction + getters --------------------------
2091
2092    #[test]
2093    fn layout_callback_info_new_defaults_to_initial_reason_and_holds_fields() {
2094        let fx = Fixture::new();
2095        let rd = fx.ref_data();
2096        let info = LayoutCallbackInfo::new(&rd, win(1280.0, 720.0, 192), WindowTheme::DarkMode);
2097
2098        assert_eq!(info.relayout_reason(), RelayoutReason::Initial);
2099        assert_eq!(info.theme, WindowTheme::DarkMode);
2100        assert_eq!(info.get_window_width(), 1280.0);
2101        assert_eq!(info.get_window_height(), 720.0);
2102        assert_eq!(info.get_dpi_factor(), 2.0);
2103        assert!(info.get_ctx().is_none());
2104
2105        // the borrowed resources are reachable through the laundered pointer
2106        assert!(core::ptr::eq(info.internal_get_image_cache(), &fx.images));
2107        assert!(core::ptr::eq(info.internal_get_system_fonts(), &fx.fonts));
2108        assert!(core::ptr::eq(info.internal_get_gl_context(), &fx.gl));
2109        assert!(info.get_gl_context().is_none());
2110    }
2111
2112    #[test]
2113    fn layout_callback_info_new_with_reason_round_trips_every_reason() {
2114        let fx = Fixture::new();
2115        let rd = fx.ref_data();
2116
2117        for reason in [
2118            RelayoutReason::Initial,
2119            RelayoutReason::RefreshDom,
2120            RelayoutReason::Resize,
2121            RelayoutReason::ThemeChange,
2122            RelayoutReason::RouteChange,
2123            RelayoutReason::Other,
2124        ] {
2125            let info = LayoutCallbackInfo::new_with_reason(
2126                &rd,
2127                WindowSize::default(),
2128                WindowTheme::LightMode,
2129                reason,
2130            );
2131            assert_eq!(info.relayout_reason(), reason);
2132            // clone must preserve it
2133            assert_eq!(info.clone().relayout_reason(), reason);
2134        }
2135
2136        assert_eq!(RelayoutReason::default(), RelayoutReason::Initial);
2137    }
2138
2139    #[test]
2140    fn layout_callback_info_get_system_style_shares_the_arc() {
2141        let fx = Fixture::new();
2142        let rd = fx.ref_data();
2143        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2144
2145        let a = info.get_system_style();
2146        let b = info.get_system_style();
2147        // it is a clone of the *same* Arc, not a fresh deep copy
2148        assert!(Arc::ptr_eq(&a, &b));
2149        assert!(Arc::ptr_eq(&a, &fx.style));
2150
2151        // repeated cloning must not leak/underflow the refcount
2152        let before = Arc::strong_count(&fx.style);
2153        for _ in 0..128 {
2154            drop(info.get_system_style());
2155        }
2156        assert_eq!(Arc::strong_count(&fx.style), before);
2157    }
2158
2159    #[test]
2160    fn layout_callback_info_get_ctx_is_none_until_set_then_clones_safely() {
2161        let fx = Fixture::new();
2162        let rd = fx.ref_data();
2163        let mut info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2164
2165        assert!(info.get_ctx().is_none(), "native path must have a null ctx");
2166
2167        let callable = OptionRefAny::Some(RefAny::new(7u64));
2168        info.set_callable_ptr(&callable);
2169
2170        for _ in 0..64 {
2171            assert!(info.get_ctx().is_some());
2172        }
2173
2174        let mut got = info.get_ctx();
2175        match got {
2176            OptionRefAny::Some(ref mut r) => assert_eq!(*r.downcast_ref::<u64>().unwrap(), 7),
2177            OptionRefAny::None => panic!("get_ctx() lost the callable"),
2178        }
2179        drop(got);
2180
2181        // a clone of the info keeps pointing at the same callable
2182        let cloned = info.clone();
2183        assert!(cloned.get_ctx().is_some());
2184    }
2185
2186    #[test]
2187    fn layout_callback_info_get_system_fonts_is_empty_for_an_empty_cache() {
2188        let fx = Fixture::new();
2189        let rd = fx.ref_data();
2190        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2191
2192        // an empty FcFontCache must yield an empty list, not panic
2193        let fonts: Vec<AzStringPair> = info.get_system_fonts();
2194        assert!(fonts.is_empty());
2195        // and be stable across calls
2196        assert_eq!(info.get_system_fonts().len(), fonts.len());
2197    }
2198
2199    // ---- LayoutCallbackInfo::get_image --------------------------------------
2200
2201    #[test]
2202    fn get_image_returns_none_for_missing_empty_and_hostile_ids() {
2203        let fx = Fixture::new();
2204        let rd = fx.ref_data();
2205        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2206
2207        assert!(info.get_image(&s("")).is_none());
2208        assert!(info.get_image(&s("   ")).is_none());
2209        assert!(info.get_image(&s("nope")).is_none());
2210        assert!(info.get_image(&s("\u{1F600}\u{0301}")).is_none());
2211        assert!(info.get_image(&s("\0")).is_none());
2212        assert!(info.get_image(&s(&"x".repeat(100_000))).is_none());
2213    }
2214
2215    #[test]
2216    fn get_image_finds_an_inserted_id_and_is_exact_match() {
2217        let mut fx = Fixture::new();
2218        fx.images.add_css_image_id(
2219            s("logo"),
2220            ImageRef::null_image(2, 2, RawImageFormat::RGBA8, Vec::new()),
2221        );
2222        let rd = fx.ref_data();
2223        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2224
2225        assert!(info.get_image(&s("logo")).is_some(), "positive control");
2226
2227        // lookup is exact: no trimming, no case folding, no prefix matching
2228        assert!(info.get_image(&s("Logo")).is_none());
2229        assert!(info.get_image(&s(" logo")).is_none());
2230        assert!(info.get_image(&s("logo ")).is_none());
2231        assert!(info.get_image(&s("log")).is_none());
2232        assert!(info.get_image(&s("logos")).is_none());
2233    }
2234
2235    // ---- LayoutCallbackInfo::get_active_route / get_route_param -------------
2236
2237    #[test]
2238    fn get_route_param_returns_none_when_no_route_is_active() {
2239        let fx = Fixture::new();
2240        let rd = fx.ref_data();
2241        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2242
2243        assert!(info.get_active_route().is_none());
2244
2245        // every hostile key must take the `?` early-out, never panic
2246        for key in ["", " ", "\t\n", "id", "\u{1F600}", "\0", "../../etc/passwd"] {
2247            assert!(info.get_route_param(key).is_none(), "key {key:?}");
2248        }
2249    }
2250
2251    #[test]
2252    fn get_route_param_valid_minimal_and_unicode_positive_controls() {
2253        let fx = Fixture::with_route(user_route());
2254        let rd = fx.ref_data();
2255        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2256
2257        let route = info.get_active_route().expect("route was configured");
2258        assert_eq!(route.pattern.as_str(), "/user/:id");
2259
2260        // positive control
2261        assert_eq!(info.get_route_param("id").map(AzString::as_str), Some("42"));
2262        // multibyte key round-trips
2263        assert_eq!(
2264            info.get_route_param("\u{1F600}").map(AzString::as_str),
2265            Some("emoji")
2266        );
2267    }
2268
2269    #[test]
2270    fn get_route_param_rejects_malformed_keys_without_trimming_or_folding() {
2271        let fx = Fixture::with_route(user_route());
2272        let rd = fx.ref_data();
2273        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2274
2275        // empty / whitespace-only
2276        assert!(info.get_route_param("").is_none());
2277        assert!(info.get_route_param("   ").is_none());
2278        assert!(info.get_route_param("\t\n").is_none());
2279
2280        // leading/trailing junk is NOT trimmed, and lookup is case-sensitive
2281        assert!(info.get_route_param(" id").is_none());
2282        assert!(info.get_route_param("id ").is_none());
2283        assert!(info.get_route_param("  id  ").is_none());
2284        assert!(info.get_route_param("id;garbage").is_none());
2285        assert!(info.get_route_param("ID").is_none());
2286        assert!(info.get_route_param("Id").is_none());
2287
2288        // no prefix / substring matching
2289        assert!(info.get_route_param("i").is_none());
2290        assert!(info.get_route_param("idd").is_none());
2291
2292        // garbage bytes, NUL, control chars
2293        assert!(info.get_route_param("\0").is_none());
2294        assert!(info.get_route_param("id\0").is_none());
2295        assert!(info.get_route_param("\u{7F}\u{1}\u{2}").is_none());
2296
2297        // boundary numeric strings
2298        for key in [
2299            "0",
2300            "-0",
2301            "9223372036854775807",
2302            "-9223372036854775808",
2303            "18446744073709551616",
2304            "NaN",
2305            "inf",
2306            "-inf",
2307            "1e400",
2308            "0.0000000000000000001",
2309        ] {
2310            assert!(info.get_route_param(key).is_none(), "key {key:?}");
2311        }
2312
2313        // non-ASCII that is *not* a param, incl. combining marks
2314        assert!(info.get_route_param("i\u{0301}d").is_none());
2315        assert!(info.get_route_param("\u{1F600}\u{1F600}").is_none());
2316    }
2317
2318    #[test]
2319    fn get_route_param_handles_pathological_key_sizes_and_nesting() {
2320        let fx = Fixture::with_route(user_route());
2321        let rd = fx.ref_data();
2322        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2323
2324        // extremely long key: must return None quickly, not hang or overflow
2325        let huge = "x".repeat(1_000_000);
2326        assert!(info.get_route_param(&huge).is_none());
2327
2328        // a long key that *shares a prefix* with a real param
2329        let long_id = alloc::format!("id{}", "0".repeat(1_000_000));
2330        assert!(info.get_route_param(&long_id).is_none());
2331
2332        // deeply nested brackets: the lookup is a flat scan, so this must not
2333        // recurse or stack-overflow
2334        let nested = "[".repeat(10_000) + &"]".repeat(10_000);
2335        assert!(info.get_route_param(&nested).is_none());
2336    }
2337
2338    #[test]
2339    fn get_route_param_preserves_huge_and_unicode_values() {
2340        let big = "v".repeat(200_000);
2341        let route = RouteMatch {
2342            pattern: s("/blob/:data"),
2343            params: StringPairVec::from_vec(Vec::from([AzStringPair {
2344                key: s("data"),
2345                value: s(&big),
2346            }])),
2347        };
2348        let fx = Fixture::with_route(route);
2349        let rd = fx.ref_data();
2350        let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2351
2352        let got = info.get_route_param("data").expect("param exists");
2353        assert_eq!(got.as_str().len(), 200_000);
2354    }
2355
2356    // ---- LayoutCallbackInfo: responsive predicates ---------------------------
2357
2358    #[test]
2359    fn window_predicates_obey_trichotomy_and_the_between_identity() {
2360        let fx = Fixture::new();
2361        let rd = fx.ref_data();
2362
2363        let probes = [
2364            0.0f32,
2365            -0.0,
2366            1.0,
2367            -1.0,
2368            640.0,
2369            f32::MIN,
2370            f32::MAX,
2371            f32::MIN_POSITIVE,
2372            f32::INFINITY,
2373            f32::NEG_INFINITY,
2374        ];
2375
2376        for &dim in &probes {
2377            let info = LayoutCallbackInfo::new(&rd, win(dim, dim, 96), WindowTheme::LightMode);
2378
2379            for &px in &probes {
2380                let lt = info.window_width_less_than(px);
2381                let gt = info.window_width_greater_than(px);
2382                let eq = info.get_window_width() == px;
2383
2384                // exactly one of <, >, == holds for non-NaN operands
2385                assert_eq!(
2386                    u8::from(lt) + u8::from(gt) + u8::from(eq),
2387                    1,
2388                    "trichotomy broken for width {dim} vs {px}"
2389                );
2390
2391                // height predicates mirror the width ones on a square window
2392                assert_eq!(info.window_height_less_than(px), lt);
2393                assert_eq!(info.window_height_greater_than(px), gt);
2394
2395                for &px2 in &probes {
2396                    // between(a, b) == !(w < a) && !(w > b)
2397                    assert_eq!(
2398                        info.window_width_between(px, px2),
2399                        !info.window_width_less_than(px) && !info.window_width_greater_than(px2),
2400                        "between identity broken for width {dim} in [{px}, {px2}]"
2401                    );
2402                    assert_eq!(
2403                        info.window_height_between(px, px2),
2404                        info.window_width_between(px, px2)
2405                    );
2406                }
2407            }
2408        }
2409    }
2410
2411    #[test]
2412    fn window_predicates_with_inverted_and_degenerate_ranges() {
2413        let fx = Fixture::new();
2414        let rd = fx.ref_data();
2415        let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
2416
2417        // inverted range is always empty
2418        assert!(!info.window_width_between(1000.0, 100.0));
2419        assert!(!info.window_height_between(1000.0, 100.0));
2420
2421        // degenerate (min == max) range is inclusive on both ends
2422        assert!(info.window_width_between(640.0, 640.0));
2423        assert!(info.window_height_between(480.0, 480.0));
2424        assert!(!info.window_width_between(639.9, 639.95));
2425
2426        // inclusive boundaries
2427        assert!(info.window_width_between(640.0, 1000.0));
2428        assert!(info.window_width_between(0.0, 640.0));
2429
2430        // strictness at the exact boundary
2431        assert!(!info.window_width_less_than(640.0));
2432        assert!(!info.window_width_greater_than(640.0));
2433        assert!(info.window_width_less_than(640.001));
2434        assert!(info.window_width_greater_than(639.999));
2435
2436        // the widest possible range contains a finite width
2437        assert!(info.window_width_between(f32::NEG_INFINITY, f32::INFINITY));
2438    }
2439
2440    #[test]
2441    fn window_predicates_are_all_false_for_nan_probes() {
2442        let fx = Fixture::new();
2443        let rd = fx.ref_data();
2444        let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
2445
2446        // every comparison against NaN is false - no panic, no accidental `true`
2447        assert!(!info.window_width_less_than(f32::NAN));
2448        assert!(!info.window_width_greater_than(f32::NAN));
2449        assert!(!info.window_width_between(f32::NAN, f32::NAN));
2450        assert!(!info.window_width_between(f32::NAN, 10_000.0));
2451        assert!(!info.window_width_between(0.0, f32::NAN));
2452
2453        assert!(!info.window_height_less_than(f32::NAN));
2454        assert!(!info.window_height_greater_than(f32::NAN));
2455        assert!(!info.window_height_between(f32::NAN, f32::NAN));
2456        assert!(!info.window_height_between(f32::NAN, 10_000.0));
2457        assert!(!info.window_height_between(0.0, f32::NAN));
2458    }
2459
2460    #[test]
2461    fn window_predicates_are_all_false_for_a_nan_sized_window() {
2462        let fx = Fixture::new();
2463        let rd = fx.ref_data();
2464        let info = LayoutCallbackInfo::new(
2465            &rd,
2466            win(f32::NAN, f32::NAN, 96),
2467            WindowTheme::LightMode,
2468        );
2469
2470        assert!(info.get_window_width().is_nan());
2471        assert!(info.get_window_height().is_nan());
2472
2473        // a NaN window is neither smaller, larger, nor within any range
2474        for px in [0.0f32, 640.0, f32::MAX, f32::INFINITY, f32::NEG_INFINITY] {
2475            assert!(!info.window_width_less_than(px));
2476            assert!(!info.window_width_greater_than(px));
2477            assert!(!info.window_height_less_than(px));
2478            assert!(!info.window_height_greater_than(px));
2479            assert!(!info.window_width_between(f32::NEG_INFINITY, px));
2480            assert!(!info.window_height_between(px, f32::INFINITY));
2481        }
2482    }
2483
2484    #[test]
2485    fn get_dpi_factor_at_zero_and_u32_limits() {
2486        let fx = Fixture::new();
2487        let rd = fx.ref_data();
2488
2489        // 96 DPI is the 1.0 baseline
2490        let base = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 96), WindowTheme::LightMode);
2491        assert_eq!(base.get_dpi_factor(), 1.0);
2492
2493        let hidpi = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 192), WindowTheme::LightMode);
2494        assert_eq!(hidpi.get_dpi_factor(), 2.0);
2495
2496        // dpi = 0 must not divide-by-zero-panic; it yields 0.0
2497        let zero = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 0), WindowTheme::LightMode);
2498        assert_eq!(zero.get_dpi_factor(), 0.0);
2499
2500        // u32::MAX must not overflow the f32 cast - it stays finite
2501        let max = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, u32::MAX), WindowTheme::LightMode);
2502        let f = max.get_dpi_factor();
2503        assert!(f.is_finite() && f > 0.0, "dpi factor {f} is not finite");
2504        assert_eq!(f, (u32::MAX as f32) / 96.0);
2505
2506        // dpi = 1 rounds to a tiny-but-positive factor rather than 0
2507        let one = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 1), WindowTheme::LightMode);
2508        assert!(one.get_dpi_factor() > 0.0);
2509    }
2510
2511    // ---- HidpiAdjustedBounds -------------------------------------------------
2512
2513    #[test]
2514    fn hidpi_adjusted_bounds_from_bounds_holds_its_fields() {
2515        let b = HidpiAdjustedBounds::from_bounds(
2516            LayoutSize::new(800, 600),
2517            DpiScaleFactor::new(1.5),
2518        );
2519        assert_eq!(b.get_logical_size(), LogicalSize::new(800.0, 600.0));
2520        assert_eq!(b.get_hidpi_factor(), DpiScaleFactor::new(1.5));
2521        assert_eq!(b.logical_size, b.get_logical_size());
2522        assert_eq!(b.hidpi_factor, b.get_hidpi_factor());
2523
2524        let p = b.get_physical_size();
2525        assert_eq!(p.width, 1200);
2526        assert_eq!(p.height, 900);
2527    }
2528
2529    #[test]
2530    fn hidpi_adjusted_bounds_at_zero() {
2531        let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(0, 0), DpiScaleFactor::new(1.0));
2532        assert_eq!(b.get_logical_size(), LogicalSize::zero());
2533        let p = b.get_physical_size();
2534        assert_eq!(p.width, 0);
2535        assert_eq!(p.height, 0);
2536
2537        // a zero scale factor collapses any size to 0x0 without panicking
2538        let z = HidpiAdjustedBounds::from_bounds(
2539            LayoutSize::new(1920, 1080),
2540            DpiScaleFactor::new(0.0),
2541        );
2542        let zp = z.get_physical_size();
2543        assert_eq!(zp.width, 0);
2544        assert_eq!(zp.height, 0);
2545    }
2546
2547    /// `get_physical_size` funnels through `roundf(x) as u32`, which is a
2548    /// *saturating* float->int cast in Rust: negatives clamp to 0, huge values
2549    /// clamp to u32::MAX, NaN becomes 0. Pin that down so a future refactor to
2550    /// an unchecked cast (UB) or a panicking one is caught.
2551    #[test]
2552    fn hidpi_adjusted_bounds_physical_size_saturates_on_negative_input() {
2553        let b = HidpiAdjustedBounds::from_bounds(
2554            LayoutSize::new(-100, -50),
2555            DpiScaleFactor::new(1.0),
2556        );
2557        assert_eq!(b.get_logical_size(), LogicalSize::new(-100.0, -50.0));
2558
2559        let p = b.get_physical_size();
2560        assert_eq!(p.width, 0, "negative logical width must clamp to 0, not wrap");
2561        assert_eq!(p.height, 0, "negative logical height must clamp to 0, not wrap");
2562
2563        // negative scale factor on a positive size clamps the same way
2564        let neg_scale = HidpiAdjustedBounds::from_bounds(
2565            LayoutSize::new(100, 100),
2566            DpiScaleFactor::new(-2.0),
2567        );
2568        let np = neg_scale.get_physical_size();
2569        assert_eq!(np.width, 0);
2570        assert_eq!(np.height, 0);
2571    }
2572
2573    #[test]
2574    fn hidpi_adjusted_bounds_physical_size_saturates_at_the_upper_limit() {
2575        // isize::MAX logical px * 1.0 overflows u32 -> must saturate, not wrap
2576        let b = HidpiAdjustedBounds::from_bounds(
2577            LayoutSize::new(isize::MAX, isize::MAX),
2578            DpiScaleFactor::new(1.0),
2579        );
2580        let p = b.get_physical_size();
2581        assert_eq!(p.width, u32::MAX);
2582        assert_eq!(p.height, u32::MAX);
2583
2584        // isize::MIN saturates downwards to 0
2585        let min = HidpiAdjustedBounds::from_bounds(
2586            LayoutSize::new(isize::MIN, isize::MIN),
2587            DpiScaleFactor::new(1.0),
2588        );
2589        let mp = min.get_physical_size();
2590        assert_eq!(mp.width, 0);
2591        assert_eq!(mp.height, 0);
2592
2593        // a huge scale factor on a modest size also saturates
2594        let huge_scale = HidpiAdjustedBounds::from_bounds(
2595            LayoutSize::new(1000, 1000),
2596            DpiScaleFactor::new(f32::MAX),
2597        );
2598        let hp = huge_scale.get_physical_size();
2599        assert_eq!(hp.width, u32::MAX);
2600        assert_eq!(hp.height, u32::MAX);
2601    }
2602
2603    /// `DpiScaleFactor` stores its f32 in a fixed-point `isize` (x1000), so
2604    /// NaN quantizes to 0 and +/-inf quantize to the isize limits. Assert the
2605    /// *observable* consequence rather than a panic.
2606    #[test]
2607    fn hidpi_adjusted_bounds_physical_size_with_nan_and_infinite_scale() {
2608        let nan = HidpiAdjustedBounds::from_bounds(
2609            LayoutSize::new(100, 100),
2610            DpiScaleFactor::new(f32::NAN),
2611        );
2612        // NaN -> fixed-point 0 -> 0.0 scale -> 0x0 physical
2613        assert_eq!(nan.get_hidpi_factor().inner.get(), 0.0);
2614        let np = nan.get_physical_size();
2615        assert_eq!(np.width, 0);
2616        assert_eq!(np.height, 0);
2617
2618        let inf = HidpiAdjustedBounds::from_bounds(
2619            LayoutSize::new(100, 100),
2620            DpiScaleFactor::new(f32::INFINITY),
2621        );
2622        // +inf -> saturated fixed-point -> huge (but finite) scale
2623        assert!(inf.get_hidpi_factor().inner.get().is_finite());
2624        let ip = inf.get_physical_size();
2625        assert_eq!(ip.width, u32::MAX);
2626        assert_eq!(ip.height, u32::MAX);
2627
2628        let neg_inf = HidpiAdjustedBounds::from_bounds(
2629            LayoutSize::new(100, 100),
2630            DpiScaleFactor::new(f32::NEG_INFINITY),
2631        );
2632        let nip = neg_inf.get_physical_size();
2633        assert_eq!(nip.width, 0);
2634        assert_eq!(nip.height, 0);
2635    }
2636
2637    #[test]
2638    fn hidpi_adjusted_bounds_physical_size_rounds_to_nearest() {
2639        // 0.5px rounds away from zero (libm::roundf), not truncates
2640        let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(3, 3), DpiScaleFactor::new(1.5));
2641        let p = b.get_physical_size();
2642        assert_eq!(p.width, 5, "3 * 1.5 = 4.5 must round to 5");
2643        assert_eq!(p.height, 5);
2644
2645        // idempotent: repeated calls give the same answer
2646        let p2 = b.get_physical_size();
2647        assert_eq!(p.width, p2.width);
2648        assert_eq!(p.height, p2.height);
2649    }
2650
2651    // ---- CoreCallbackDataVec -------------------------------------------------
2652
2653    fn cb_data(cb: usize) -> CoreCallbackData {
2654        CoreCallbackData {
2655            event: EventFilter::Hover(HoverEventFilter::MouseOver),
2656            callback: CoreCallback::from(cb),
2657            refany: RefAny::new(cb),
2658        }
2659    }
2660
2661    #[test]
2662    fn core_callback_data_vec_as_container_on_empty_vecs_does_not_panic() {
2663        // both the const-empty and the heap-empty representation must produce
2664        // a valid (length-0) container - a null-ptr slice here would be UB
2665        let empty = CoreCallbackDataVec::new();
2666        assert_eq!(empty.as_container().len(), 0);
2667        assert!(empty.as_container().internal.is_empty());
2668
2669        let from_empty_vec = CoreCallbackDataVec::from_vec(Vec::new());
2670        assert_eq!(from_empty_vec.as_container().len(), 0);
2671
2672        let mut mut_empty = CoreCallbackDataVec::from_vec(Vec::new());
2673        assert!(mut_empty.as_container_mut().internal.is_empty());
2674    }
2675
2676    #[test]
2677    fn core_callback_data_vec_as_container_matches_the_backing_vec() {
2678        let v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2), cb_data(3)]));
2679
2680        let c = v.as_container();
2681        assert_eq!(c.len(), 3);
2682        assert_eq!(c.len(), v.len());
2683        assert_eq!(c.internal[0].callback.cb, 1);
2684        assert_eq!(c.internal[2].callback.cb, 3);
2685
2686        // the container borrows - it does not copy
2687        assert!(core::ptr::eq(c.internal.as_ptr(), v.as_slice().as_ptr()));
2688    }
2689
2690    #[test]
2691    fn core_callback_data_vec_as_container_mut_writes_through() {
2692        let mut v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2)]));
2693
2694        {
2695            let mut c = v.as_container_mut();
2696            assert_eq!(c.internal.len(), 2);
2697            c.internal[0].callback.cb = 99;
2698            c.internal[1].event = EventFilter::Hover(HoverEventFilter::MouseDown);
2699        }
2700
2701        // mutations are visible through the immutable container
2702        let c = v.as_container();
2703        assert_eq!(c.internal[0].callback.cb, 99);
2704        assert_eq!(
2705            c.internal[1].event,
2706            EventFilter::Hover(HoverEventFilter::MouseDown)
2707        );
2708        assert_eq!(c.len(), 2);
2709    }
2710}
2711
2712/// Tests for the recorded window-size queries — the responsive helpers
2713/// (`window_width_less_than` & co.) every `layout()` should branch on, and the
2714/// mechanism that lets a resize skip `layout()` when no recorded answer flips.
2715#[cfg(test)]
2716#[allow(clippy::float_cmp)]
2717mod size_query_tests {
2718    use super::*;
2719    use crate::geom::LogicalSize;
2720
2721    fn win(width: f32, height: f32) -> WindowSize {
2722        WindowSize {
2723            dimensions: LogicalSize::new(width, height),
2724            ..WindowSize::default()
2725        }
2726    }
2727
2728    fn info_at(rd: &LayoutCallbackInfoRefData<'_>, w: f32, h: f32) -> LayoutCallbackInfo {
2729        LayoutCallbackInfo::new(rd, win(w, h), WindowTheme::LightMode)
2730    }
2731
2732    fn drain() -> (alloc::vec::Vec<SizeQuery>, bool) {
2733        take_recorded_size_queries()
2734    }
2735
2736    /// Build the minimal ref-data a `LayoutCallbackInfo` needs. The queries
2737    /// only read `window_size`, so everything else can be empty.
2738    struct Rd {
2739        image_cache: crate::resources::ImageCache,
2740        gl: crate::gl::OptionGlContextPtr,
2741        fonts: rust_fontconfig::FcFontCache,
2742        style: alloc::sync::Arc<azul_css::system::SystemStyle>,
2743    }
2744    impl Rd {
2745        fn new() -> Self {
2746            Self {
2747                image_cache: crate::resources::ImageCache::default(),
2748                gl: crate::gl::OptionGlContextPtr::None,
2749                fonts: rust_fontconfig::FcFontCache::default(),
2750                style: alloc::sync::Arc::new(azul_css::system::SystemStyle::default()),
2751            }
2752        }
2753        fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
2754            LayoutCallbackInfoRefData {
2755                image_cache: &self.image_cache,
2756                gl_context: &self.gl,
2757                system_fonts: &self.fonts,
2758                system_style: self.style.clone(),
2759                active_route: None,
2760                monitors: crate::window::MonitorVec::from_const_slice(&[]),
2761            }
2762        }
2763    }
2764
2765    #[test]
2766    fn every_responsive_helper_records_with_its_exact_operator() {
2767        let rd = Rd::new();
2768        let rd = rd.ref_data();
2769        let _ = drain();
2770
2771        let info = info_at(&rd, 800.0, 600.0);
2772        assert!(!info.window_width_less_than(800.0), "strict <: boundary is false");
2773        assert!(!info.window_width_greater_than(800.0), "strict >: boundary is false");
2774        assert!(info.window_width_between(800.0, 1024.0), "between is inclusive");
2775        assert!(info.window_height_less_than(601.0));
2776        assert!(!info.window_height_greater_than(600.0));
2777        assert!(info.window_height_between(0.0, 600.0));
2778
2779        let (recorded, overflowed) = drain();
2780        // between records BOTH of its bounds, so 4 single-bound calls + 2
2781        // between calls = 8 queries.
2782        assert_eq!(recorded.len(), 8, "every call recorded; between records two bounds");
2783        assert!(!overflowed);
2784        assert_eq!(recorded[0].op, SizeQueryOp::LessThan);
2785        assert_eq!(recorded[1].op, SizeQueryOp::GreaterThan);
2786        assert_eq!(recorded[2].op, SizeQueryOp::GreaterOrEqual);
2787        assert_eq!(recorded[3].op, SizeQueryOp::LessOrEqual);
2788    }
2789
2790    #[test]
2791    fn flips_at_detects_exactly_the_crossings() {
2792        let rd = Rd::new();
2793        let rd = rd.ref_data();
2794        let _ = drain();
2795
2796        let info = info_at(&rd, 800.0, 600.0);
2797        let mobile = info.window_width_less_than(640.0); // false at 800
2798        assert!(!mobile);
2799        let (recorded, _) = drain();
2800        let q = recorded[0];
2801
2802        // Shrinking within the desktop range does not flip…
2803        assert!(!q.flips_at(LogicalSize::new(700.0, 600.0)));
2804        assert!(!q.flips_at(LogicalSize::new(640.0, 600.0)), "strict <: 640 is still false");
2805        // …crossing the queried threshold does…
2806        assert!(q.flips_at(LogicalSize::new(639.9, 600.0)));
2807        assert!(q.flips_at(LogicalSize::new(320.0, 600.0)));
2808        // …and the other axis is irrelevant to a width query.
2809        assert!(!q.flips_at(LogicalSize::new(700.0, 10.0)));
2810    }
2811
2812    /// `between` must flip on BOTH of its bounds, inclusively — the reason
2813    /// [`SizeQueryOp`] has four exact operators instead of a bool.
2814    #[test]
2815    fn between_flips_on_either_bound_with_inclusive_semantics() {
2816        let rd = Rd::new();
2817        let rd = rd.ref_data();
2818        let _ = drain();
2819
2820        let info = info_at(&rd, 800.0, 600.0);
2821        assert!(info.window_width_between(768.0, 1024.0));
2822        let (recorded, _) = drain();
2823        let lower = recorded[0];
2824        let upper = recorded[1];
2825
2826        assert!(!lower.flips_at(LogicalSize::new(768.0, 600.0)), ">= 768: boundary holds");
2827        assert!(lower.flips_at(LogicalSize::new(767.9, 600.0)));
2828        assert!(!upper.flips_at(LogicalSize::new(1024.0, 600.0)), "<= 1024: boundary holds");
2829        assert!(upper.flips_at(LogicalSize::new(1024.1, 600.0)));
2830    }
2831
2832    #[test]
2833    fn drain_resets_the_recording() {
2834        let rd = Rd::new();
2835        let rd = rd.ref_data();
2836        let _ = drain();
2837
2838        let info = info_at(&rd, 1024.0, 768.0);
2839        let _ = info.window_width_greater_than(640.0);
2840        let (first, _) = drain();
2841        assert_eq!(first.len(), 1);
2842        let (second, overflowed) = drain();
2843        assert!(second.is_empty(), "drain must reset");
2844        assert!(!overflowed);
2845    }
2846
2847    #[test]
2848    fn overflow_latches_and_reports_rather_than_dropping_silently() {
2849        let rd = Rd::new();
2850        let rd = rd.ref_data();
2851        let _ = drain();
2852
2853        let info = info_at(&rd, 1024.0, 768.0);
2854        for i in 0..(size_query_recorder::SIZE_QUERY_CAP + 10) {
2855            let _ = info.window_width_greater_than(i as f32);
2856        }
2857        let (recorded, overflowed) = drain();
2858        assert_eq!(recorded.len(), size_query_recorder::SIZE_QUERY_CAP);
2859        assert!(
2860            overflowed,
2861            "past the cap the drain MUST say the list is incomplete — silence \
2862             here is a resize skipping a layout() that would have branched"
2863        );
2864        // And the latch itself resets with the drain.
2865        let (_, overflowed2) = drain();
2866        assert!(!overflowed2);
2867    }
2868}