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::{NodeHierarchyItemId, NodeHierarchyItemVec, StyledNode, StyledNodeVec},
63    task::{
64        Duration as AzDuration, GetSystemTimeCallback, Instant as AzInstant, Instant,
65        TerminateTimer, ThreadId, ThreadReceiver, ThreadSendMsg, TimerId,
66    },
67    window::{
68        AzStringPair, KeyboardState, MouseState, OptionChar, RawWindowHandle, UpdateFocusWarning,
69        WindowFlags, WindowFrame, WindowSize, WindowTheme,
70    },
71    FastBTreeSet, OrderedMap,
72};
73
74/// Specifies if the screen should be updated after the callback function has returned
75#[repr(C)]
76#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub enum Update {
78    /// The screen does not need to redraw after the callback has been called
79    DoNothing,
80    /// After the callback is called, the screen needs to redraw (`layout()` function being called
81    /// again)
82    RefreshDom,
83    /// The layout has to be re-calculated for all windows
84    RefreshDomAllWindows,
85}
86
87impl Update {
88    pub fn max_self(&mut self, other: Self) {
89        if (*self == Self::DoNothing && other != Self::DoNothing)
90            || (*self == Self::RefreshDom && other == Self::RefreshDomAllWindows)
91        {
92            *self = other;
93        }
94    }
95}
96
97// -- layout callback
98
99/// Callback function pointer (has to be a function pointer in
100/// order to be compatible with C APIs later on).
101///
102/// IMPORTANT: The callback needs to deallocate the `RefAnyPtr` and `LayoutCallbackInfoPtr`,
103/// otherwise that memory is leaked. If you use the official auto-generated
104/// bindings, this is already done for you.
105///
106/// NOTE: The original callback was `fn(&self, LayoutCallbackInfo) -> Dom`
107/// which then evolved to `fn(&RefAny, LayoutCallbackInfo) -> Dom`.
108/// The indirection is necessary because of the memory management
109/// around the C API
110///
111/// The memory management across the callback boundary is handled by
112/// the caller (see `LayoutCallback` and `LayoutCallbackInfo`).
113pub type LayoutCallbackType = extern "C" fn(RefAny, LayoutCallbackInfo) -> Dom;
114
115extern "C" fn default_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
116    Dom::create_body()
117}
118
119/// Wrapper around the layout callback
120///
121/// For FFI languages (Python, Java, etc.), the `RefAny` contains both:
122/// - The user's application data
123/// - The callback function object from the foreign language
124///
125/// The trampoline function (stored in `cb`) knows how to extract both
126/// from the `RefAny` and invoke the foreign callback with the user data.
127#[repr(C)]
128pub struct LayoutCallback {
129    pub cb: LayoutCallbackType,
130    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
131    /// Native Rust code sets this to None
132    pub ctx: OptionRefAny,
133}
134
135impl_callback!(LayoutCallback, LayoutCallbackType);
136
137impl LayoutCallback {
138    pub fn create<I: Into<Self>>(cb: I) -> Self {
139        cb.into()
140    }
141}
142
143// Host-invoker plumbing for managed-FFI bindings (Lua, Ruby, Perl, …):
144// expands to a static `az_layout_callback_thunk` (the `cb` we hand to the
145// framework when the host calls `LayoutCallback::create_from_host_handle`),
146// an `AzLayoutCallback_createFromHostHandle` C-ABI export, plus the
147// `AzApp_setLayoutCallbackInvoker` setter the host calls once at module
148// load. See `crate::host_invoker` for the design.
149crate::impl_managed_callback! {
150    wrapper:        LayoutCallback,
151    info_ty:        LayoutCallbackInfo,
152    return_ty:      Dom,
153    default_ret:    Dom::create_body(),
154    invoker_static: LAYOUT_CALLBACK_INVOKER,
155    invoker_ty:     AzLayoutCallbackInvoker,
156    thunk_fn:       az_layout_callback_thunk,
157    setter_fn:      AzApp_setLayoutCallbackInvoker,
158    from_handle_fn: AzLayoutCallback_createFromHostHandle,
159}
160
161impl Default for LayoutCallback {
162    fn default() -> Self {
163        Self {
164            cb: default_layout_callback,
165            ctx: OptionRefAny::None,
166        }
167    }
168}
169
170// -- virtualized view callback
171
172pub type VirtualViewCallbackType =
173    extern "C" fn(RefAny, VirtualViewCallbackInfo) -> VirtualViewReturn;
174
175/// Callback that, given a rectangle area on the screen, returns the DOM
176/// appropriate for that bounds (useful for infinite lists)
177#[repr(C)]
178pub struct VirtualViewCallback {
179    pub cb: VirtualViewCallbackType,
180    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
181    /// Native Rust code sets this to None
182    pub ctx: OptionRefAny,
183}
184impl_callback!(VirtualViewCallback, VirtualViewCallbackType);
185
186// Host-invoker plumbing for VirtualViewCallback. See `crate::host_invoker`.
187crate::impl_managed_callback! {
188    wrapper:        VirtualViewCallback,
189    info_ty:        VirtualViewCallbackInfo,
190    return_ty:      VirtualViewReturn,
191    default_ret:    VirtualViewReturn::default(),
192    invoker_static: VIRTUAL_VIEW_CALLBACK_INVOKER,
193    invoker_ty:     AzVirtualViewCallbackInvoker,
194    thunk_fn:       az_virtual_view_callback_thunk,
195    setter_fn:      AzApp_setVirtualViewCallbackInvoker,
196    from_handle_fn: AzVirtualViewCallback_createFromHostHandle,
197}
198
199impl VirtualViewCallback {
200    pub fn create(cb: VirtualViewCallbackType) -> Self {
201        Self {
202            cb,
203            ctx: OptionRefAny::None,
204        }
205    }
206}
207
208// -- caret / selection tween callbacks (system text animations)
209//
210// The framework animates the caret and the selection highlight between their
211// previous and current geometry ("tween"). The MATH of the tween is a user-
212// replaceable C-ABI function set in `AppConfig.system_animations` (defaults
213// below): the framework drives a short timer, computes the linear progress
214// `t = elapsed / configured duration`, and calls the function to obtain the
215// geometry to RENDER this frame. While a tween is in flight the caret blink
216// is suppressed (the caret stays solid while it moves).
217
218/// Inputs for one caret-tween evaluation.
219#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
220#[repr(C)]
221pub struct CaretTweenInfo {
222    /// Caret rectangle the previous frame RENDERED (mid-flight retargets
223    /// start from the interpolated position, not the old logical one).
224    pub past: LogicalRect,
225    /// Caret rectangle the current layout actually wants.
226    pub current: LogicalRect,
227    /// Linear time progress `0.0..=1.0` (elapsed / configured duration).
228    /// Easing/curves are this function's job.
229    pub t: f32,
230}
231
232/// Returns the caret rectangle to render at progress `info.t`.
233pub type CaretTweenCallbackType = extern "C" fn(RefAny, CaretTweenInfo) -> LogicalRect;
234
235/// User-settable caret tween interpolator (see [`CaretTweenInfo`]).
236#[repr(C)]
237pub struct CaretTweenCallback {
238    pub cb: CaretTweenCallbackType,
239    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
240    /// Native Rust code sets this to None
241    pub ctx: OptionRefAny,
242}
243impl_callback!(CaretTweenCallback, CaretTweenCallbackType);
244
245impl CaretTweenCallback {
246    pub fn create(cb: CaretTweenCallbackType) -> Self {
247        Self {
248            cb,
249            ctx: OptionRefAny::None,
250        }
251    }
252}
253
254/// Inputs for one selection-tween evaluation.
255///
256/// Carries the full PAST and CURRENT selection band geometry: all rectangles
257/// of the selection highlight, in display-list order — spanning multiple
258/// lines and, for a cross-block selection, multiple nodes.
259#[derive(Debug, Clone, PartialEq, PartialOrd)]
260#[repr(C)]
261pub struct SelectionTweenInfo {
262    /// Selection rectangles the previous frame RENDERED.
263    pub past: LogicalRectVec,
264    /// Selection rectangles the current layout actually wants.
265    pub current: LogicalRectVec,
266    /// Linear time progress `0.0..=1.0` (elapsed / configured duration).
267    pub t: f32,
268}
269
270/// Returns the selection rectangles to render at progress `info.t`.
271/// MUST return exactly `info.current.len()` rectangles — a mismatched
272/// length makes the framework fall back to `info.current` unanimated.
273pub type SelectionTweenCallbackType = extern "C" fn(RefAny, SelectionTweenInfo) -> LogicalRectVec;
274
275/// User-settable selection tween interpolator (see [`SelectionTweenInfo`]).
276#[repr(C)]
277pub struct SelectionTweenCallback {
278    pub cb: SelectionTweenCallbackType,
279    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
280    /// Native Rust code sets this to None
281    pub ctx: OptionRefAny,
282}
283impl_callback!(SelectionTweenCallback, SelectionTweenCallbackType);
284
285impl SelectionTweenCallback {
286    pub fn create(cb: SelectionTweenCallbackType) -> Self {
287        Self {
288            cb,
289            ctx: OptionRefAny::None,
290        }
291    }
292}
293
294/// Trapezoidal velocity profile: velocity ramps up HARD over the first
295/// `RAMP` of the duration, cruises at constant speed, and ramps down hard
296/// over the last `RAMP` — a `/‾‾‾\` velocity curve. In position terms:
297/// a brief quadratic ease-in, a LINEAR middle, a brief quadratic ease-out.
298/// Chosen over ease-out-cubic for the caret/selection defaults: at the
299/// very short default durations the motion should read as "barely
300/// noticeable glide", not as a spring (user directive). Analytic integral,
301/// exact — a cubic bezier cannot express the flat-velocity plateau.
302#[inline]
303fn trapezoid_ease(t: f32) -> f32 {
304    const RAMP: f32 = 0.25;
305    // Peak velocity so the total distance integrates to exactly 1.
306    const V: f32 = 1.0 / (1.0 - RAMP);
307    let t = t.clamp(0.0, 1.0);
308    if t < RAMP {
309        V * t * t / (2.0 * RAMP)
310    } else if t <= 1.0 - RAMP {
311        V * (RAMP / 2.0 + (t - RAMP))
312    } else {
313        let inv = 1.0 - t;
314        1.0 - V * inv * inv / (2.0 * RAMP)
315    }
316}
317
318#[inline]
319// Plain `a + (b - a) * e`, NOT mul_add: fused multiply-add changes f32
320// results, and tween geometry must be bit-reproducible across builds (the
321// e2e corpus pins pixel-exact frames).
322#[allow(clippy::suboptimal_flops)]
323fn lerp_rect(from: LogicalRect, to: LogicalRect, e: f32) -> LogicalRect {
324    LogicalRect {
325        origin: LogicalPosition {
326            x: from.origin.x + (to.origin.x - from.origin.x) * e,
327            y: from.origin.y + (to.origin.y - from.origin.y) * e,
328        },
329        size: LogicalSize {
330            width: from.size.width + (to.size.width - from.size.width) * e,
331            height: from.size.height + (to.size.height - from.size.height) * e,
332        },
333    }
334}
335
336/// Default caret tween: trapezoidal-velocity lerp of origin and size
337/// (hard rise, linear cruise, hard fall — see [`trapezoid_ease`]).
338#[must_use]
339pub extern "C" fn default_caret_tween(_data: RefAny, info: CaretTweenInfo) -> LogicalRect {
340    lerp_rect(info.past, info.current, trapezoid_ease(info.t))
341}
342
343/// Default selection tween: trapezoidal-velocity lerp, rectangles paired by
344/// the LINE they sit on — not by their position in the list.
345///
346/// Index pairing broke every UPWARD extension: growing the selection upward
347/// prepends a rect, which shifts every later rect one slot, so each line lerped
348/// from the geometry of the line ABOVE it and the whole band visibly slid.
349/// Geometric pairing is stable under insertion at either end.
350///
351/// Rectangles with no counterpart on their line (a line the selection did not
352/// cover before) appear at their final geometry immediately. Each past
353/// rectangle is consumed at most once, so a line that bidi splits into several
354/// rectangles still pairs one-to-one.
355#[must_use]
356pub extern "C" fn default_selection_tween(
357    _data: RefAny,
358    info: SelectionTweenInfo,
359) -> LogicalRectVec {
360    let e = trapezoid_ease(info.t);
361    let past = info.past.as_ref();
362    let mut taken = alloc::vec![false; past.len()];
363    let out: Vec<LogicalRect> = info
364        .current
365        .as_ref()
366        .iter()
367        .map(|cur| {
368            take_same_line_rect(past, &mut taken, *cur).map_or(*cur, |p| lerp_rect(p, *cur, e))
369        })
370        .collect();
371    out.into()
372}
373
374/// The not-yet-consumed `past` rectangle sitting on the same line as `cur` —
375/// the closest one vertically, ties to the earlier one — marked consumed.
376/// `None` when no past rectangle shares that line.
377///
378/// "Same line" means the vertical CENTRES are within half the shorter
379/// rectangle's height of each other: a line that shifted by a fraction of its
380/// own height is still recognised (and glides there), a different line never
381/// is. The test is a positive comparison, which NaN fails, so garbage geometry
382/// pops instead of pairing wrongly.
383fn take_same_line_rect(
384    past: &[LogicalRect],
385    taken: &mut [bool],
386    cur: LogicalRect,
387) -> Option<LogicalRect> {
388    let cur_centre = cur.origin.y + cur.size.height / 2.0;
389    let mut best: Option<(usize, f32)> = None;
390
391    for (i, p) in past.iter().enumerate() {
392        if taken.get(i).copied().unwrap_or(true) {
393            continue;
394        }
395        let dy = (p.origin.y + p.size.height / 2.0 - cur_centre).abs();
396        let tolerance = p.size.height.min(cur.size.height) / 2.0;
397        if dy <= tolerance && best.is_none_or(|(_, best_dy)| dy < best_dy) {
398            best = Some((i, dy));
399        }
400    }
401
402    let (idx, _) = best?;
403    if let Some(slot) = taken.get_mut(idx) {
404        *slot = true;
405    }
406    past.get(idx).copied()
407}
408
409/// Reason why a `VirtualView` callback is being invoked.
410///
411/// This helps the callback optimize its behavior based on why it's being called.
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413#[repr(C, u8)]
414pub enum VirtualViewCallbackReason {
415    /// Initial render - first time the `VirtualView` appears
416    InitialRender,
417    /// Parent DOM was recreated (cache invalidated)
418    DomRecreated,
419    /// Window/VirtualView bounds expanded beyond current `scroll_size`
420    BoundsExpanded,
421    /// Scroll position is near an edge (within `EDGE_THRESHOLD`, currently 200px)
422    EdgeScrolled(EdgeType),
423    /// Scroll position extends beyond current `scroll_size`
424    ScrollBeyondContent,
425}
426
427/// Which edge triggered a scroll-based re-invocation
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429#[repr(C)]
430pub enum EdgeType {
431    Top,
432    Bottom,
433    Left,
434    Right,
435}
436
437#[derive(Debug)]
438#[repr(C)]
439pub struct VirtualViewCallbackInfo {
440    pub reason: VirtualViewCallbackReason,
441    pub system_fonts: *const FcFontCache,
442    pub image_cache: *const ImageCache,
443    pub window_theme: WindowTheme,
444    /// The window's CURRENT frame: normal, minimized, maximized, fullscreen.
445    ///
446    /// Here for the same reason `window_theme` is: a view whose content
447    /// depends on a window-level fact should read that fact, not be handed a
448    /// copy of it at build time and then kept in sync. The window control that
449    /// has to draw "maximize" or "restore" is the case - a seeded copy is
450    /// wrong the moment the window manager maximizes the window itself
451    /// (super+up, edge snap, a tiling rule), which never goes through the
452    /// button's own callback.
453    pub window_frame: WindowFrame,
454    /// RECT 1 - THE CONTAINER: the `VirtualView`'s on-screen box, computed by
455    /// the framework from the outer DOM. You do not set this; you render into
456    /// it.
457    pub bounds: HidpiAdjustedBounds,
458    /// RECT 2 - WHAT IS CURRENTLY MATERIALIZED, in VIRTUAL space: the window
459    /// you returned last time (`origin` = where it starts in the document,
460    /// `size` = its extent). Zero-sized on the first invoke.
461    pub materialized: LogicalRect,
462    /// RECT 3 - THE DOCUMENT, in VIRTUAL space: the extent you last declared,
463    /// which is what the scrollbar currently represents.
464    pub virtual_rect: LogicalRect,
465    /// WHERE THE USER IS LOOKING: the live scroll offset in virtual space.
466    ///
467    /// This is the input your "which slice do I render?" math keys off. It was
468    /// previously spelled `virtual_scroll_offset` and the engine hardcoded
469    /// that to zero, so apps computing a page index from it always rendered
470    /// the first page — one of the two reasons a `VirtualView` could not
471    /// scroll.
472    pub scroll_offset: LogicalPosition,
473    /// Pointer to the callable (`OptionRefAny`) for FFI language bindings (Python, etc.)
474    /// Set by the caller before invoking the callback. Native Rust callbacks have this as null.
475    callable_ptr: *const OptionRefAny,
476    /// Headless DOM measurement hook (see [`Self::measure_dom`]): a
477    /// layout-crate trampoline (a [`MeasureDomFn`] stored as an opaque
478    /// pointer, null = no hook) + its `LayoutWindow` context, injected at
479    /// invoke time. Null on paths that cannot measure (then `measure_dom`
480    /// returns zero).
481    measure_dom_fn: *const c_void,
482    measure_dom_ctx: *mut c_void,
483    /// Extension for future ABI stability (mutable data)
484    _abi_mut: *mut c_void,
485}
486
487/// Trampoline signature for [`VirtualViewCallbackInfo::measure_dom`] and
488/// [`VirtualViewCallbackInfo::measure_dom_shrink_to_fit`]:
489/// `(layout_window_ctx, dom, available, mode) -> content extent`. The `Dom`
490/// is passed by pointer and CONSUMED (moved out) by the trampoline.
491pub type MeasureDomFn =
492    extern "C" fn(*mut c_void, *mut Dom, LogicalSize, MeasureDomMode) -> LogicalSize;
493
494/// Which question a [`MeasureDomFn`] answers about a DOM.
495#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
496#[repr(C)]
497pub enum MeasureDomMode {
498    /// Lay the DOM out against the given box and report the union of every
499    /// node's bounds. A block root stretches to the box's width, so this
500    /// reports the box's width for any block content - the right answer for
501    /// "how tall is this item at this width".
502    Extent,
503    /// Lay the DOM out at its own max-content width (no wider than the given
504    /// box) and report that - "how big does this content want to be", the
505    /// answer a label or a popup needs.
506    ShrinkToFit,
507}
508
509impl Clone for VirtualViewCallbackInfo {
510    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
511    fn clone(&self) -> Self {
512        Self {
513            reason: self.reason,
514            system_fonts: self.system_fonts,
515            image_cache: self.image_cache,
516            window_theme: self.window_theme,
517            window_frame: self.window_frame,
518            bounds: self.bounds,
519            materialized: self.materialized,
520            virtual_rect: self.virtual_rect,
521            scroll_offset: self.scroll_offset,
522            callable_ptr: self.callable_ptr,
523            measure_dom_fn: self.measure_dom_fn,
524            measure_dom_ctx: self.measure_dom_ctx,
525            _abi_mut: self._abi_mut,
526        }
527    }
528}
529
530impl VirtualViewCallbackInfo {
531    #[must_use]
532    pub const fn new<'a>(
533        reason: VirtualViewCallbackReason,
534        system_fonts: &'a FcFontCache,
535        image_cache: &'a ImageCache,
536        window_theme: WindowTheme,
537        window_frame: WindowFrame,
538        bounds: HidpiAdjustedBounds,
539        materialized: LogicalRect,
540        virtual_rect: LogicalRect,
541        scroll_offset: LogicalPosition,
542    ) -> Self {
543        Self {
544            reason,
545            system_fonts: core::ptr::from_ref::<FcFontCache>(system_fonts),
546            image_cache: core::ptr::from_ref::<ImageCache>(image_cache),
547            window_theme,
548            window_frame,
549            bounds,
550            materialized,
551            virtual_rect,
552            scroll_offset,
553            callable_ptr: core::ptr::null(),
554            measure_dom_fn: core::ptr::null(),
555            measure_dom_ctx: core::ptr::null_mut(),
556            _abi_mut: core::ptr::null_mut(),
557        }
558    }
559
560    /// Set the callable pointer for FFI language bindings
561    pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
562        self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
563    }
564
565    /// Inject the headless-measure trampoline (called by the layout crate
566    /// right before the user callback is invoked).
567    pub fn set_measure_dom_fn(&mut self, f: MeasureDomFn, ctx: *mut c_void) {
568        self.measure_dom_fn = f as *const c_void;
569        self.measure_dom_ctx = ctx;
570    }
571
572    /// Measure a DOM headlessly: style + lay it out against `available`
573    /// constraints using the host window's fonts and system style, without
574    /// touching the live layout. Returns the union of all node bounds.
575    ///
576    /// Use a very tall `available.height` (e.g. `1_000_000.0`) to obtain a
577    /// DOM's natural height at a fixed width - the building block for
578    /// virtual-scroll sizing: measure one (or a few) item template(s), then
579    /// `virtual_scroll_size.height = item_height * item_count` and render
580    /// only the visible window of items. Each call is a full cold layout
581    /// pass, so cache measured sizes per item template.
582    ///
583    /// Returns `LogicalSize::zero()` when no measure hook was injected.
584    #[must_use]
585    pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
586        if self.measure_dom_fn.is_null() {
587            return LogicalSize::zero();
588        }
589        // SAFETY: measure_dom_fn is only ever set via set_measure_dom_fn,
590        // which stores a valid MeasureDomFn.
591        let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
592        let mut dom = core::mem::ManuallyDrop::new(dom);
593        f(
594            self.measure_dom_ctx,
595            core::ptr::from_mut::<Dom>(&mut dom),
596            available,
597            MeasureDomMode::Extent,
598        )
599    }
600
601    /// Measure a DOM headlessly at the size its CONTENT asks for: as wide as
602    /// its max-content width (no wider than `bound`), and as tall as that
603    /// makes it. The natural-size counterpart of [`Self::measure_dom`],
604    /// whose block root stretches to whatever width it is given - so a
605    /// label measured that way reports the box, not the text.
606    ///
607    /// This is what a content-sized view returns as its `materialized`
608    /// rect: a text label, a badge, a popup panel. Pass a generous `bound`
609    /// (a few thousand pixels) for "unconstrained"; the view is then laid
610    /// out at the size it reports (a `VirtualView` with `width: auto` is
611    /// sized by what it returns).
612    ///
613    /// Returns `LogicalSize::zero()` when no measure hook was injected.
614    #[must_use]
615    pub fn measure_dom_shrink_to_fit(&self, dom: Dom, bound: LogicalSize) -> LogicalSize {
616        if self.measure_dom_fn.is_null() {
617            return LogicalSize::zero();
618        }
619        // SAFETY: see `measure_dom`.
620        let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
621        let mut dom = core::mem::ManuallyDrop::new(dom);
622        f(
623            self.measure_dom_ctx,
624            core::ptr::from_mut::<Dom>(&mut dom),
625            bound,
626            MeasureDomMode::ShrinkToFit,
627        )
628    }
629
630    /// Get the callable for FFI language bindings (Python, etc.)
631    #[must_use]
632    pub fn get_ctx(&self) -> OptionRefAny {
633        if self.callable_ptr.is_null() {
634            OptionRefAny::None
635        } else {
636            unsafe { (*self.callable_ptr).clone() }
637        }
638    }
639
640    #[must_use]
641    pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
642        self.bounds
643    }
644
645    const fn internal_get_system_fonts(&self) -> &FcFontCache {
646        unsafe { &*self.system_fonts }
647    }
648    const fn internal_get_image_cache(&self) -> &ImageCache {
649        unsafe { &*self.image_cache }
650    }
651}
652
653/// Return value for a `VirtualView` rendering callback.
654///
655/// Contains two size/offset pairs for lazy loading and virtualization:
656///
657/// - `scroll_size` / `scroll_offset`: Size and position of actually rendered content
658/// - `virtual_scroll_size` / `virtual_scroll_offset`: Size for scrollbar representation
659///
660/// The callback is re-invoked on: initial render, parent DOM recreation, window expansion
661/// beyond `scroll_size`, or scrolling near content edges (`EDGE_THRESHOLD`, currently 200px).
662///
663/// Return `OptionDom::None` to keep the current DOM and only update scroll bounds.
664#[derive(Debug, Clone, PartialEq, Eq)]
665#[repr(C)]
666pub struct VirtualViewReturn {
667    /// The DOM with actual rendered content, or None to keep current DOM.
668    ///
669    /// - `OptionDom::Some(dom)` - Replace current content with this new DOM
670    /// - `OptionDom::None` - Keep using the previous DOM, only update scroll bounds
671    ///
672    /// Returning `None` is an optimization when the callback determines that the
673    /// current content is sufficient (e.g., already rendered ahead of scroll position).
674    pub dom: OptionDom,
675
676    /// WHAT THIS CALLBACK MATERIALIZED, in VIRTUAL space.
677    ///
678    /// `origin` = where this window of content begins in the document;
679    /// `size` = how much of the document it covers.
680    ///
681    /// One rect, not a loose offset + size: they are a single fact about a
682    /// single window, and storing them apart is exactly how the origin came
683    /// to be dropped on the floor (content could not be placed, so a
684    /// `VirtualView` could never actually scroll).
685    ///
686    /// The engine places the content at
687    /// `container.origin + (materialized.origin - current_scroll_offset)`.
688    ///
689    /// **Example**: a table showing rows 10-30 at 30px each reports
690    /// `origin.y = 300`, `size.height = 600`.
691    pub materialized: LogicalRect,
692
693    /// THE WHOLE DOCUMENT, in VIRTUAL space — what the scrollbar represents.
694    ///
695    /// `origin` is normally zero; `size` is your current best estimate and MAY
696    /// change as work completes (e.g. a background pagination pass refining a
697    /// page count). Refining it is cheap and safe: **only the scrollbar reads
698    /// this**, so the thumb resizes and no content moves.
699    ///
700    /// **Example**: a 1000-row table reports `size.height = 30_000` even
701    /// though `materialized` covers 600px of it.
702    pub virtual_rect: LogicalRect,
703}
704
705impl Default for VirtualViewReturn {
706    fn default() -> Self {
707        Self {
708            dom: OptionDom::None,
709            materialized: LogicalRect::zero(),
710            virtual_rect: LogicalRect::zero(),
711        }
712    }
713}
714
715impl VirtualViewReturn {
716    /// Creates a new `VirtualViewReturn` with updated DOM content.
717    ///
718    /// Use this when the callback has rendered new content to display.
719    ///
720    /// # Arguments
721    /// - `dom` - The new DOM to render
722    /// - `materialized` - what you rendered, and where it sits in the document
723    /// - `virtual_rect` - how big the document is (scrollbar sizing)
724    #[must_use]
725    pub const fn with_dom(dom: Dom, materialized: LogicalRect, virtual_rect: LogicalRect) -> Self {
726        Self {
727            dom: OptionDom::Some(dom),
728            materialized,
729            virtual_rect,
730        }
731    }
732
733    /// Creates a return value that keeps the current DOM unchanged.
734    ///
735    /// Use this when the callback determines that the existing content
736    /// is sufficient (e.g., already rendered ahead of scroll position).
737    /// This is an optimization to avoid rebuilding the DOM unnecessarily.
738    ///
739    /// # Arguments
740    /// - `materialized` - the window currently rendered, and where it sits
741    /// - `virtual_rect` - how big the document is (scrollbar sizing)
742    #[must_use]
743    pub const fn keep_current(materialized: LogicalRect, virtual_rect: LogicalRect) -> Self {
744        Self {
745            dom: OptionDom::None,
746            materialized,
747            virtual_rect,
748        }
749    }
750}
751
752// --  thread callback
753
754// -- timer callback
755
756#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
757#[repr(C)]
758pub struct TimerCallbackReturn {
759    pub should_update: Update,
760    pub should_terminate: TerminateTimer,
761}
762
763impl TimerCallbackReturn {
764    /// Creates a new `TimerCallbackReturn` with the given update and terminate flags.
765    #[must_use]
766    pub const fn create(should_update: Update, should_terminate: TerminateTimer) -> Self {
767        Self {
768            should_update,
769            should_terminate,
770        }
771    }
772
773    /// Timer continues running, no DOM update needed.
774    #[must_use]
775    pub const fn continue_unchanged() -> Self {
776        Self {
777            should_update: Update::DoNothing,
778            should_terminate: TerminateTimer::Continue,
779        }
780    }
781
782    /// Timer continues running and DOM should be refreshed.
783    #[must_use]
784    pub const fn continue_and_refresh_dom() -> Self {
785        Self {
786            should_update: Update::RefreshDom,
787            should_terminate: TerminateTimer::Continue,
788        }
789    }
790
791    /// Timer should stop, no DOM update needed.
792    #[must_use]
793    pub const fn terminate_unchanged() -> Self {
794        Self {
795            should_update: Update::DoNothing,
796            should_terminate: TerminateTimer::Terminate,
797        }
798    }
799
800    /// Timer should stop and DOM should be refreshed.
801    #[must_use]
802    pub const fn terminate_and_refresh_dom() -> Self {
803        Self {
804            should_update: Update::RefreshDom,
805            should_terminate: TerminateTimer::Terminate,
806        }
807    }
808}
809
810impl Default for TimerCallbackReturn {
811    fn default() -> Self {
812        Self::continue_unchanged()
813    }
814}
815
816/// Gives the `layout()` function access to the `RendererResources` and the `Window`
817/// (for querying images and fonts, as well as width / height)
818///
819#[derive(Debug)]
820#[repr(C)]
821/// Reference data container for `LayoutCallbackInfo` (all read-only fields)
822///
823/// This struct consolidates all readonly references that layout callbacks need to query state.
824/// By grouping these into a single struct, we reduce the number of parameters to
825/// `LayoutCallbackInfo::new()` from 6 to 2, making the API more maintainable and easier to extend.
826///
827/// This is pure syntax sugar - the struct lives on the stack in the caller and is passed by
828/// reference.
829pub struct LayoutCallbackInfoRefData<'a> {
830    /// Allows the `layout()` function to reference image IDs
831    pub image_cache: &'a ImageCache,
832    /// OpenGL context so that the `layout()` function can render textures
833    pub gl_context: &'a OptionGlContextPtr,
834    /// Reference to the system font cache
835    pub system_fonts: &'a FcFontCache,
836    /// Platform-specific system style (colors, spacing, etc.)
837    /// Used for CSD rendering and menu windows.
838    pub system_style: Arc<SystemStyle>,
839    /// Active route match (if routing is configured).
840    /// Contains the matched pattern and extracted parameters.
841    pub active_route: Option<&'a crate::resources::RouteMatch>,
842    /// #28 (d): SNAPSHOT of the system's monitors, taken (locked + cloned)
843    /// by the caller right before invoking the layout callback. A snapshot —
844    /// not the live `Arc<Mutex<…>>` handle — because `azul-core` is `no_std`
845    /// (no Mutex) and the list is read-only during a layout pass anyway.
846    /// Lets `layout()` bound how much content it builds on first layout
847    /// (e.g. at most monitor-height lines / monitor-area characters), so
848    /// opening a huge file can never build an unbounded DOM.
849    pub monitors: crate::window::MonitorVec,
850    /// Safe-area insets: system bars, notch/cutout, and the on-screen
851    /// keyboard's height. Live values, not the platform defaults.
852    ///
853    /// `layout()` needs these and could not reach them: they live on
854    /// `LayoutWindow` and were exposed only through `CallbackInfo`, which is
855    /// the EVENT callback. So an app could read the notch from a click handler
856    /// and not from the function that decides where to draw — which is the one
857    /// place it matters. A mobile app that must not draw under the status bar
858    /// had no way to ask how tall it is.
859    pub safe_area: azul_css::system::SafeAreaInsets,
860}
861
862/// What triggered the current `layout()` invocation.
863///
864/// The framework re-invokes the layout callback for any change that may
865/// produce a structurally different DOM (resize across a CSS breakpoint,
866/// theme toggle, route switch, callback returning `Update::RefreshDom`).
867/// `LayoutCallbackInfo::relayout_reason()` exposes which trigger this
868/// particular call corresponds to so the callback can branch - for
869/// example, skip expensive analytics on `Resize` calls.
870#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
871#[repr(C)]
872#[derive(Default)]
873pub enum RelayoutReason {
874    /// First layout call for this window.
875    #[default]
876    Initial,
877    /// A user callback returned `Update::RefreshDom`.
878    RefreshDom,
879    /// Window size changed across a CSS breakpoint or DPI scale change.
880    /// The callback can branch on `info.window_width_*` to emit a
881    /// different tree (e.g. hamburger menu vs sidebar).
882    Resize,
883    /// System theme changed (light/dark).
884    ThemeChange,
885    /// `CallbackInfo::switch_route` or `set_route_param` produced a new
886    /// route match. The callback should branch on
887    /// `info.get_active_route()`.
888    RouteChange,
889    /// Catch-all for relayouts that don't fit one of the above categories.
890    Other,
891}
892
893#[repr(C)]
894pub struct LayoutCallbackInfo {
895    /// Single reference to all readonly reference data
896    /// This consolidates 4 individual parameters into 1, improving API ergonomics
897    ref_data: *const LayoutCallbackInfoRefData<'static>,
898    /// Window size (so that apps can return a different UI depending on
899    /// the window size - mobile / desktop view). Should be later removed
900    /// in favor of "resize" handlers and @media queries.
901    pub window_size: WindowSize,
902    /// Registers whether the UI is dependent on the window theme
903    pub theme: WindowTheme,
904    /// What triggered this `layout()` call. Read via `relayout_reason()`.
905    pub relayout_reason: RelayoutReason,
906    /// Pointer to the callable (`OptionRefAny`) for FFI language bindings (Python, etc.)
907    /// Set by the caller before invoking the callback. Native Rust callbacks have this as null.
908    callable_ptr: *const OptionRefAny,
909    /// Extension for future ABI stability (mutable data)
910    _abi_mut: *mut c_void,
911}
912
913/// One recorded window-size query made by a `layout()` callback.
914///
915/// See [`LayoutCallbackInfo::window_width_less_than`] & co. The engine replays
916/// these against a
917/// prospective new size to decide whether a resize could change the DOM at
918/// all: if no recorded answer flips (and no CSS breakpoint is crossed), the
919/// callback is provably size-stable across that resize and is not re-invoked.
920#[repr(C)]
921#[derive(Debug, Clone, Copy, PartialEq)]
922pub struct SizeQuery {
923    pub axis: SizeQueryAxis,
924    pub op: SizeQueryOp,
925    pub threshold_px: f32,
926    /// The answer given at recording time, evaluated against the size the
927    /// callback actually saw.
928    pub answer: bool,
929}
930
931/// Which window dimension a [`SizeQuery`] tested.
932#[repr(C)]
933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934pub enum SizeQueryAxis {
935    Width,
936    Height,
937}
938
939/// The comparison a [`SizeQuery`] performed.
940///
941/// Four variants rather than a greater/smaller bool because the recorded
942/// operator must REPLAY EXACTLY:
943/// `window_width_less_than` is a strict `<` while `window_width_between`'s
944/// lower bound is `>=`, and collapsing either onto the other misjudges a
945/// resize landing precisely on the queried boundary — the one pixel the app
946/// explicitly said it cares about.
947#[repr(C)]
948#[derive(Debug, Clone, Copy, PartialEq, Eq)]
949pub enum SizeQueryOp {
950    /// `dim < threshold` (`window_width_less_than` / `window_height_less_than`)
951    LessThan,
952    /// `dim > threshold` (`window_width_greater_than` / `window_height_greater_than`)
953    GreaterThan,
954    /// `dim >= threshold` (the lower bound of `window_*_between`)
955    GreaterOrEqual,
956    /// `dim <= threshold` (the upper bound of `window_*_between`)
957    LessOrEqual,
958}
959
960impl SizeQuery {
961    /// What this query would answer at `size` — compare with [`Self::answer`]
962    /// to detect a flip. MUST mirror the operators of the recording methods
963    /// exactly (see [`SizeQueryOp`]), or the engine would skip a `layout()`
964    /// re-invocation right at the boundary the app asked about.
965    #[must_use]
966    pub fn answer_at(&self, size: LogicalSize) -> bool {
967        let dim = match self.axis {
968            SizeQueryAxis::Width => size.width,
969            SizeQueryAxis::Height => size.height,
970        };
971        match self.op {
972            SizeQueryOp::LessThan => dim < self.threshold_px,
973            SizeQueryOp::GreaterThan => dim > self.threshold_px,
974            SizeQueryOp::GreaterOrEqual => dim >= self.threshold_px,
975            SizeQueryOp::LessOrEqual => dim <= self.threshold_px,
976        }
977    }
978
979    /// Would this query's answer differ at `size` from the recorded one?
980    #[must_use]
981    pub fn flips_at(&self, size: LogicalSize) -> bool {
982        self.answer_at(size) != self.answer
983    }
984}
985
986/// Thread-local recorder backing the responsive helpers
987/// (`LayoutCallbackInfo::window_width_less_than` & co.).
988///
989/// A thread-local (rather than a field on the FFI-frozen `LayoutCallbackInfo`)
990/// works because the layout callback is invoked SYNCHRONOUSLY on the calling
991/// thread: the engine drains the recording immediately after the callback
992/// returns, on the same thread that made the queries.
993///
994/// Bounded, and the overflow direction matters: SILENTLY dropping queries
995/// would drop exactly the flips the engine needs to see — the UNSAFE
996/// direction, a resize skipping a `layout()` that would have branched. So the
997/// cap does not drop; it latches an `overflowed` flag that the drain reports,
998/// and the engine then treats the callback as size-dependent EVERYWHERE
999/// (every resize re-invokes it — today's behaviour, merely un-optimized).
1000#[cfg(feature = "std")]
1001mod size_query_recorder {
1002    use super::SizeQuery;
1003
1004    /// More distinct thresholds than any real breakpoint scheme uses; a
1005    /// callback exceeding this is generating them programmatically.
1006    pub(super) const SIZE_QUERY_CAP: usize = 256;
1007
1008    std::thread_local! {
1009        static RECORDED: core::cell::RefCell<(Vec<SizeQuery>, bool)> =
1010            const { core::cell::RefCell::new((Vec::new(), false)) };
1011    }
1012
1013    pub(super) fn record(q: SizeQuery) {
1014        RECORDED.with(|r| {
1015            let mut r = r.borrow_mut();
1016            if r.0.len() >= SIZE_QUERY_CAP {
1017                r.1 = true; // overflowed: the drain must report "unbounded"
1018            } else {
1019                r.0.push(q);
1020            }
1021        });
1022    }
1023
1024    /// Drain the recording. Returns `(queries, overflowed)`; `overflowed`
1025    /// means the cap was hit and the list is INCOMPLETE — treat every resize
1026    /// as potentially DOM-changing.
1027    pub(super) fn take() -> (Vec<SizeQuery>, bool) {
1028        RECORDED.with(|r| {
1029            let mut r = r.borrow_mut();
1030            let overflowed = r.1;
1031            r.1 = false;
1032            (core::mem::take(&mut r.0), overflowed)
1033        })
1034    }
1035}
1036
1037#[cfg(feature = "std")]
1038fn record_size_query(q: SizeQuery) {
1039    size_query_recorder::record(q);
1040}
1041
1042/// Without `std` there is no thread-local to record into; the queries still
1043/// ANSWER correctly, the engine just cannot prove size-stability and falls
1044/// back to re-invoking `layout()` on breakpoint-relevant resizes (web builds
1045/// are out of scope for the resize fast path).
1046#[cfg(not(feature = "std"))]
1047fn record_size_query(_q: SizeQuery) {}
1048
1049/// Drain the size queries recorded since the last drain on THIS thread.
1050///
1051/// Call immediately after a `layout()` callback returns, on the same thread.
1052/// `(queries, overflowed)` — on `overflowed == true` the list is incomplete
1053/// and the caller must treat the callback as size-dependent everywhere.
1054#[cfg(feature = "std")]
1055#[must_use]
1056pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
1057    size_query_recorder::take()
1058}
1059
1060#[cfg(not(feature = "std"))]
1061#[must_use]
1062pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
1063    (alloc::vec::Vec::new(), false)
1064}
1065
1066/// Which facet of the OS style a `layout()` callback read.
1067///
1068/// An "appearance change" is never one event. The light/dark polarity flips,
1069/// or the accent colour moves, or the UI font grows, or the icon theme is
1070/// swapped — and each of those reaches a different app differently. Whether
1071/// the change can alter what `layout()` RETURNS depends entirely on what that
1072/// callback read, and only the callback knows.
1073/// [`LayoutCallbackInfo::depends_on_system_style`] is how it says so; this
1074/// enum is the vocabulary.
1075///
1076/// Deliberately coarse. The distinction that pays is between the app that
1077/// merely mirrors light/dark (its DOM is byte-identical across two different
1078/// LIGHT schemes, so an accent change must not cost it a rebuild) and the app
1079/// that baked `colors.button_face` into inline CSS inside `layout()` (its DOM
1080/// is wrong the instant the palette moves). Finer facets would be more
1081/// precise and nobody would declare them correctly.
1082#[repr(C)]
1083#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1084pub enum SystemStyleDependency {
1085    /// The light/dark polarity alone — [`LayoutCallbackInfo::get_theme`].
1086    Theme,
1087    /// The colour palette: text, background, accent, button, selection.
1088    Colors,
1089    /// The UI fonts — family, size, weight.
1090    Fonts,
1091    /// Sizing and spacing metrics: control sizes, scrollbar geometry,
1092    /// titlebar layout, input timings, focus ring.
1093    Metrics,
1094    /// The icon theme and the icon styling options.
1095    Icons,
1096    /// Accessibility and motion preferences — reduced motion, high contrast,
1097    /// animation speed.
1098    Accessibility,
1099    /// Everything: the callback took the whole [`SystemStyle`] and the engine
1100    /// cannot see which parts of it were read. The conservative answer, and
1101    /// what [`LayoutCallbackInfo::get_system_style`] records.
1102    Everything,
1103}
1104
1105impl SystemStyleDependency {
1106    /// This facet's bit in a [`SystemStyleDependencies`] mask.
1107    #[must_use]
1108    pub const fn bit(self) -> u32 {
1109        match self {
1110            Self::Theme => 1 << 0,
1111            Self::Colors => 1 << 1,
1112            Self::Fonts => 1 << 2,
1113            Self::Metrics => 1 << 3,
1114            Self::Icons => 1 << 4,
1115            Self::Accessibility => 1 << 5,
1116            Self::Everything => u32::MAX,
1117        }
1118    }
1119}
1120
1121/// The set of [`SystemStyleDependency`] facets one `layout()` call declared.
1122///
1123/// A bitmask rather than a list: the facets are few, the union is the only
1124/// operation, and it has to be cheap enough to fold on every declaration
1125/// inside a deep widget tree.
1126#[repr(C)]
1127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
1128pub struct SystemStyleDependencies {
1129    /// Bitmask over [`SystemStyleDependency::bit`]. `0` = nothing declared,
1130    /// which is NOT "depends on nothing" — see
1131    /// [`Self::dom_depends_on_change`].
1132    pub facets: u32,
1133}
1134
1135impl SystemStyleDependencies {
1136    /// Nothing declared.
1137    #[must_use]
1138    pub const fn empty() -> Self {
1139        Self { facets: 0 }
1140    }
1141
1142    /// Every facet — what an undeclared callback is treated as.
1143    #[must_use]
1144    pub const fn all() -> Self {
1145        Self { facets: u32::MAX }
1146    }
1147
1148    /// Nothing has been declared yet.
1149    #[must_use]
1150    pub const fn is_empty(&self) -> bool {
1151        self.facets == 0
1152    }
1153
1154    /// Fold one facet in.
1155    pub const fn insert(&mut self, dep: SystemStyleDependency) {
1156        self.facets |= dep.bit();
1157    }
1158
1159    /// Fold another set in.
1160    pub const fn union(&mut self, other: Self) {
1161        self.facets |= other.facets;
1162    }
1163
1164    /// Was `dep` declared? `Everything` implies every facet.
1165    #[must_use]
1166    pub const fn contains(&self, dep: SystemStyleDependency) -> bool {
1167        let bit = dep.bit();
1168        self.facets & bit == bit
1169    }
1170
1171    /// Would a system-style change from `old` to `new` alter what the
1172    /// callback that declared these dependencies returns — i.e. does the
1173    /// change need a full `Update::RefreshDom`, or only a restyle?
1174    ///
1175    /// An EMPTY set answers `true`. "Declared nothing" is not "depends on
1176    /// nothing": it is the state of every callback written before this API
1177    /// existed, and of every callback that reads the OS style through a
1178    /// widget it does not control. Skipping their rebuild would leave the
1179    /// previous palette baked into the DOM — a silent wrong-colours bug that
1180    /// only a theme switch reveals. Declaring is opt-in; conservatism is the
1181    /// default.
1182    #[must_use]
1183    pub fn dom_depends_on_change(
1184        &self,
1185        old: &azul_css::system::SystemStyle,
1186        new: &azul_css::system::SystemStyle,
1187    ) -> bool {
1188        if self.is_empty() {
1189            return old != new;
1190        }
1191        if self.contains(SystemStyleDependency::Theme) && old.theme != new.theme {
1192            return true;
1193        }
1194        if self.contains(SystemStyleDependency::Colors) && old.colors != new.colors {
1195            return true;
1196        }
1197        if self.contains(SystemStyleDependency::Fonts) && old.fonts != new.fonts {
1198            return true;
1199        }
1200        if self.contains(SystemStyleDependency::Metrics)
1201            && (old.metrics != new.metrics
1202                || old.input != new.input
1203                || old.focus_visuals != new.focus_visuals
1204                || old.scrollbar != new.scrollbar
1205                || old.scrollbar_preferences != new.scrollbar_preferences)
1206        {
1207            return true;
1208        }
1209        if self.contains(SystemStyleDependency::Icons)
1210            && (old.icon_style != new.icon_style
1211                || old.visual_hints != new.visual_hints
1212                || old.linux.icon_theme != new.linux.icon_theme)
1213        {
1214            return true;
1215        }
1216        if self.contains(SystemStyleDependency::Accessibility)
1217            && (old.accessibility != new.accessibility
1218                || old.animation != new.animation
1219                || old.prefers_reduced_motion != new.prefers_reduced_motion
1220                || old.prefers_high_contrast != new.prefers_high_contrast)
1221        {
1222            return true;
1223        }
1224        false
1225    }
1226}
1227
1228/// Thread-local recorder behind [`LayoutCallbackInfo::depends_on_system_style`].
1229///
1230/// Same shape, and for the same reason, as the size-query recorder above: the
1231/// layout callback runs SYNCHRONOUSLY on the calling thread, so the engine
1232/// drains what it declared right after it returns. A mask cannot overflow, so
1233/// unlike the size queries there is no incomplete-recording flag.
1234#[cfg(feature = "std")]
1235mod style_dep_recorder {
1236    use super::SystemStyleDependencies;
1237
1238    std::thread_local! {
1239        static DECLARED: core::cell::Cell<SystemStyleDependencies> =
1240            const { core::cell::Cell::new(SystemStyleDependencies { facets: 0 }) };
1241    }
1242
1243    pub(super) fn record(dep: super::SystemStyleDependency) {
1244        DECLARED.with(|d| {
1245            let mut set = d.get();
1246            set.insert(dep);
1247            d.set(set);
1248        });
1249    }
1250
1251    pub(super) fn take() -> SystemStyleDependencies {
1252        DECLARED.with(core::cell::Cell::take)
1253    }
1254}
1255
1256#[cfg(feature = "std")]
1257fn record_style_dependency(dep: SystemStyleDependency) {
1258    style_dep_recorder::record(dep);
1259}
1260
1261/// Without `std` there is no thread-local to record into. The declarations
1262/// still cost nothing and the engine falls back to rebuilding on every
1263/// system-style change — today's behaviour, merely un-optimized.
1264#[cfg(not(feature = "std"))]
1265fn record_style_dependency(_dep: SystemStyleDependency) {}
1266
1267/// Drain the system-style dependencies declared since the last drain on THIS
1268/// thread.
1269///
1270/// Call immediately after a `layout()` callback returns, on the same thread.
1271/// The empty set means the callback declared nothing — which
1272/// [`SystemStyleDependencies::dom_depends_on_change`] reads as "assume it
1273/// depends on all of it".
1274#[cfg(feature = "std")]
1275#[must_use]
1276pub fn take_recorded_style_dependencies() -> SystemStyleDependencies {
1277    style_dep_recorder::take()
1278}
1279
1280#[cfg(not(feature = "std"))]
1281#[must_use]
1282pub fn take_recorded_style_dependencies() -> SystemStyleDependencies {
1283    SystemStyleDependencies::empty()
1284}
1285
1286impl Clone for LayoutCallbackInfo {
1287    #[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
1288    fn clone(&self) -> Self {
1289        Self {
1290            ref_data: self.ref_data,
1291            window_size: self.window_size,
1292            theme: self.theme,
1293            relayout_reason: self.relayout_reason,
1294            callable_ptr: self.callable_ptr,
1295            _abi_mut: self._abi_mut,
1296        }
1297    }
1298}
1299
1300impl core::fmt::Debug for LayoutCallbackInfo {
1301    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1302        f.debug_struct("LayoutCallbackInfo")
1303            .field("window_size", &self.window_size)
1304            .field("theme", &self.theme)
1305            .field("relayout_reason", &self.relayout_reason)
1306            .finish_non_exhaustive()
1307    }
1308}
1309
1310impl LayoutCallbackInfo {
1311    #[must_use]
1312    pub const fn new<'a>(
1313        ref_data: &'a LayoutCallbackInfoRefData<'a>,
1314        window_size: WindowSize,
1315        theme: WindowTheme,
1316    ) -> Self {
1317        Self::new_with_reason(ref_data, window_size, theme, RelayoutReason::Initial)
1318    }
1319
1320    // the `as *const ...<'static>` is a deliberate 'a -> 'static lifetime launder
1321    // on the raw pointer (see SAFETY note below), not a redundant cast.
1322    #[allow(clippy::unnecessary_cast)]
1323    #[must_use]
1324    pub const fn new_with_reason<'a>(
1325        ref_data: &'a LayoutCallbackInfoRefData<'a>,
1326        window_size: WindowSize,
1327        theme: WindowTheme,
1328        relayout_reason: RelayoutReason,
1329    ) -> Self {
1330        Self {
1331            // SAFETY: We cast away the lifetime 'a to 'static because LayoutCallbackInfo
1332            // only lives for the duration of the callback, which is shorter than 'a
1333            ref_data: core::ptr::from_ref::<LayoutCallbackInfoRefData<'a>>(ref_data)
1334                as *const LayoutCallbackInfoRefData<'static>,
1335            window_size,
1336            theme,
1337            relayout_reason,
1338            callable_ptr: core::ptr::null(),
1339            _abi_mut: core::ptr::null_mut(),
1340        }
1341    }
1342
1343    /// Returns what triggered the current `layout()` invocation.
1344    #[must_use]
1345    pub const fn relayout_reason(&self) -> RelayoutReason {
1346        self.relayout_reason
1347    }
1348
1349    /// Is the window's LOGICAL viewport wider than `width_px`?
1350    ///
1351    /// The structural-breakpoint helper: branch on this in `layout()` to
1352    /// return an entirely different DOM per form factor
1353    /// (`ribbon.dom_desktop()` vs `ribbon.dom_mobile()`), instead of
1354    /// emitting both trees and toggling visibility with `@media` rules.
1355    ///
1356    /// CONTRACT: the framework re-invokes `layout()` on every window resize
1357    /// (`RelayoutReason::Resize` - the regenerate path never takes the
1358    /// layout-equivalence shortcut when the window size changed), so the
1359    /// answer cannot go stale: crossing the breakpoint in either direction
1360    /// re-runs `layout()` and the callback returns the other tree. If a
1361    /// future optimization ever skips DOM regeneration on resize, it must
1362    /// register the thresholds queried here and force a rebuild when one is
1363    /// crossed - grep for this comment.
1364    #[must_use]
1365    pub fn viewport_bigger_than(&self, width_px: f32) -> bool {
1366        self.window_size.dimensions.width > width_px
1367    }
1368
1369    /// Safe-area insets in logical px: system bars, notch / cutout, and the
1370    /// on-screen keyboard.
1371    ///
1372    /// The same values `CallbackInfo::get_safe_area_insets` returns, made
1373    /// reachable from `layout()`. They were only available from EVENT
1374    /// callbacks, which is the wrong half: an app could read the notch from a
1375    /// click handler but not from the function that decides where to draw.
1376    ///
1377    /// `keyboard` is kept separate from `bottom` deliberately. The bar is
1378    /// fixed and the keyboard moves, so a layout that must stay above the IME
1379    /// adds them, and one that only wants to avoid the home indicator does
1380    /// not.
1381    #[must_use]
1382    pub fn get_safe_area_insets(&self) -> azul_css::system::SafeAreaInsets {
1383        // SAFETY: same contract as `get_monitors` above — `ref_data` is set
1384        // for the duration of the layout call.
1385        unsafe { (*self.ref_data).safe_area }
1386    }
1387
1388    /// Set the callable pointer for FFI language bindings
1389    pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
1390        self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
1391    }
1392
1393    /// Get the callable for FFI language bindings (Python, etc.)
1394    #[must_use]
1395    pub fn get_ctx(&self) -> OptionRefAny {
1396        if self.callable_ptr.is_null() {
1397            OptionRefAny::None
1398        } else {
1399            unsafe { (*self.callable_ptr).clone() }
1400        }
1401    }
1402
1403    /// Declare that the DOM this callback returns depends on `dep`.
1404    ///
1405    /// THE seam between "the OS appearance changed" and "this app's DOM is
1406    /// now wrong". A theme switch, an accent-colour change, a UI-font resize
1407    /// all arrive as the same kind of event, and the engine has no way to see
1408    /// which of them can change what `layout()` builds — only the callback
1409    /// knows.
1410    ///
1411    /// Declare narrowly and a change outside what you declared costs a
1412    /// RESTYLE (the cascade re-resolves `system-*` colours and `@theme`
1413    /// conditions against the new style, warm layout caches intact) instead
1414    /// of a full `Update::RefreshDom` (re-invoke `layout()`, rebuild the
1415    /// `StyledDom`, re-cascade, re-shape every run of text).
1416    ///
1417    /// ```ignore
1418    /// // "I mirror light/dark and nothing else": switching between two
1419    /// // light colour schemes cannot change my DOM.
1420    /// info.depends_on_system_style(SystemStyleDependency::Theme);
1421    /// let dark = info.get_theme() == WindowTheme::DarkMode;
1422    ///
1423    /// // "I paint my own buttons from the OS palette": ANY palette move
1424    /// // invalidates my DOM, light-to-light included.
1425    /// info.depends_on_system_style(SystemStyleDependency::Colors);
1426    /// ```
1427    ///
1428    /// Declarations UNION over the whole callback, widgets included, and the
1429    /// union is conservative: one widget calling
1430    /// [`Self::get_system_style`] declares [`SystemStyleDependency::Everything`]
1431    /// for the entire tree, because a whole-struct read is opaque.
1432    ///
1433    /// Declaring NOTHING is not "depends on nothing" — an undeclared callback
1434    /// is rebuilt on every system-style change, exactly as before this API
1435    /// existed. Reading the `theme` field directly (`info.theme`) declares
1436    /// nothing either: the engine cannot see a field read, the same way it
1437    /// cannot see `info.window_size` being used to branch the DOM.
1438    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
1439    pub fn depends_on_system_style(&self, dep: SystemStyleDependency) {
1440        record_style_dependency(dep);
1441    }
1442
1443    /// The window's light/dark polarity, declaring
1444    /// [`SystemStyleDependency::Theme`].
1445    ///
1446    /// The tracked way to read what the `theme` field also holds. Use this
1447    /// and a change that leaves the polarity alone — a new accent colour, a
1448    /// different light scheme — will not rebuild the DOM.
1449    #[must_use]
1450    pub fn get_theme(&self) -> WindowTheme {
1451        self.depends_on_system_style(SystemStyleDependency::Theme);
1452        self.theme
1453    }
1454
1455    /// Get a clone of the system style Arc.
1456    ///
1457    /// Declares [`SystemStyleDependency::Everything`]: handing out the whole
1458    /// struct makes the read opaque, so the honest answer is that any part of
1459    /// it may have reached the DOM. A callback that only wants the palette or
1460    /// the fonts should say so with [`Self::depends_on_system_style`] and
1461    /// reach for [`Self::get_system_style_untracked`].
1462    #[must_use]
1463    pub fn get_system_style(&self) -> Arc<SystemStyle> {
1464        self.depends_on_system_style(SystemStyleDependency::Everything);
1465        self.get_system_style_untracked()
1466    }
1467
1468    /// The system style WITHOUT declaring a dependency on all of it.
1469    ///
1470    /// For a callback that has already declared what it actually reads, and
1471    /// for engine-internal readers (CSD, menus) whose output is rebuilt by
1472    /// the engine itself rather than by the app's `layout()`.
1473    #[must_use]
1474    pub fn get_system_style_untracked(&self) -> Arc<SystemStyle> {
1475        unsafe { (*self.ref_data).system_style.clone() }
1476    }
1477
1478    /// #28 (d): snapshot of the system's monitors, taken by the caller right
1479    /// before this layout pass. Empty when the platform hasn't populated
1480    /// monitor info (headless, web, very early startup).
1481    #[must_use]
1482    pub fn get_monitors(&self) -> crate::window::MonitorVec {
1483        unsafe { (*self.ref_data).monitors.clone() }
1484    }
1485
1486    /// #28 (d): the LARGEST monitor size in physical px — the safe upper
1487    /// bound for "how much content could possibly be visible at once" when
1488    /// the window's own monitor is not yet known at first layout. Apps use
1489    /// it to bound how much content the first `layout()` builds (e.g. at
1490    /// most monitor-height text lines, or monitor-width × monitor-height
1491    /// characters for a single unbroken line), so opening a huge file never
1492    /// builds an unbounded DOM. `None` when no monitor info is available.
1493    #[must_use]
1494    pub fn get_max_monitor_size(&self) -> azul_css::props::basic::OptionLayoutSize {
1495        let monitors = unsafe { &(*self.ref_data).monitors };
1496        let mut best: Option<LayoutSize> = None;
1497        for m in monitors.as_ref() {
1498            let s = m.size;
1499            let better = best.is_none_or(|b| (s.width * s.height) > (b.width * b.height));
1500            if better {
1501                best = Some(s);
1502            }
1503        }
1504        best.into()
1505    }
1506
1507    const fn internal_get_image_cache(&self) -> &ImageCache {
1508        unsafe { (*self.ref_data).image_cache }
1509    }
1510    const fn internal_get_system_fonts(&self) -> &FcFontCache {
1511        unsafe { (*self.ref_data).system_fonts }
1512    }
1513    const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
1514        unsafe { (*self.ref_data).gl_context }
1515    }
1516
1517    #[must_use]
1518    pub fn get_gl_context(&self) -> OptionGlContextPtr {
1519        self.internal_get_gl_context().clone()
1520    }
1521
1522    #[must_use]
1523    pub fn get_system_fonts(&self) -> Vec<AzStringPair> {
1524        let fc_cache = self.internal_get_system_fonts();
1525
1526        fc_cache
1527            .list()
1528            .into_iter()
1529            .filter_map(|(pattern, font_id)| {
1530                let source = fc_cache.get_font_by_id(&font_id)?;
1531                match source {
1532                    OwnedFontSource::Memory(_) => None,
1533                    OwnedFontSource::Disk(d) => Some((pattern.name.as_ref()?.clone(), d.path)),
1534                }
1535            })
1536            .map(|(k, v)| AzStringPair {
1537                key: k.into(),
1538                value: v.into(),
1539            })
1540            .collect()
1541    }
1542
1543    /// The window's ALREADY-BUILT system font cache.
1544    ///
1545    /// `get_system_fonts` only hands back stringified name/path pairs, which
1546    /// is useless to a layout callback that wants to run engine layout of
1547    /// its own (paginating a document, measuring for an export). Such an app
1548    /// had to call `build_font_cache()` and re-scan every font on the
1549    /// machine — measured at ~5 SECONDS on the first frame, during which the
1550    /// client cannot answer the compositor's configure/ping handshake and
1551    /// loses its surface.
1552    ///
1553    /// The cache is internally `Arc<RwLock<_>>` (rust-fontconfig 4.1+), so
1554    /// this clone is a handle, not a copy: the caller sees the same fonts
1555    /// the window already resolved, including builder-thread additions.
1556    #[must_use]
1557    pub fn get_font_cache(&self) -> FcFontCache {
1558        self.internal_get_system_fonts().clone()
1559    }
1560
1561    #[must_use]
1562    pub fn get_image(&self, image_id: &AzString) -> Option<ImageRef> {
1563        self.internal_get_image_cache()
1564            .get_css_image_id(image_id)
1565            .cloned()
1566    }
1567
1568    /// Get the active route match (pattern + extracted parameters).
1569    ///
1570    /// Returns `None` if no routes are configured or no route is active.
1571    #[must_use]
1572    pub const fn get_active_route(&self) -> Option<&crate::resources::RouteMatch> {
1573        unsafe { (*self.ref_data).active_route }
1574    }
1575
1576    /// Get a route parameter by key (e.g. `get_route_param("id")` for `/user/:id`).
1577    ///
1578    /// Returns `None` if no route is active or the parameter doesn't exist.
1579    #[must_use]
1580    pub fn get_route_param(&self, key: &str) -> Option<&AzString> {
1581        self.get_active_route()?.get_param(key)
1582    }
1583
1584    /// The pattern of the route this layout callback is rendering, e.g.
1585    /// `"/user/:id"`.
1586    ///
1587    /// `"/"` when the app configured no routes: an app without routing is on
1588    /// the default route, so a callback that branches on the pattern always
1589    /// has one string to branch on rather than an empty one.
1590    ///
1591    /// # C API
1592    /// ```c
1593    /// AzString pattern = AzLayoutCallbackInfo_getRoutePattern(&info);
1594    /// ```
1595    #[must_use]
1596    pub fn get_route_pattern(&self) -> AzString {
1597        self.get_active_route().map_or_else(
1598            || AzString::from_const_str("/"),
1599            |route| route.pattern.clone(),
1600        )
1601    }
1602
1603    /// A route parameter by key, empty when the parameter or the route is
1604    /// absent. The owned-key, owned-return form the FFI needs;
1605    /// [`Self::get_route_param`] is the borrowing Rust one.
1606    ///
1607    /// # C API
1608    /// ```c
1609    /// AzString id = AzLayoutCallbackInfo_getRouteParamOrEmpty(&info,
1610    ///     AzString_fromConstStr("id"));
1611    /// ```
1612    #[allow(clippy::needless_pass_by_value)]
1613    #[must_use]
1614    pub fn get_route_param_or_empty(&self, key: AzString) -> AzString {
1615        self.get_route_param(key.as_str())
1616            .cloned()
1617            .unwrap_or_else(|| AzString::from_const_str(""))
1618    }
1619
1620    // Responsive layout helper methods.
1621    //
1622    // These are THE sanctioned way for `layout()` to branch on window size
1623    // (mobile vs desktop DOM shapes, instead of `display:none` stacks). Every
1624    // call is RECORDED, and the recording is what makes resize cheap: a resize
1625    // that flips none of the recorded answers (and crosses no CSS breakpoint)
1626    // provably cannot change what the callback returns through this channel,
1627    // so the engine re-flows the existing DOM instead of re-invoking it
1628    // (`LayoutWindow::resize_needs_full_regeneration`). Reading the size
1629    // imperatively (`get_window_width()`, `info.window_size`) to branch the
1630    // DOM is a bug in the app: the engine cannot see that read, so the DOM
1631    // goes stale across exactly the resizes the app cared about.
1632
1633    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
1634    fn record_width_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
1635        record_size_query(SizeQuery {
1636            axis: SizeQueryAxis::Width,
1637            op,
1638            threshold_px,
1639            answer,
1640        });
1641        answer
1642    }
1643
1644    #[allow(clippy::unused_self)] // C-ABI-shaped method: receiver kept for API symmetry
1645    fn record_height_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
1646        record_size_query(SizeQuery {
1647            axis: SizeQueryAxis::Height,
1648            op,
1649            threshold_px,
1650            answer,
1651        });
1652        answer
1653    }
1654
1655    /// Returns true if the window width is less than the given pixel value.
1656    /// Recorded — see the note above these helpers.
1657    #[must_use]
1658    pub fn window_width_less_than(&self, px: f32) -> bool {
1659        let answer = self.window_size.dimensions.width < px;
1660        self.record_width_query(SizeQueryOp::LessThan, px, answer)
1661    }
1662
1663    /// Returns true if the window width is greater than the given pixel value.
1664    /// Recorded — see the note above these helpers.
1665    #[must_use]
1666    pub fn window_width_greater_than(&self, px: f32) -> bool {
1667        let answer = self.window_size.dimensions.width > px;
1668        self.record_width_query(SizeQueryOp::GreaterThan, px, answer)
1669    }
1670
1671    /// Returns true if the window width is between min and max (inclusive).
1672    /// Recorded as its two bounds — see the note above these helpers.
1673    #[must_use]
1674    pub fn window_width_between(&self, min_px: f32, max_px: f32) -> bool {
1675        let width = self.window_size.dimensions.width;
1676        self.record_width_query(SizeQueryOp::GreaterOrEqual, min_px, width >= min_px)
1677            & self.record_width_query(SizeQueryOp::LessOrEqual, max_px, width <= max_px)
1678    }
1679
1680    /// Returns true if the window height is less than the given pixel value.
1681    /// Recorded — see the note above these helpers.
1682    #[must_use]
1683    pub fn window_height_less_than(&self, px: f32) -> bool {
1684        let answer = self.window_size.dimensions.height < px;
1685        self.record_height_query(SizeQueryOp::LessThan, px, answer)
1686    }
1687
1688    /// Returns true if the window height is greater than the given pixel value.
1689    /// Recorded — see the note above these helpers.
1690    #[must_use]
1691    pub fn window_height_greater_than(&self, px: f32) -> bool {
1692        let answer = self.window_size.dimensions.height > px;
1693        self.record_height_query(SizeQueryOp::GreaterThan, px, answer)
1694    }
1695
1696    /// Returns true if the window height is between min and max (inclusive).
1697    /// Recorded as its two bounds — see the note above these helpers.
1698    #[must_use]
1699    pub fn window_height_between(&self, min_px: f32, max_px: f32) -> bool {
1700        let height = self.window_size.dimensions.height;
1701        self.record_height_query(SizeQueryOp::GreaterOrEqual, min_px, height >= min_px)
1702            & self.record_height_query(SizeQueryOp::LessOrEqual, max_px, height <= max_px)
1703    }
1704
1705    /// Returns the current window width in pixels
1706    #[must_use]
1707    pub const fn get_window_width(&self) -> f32 {
1708        self.window_size.dimensions.width
1709    }
1710
1711    /// Returns the current window height in pixels
1712    #[must_use]
1713    pub const fn get_window_height(&self) -> f32 {
1714        self.window_size.dimensions.height
1715    }
1716
1717    /// Returns the current window DPI scale factor (1.0 = 96 DPI, 2.0 = 192 DPI)
1718    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
1719    #[must_use]
1720    pub fn get_dpi_factor(&self) -> f32 {
1721        self.window_size.dpi as f32 / 96.0
1722    }
1723}
1724
1725/// Information about the bounds of a laid-out div rectangle.
1726///
1727/// Necessary when invoking `VirtualViewCallbacks` and `RenderImageCallbacks`, so
1728/// that they can change what their content is based on their size.
1729#[derive(Debug, Copy, Clone)]
1730#[repr(C)]
1731pub struct HidpiAdjustedBounds {
1732    pub logical_size: LogicalSize,
1733    pub hidpi_factor: DpiScaleFactor,
1734}
1735
1736impl HidpiAdjustedBounds {
1737    #[inline]
1738    #[allow(clippy::cast_precision_loss)] // bounded DPI/dimension/number conversion
1739    #[must_use]
1740    pub const fn from_bounds(bounds: LayoutSize, hidpi_factor: DpiScaleFactor) -> Self {
1741        let logical_size = LogicalSize::new(bounds.width as f32, bounds.height as f32);
1742        Self {
1743            logical_size,
1744            hidpi_factor,
1745        }
1746    }
1747
1748    #[must_use]
1749    pub fn get_physical_size(&self) -> PhysicalSize<u32> {
1750        self.get_logical_size()
1751            .to_physical(self.get_hidpi_factor().inner.get())
1752    }
1753
1754    #[must_use]
1755    pub const fn get_logical_size(&self) -> LogicalSize {
1756        self.logical_size
1757    }
1758
1759    #[must_use]
1760    pub const fn get_hidpi_factor(&self) -> DpiScaleFactor {
1761        self.hidpi_factor
1762    }
1763}
1764
1765/// Defines the `focus_targeted` node ID for the next frame
1766#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1767#[repr(C, u8)]
1768pub enum FocusTarget {
1769    Id(DomNodeId),
1770    Path(FocusTargetPath),
1771    Previous,
1772    Next,
1773    First,
1774    Last,
1775    NoFocus,
1776    /// Move focus in a SPATIAL direction rather than along the tab order.
1777    /// APPENDED at the end for ABI stability.
1778    ///
1779    /// Tab order is a single sequence; a grid of thumbnails, a TV menu or a
1780    /// media-player transport row is two-dimensional, and pressing Right in
1781    /// one should land on the thing to the right, not on whatever happens to
1782    /// be next in document order.
1783    ///
1784    /// This is not really a device feature — it is a focus-engine feature that
1785    /// a dozen devices drive: a TV remote's D-pad, a game controller, a car's
1786    /// jog dial, and switch access. Android models it as a first-class input
1787    /// source (`SOURCE_DPAD`) and the W3C specifies it in `css-nav-1`. It
1788    /// needs no shell code at all: the search is geometric, over the focusable
1789    /// set the engine already computes.
1790    Directional(FocusDirection),
1791}
1792
1793/// Which way [`FocusTarget::Directional`] should move.
1794#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1795#[repr(C)]
1796pub enum FocusDirection {
1797    Up,
1798    Down,
1799    Left,
1800    Right,
1801}
1802
1803#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1804#[repr(C)]
1805pub struct FocusTargetPath {
1806    pub dom: DomId,
1807    pub css_path: CssPath,
1808}
1809
1810// -- normal callback
1811
1812// core callback types (usize-based placeholders)
1813//
1814// These types use `usize` instead of function pointers to avoid creating
1815// a circular dependency between azul-core and azul-layout.
1816//
1817// The actual function pointers will be stored in azul-layout, which will
1818// use unsafe code to transmute between usize and the real function pointers.
1819//
1820// IMPORTANT: The memory layout must be identical to the real types!
1821//
1822// Naming convention: "Core" prefix indicates these are the low-level types
1823
1824/// Core callback type - uses usize instead of function pointer to avoid circular dependencies.
1825///
1826/// **IMPORTANT**: This is NOT actually a usize at runtime - it's a function pointer that is
1827/// cast to usize for storage in the data model. When invoking the callback, this usize is
1828/// unsafely cast back to the actual function pointer type:
1829/// `extern "C" fn(RefAny, CallbackInfo) -> Update`
1830///
1831/// This design allows azul-core to store callbacks without depending on azul-layout's `CallbackInfo`
1832/// type. The actual function pointer type is defined in azul-layout as `CallbackType`.
1833pub type CoreCallbackType = usize;
1834
1835/// Stores a callback as usize (actually a function pointer cast to usize)
1836///
1837/// **IMPORTANT**: The `cb` field stores a function pointer disguised as usize to avoid
1838/// circular dependencies between azul-core and azul-layout. When creating a `CoreCallback`,
1839/// you can directly assign a function pointer - Rust will implicitly cast it to usize.
1840/// When invoking, the usize must be unsafely cast back to the function pointer type.
1841///
1842/// Must return an `Update` that denotes if the screen should be redrawn.
1843#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1844#[repr(C)]
1845pub struct CoreCallback {
1846    pub cb: CoreCallbackType,
1847    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1848    /// Native Rust code sets this to None
1849    pub ctx: OptionRefAny,
1850}
1851
1852/// Allow creating `CoreCallback` from a raw function pointer (as usize)
1853/// Sets callable to None (for native Rust/C usage)
1854impl From<CoreCallbackType> for CoreCallback {
1855    fn from(cb: CoreCallbackType) -> Self {
1856        Self {
1857            cb,
1858            ctx: OptionRefAny::None,
1859        }
1860    }
1861}
1862
1863impl_option!(
1864    CoreCallback,
1865    OptionCoreCallback,
1866    [Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash]
1867);
1868
1869/// Data associated with a callback (event filter, callback, and user data)
1870#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1871#[repr(C)]
1872pub struct CoreCallbackData {
1873    pub event: EventFilter,
1874    pub callback: CoreCallback,
1875    pub refany: RefAny,
1876}
1877
1878impl_option!(
1879    CoreCallbackData,
1880    OptionCoreCallbackData,
1881    copy = false,
1882    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1883);
1884
1885impl_vec!(
1886    CoreCallbackData,
1887    CoreCallbackDataVec,
1888    CoreCallbackDataVecDestructor,
1889    CoreCallbackDataVecDestructorType,
1890    CoreCallbackDataVecSlice,
1891    OptionCoreCallbackData
1892);
1893impl_vec_clone!(
1894    CoreCallbackData,
1895    CoreCallbackDataVec,
1896    CoreCallbackDataVecDestructor
1897);
1898impl_vec_mut!(CoreCallbackData, CoreCallbackDataVec);
1899impl_vec_debug!(CoreCallbackData, CoreCallbackDataVec);
1900impl_vec_partialord!(CoreCallbackData, CoreCallbackDataVec);
1901impl_vec_ord!(CoreCallbackData, CoreCallbackDataVec);
1902impl_vec_partialeq!(CoreCallbackData, CoreCallbackDataVec);
1903impl_vec_eq!(CoreCallbackData, CoreCallbackDataVec);
1904impl_vec_hash!(CoreCallbackData, CoreCallbackDataVec);
1905
1906impl CoreCallbackDataVec {
1907    #[inline]
1908    #[must_use]
1909    pub fn as_container(&self) -> NodeDataContainerRef<'_, CoreCallbackData> {
1910        NodeDataContainerRef {
1911            internal: self.as_ref(),
1912        }
1913    }
1914    #[inline]
1915    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, CoreCallbackData> {
1916        NodeDataContainerRefMut {
1917            internal: self.as_mut(),
1918        }
1919    }
1920}
1921
1922// -- image rendering callback
1923
1924/// Image rendering callback type - uses usize instead of function pointer
1925pub type CoreRenderImageCallbackType = usize;
1926
1927/// Callback that returns a rendered OpenGL texture (usize placeholder)
1928#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1929#[repr(C)]
1930pub struct CoreRenderImageCallback {
1931    pub cb: CoreRenderImageCallbackType,
1932    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1933    /// Native Rust code sets this to None
1934    pub ctx: OptionRefAny,
1935}
1936
1937/// Allow creating `CoreRenderImageCallback` from a raw function pointer (as usize)
1938/// Sets callable to None (for native Rust/C usage)
1939impl From<CoreRenderImageCallbackType> for CoreRenderImageCallback {
1940    fn from(cb: CoreRenderImageCallbackType) -> Self {
1941        Self {
1942            cb,
1943            ctx: OptionRefAny::None,
1944        }
1945    }
1946}
1947
1948/// Image callback with associated data
1949#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1950#[repr(C)]
1951pub struct CoreImageCallback {
1952    pub refany: RefAny,
1953    pub callback: CoreRenderImageCallback,
1954}
1955
1956impl_option!(
1957    CoreImageCallback,
1958    OptionCoreImageCallback,
1959    copy = false,
1960    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1961);
1962
1963#[cfg(test)]
1964#[path = "callbacks_test.rs"]
1965mod callbacks_test;