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;