Skip to main content

azul_core/
resources.rs

1//! Resource management types for the application.
2//!
3//! This module contains the core types for managing application resources:
4//! - `AppConfig`: application-level configuration (logging, fonts, routes, components)
5//! - `ImageRef` / `ImageRefHash`: reference-counted decoded image handles
6//! - `FontKey` / `FontInstanceKey` / `ImageKey`: renderer-scoped resource keys
7//! - `RendererResources`: per-window font/image registry with frame-based GC
8//! - `RawImage`: CPU-side pixel data with format conversion to BGRA8
9//! - `build_add_font_resource_updates` / `build_add_image_resource_updates`:
10//!   diff current frame against registered resources and produce WebRender updates
11
12#[cfg(not(feature = "std"))]
13use alloc::string::ToString;
14use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
15use core::{
16    fmt,
17    hash::{Hash, Hasher},
18    sync::atomic::{AtomicU64, AtomicUsize, Ordering as AtomicOrdering},
19};
20
21use azul_css::{
22    codegen::format::GetHash,
23    props::basic::{
24        pixel::DEFAULT_FONT_SIZE, ColorU, FloatValue, FontRef, LayoutRect, LayoutSize,
25        StyleFontFamily, StyleFontFamilyVec, StyleFontSize,
26    },
27    props::style::scrollbar::OptionScrollPhysics,
28    system::SystemStyle,
29    AzString, F32Vec, LayoutDebugMessage, OptionI32, StringVec, U16Vec, U32Vec, U8Vec,
30};
31use rust_fontconfig::FcFontCache;
32
33// Re-export Core* callback types for public use
34pub use crate::callbacks::{
35    CoreImageCallback, CoreRenderImageCallback, CoreRenderImageCallbackType,
36};
37use crate::{
38    callbacks::{LayoutCallback, VirtualViewCallback},
39    dom::{DomId, NodeData, NodeType},
40    geom::{LogicalPosition, LogicalRect, LogicalSize},
41    gl::{OptionGlContextPtr, Texture},
42    hit_test::DocumentId,
43    id::NodeId,
44    prop_cache::CssPropertyCache,
45    refany::RefAny,
46    styled_dom::{
47        NodeHierarchyItemId, StyleFontFamiliesHash, StyleFontFamilyHash, StyledDom, StyledNodeState,
48    },
49    ui_solver::GlyphInstance,
50    window::{AzStringPair, OptionChar, StringPairVec},
51    xml::{
52        ComponentDef, ComponentDefVec, ComponentId, ComponentLibrary, ComponentLibraryVec,
53        ComponentSource, RegisterComponentFn, RegisterComponentLibraryFn,
54    },
55    FastBTreeSet, OrderedMap,
56};
57
58/// Selects which image layer of an element a node-image update applies to.
59///
60/// Used by `CallbackInfo::change_node_image` to distinguish between replacing an
61/// element's CSS `background` image and replacing its main content image (e.g. an
62/// animated GL texture re-rendered on resize).
63#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
64#[repr(C)]
65pub enum UpdateImageType {
66    /// The update targets the element's background.
67    Background,
68    /// The update targets the element's main content.
69    Content,
70}
71
72#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73#[repr(C)]
74pub struct DpiScaleFactor {
75    pub inner: FloatValue,
76}
77
78impl DpiScaleFactor {
79    #[must_use] pub fn new(f: f32) -> Self {
80        Self {
81            inner: FloatValue::new(f),
82        }
83    }
84}
85
86/// Determines what happens when all application windows are closed
87#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
88#[repr(C)]
89#[derive(Default)]
90pub enum AppTerminationBehavior {
91    /// Return control to `main()` when all windows are closed (if platform supports it).
92    /// On macOS, this exits the `NSApplication` run loop and returns to `main()`.
93    /// This is useful if you want to clean up resources or restart the event loop.
94    ReturnToMain,
95    /// Keep the application running even when all windows are closed.
96    /// This is the standard macOS behavior (app stays in dock until explicitly quit).
97    RunForever,
98    /// Immediately terminate the process when all windows are closed.
99    /// Calls `std::process::exit(0)`.
100    #[default]
101    EndProcess,
102}
103
104
105/// A named font bundled with the application (name + raw bytes).
106/// The name is used to reference the font in CSS (e.g. `font-family: "MyFont"`).
107#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
108#[repr(C)]
109pub struct NamedFont {
110    /// The font family name to use in CSS (e.g. "Roboto", "`MyCustomFont`")
111    pub name: AzString,
112    /// Raw font file bytes (TTF, OTF, etc.)
113    pub bytes: U8Vec,
114}
115
116impl_option!(
117    NamedFont,
118    OptionNamedFont,
119    copy = false,
120    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
121);
122
123impl NamedFont {
124    #[must_use] pub const fn new(name: AzString, bytes: U8Vec) -> Self {
125        Self { name, bytes }
126    }
127}
128
129impl_vec!(NamedFont, NamedFontVec, NamedFontVecDestructor, NamedFontVecDestructorType, NamedFontVecSlice, OptionNamedFont);
130impl_vec_mut!(NamedFont, NamedFontVec);
131impl_vec_debug!(NamedFont, NamedFontVec);
132impl_vec_partialeq!(NamedFont, NamedFontVec);
133impl_vec_eq!(NamedFont, NamedFontVec);
134impl_vec_partialord!(NamedFont, NamedFontVec);
135impl_vec_ord!(NamedFont, NamedFontVec);
136impl_vec_hash!(NamedFont, NamedFontVec);
137impl_vec_clone!(NamedFont, NamedFontVec, NamedFontVecDestructor);
138
139/// Descriptor for a font that the layout engine currently has loaded in its
140/// font cache.
141///
142/// Returned by `CallbackInfo::get_loaded_fonts()`. The `font_hash` field is
143/// the same `u64` carried by `DisplayListItem::Text` glyph runs, so a callback
144/// can correlate a loaded font with the text runs that use it and then fetch
145/// the raw bytes via `CallbackInfo::get_loaded_font_bytes(font_hash)` (e.g. to
146/// embed every font the layout actually used into a generated PDF).
147#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
148#[repr(C)]
149pub struct LoadedFont {
150    /// Stable hash of the parsed font, identical to the `font_hash` stored on
151    /// `DisplayListItem::Text` glyph runs. Use this to look up the bytes with
152    /// `CallbackInfo::get_loaded_font_bytes`.
153    pub font_hash: u64,
154    /// PostScript / family name from the font's `name` table, or an empty
155    /// string if the font did not provide one.
156    pub family_name: AzString,
157    /// Total number of glyphs in the font (from the `maxp` table).
158    pub num_glyphs: u32,
159    /// `true` if the source font bytes are retained and can be retrieved with
160    /// `CallbackInfo::get_loaded_font_bytes(font_hash)`. Fonts loaded on the
161    /// production (lazy mmap) path retain their bytes; some test-only fonts do
162    /// not.
163    pub has_bytes: bool,
164}
165
166impl_option!(
167    LoadedFont,
168    OptionLoadedFont,
169    copy = false,
170    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
171);
172
173impl LoadedFont {
174    #[must_use] pub const fn new(font_hash: u64, family_name: AzString, num_glyphs: u32, has_bytes: bool) -> Self {
175        Self {
176            font_hash,
177            family_name,
178            num_glyphs,
179            has_bytes,
180        }
181    }
182}
183
184impl_vec!(LoadedFont, LoadedFontVec, LoadedFontVecDestructor, LoadedFontVecDestructorType, LoadedFontVecSlice, OptionLoadedFont);
185impl_vec_mut!(LoadedFont, LoadedFontVec);
186impl_vec_debug!(LoadedFont, LoadedFontVec);
187impl_vec_partialeq!(LoadedFont, LoadedFontVec);
188impl_vec_eq!(LoadedFont, LoadedFontVec);
189impl_vec_partialord!(LoadedFont, LoadedFontVec);
190impl_vec_ord!(LoadedFont, LoadedFontVec);
191impl_vec_hash!(LoadedFont, LoadedFontVec);
192impl_vec_clone!(LoadedFont, LoadedFontVec, LoadedFontVecDestructor);
193#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
194/// Configuration for how fonts should be loaded at app startup.
195#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
196#[repr(C, u8)]
197#[derive(Default)]
198pub enum FontLoadingConfig {
199    /// Load all system fonts (default behavior, can be slow on systems with many fonts)
200    #[default]
201    LoadAllSystemFonts,
202    /// Only load fonts for specific families (faster startup).
203    /// Generic families like "sans-serif" are automatically expanded to OS-specific fonts.
204    LoadOnlyFamilies(StringVec),
205    /// Don't load any system fonts, only use bundled fonts
206    BundledFontsOnly,
207}
208
209
210/// Mock environment for CSS evaluation.
211/// 
212/// Allows overriding auto-detected system properties for testing and development.
213/// Any field set to `None` will use the auto-detected value.
214/// Any field set to `Some(...)` will override the auto-detected value.
215/// 
216/// # Example
217/// ```rust
218/// # use azul_core::resources::CssMockEnvironment;
219/// use azul_css::dynamic_selector::{
220///     OsCondition, ThemeCondition, OsVersion,
221///     OptionOsCondition, OptionThemeCondition, OptionOsVersion,
222/// };
223/// 
224/// // Mock a Linux dark theme environment on any platform
225/// let mock = CssMockEnvironment {
226///     os: OptionOsCondition::Some(OsCondition::Linux),
227///     theme: OptionThemeCondition::Some(ThemeCondition::Dark),
228///     ..Default::default()
229/// };
230/// 
231/// // Mock Windows XP for retro testing
232/// let mock = CssMockEnvironment {
233///     os: OptionOsCondition::Some(OsCondition::Windows),
234///     os_version: OptionOsVersion::Some(OsVersion::WIN_XP),
235///     ..Default::default()
236/// };
237/// ```
238#[derive(Debug, Clone, Default)]
239#[repr(C)]
240pub struct CssMockEnvironment {
241    /// Override the current theme (light/dark)
242    pub theme: azul_css::dynamic_selector::OptionThemeCondition,
243    /// Override the current language (BCP 47 tag, e.g., "de-DE", "en-US")
244    pub language: azul_css::OptionString,
245    /// Override the detected OS version
246    pub os_version: azul_css::dynamic_selector::OptionOsVersion,
247    /// Override the detected operating system
248    pub os: azul_css::dynamic_selector::OptionOsCondition,
249    /// Override the Linux desktop environment (only applies when os = Linux)
250    pub desktop_env: azul_css::dynamic_selector::OptionLinuxDesktopEnv,
251    /// Override viewport dimensions (for @media queries)
252    /// Only use for testing - normally set by window size
253    pub viewport_width: azul_css::OptionF32,
254    pub viewport_height: azul_css::OptionF32,
255    /// Override the reduced motion preference
256    pub prefers_reduced_motion: azul_css::OptionBool,
257    /// Override the high contrast preference
258    pub prefers_high_contrast: azul_css::OptionBool,
259}
260
261impl CssMockEnvironment {
262    /// Create a mock for Linux environment
263    #[must_use] pub fn linux() -> Self {
264        Self {
265            os: azul_css::dynamic_selector::OptionOsCondition::Some(azul_css::dynamic_selector::OsCondition::Linux),
266            ..Default::default()
267        }
268    }
269    
270    /// Create a mock for Windows environment
271    #[must_use] pub fn windows() -> Self {
272        Self {
273            os: azul_css::dynamic_selector::OptionOsCondition::Some(azul_css::dynamic_selector::OsCondition::Windows),
274            ..Default::default()
275        }
276    }
277    
278    /// Create a mock for macOS environment
279    #[must_use] pub fn macos() -> Self {
280        Self {
281            os: azul_css::dynamic_selector::OptionOsCondition::Some(azul_css::dynamic_selector::OsCondition::MacOS),
282            ..Default::default()
283        }
284    }
285    
286    /// Create a mock for dark theme
287    #[must_use] pub fn dark_theme() -> Self {
288        Self {
289            theme: azul_css::dynamic_selector::OptionThemeCondition::Some(azul_css::dynamic_selector::ThemeCondition::Dark),
290            ..Default::default()
291        }
292    }
293    
294    /// Create a mock for light theme
295    #[must_use] pub fn light_theme() -> Self {
296        Self {
297            theme: azul_css::dynamic_selector::OptionThemeCondition::Some(azul_css::dynamic_selector::ThemeCondition::Light),
298            ..Default::default()
299        }
300    }
301    
302    /// Apply this mock to a `DynamicSelectorContext`
303    pub fn apply_to(&self, ctx: &mut azul_css::dynamic_selector::DynamicSelectorContext) {
304        if let azul_css::dynamic_selector::OptionOsCondition::Some(os) = self.os {
305            ctx.os = os;
306        }
307        if let azul_css::dynamic_selector::OptionOsVersion::Some(os_version) = self.os_version {
308            ctx.os_version = os_version;
309        }
310        if let azul_css::dynamic_selector::OptionLinuxDesktopEnv::Some(de) = self.desktop_env {
311            ctx.desktop_env = azul_css::dynamic_selector::OptionLinuxDesktopEnv::Some(de);
312        }
313        if let azul_css::dynamic_selector::OptionThemeCondition::Some(ref theme) = self.theme {
314            ctx.theme = theme.clone();
315        }
316        if let azul_css::OptionString::Some(ref lang) = self.language {
317            ctx.language = lang.clone();
318        }
319        if let azul_css::OptionBool::Some(reduced) = self.prefers_reduced_motion {
320            ctx.prefers_reduced_motion = if reduced {
321                azul_css::dynamic_selector::BoolCondition::True
322            } else {
323                azul_css::dynamic_selector::BoolCondition::False
324            };
325        }
326        if let azul_css::OptionBool::Some(high_contrast) = self.prefers_high_contrast {
327            ctx.prefers_high_contrast = if high_contrast {
328                azul_css::dynamic_selector::BoolCondition::True
329            } else {
330                azul_css::dynamic_selector::BoolCondition::False
331            };
332        }
333        if let azul_css::OptionF32::Some(w) = self.viewport_width {
334            ctx.viewport_width = w;
335        }
336        if let azul_css::OptionF32::Some(h) = self.viewport_height {
337            ctx.viewport_height = h;
338        }
339    }
340}
341
342impl_option!(
343    CssMockEnvironment,
344    OptionCssMockEnvironment,
345    copy = false,
346    [Debug, Clone]
347);
348
349/// A route mapping a URL pattern to a layout callback.
350///
351/// Routes are cross-platform: on desktop, switching routes swaps the
352/// active layout callback and triggers `RefreshDom`. On web, it also
353/// calls `history.pushState()` for browser navigation.
354///
355/// # Pattern syntax
356///
357/// - `"/"` — exact root
358/// - `"/about"` — exact path
359/// - `"/user/:id"` — parameterized segment, `/user/42` yields `id = "42"`
360///
361/// # C API
362/// ```c
363/// AzAppConfig_addRoute(&config, AzString_fromConstStr("/user/:id"), layout_user);
364/// ```
365#[repr(C)]
366pub struct Route {
367    /// URL pattern (e.g. `"/"`, `"/about"`, `"/user/:id"`)
368    pub pattern: AzString,
369    /// Layout callback invoked when this route is active
370    pub layout_callback: LayoutCallback,
371}
372
373impl Clone for Route {
374    fn clone(&self) -> Self {
375        Self { pattern: self.pattern.clone(), layout_callback: self.layout_callback.clone() }
376    }
377}
378impl fmt::Debug for Route {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        f.debug_struct("Route")
381            .field("pattern", &self.pattern)
382            .field("layout_callback", &self.layout_callback)
383            .finish()
384    }
385}
386impl PartialEq for Route { fn eq(&self, o: &Self) -> bool { self.pattern == o.pattern && self.layout_callback == o.layout_callback } }
387impl Eq for Route {}
388impl PartialOrd for Route { fn partial_cmp(&self, o: &Self) -> Option<core::cmp::Ordering> { Some(self.cmp(o)) } }
389impl Ord for Route { fn cmp(&self, o: &Self) -> core::cmp::Ordering { self.pattern.cmp(&o.pattern).then_with(|| self.layout_callback.cmp(&o.layout_callback)) } }
390impl Hash for Route { fn hash<H: Hasher>(&self, state: &mut H) { self.pattern.hash(state); self.layout_callback.hash(state); } }
391
392impl_option!(Route, OptionRoute, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
393impl_vec!(Route, RouteVec, RouteVecDestructor, RouteVecDestructorType, RouteVecSlice, OptionRoute);
394impl_vec_mut!(Route, RouteVec);
395impl_vec_debug!(Route, RouteVec);
396impl_vec_clone!(Route, RouteVec, RouteVecDestructor);
397impl_vec_partialeq!(Route, RouteVec);
398impl_vec_eq!(Route, RouteVec);
399impl_vec_partialord!(Route, RouteVec);
400impl_vec_ord!(Route, RouteVec);
401impl_vec_hash!(Route, RouteVec);
402
403/// Result of matching a URL against a route pattern.
404///
405/// Stores the matched pattern and any extracted parameters.
406/// Available to layout callbacks via `LayoutCallbackInfo::get_route_param()`.
407#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
408#[repr(C)]
409pub struct RouteMatch {
410    /// The matched route pattern (e.g. `"/user/:id"`)
411    pub pattern: AzString,
412    /// Extracted parameters (e.g. `[("id", "42")]`)
413    pub params: StringPairVec,
414}
415
416impl RouteMatch {
417    /// Get a route parameter by key.
418    #[must_use] pub fn get_param(&self, key: &str) -> Option<&AzString> {
419        self.params.get_key(key)
420    }
421}
422
423impl_option!(RouteMatch, OptionRouteMatch, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
424
425/// Match a URL path against a route pattern, extracting parameters.
426///
427/// Returns `Some(RouteMatch)` with extracted params on match, `None` otherwise.
428///
429/// # Examples
430/// - pattern `"/user/:id"`, path `"/user/42"` → `Some(RouteMatch { params: [("id","42")] })`
431/// - pattern `"/"`, path `"/"` → `Some(RouteMatch { params: [] })`
432/// - pattern `"/about"`, path `"/settings"` → `None`
433#[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
434#[must_use] pub fn match_route(pattern: &str, path: &str) -> Option<RouteMatch> {
435    let pat_segs: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
436    let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
437
438    if pat_segs.len() != path_segs.len() {
439        return None;
440    }
441
442    let mut params = Vec::new();
443    for (pat, val) in pat_segs.iter().zip(path_segs.iter()) {
444        if let Some(param_name) = pat.strip_prefix(':') {
445            params.push(AzStringPair {
446                key: AzString::from(param_name.to_string()),
447                value: AzString::from((*val).to_string()),
448            });
449        } else if pat != val {
450            return None;
451        }
452    }
453
454    Some(RouteMatch {
455        pattern: AzString::from(pattern.to_string()),
456        params: StringPairVec::from_vec(params),
457    })
458}
459
460/// Configuration of the SYSTEM-driven animations: physics-based scrolling
461/// and the caret / selection tweens. Lives on [`AppConfig`] so a platform or
462/// application can tune the feel without rebuilding azul.
463///
464/// The scroll physics override is applied ON TOP of the platform-discovered
465/// [`SystemStyle`] at `App::create` time (`None` keeps the per-platform
466/// preset). The tween slots always apply; set a duration to `0` to disable
467/// that tween (the caret / selection then jumps, the classic behavior).
468#[derive(Debug, Clone)]
469#[repr(C)]
470pub struct SystemAnimations {
471    // Field order: decreasing alignment (8-aligned callbacks/RefAny first,
472    // then the 4-aligned option/durations, bool last) — the autofix padding
473    // lint enforces this, and the api.json struct_fields order must match
474    // (the field-order lint enforces THAT).
475    /// The caret tween MATH: called every animation frame with the past /
476    /// current caret rectangles and linear progress `t`; returns the
477    /// rectangle to render. Default: ease-out cubic lerp.
478    pub caret_tween: crate::callbacks::CaretTweenCallback,
479    /// The selection tween MATH: called every animation frame with the
480    /// past / current selection band rectangles and linear progress `t`;
481    /// returns the rectangles to render (must match the current count).
482    /// Default: ease-out cubic lerp, rectangles paired by index.
483    pub selection_tween: crate::callbacks::SelectionTweenCallback,
484    /// User data passed to `caret_tween` on every invocation.
485    pub caret_tween_data: RefAny,
486    /// User data passed to `selection_tween` on every invocation.
487    pub selection_tween_data: RefAny,
488    /// Overrides `SystemStyle.scroll_physics` (momentum, overscroll /
489    /// rubber-band, wheel-vs-trackpad curves). `None` = platform default.
490    pub scroll_physics: OptionScrollPhysics,
491    /// Duration of the caret-move tween in ms. `0` disables the tween.
492    /// While the tween runs, caret blinking is suppressed (caret stays
493    /// solid while it moves).
494    pub caret_tween_duration_ms: u32,
495    /// Duration of the selection tween in ms. `0` disables the tween.
496    pub selection_tween_duration_ms: u32,
497    /// Focus-ring glide duration in ms (ledger #29). `0` (the DEFAULT)
498    /// disables the ring entirely — no visual change for existing apps;
499    /// an app that opts in gets a focus outline that GLIDES between
500    /// focused elements using the `caret_tween` interpolator (the ring is
501    /// suppressed while a text-editing session owns focus — there the
502    /// caret is the indicator).
503    pub focus_ring_duration_ms: u32,
504    /// Whether "scroll the caret into view" glides via the scroll-physics
505    /// spring (true, the Word feel) or jumps instantly (false — also what
506    /// [`Self::disabled`] sets, keeping e2e screenshots deterministic).
507    pub caret_scroll_glide: bool,
508}
509
510impl SystemAnimations {
511    /// All system animations disabled: no scroll-physics override, tween
512    /// durations 0 (caret / selection jump). Test drivers and deterministic
513    /// harnesses use this so screenshots never catch geometry mid-glide.
514    #[must_use] pub fn disabled() -> Self {
515        Self {
516            caret_tween_duration_ms: 0,
517            selection_tween_duration_ms: 0,
518            caret_scroll_glide: false,
519            focus_ring_duration_ms: 0,
520            ..Self::default()
521        }
522    }
523}
524
525impl Default for SystemAnimations {
526    fn default() -> Self {
527        Self {
528            scroll_physics: OptionScrollPhysics::None,
529            // Barely noticeable by design (user directive): a short glide,
530            // not an animation the eye waits for.
531            caret_tween_duration_ms: 60,
532            caret_tween: crate::callbacks::CaretTweenCallback::create(
533                crate::callbacks::default_caret_tween,
534            ),
535            caret_tween_data: RefAny::new(()),
536            selection_tween_duration_ms: 60,
537            selection_tween: crate::callbacks::SelectionTweenCallback::create(
538                crate::callbacks::default_selection_tween,
539            ),
540            selection_tween_data: RefAny::new(()),
541            caret_scroll_glide: true,
542            // Opt-in: 0 = no ring (existing apps unchanged). The Word app
543            // enables it at hookup.
544            focus_ring_duration_ms: 0,
545        }
546    }
547}
548
549/// Configuration for optional features, such as whether to enable logging or panic hooks
550#[derive(Debug, Clone)]
551#[repr(C)]
552pub struct AppConfig {
553    /// If enabled, logs error and info messages.
554    ///
555    /// Default is `LevelFilter::Error` to log all errors by default
556    pub log_level: AppLogLevel,
557    /// If the app crashes / panics, a window with a message box pops up.
558    /// Setting this to `false` disables the popup box.
559    pub enable_visual_panic_hook: bool,
560    /// If this is set to `true` (the default), a backtrace + error information
561    /// gets logged to stdout and the logging file (only if logging is enabled).
562    pub enable_logging_on_panic: bool,
563    /// Determines what happens when all windows are closed.
564    /// Default: `EndProcess` (terminate when last window closes).
565    pub termination_behavior: AppTerminationBehavior,
566    /// Icon provider for the application.
567    /// Register icons here before calling `App::run()`.
568    /// Each window will clone this provider (cheap, Arc-based).
569    pub icon_provider: crate::icon::IconProviderHandle,
570    /// Fonts bundled with the application.
571    /// These fonts are loaded into memory and take priority over system fonts.
572    pub bundled_fonts: NamedFontVec,
573    /// Configuration for how system fonts should be loaded.
574    /// Default: `LoadAllSystemFonts` (scan all system fonts at startup)
575    pub font_loading: FontLoadingConfig,
576    /// Optional mock environment for CSS evaluation.
577    /// 
578    /// When set, this overrides the auto-detected system properties (OS, theme, etc.)
579    /// for CSS @-rules and dynamic selectors. This is useful for:
580    /// - Testing OS-specific styles on a different platform
581    /// - Screenshot testing with consistent environment
582    /// - Previewing how the app looks on different systems
583    /// 
584    /// Default: None (use auto-detected system properties)
585    pub mock_css_environment: OptionCssMockEnvironment,
586    /// System style detected at startup (theme, colors, fonts, etc.)
587    /// 
588    /// This is detected once at `AppConfig::create()` and passed to all windows.
589    /// You can override this after creation to use a custom system style,
590    /// for example to test how your app looks on a different platform.
591    pub system_style: SystemStyle,
592    /// Component libraries registered at startup.
593    ///
594    /// Use `add_component()` to register individual components, or
595    /// `add_component_library()` to register entire libraries.
596    /// User-registered (and built-in) component libraries.
597    ///
598    /// The 52 built-in HTML elements are automatically registered by
599    /// `AppConfig::create()` via `register_builtin_components`.
600    /// Additional libraries can be added with `add_component_library`.
601    pub component_libraries: ComponentLibraryVec,
602    /// Registered routes mapping URL patterns to layout callbacks.
603    ///
604    /// Cross-platform: on desktop, the active route determines which layout
605    /// callback runs. On web, routes map to HTTP endpoints and browser URLs.
606    ///
607    /// The first route (or `"/"`) is the default. Use `add_route()` to register.
608    pub routes: RouteVec,
609    /// System-animation configuration (scroll physics override, caret /
610    /// selection tween hooks). See [`SystemAnimations`].
611    pub system_animations: SystemAnimations,
612    /// Handler for E2E ops the engine does not implement, letting a scenario
613    /// drive application-level actions ("now load the document") that the
614    /// engine cannot express on the app's behalf.
615    ///
616    /// The default recognises nothing, so a scenario naming a custom op fails
617    /// unless the app installed a handler.
618    pub custom_e2e_op: crate::events::CustomE2eOpCallback,
619}
620
621impl AppConfig {
622    #[must_use] pub fn create() -> Self {
623        let log_level = AppLogLevel::Error;
624        let icon_provider = crate::icon::IconProviderHandle::new();
625        let bundled_fonts = NamedFontVec::from_const_slice(&[]);
626        let font_loading = FontLoadingConfig::default();
627        let system_style = SystemStyle::detect();
628        let mut s = Self {
629            log_level,
630            enable_visual_panic_hook: false,
631            enable_logging_on_panic: true,
632            termination_behavior: AppTerminationBehavior::default(),
633            icon_provider,
634            bundled_fonts,
635            font_loading,
636            mock_css_environment: OptionCssMockEnvironment::None,
637            system_style,
638            component_libraries: ComponentLibraryVec::from_const_slice(&[]),
639            routes: RouteVec::from_const_slice(&[]),
640            system_animations: SystemAnimations::default(),
641            custom_e2e_op: crate::events::CustomE2eOpCallback::default(),
642        };
643        // Dogfood: register the 52 built-in HTML elements via the
644        // same `add_component_library` API that users call.
645        // Annotated binding coerces the fn item to the fn-pointer type that
646        // `Into<RegisterComponentLibraryFn>` is implemented for (no `as` cast).
647        let register_builtin: crate::xml::RegisterComponentLibraryFnType =
648            crate::xml::register_builtin_components;
649        s.add_component_library(
650            AzString::from_const_str("builtin"),
651            register_builtin,
652        );
653        s
654    }
655    
656    /// Create config with a mock CSS environment for testing
657    /// 
658    /// This allows you to simulate how your app would look on a different OS,
659    /// with a different theme, language, or accessibility settings.
660    /// 
661    /// # Example
662    /// ```rust
663    /// # use azul_core::resources::{AppConfig, CssMockEnvironment};
664    /// # use azul_css::dynamic_selector::{OsCondition, OptionOsCondition, ThemeCondition, OptionThemeCondition};
665    /// let config = AppConfig::create()
666    ///     .with_mock_environment(CssMockEnvironment {
667    ///         os: OptionOsCondition::Some(OsCondition::Linux),
668    ///         theme: OptionThemeCondition::Some(ThemeCondition::Dark),
669    ///         ..Default::default()
670    ///     });
671    /// ```
672    #[must_use] pub fn with_mock_environment(mut self, env: CssMockEnvironment) -> Self {
673        self.mock_css_environment = OptionCssMockEnvironment::Some(env);
674        self
675    }
676
677    /// Register a single component into a named library.
678    ///
679    /// Calls `register_fn` immediately and adds the returned `ComponentDef`
680    /// to the library named `library`. If no library with that name exists,
681    /// a new one is created. If a component with the same `id.name` already
682    /// exists in the library, it is replaced.
683    ///
684    /// # C API
685    /// ```c
686    /// AzAppConfig_addComponent(&config, AzString_fromConstStr("mylib"), my_register_fn);
687    /// ```
688    pub fn add_component<R: Into<RegisterComponentFn>>(&mut self, library: AzString, register_fn: R) {
689        let register_fn = register_fn.into();
690        let component = (register_fn.cb)();
691        let empty_libs = ComponentLibraryVec::from_const_slice(&[]);
692        let mut libs = core::mem::replace(&mut self.component_libraries, empty_libs).into_library_owned_vec();
693
694        if let Some(existing_lib) = libs.iter_mut().find(|l| l.name.as_str() == library.as_str()) {
695            let empty_comps = ComponentDefVec::from_const_slice(&[]);
696            let mut comps = core::mem::replace(&mut existing_lib.components, empty_comps).into_library_owned_vec();
697            if let Some(ec) = comps.iter_mut().find(|c| c.id.name.as_str() == component.id.name.as_str()) {
698                *ec = component;
699            } else {
700                comps.push(component);
701            }
702            existing_lib.components = ComponentDefVec::from_vec(comps);
703        } else {
704            libs.push(ComponentLibrary {
705                name: library,
706                version: AzString::from_const_str("1.0.0"),
707                description: AzString::from_const_str(""),
708                components: ComponentDefVec::from_vec(alloc::vec![component]),
709                exportable: true,
710                modifiable: true,
711                data_models: crate::xml::ComponentDataModelVec::from_const_slice(&[]),
712                enum_models: crate::xml::ComponentEnumModelVec::from_const_slice(&[]),
713            });
714        }
715
716        self.component_libraries = ComponentLibraryVec::from_vec(libs);
717    }
718
719    /// Register an entire component library.
720    ///
721    /// Calls `register_fn` immediately and adds the returned
722    /// `ComponentLibrary` to the config. Uses `name` as the library name
723    /// (overriding whatever the function sets). If a library with the same
724    /// name already exists, it is replaced wholesale.
725    ///
726    /// # C API
727    /// ```c
728    /// AzAppConfig_addComponentLibrary(&config, AzString_fromConstStr("vendor"), my_lib_fn);
729    /// ```
730    pub fn add_component_library<R: Into<RegisterComponentLibraryFn>>(&mut self, name: AzString, register_fn: R) {
731        let register_fn = register_fn.into();
732        let mut library = (register_fn.cb)();
733        library.name = name;
734
735        let empty_libs = ComponentLibraryVec::from_const_slice(&[]);
736        let mut libs = core::mem::replace(&mut self.component_libraries, empty_libs).into_library_owned_vec();
737        if let Some(existing) = libs.iter_mut().find(|l| l.name.as_str() == library.name.as_str()) {
738            *existing = library;
739        } else {
740            libs.push(library);
741        }
742
743        self.component_libraries = ComponentLibraryVec::from_vec(libs);
744    }
745
746    /// Register a route mapping a URL pattern to a layout callback.
747    ///
748    /// On web: each route becomes an HTTP endpoint. On desktop: the first
749    /// route (or `"/"`) is the initial layout, and `CallbackInfo::switch_route()`
750    /// swaps the active callback.
751    ///
752    /// # C API
753    /// ```c
754    /// AzAppConfig_addRoute(&config, AzString_fromConstStr("/user/:id"), layout_user);
755    /// ```
756    pub fn add_route<P: Into<AzString>, L: Into<LayoutCallback>>(&mut self, pattern: P, layout_fn: L) {
757        let route = Route {
758            pattern: pattern.into(),
759            layout_callback: layout_fn.into(),
760        };
761        let empty = RouteVec::from_const_slice(&[]);
762        let mut routes = core::mem::replace(&mut self.routes, empty).into_library_owned_vec();
763        // Replace existing route with the same pattern
764        if let Some(existing) = routes.iter_mut().find(|r| r.pattern.as_str() == route.pattern.as_str()) {
765            *existing = route;
766        } else {
767            routes.push(route);
768        }
769        self.routes = RouteVec::from_vec(routes);
770    }
771
772    /// Find the route matching a given URL path.
773    ///
774    /// Returns the matched `Route` and a `RouteMatch` with extracted parameters.
775    #[must_use] pub fn match_route_for_path(&self, path: &str) -> Option<(&Route, RouteMatch)> {
776        for route in self.routes.as_ref() {
777            if let Some(m) = match_route(route.pattern.as_str(), path) {
778                return Some((route, m));
779            }
780        }
781        None
782    }
783}
784
785impl Default for AppConfig {
786    fn default() -> Self {
787        Self::create()
788    }
789}
790
791#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
792#[repr(C)]
793pub enum AppLogLevel {
794    Off,
795    Error,
796    Warn,
797    Info,
798    Debug,
799    Trace,
800}
801
802/// Metadata (but not storage) describing an image In `WebRender`.
803#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
804#[repr(C)]
805pub struct ImageDescriptor {
806    /// Format of the image data.
807    pub format: RawImageFormat,
808    /// Width and height of the image data, in pixels.
809    pub width: usize,
810    pub height: usize,
811    /// The number of bytes from the start of one row to the next. If non-None,
812    /// `compute_stride` will return this value, otherwise it returns
813    /// `width * bpp`. Different source of images have different alignment
814    /// constraints for rows, so the stride isn't always equal to width * bpp.
815    pub stride: OptionI32,
816    /// Offset in bytes of the first pixel of this image in its backing buffer.
817    /// This is used for tiling, wherein `WebRender` extracts chunks of input images
818    /// in order to cache, manipulate, and render them individually. This offset
819    /// tells the texture upload machinery where to find the bytes to upload for
820    /// this tile. Non-tiled images generally set this to zero.
821    pub offset: i32,
822    /// Various bool flags related to this descriptor.
823    pub flags: ImageDescriptorFlags,
824}
825
826/// Various flags that are part of an image descriptor.
827#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
828#[repr(C)]
829pub struct ImageDescriptorFlags {
830    /// Whether this image is opaque, or has an alpha channel. Avoiding blending
831    /// for opaque surfaces is an important optimization.
832    pub is_opaque: bool,
833    /// Whether to allow the driver to automatically generate mipmaps. If images
834    /// are already downscaled appropriately, mipmap generation can be wasted
835    /// work, and cause performance problems on some cards/drivers.
836    ///
837    /// See <https://github.com/servo/webrender/pull/2555>/
838    pub allow_mipmaps: bool,
839}
840
841#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
842pub struct IdNamespace(pub u32);
843
844impl ::core::fmt::Display for IdNamespace {
845    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
846        write!(f, "IdNamespace({})", self.0)
847    }
848}
849
850impl ::core::fmt::Debug for IdNamespace {
851    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
852        write!(f, "{self}")
853    }
854}
855
856#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
857#[repr(C)]
858pub enum RawImageFormat {
859    R8,
860    RG8,
861    RGB8,
862    RGBA8,
863    R16,
864    RG16,
865    RGB16,
866    RGBA16,
867    BGR8,
868    BGRA8,
869    RGBF32,
870    RGBAF32,
871}
872
873// NOTE: starts at 1 (0 = DUMMY)
874static IMAGE_KEY: AtomicU64 = AtomicU64::new(1);
875static FONT_KEY: AtomicU64 = AtomicU64::new(0);
876static FONT_INSTANCE_KEY: AtomicU64 = AtomicU64::new(0);
877
878#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
879pub struct ImageKey {
880    pub namespace: IdNamespace,
881    pub key: u64,
882}
883
884impl ImageKey {
885    pub const DUMMY: Self = Self {
886        namespace: IdNamespace(0),
887        key: 0,
888    };
889
890    pub fn unique(render_api_namespace: IdNamespace) -> Self {
891        Self {
892            namespace: render_api_namespace,
893            key: IMAGE_KEY.fetch_add(1, AtomicOrdering::SeqCst),
894        }
895    }
896}
897
898#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
899pub struct FontKey {
900    pub namespace: IdNamespace,
901    pub key: u64,
902}
903
904impl FontKey {
905    pub fn unique(render_api_namespace: IdNamespace) -> Self {
906        Self {
907            namespace: render_api_namespace,
908            key: FONT_KEY.fetch_add(1, AtomicOrdering::SeqCst),
909        }
910    }
911}
912
913#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
914pub struct FontInstanceKey {
915    pub namespace: IdNamespace,
916    pub key: u64,
917}
918
919impl FontInstanceKey {
920    pub fn unique(render_api_namespace: IdNamespace) -> Self {
921        Self {
922            namespace: render_api_namespace,
923            key: FONT_INSTANCE_KEY.fetch_add(1, AtomicOrdering::SeqCst),
924        }
925    }
926}
927
928// NOTE: This type should NOT be exposed in the API!
929// The only public functions are the constructors
930#[derive(Debug)]
931pub enum DecodedImage {
932    /// Image that has a reserved key, but no data, i.e it is not yet rendered
933    /// or there was an error during rendering
934    NullImage {
935        width: usize,
936        height: usize,
937        format: RawImageFormat,
938        /// Sometimes images need to be tagged with extra data
939        tag: Vec<u8>,
940    },
941    // OpenGl texture
942    Gl(Texture),
943    // Image backed by CPU-rendered pixels
944    Raw((ImageDescriptor, ImageData)),
945    // Same as `Texture`, but rendered AFTER the layout has been done
946    Callback(CoreImageCallback),
947    // YUVImage(...)
948    // VulkanSurface(...)
949    // MetalSurface(...),
950    // DirectXSurface(...)
951}
952
953#[derive(Debug)]
954#[repr(C)]
955pub struct ImageRef {
956    /// Shared pointer to an opaque implementation of the decoded image
957    pub data: *const DecodedImage,
958    /// How many copies does this image have (if 0, the font data will be deleted on drop)
959    pub copies: *const AtomicUsize,
960    /// Process-unique, monotonically-assigned identity of the *decoded image*
961    /// (see [`ImageRefHash`]). Shared by shallow clones (they are the same
962    /// image), fresh for [`ImageRef::deep_copy`] and every `new_*` (a
963    /// different image). Unlike the old `data`-pointer identity this is drawn
964    /// from a never-reused counter, so freeing an image and reusing its heap
965    /// address can never make a *new* image collide with a stale key — the
966    /// prerequisite for image GC (see resources.rs `image_ref_get_hash`).
967    pub id: u64,
968    pub run_destructor: bool,
969}
970
971/// Never-reused source of [`ImageRef::id`]. Starts at 1 so `id == 0` can flag
972/// an un-initialised / raw-reconstructed handle.
973static IMAGE_REF_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
974
975#[must_use]
976fn next_image_ref_id() -> u64 {
977    IMAGE_REF_ID_COUNTER.fetch_add(1, AtomicOrdering::SeqCst)
978}
979
980impl ImageRef {
981    #[must_use] pub const fn get_hash(&self) -> ImageRefHash {
982        image_ref_get_hash(self)
983    }
984}
985
986#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Ord, Eq)]
987#[repr(C)]
988pub struct ImageRefHash {
989    pub inner: u64,
990}
991
992impl_option!(
993    ImageRef,
994    OptionImageRef,
995    copy = false,
996    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
997);
998
999impl ImageRef {
1000    /// If *copies = 1, returns the internal image data
1001    #[must_use] pub fn into_inner(self) -> Option<DecodedImage> {
1002        // SAFETY: `data`/`copies` are non-null heap allocations from `Box::into_raw`
1003        // in `new()` (never mutated afterwards). When `copies == 1` we are the sole
1004        // owner, so reclaiming both Boxes and `forget`-ing `self` transfers ownership
1005        // without a double free / running the destructor twice.
1006        unsafe {
1007            if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
1008                let data = Box::from_raw(self.data.cast_mut());
1009                drop(Box::from_raw(self.copies.cast_mut()));
1010                core::mem::forget(self); // do not run the destructor
1011                Some(*data)
1012            } else {
1013                None
1014            }
1015        }
1016    }
1017
1018    #[must_use] pub const fn get_data(&self) -> &DecodedImage {
1019        // SAFETY: `data` is a non-null, live `Box` allocation owned by this handle
1020        // (and its shallow clones) until the last copy drops; the returned borrow is
1021        // tied to `&self`, so it cannot outlive the allocation.
1022        unsafe { &*self.data }
1023    }
1024
1025    #[must_use] pub fn get_image_callback(&self) -> Option<&CoreImageCallback> {
1026        // SAFETY: `copies` is a non-null, live allocation for the lifetime of `&self`.
1027        if unsafe { self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) != Some(1) } {
1028            return None; // not safe: shared, so no exclusive borrow of the data
1029        }
1030
1031        // SAFETY: `data` is a non-null, live `Box` allocation; borrow tied to `&self`.
1032        match unsafe { &*self.data } {
1033            DecodedImage::Callback(gl_texture_callback) => Some(gl_texture_callback),
1034            _ => None,
1035        }
1036    }
1037
1038    pub fn get_image_callback_mut(&mut self) -> Option<&mut CoreImageCallback> {
1039        // SAFETY: `copies` is a non-null, live allocation for the lifetime of `&self`.
1040        if unsafe { self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) != Some(1) } {
1041            return None; // not safe: shared, so a &mut would alias other clones' data
1042        }
1043
1044        // SAFETY: `copies == 1` proven above, so `&mut self` is the unique owner of
1045        // the `data` allocation; the exclusive borrow is tied to `&mut self`.
1046        match unsafe { &mut *self.data.cast_mut() } {
1047            DecodedImage::Callback(gl_texture_callback) => Some(gl_texture_callback),
1048            _ => None,
1049        }
1050    }
1051
1052    /// In difference to the default shallow copy, creates a new image ref
1053    #[must_use] pub fn deep_copy(&self) -> Self {
1054        let new_data = match self.get_data() {
1055            DecodedImage::NullImage {
1056                width,
1057                height,
1058                format,
1059                tag,
1060            } => DecodedImage::NullImage {
1061                width: *width,
1062                height: *height,
1063                format: *format,
1064                tag: tag.clone(),
1065            },
1066            // NOTE: textures cannot be deep-copied yet (since the OpenGL calls for that
1067            // are missing from the trait), so calling clone() on a GL texture will result in an
1068            // empty image
1069            DecodedImage::Gl(tex) => DecodedImage::NullImage {
1070                width: tex.size.width as usize,
1071                height: tex.size.height as usize,
1072                format: tex.format,
1073                tag: Vec::new(),
1074            },
1075            // WARNING: the data may still be a U8Vec<'static> - the data may still not be
1076            // actually cloned. The data only gets cloned on a write operation
1077            DecodedImage::Raw((descriptor, data)) => {
1078                DecodedImage::Raw((*descriptor, data.clone()))
1079            }
1080            DecodedImage::Callback(cb) => DecodedImage::Callback(cb.clone()),
1081        };
1082
1083        Self::new(new_data)
1084    }
1085
1086    #[must_use] pub const fn is_null_image(&self) -> bool {
1087        matches!(self.get_data(), DecodedImage::NullImage { .. })
1088    }
1089
1090    #[must_use] pub const fn is_gl_texture(&self) -> bool {
1091        matches!(self.get_data(), DecodedImage::Gl(_))
1092    }
1093
1094    #[must_use] pub const fn is_raw_image(&self) -> bool {
1095        matches!(self.get_data(), DecodedImage::Raw((_, _)))
1096    }
1097
1098    #[must_use] pub const fn is_callback(&self) -> bool {
1099        matches!(self.get_data(), DecodedImage::Callback(_))
1100    }
1101
1102    // OptionRawImage
1103    #[must_use] pub fn get_rawimage(&self) -> Option<RawImage> {
1104        match self.get_data() {
1105            DecodedImage::Raw((image_descriptor, image_data)) => Some(RawImage {
1106                pixels: match image_data {
1107                    ImageData::Raw(shared_data) => {
1108                        // Clone the SharedRawImageData (increments ref count),
1109                        // then try to extract or convert to U8Vec
1110                        let data_clone = shared_data.clone();
1111                        data_clone.into_inner().map_or_else(|| RawImageData::U8(shared_data.as_ref().to_vec().into()), RawImageData::U8)
1112                    }
1113                    ImageData::External(_) => return None,
1114                },
1115                width: image_descriptor.width,
1116                height: image_descriptor.height,
1117                premultiplied_alpha: true,
1118                data_format: image_descriptor.format,
1119                tag: Vec::new().into(),
1120            }),
1121            _ => None,
1122        }
1123    }
1124
1125    /// Get raw bytes from the image as a slice
1126    /// Returns None if this is not a Raw image or if it's an External image
1127    #[must_use] pub fn get_bytes(&self) -> Option<&[u8]> {
1128        match self.get_data() {
1129            DecodedImage::Raw((_, image_data)) => match image_data {
1130                ImageData::Raw(shared_data) => Some(shared_data.as_ref()),
1131                ImageData::External(_) => None,
1132            },
1133            _ => None,
1134        }
1135    }
1136
1137    /// Get a pointer to the raw bytes for debugging/profiling purposes
1138    /// Returns a unique pointer for this `ImageRef`'s data
1139    #[must_use] pub fn get_bytes_ptr(&self) -> *const u8 {
1140        match self.get_data() {
1141            DecodedImage::Raw((_, image_data)) => match image_data {
1142                ImageData::Raw(shared_data) => shared_data.as_ptr(),
1143                ImageData::External(_) => core::ptr::null(),
1144            },
1145            _ => core::ptr::null(),
1146        }
1147    }
1148
1149    /// NOTE: returns (0, 0) for a Callback
1150    #[allow(clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1151    #[must_use] pub const fn get_size(&self) -> LogicalSize {
1152        match self.get_data() {
1153            DecodedImage::NullImage { width, height, .. } => {
1154                LogicalSize::new(*width as f32, *height as f32)
1155            }
1156            DecodedImage::Gl(tex) => {
1157                LogicalSize::new(tex.size.width as f32, tex.size.height as f32)
1158            }
1159            DecodedImage::Raw((image_descriptor, _)) => LogicalSize::new(
1160                image_descriptor.width as f32,
1161                image_descriptor.height as f32,
1162            ),
1163            DecodedImage::Callback(_) => LogicalSize::new(0.0, 0.0),
1164        }
1165    }
1166
1167    #[must_use] pub fn null_image(width: usize, height: usize, format: RawImageFormat, tag: Vec<u8>) -> Self {
1168        Self::new(DecodedImage::NullImage {
1169            width,
1170            height,
1171            format,
1172            tag,
1173        })
1174    }
1175
1176    pub fn callback<C: Into<CoreRenderImageCallback>>(callback: C, data: RefAny) -> Self {
1177        Self::new(DecodedImage::Callback(CoreImageCallback {
1178            callback: callback.into(),
1179            refany: data,
1180        }))
1181    }
1182
1183    #[must_use] pub fn new_rawimage(image_data: RawImage) -> Option<Self> {
1184        let (image_data, image_descriptor) = image_data.into_loaded_image_source()?;
1185        Some(Self::new(DecodedImage::Raw((image_descriptor, image_data))))
1186    }
1187
1188    #[must_use] pub fn new_gltexture(texture: Texture) -> Self {
1189        Self::new(DecodedImage::Gl(texture))
1190    }
1191
1192    fn new(data: DecodedImage) -> Self {
1193        Self {
1194            data: Box::into_raw(Box::new(data)),
1195            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
1196            id: next_image_ref_id(),
1197            run_destructor: true,
1198        }
1199    }
1200
1201    // pub fn new_vulkan(...) -> Self
1202}
1203
1204// SAFETY: the raw pointers only ever address heap `Box`es whose contents are
1205// themselves `Send`/`Sync`, and all cross-thread refcount mutation goes through the
1206// `AtomicUsize` in `copies`, so sharing/moving a handle across threads is sound.
1207unsafe impl Send for ImageRef {}
1208unsafe impl Sync for ImageRef {}
1209
1210// Identity is the never-reused `id`, NOT the `data` pointer: two shallow
1211// clones of one image share an `id` (equal); distinct images (incl. a
1212// `deep_copy`) get distinct ids; a freed image's id is never handed to a
1213// later image, so a reused heap address can't forge equality.
1214impl PartialEq for ImageRef {
1215    fn eq(&self, rhs: &Self) -> bool {
1216        self.id == rhs.id
1217    }
1218}
1219
1220impl PartialOrd for ImageRef {
1221    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
1222        Some(self.id.cmp(&other.id))
1223    }
1224}
1225
1226impl Ord for ImageRef {
1227    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1228        self.id.cmp(&other.id)
1229    }
1230}
1231
1232impl Eq for ImageRef {}
1233
1234impl Hash for ImageRef {
1235    fn hash<H>(&self, state: &mut H)
1236    where
1237        H: Hasher,
1238    {
1239        self.id.hash(state);
1240    }
1241}
1242
1243impl Clone for ImageRef {
1244    fn clone(&self) -> Self {
1245        // SAFETY: `copies` is a non-null, live `AtomicUsize` allocation shared by all
1246        // clones; the atomic increment balances the `fetch_sub` in `Drop`.
1247        unsafe {
1248            self.copies
1249                .as_ref()
1250                .map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
1251        }
1252        Self {
1253            data: self.data,     // copy the pointer
1254            copies: self.copies, // copy the pointer
1255            id: self.id,         // same image → same identity
1256            run_destructor: true,
1257        }
1258    }
1259}
1260
1261impl Drop for ImageRef {
1262    fn drop(&mut self) {
1263        self.run_destructor = false;
1264        // SAFETY: `data`/`copies` are non-null, live `Box` allocations shared by all
1265        // clones. `fetch_sub` returns the pre-decrement count, so `== 1` means this is
1266        // the last owner; only then do we reclaim both Boxes exactly once.
1267        unsafe {
1268            let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
1269            if copies == 1 {
1270                drop(Box::from_raw(self.data.cast_mut()));
1271                drop(Box::from_raw(self.copies.cast_mut()));
1272            }
1273        }
1274    }
1275}
1276
1277#[must_use] pub const fn image_ref_get_hash(ir: &ImageRef) -> ImageRefHash {
1278    // The identity is the never-reused `id`, not the freeable `data` pointer
1279    // (see the `id` field docs). This is what makes an ImageKey safe to
1280    // DeleteImage: once an image is dropped its id is retired forever, so a
1281    // future image that reuses the same heap address gets a *different* key
1282    // and is registered/uploaded correctly instead of aliasing the stale one.
1283    ImageRefHash {
1284        inner: ir.id,
1285    }
1286}
1287
1288/// Convert a stable `ImageRefHash` directly to an `ImageKey`.
1289///
1290/// `ImageKey.key` is a `u64` and `ImageRefHash.inner` is the `ImageRef` `id`
1291/// (a `u64` counter) stored in a `usize`; on a 32-bit host that truncates the
1292/// top 32 bits, which is fine — a run would need 4 billion live images for the
1293/// low 32 bits to collide.
1294#[must_use] pub const fn image_ref_hash_to_image_key(hash: ImageRefHash, namespace: IdNamespace) -> ImageKey {
1295    ImageKey {
1296        namespace,
1297        key: hash.inner,
1298    }
1299}
1300
1301#[must_use] pub fn font_ref_get_hash(fr: &FontRef) -> u64 {
1302    fr.get_hash()
1303}
1304
1305/// Stores the resources for the application, such as fonts, images and cached
1306/// texts, also clipboard strings
1307///
1308/// Images and fonts can be references across window contexts (not yet tested,
1309/// but should work).
1310#[derive(Debug)]
1311#[derive(Default)]
1312pub struct ImageCache {
1313    /// The `AzString` is the string used in the CSS, i.e. `url("my_image`") = "`my_image`" -> ImageId(4)
1314    ///
1315    /// NOTE: This is the only map that is modifiable by the user and that has to be manually
1316    /// managed all other maps are library-internal only and automatically delete their
1317    /// resources once they aren't needed anymore
1318    pub image_id_map: OrderedMap<AzString, ImageRef>,
1319}
1320
1321
1322impl ImageCache {
1323    #[must_use] pub fn new() -> Self {
1324        Self::default()
1325    }
1326
1327    // -- ImageId cache
1328
1329    pub fn add_css_image_id(&mut self, css_id: AzString, image: ImageRef) {
1330        self.image_id_map.insert(css_id, image);
1331    }
1332
1333    #[must_use] pub fn get_css_image_id(&self, css_id: &AzString) -> Option<&ImageRef> {
1334        self.image_id_map.get(css_id)
1335    }
1336
1337    pub fn delete_css_image_id(&mut self, css_id: &AzString) {
1338        self.image_id_map.remove(css_id);
1339    }
1340}
1341
1342#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1343pub struct ResolvedImage {
1344    pub key: ImageKey,
1345    pub descriptor: ImageDescriptor,
1346}
1347
1348/// Trait for accessing font resources
1349pub trait RendererResourcesTrait: fmt::Debug {
1350    /// Get a font family hash from a font families hash
1351    fn get_font_family(
1352        &self,
1353        style_font_families_hash: &StyleFontFamiliesHash,
1354    ) -> Option<&StyleFontFamilyHash>;
1355
1356    /// Get a font key from a font family hash
1357    fn get_font_key(&self, style_font_family_hash: &StyleFontFamilyHash) -> Option<&FontKey>;
1358
1359    /// Get a registered font and its instances from a font key
1360    fn get_registered_font(
1361        &self,
1362        font_key: &FontKey,
1363    ) -> Option<&(FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)>;
1364
1365    /// Get image information from an image hash
1366    fn get_image(&self, hash: &ImageRefHash) -> Option<&ResolvedImage>;
1367
1368    /// Update an image descriptor for an existing image hash
1369    fn update_image(
1370        &mut self,
1371        image_ref_hash: &ImageRefHash,
1372        descriptor: ImageDescriptor,
1373    );
1374}
1375
1376// Implementation for the original RendererResources struct
1377impl RendererResourcesTrait for RendererResources {
1378    fn get_font_family(
1379        &self,
1380        style_font_families_hash: &StyleFontFamiliesHash,
1381    ) -> Option<&StyleFontFamilyHash> {
1382        self.font_families_map.get(style_font_families_hash)
1383    }
1384
1385    fn get_font_key(&self, style_font_family_hash: &StyleFontFamilyHash) -> Option<&FontKey> {
1386        self.font_id_map.get(style_font_family_hash)
1387    }
1388
1389    fn get_registered_font(
1390        &self,
1391        font_key: &FontKey,
1392    ) -> Option<&(FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)> {
1393        self.currently_registered_fonts.get(font_key)
1394    }
1395
1396    fn get_image(&self, hash: &ImageRefHash) -> Option<&ResolvedImage> {
1397        self.currently_registered_images.get(hash)
1398    }
1399
1400    fn update_image(
1401        &mut self,
1402        image_ref_hash: &ImageRefHash,
1403        descriptor: ImageDescriptor,
1404    ) {
1405        if let Some(s) = self.currently_registered_images.get_mut(image_ref_hash) {
1406            s.descriptor = descriptor;
1407        }
1408    }
1409}
1410
1411/// Renderer resources that manage font, image and font instance keys.
1412/// `RendererResources` are local to each renderer / window, since the
1413/// keys are not shared across renderers
1414///
1415/// The resources are automatically managed, meaning that they each new frame
1416/// (signified by `start_frame_gc` and `end_frame_gc`)
1417#[derive(Default)]
1418pub struct RendererResources {
1419    /// All image keys currently active in the `RenderApi`
1420    pub currently_registered_images: OrderedMap<ImageRefHash, ResolvedImage>,
1421    /// Reverse lookup: `ImageKey` -> `ImageRefHash` for display list translation
1422    pub image_key_map: OrderedMap<ImageKey, ImageRefHash>,
1423    /// Image GC bookkeeping: last epoch (as `u32`) each registered image was
1424    /// seen referenced by a display list. An image absent for more than
1425    /// `IMAGE_GC_KEEP_EPOCHS` frames is `DeleteImage`d and evicted — this is
1426    /// what stops the unbounded texture growth of a window that swaps images
1427    /// every frame (video / capture / animated charts). Safe because
1428    /// `ImageRefHash` is now a never-reused id, not a freeable pointer.
1429    pub image_last_seen_epoch: OrderedMap<ImageRefHash, u32>,
1430    /// All font keys currently active in the `RenderApi`
1431    pub currently_registered_fonts:
1432        OrderedMap<FontKey, (FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)>,
1433    /// Fonts registered on the last frame
1434    ///
1435    /// Fonts differ from images in that regard that we can't immediately
1436    /// delete them on a new frame, instead we have to delete them on "current frame + 1"
1437    /// This is because when the frame is being built, we do not know
1438    /// whether the font will actually be successfully loaded
1439    pub last_frame_registered_fonts:
1440        OrderedMap<FontKey, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>>,
1441    /// Map from the calculated families vec (`["Arial", "Helvetica"]`)
1442    /// to the final loaded font that could be loaded
1443    /// (in this case "Arial" on Windows and "Helvetica" on Mac,
1444    /// because the fonts are loaded in fallback-order)
1445    pub font_families_map: OrderedMap<StyleFontFamiliesHash, StyleFontFamilyHash>,
1446    /// Same as `AzString` -> `ImageId`, but for fonts, i.e. "Roboto" -> FontId(9)
1447    pub font_id_map: OrderedMap<StyleFontFamilyHash, FontKey>,
1448    /// Direct mapping from font hash (from `FontRef`) to `FontKey`
1449    /// TODO: This should become part of `SharedFontRegistry`
1450    pub font_hash_map: OrderedMap<u64, FontKey>,
1451}
1452
1453impl fmt::Debug for RendererResources {
1454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1455        write!(
1456            f,
1457            "RendererResources {{
1458                currently_registered_images: {:#?},
1459                currently_registered_fonts: {:#?},
1460                font_families_map: {:#?},
1461                font_id_map: {:#?},
1462            }}",
1463            self.currently_registered_images.keys().collect::<Vec<_>>(),
1464            self.currently_registered_fonts.keys().collect::<Vec<_>>(),
1465            self.font_families_map.keys().collect::<Vec<_>>(),
1466            self.font_id_map.keys().collect::<Vec<_>>(),
1467        )
1468    }
1469}
1470
1471
1472impl RendererResources {
1473    #[must_use] pub fn get_renderable_font_data(
1474        &self,
1475        font_instance_key: &FontInstanceKey,
1476    ) -> Option<(&FontRef, Au, DpiScaleFactor)> {
1477        self.currently_registered_fonts
1478            .iter()
1479            .find_map(|(font_key, (font_ref, instances))| {
1480                instances.iter().find_map(|((au, dpi), instance_key)| {
1481                    if *instance_key == *font_instance_key {
1482                        Some((font_ref, *au, *dpi))
1483                    } else {
1484                        None
1485                    }
1486                })
1487            })
1488    }
1489
1490    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
1491    pub fn get_font_instance_key_for_text(
1492        &self,
1493        font_size_px: f32,
1494        css_property_cache: &CssPropertyCache,
1495        node_data: &NodeData,
1496        node_id: &NodeId,
1497        styled_node_state: &StyledNodeState,
1498        dpi_scale: f32,
1499    ) -> Option<FontInstanceKey> {
1500        // Convert font size to StyleFontSize.
1501        //
1502        // `font_size_px as isize` saturates +inf / f32::MAX to isize::MAX (and
1503        // -inf / -f32::MAX to isize::MIN). `const_px` then multiplies by 1000
1504        // (FP_PRECISION_MULTIPLIER) inside `FloatValue::const_new`, which would
1505        // overflow. Clamp to the range that survives that multiply so an absurd
1506        // size misses cleanly instead of panicking.
1507        let font_size_isize =
1508            (font_size_px as isize).clamp(isize::MIN / 1000, isize::MAX / 1000);
1509        let font_size = StyleFontSize {
1510            inner: azul_css::props::basic::PixelValue::const_px(font_size_isize),
1511        };
1512
1513        // Convert to application units
1514        let font_size_au = font_size_to_au(font_size);
1515
1516        // Create DPI scale factor
1517        let dpi_scale_factor = DpiScaleFactor {
1518            inner: FloatValue::new(dpi_scale),
1519        };
1520
1521        // Get font family
1522        let font_family =
1523            css_property_cache.get_font_id_or_default(node_data, node_id, styled_node_state);
1524
1525        // Calculate hash and lookup font instance key
1526        let font_families_hash = StyleFontFamiliesHash::new(font_family.as_ref());
1527
1528        self.get_font_instance_key(&font_families_hash, font_size_au, dpi_scale_factor)
1529    }
1530
1531    #[must_use] pub fn get_font_instance_key(
1532        &self,
1533        font_families_hash: &StyleFontFamiliesHash,
1534        font_size_au: Au,
1535        dpi_scale: DpiScaleFactor,
1536    ) -> Option<FontInstanceKey> {
1537        let font_family_hash = self.get_font_family(font_families_hash)?;
1538        let font_key = self.get_font_key(font_family_hash)?;
1539        let (_, instances) = self.get_registered_font(font_key)?;
1540        instances.get(&(font_size_au, dpi_scale)).copied()
1541    }
1542
1543    // Delete all font family hashes that do not have a font key anymore
1544    //
1545    // AUDIT-TODO (font GC, resources.rs font leak — 2026-07-08):
1546    // Fonts and font instances are currently NEVER garbage-collected. This helper
1547    // only prunes `font_id_map` / `font_families_map` entries whose `FontKey` has
1548    // *already* vanished from `currently_registered_fonts` — but nothing ever
1549    // removes fonts from `currently_registered_fonts` in the first place, and this
1550    // helper itself has no callers. No `DeleteFont` / `DeleteFontInstance`
1551    // `ResourceUpdate` is ever emitted, so WebRender font memory grows unbounded
1552    // when an app cycles fonts (font pickers, editors, live CSS).
1553    //
1554    // To wire a real font GC mirroring the image GC (see `dll/.../wr_translate2.rs`
1555    // `garbage_collect_images` + `image_last_seen_epoch`), the following are needed
1556    // and MUST be done together (do not half-implement):
1557    //   1. Add `font_last_seen_epoch: OrderedMap<FontKey, u32>` (and, if instance-
1558    //      level GC is wanted, per-`FontInstanceKey` epochs) to `RendererResources`.
1559    //   2. In the display-list build (dll crate), after resolving each glyph run's
1560    //      `FontInstanceKey`, mark the owning `FontKey` (and instance) seen at the
1561    //      current epoch — exactly as images are marked in the image GC.
1562    //   3. Add a `garbage_collect_fonts(&mut self, now, keep_epochs, updates)` that,
1563    //      for every `FontKey` unseen for > keep_epochs frames, emits
1564    //      `DeleteFontInstance` for each of its instances then `DeleteFont`, and
1565    //      evicts the key from `currently_registered_fonts`, `font_hash_map`,
1566    //      `last_frame_registered_fonts`, and `font_id_map`/`font_families_map`
1567    //      (via this helper). Respect the "delete on current frame + 1" rule already
1568    //      documented on `last_frame_registered_fonts`.
1569    //   4. Call it once per frame from the same site as the image GC.
1570    // Left as a TODO because steps 2 and 4 are cross-crate (dll) and cannot be
1571    // implemented from `azul-core` alone; adding a GC method here without a caller
1572    // would just be more dead code.
1573    #[allow(dead_code)]
1574    fn remove_font_families_with_zero_references(&mut self) {
1575        let font_family_to_delete = self
1576            .font_id_map
1577            .iter()
1578            .filter_map(|(font_family, font_key)| {
1579                if self.currently_registered_fonts.contains_key(font_key) {
1580                    None
1581                } else {
1582                    Some(*font_family)
1583                }
1584            })
1585            .collect::<Vec<_>>();
1586
1587        for f in font_family_to_delete {
1588            self.font_id_map.remove(&f); // font key does not exist anymore
1589        }
1590
1591        let font_families_to_delete = self
1592            .font_families_map
1593            .iter()
1594            .filter_map(|(font_families, font_family)| {
1595                if self.font_id_map.contains_key(font_family) {
1596                    None
1597                } else {
1598                    Some(*font_families)
1599                }
1600            })
1601            .collect::<Vec<_>>();
1602
1603        for f in font_families_to_delete {
1604            self.font_families_map.remove(&f); // font family does not exist anymore
1605        }
1606    }
1607}
1608
1609// Result returned from rerender_image_callback() - should be used as:
1610//
1611// ```rust
1612// txn.update_image(
1613//     wr_translate_image_key(key),
1614//     wr_translate_image_descriptor(descriptor),
1615//     wr_translate_image_data(data),
1616//     &WrImageDirtyRect::All,
1617// );
1618// ```
1619#[derive(Debug, Clone)]
1620pub struct UpdateImageResult {
1621    pub key_to_update: ImageKey,
1622    pub new_descriptor: ImageDescriptor,
1623    pub new_image_data: ImageData,
1624}
1625
1626#[derive(Debug, Default)]
1627pub struct GlTextureCache {
1628    pub solved_textures:
1629        BTreeMap<DomId, BTreeMap<NodeId, (ImageKey, ImageDescriptor, ExternalImageId)>>,
1630    pub hashes: BTreeMap<(DomId, NodeId, ImageRefHash), ImageRefHash>,
1631}
1632
1633// necessary so the display list can be built in parallel
1634// SAFETY: only the raw pointers inside the contained `ImageRefHash`/key maps are
1635// non-`Send`-inferring; every stored value is a plain POD id/descriptor with no
1636// interior aliasing, so moving the cache to another thread is sound.
1637unsafe impl Send for GlTextureCache {}
1638
1639impl GlTextureCache {
1640    /// Initializes an empty cache
1641    #[must_use] pub const fn empty() -> Self {
1642        Self {
1643            solved_textures: BTreeMap::new(),
1644            hashes: BTreeMap::new(),
1645        }
1646    }
1647
1648    /// Updates a given texture
1649    ///
1650    /// This is called when a texture needs to be re-rendered (e.g., on resize or animation frame).
1651    /// It updates the texture in the `WebRender` external image cache and updates the internal
1652    /// descriptor to reflect the new size.
1653    ///
1654    /// # Arguments
1655    ///
1656    /// * `dom_id` - The DOM ID containing the texture
1657    /// * `node_id` - The node ID of the image element
1658    /// * `document_id` - The `WebRender` document ID
1659    /// * `epoch` - The current frame epoch
1660    /// * `new_texture` - The new texture to use
1661    /// * `insert_into_active_gl_textures_fn` - Function to insert the texture into the cache
1662    ///
1663    /// # Returns
1664    ///
1665    /// The `ExternalImageId` if successful, None if the texture wasn't found in the cache
1666    pub fn update_texture(
1667        &mut self,
1668        dom_id: DomId,
1669        node_id: NodeId,
1670        document_id: DocumentId,
1671        epoch: Epoch,
1672        new_texture: Texture,
1673        insert_into_active_gl_textures_fn: &GlStoreImageFn,
1674    ) -> Option<ExternalImageId> {
1675        let new_descriptor = new_texture.get_descriptor();
1676        let di_map = self.solved_textures.get_mut(&dom_id)?;
1677        let entry = di_map.get_mut(&node_id)?;
1678
1679        // Update the descriptor
1680        entry.1 = new_descriptor;
1681
1682        // The ExternalImageId is deterministic from (dom_id, node_id), so the cache
1683        // entry can keep referencing the same id across re-renders.
1684        let external_image_id = texture_external_image_id(dom_id, node_id);
1685        (insert_into_active_gl_textures_fn)(document_id, epoch, new_texture, external_image_id);
1686        entry.2 = external_image_id;
1687
1688        Some(external_image_id)
1689    }
1690}
1691
1692macro_rules! unique_id {
1693    ($struct_name:ident, $counter_name:ident) => {
1694        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
1695        #[repr(C)]
1696        pub struct $struct_name {
1697            pub id: usize,
1698        }
1699
1700        impl $struct_name {
1701            pub fn unique() -> Self {
1702                Self {
1703                    id: $counter_name.fetch_add(1, AtomicOrdering::SeqCst),
1704                }
1705            }
1706        }
1707    };
1708}
1709
1710// NOTE: the property key is unique across transform, color and opacity properties
1711static PROPERTY_KEY_COUNTER: AtomicUsize = AtomicUsize::new(0);
1712unique_id!(TransformKey, PROPERTY_KEY_COUNTER);
1713unique_id!(ColorKey, PROPERTY_KEY_COUNTER);
1714unique_id!(OpacityKey, PROPERTY_KEY_COUNTER);
1715
1716static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
1717unique_id!(ImageId, IMAGE_ID_COUNTER);
1718static FONT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
1719unique_id!(FontId, FONT_ID_COUNTER);
1720
1721#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1722#[repr(C)]
1723pub struct ImageMask {
1724    pub image: ImageRef,
1725    pub rect: LogicalRect,
1726    pub repeat: bool,
1727}
1728
1729impl_option!(
1730    ImageMask,
1731    OptionImageMask,
1732    copy = false,
1733    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
1734);
1735
1736#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1737pub enum ImmediateFontId {
1738    Resolved((StyleFontFamilyHash, FontKey)),
1739    Unresolved(StyleFontFamilyVec),
1740}
1741
1742#[derive(Debug, Clone, PartialEq, PartialOrd)]
1743#[repr(C, u8)]
1744pub enum RawImageData {
1745    // 8-bit image data
1746    U8(U8Vec),
1747    // 16-bit image data
1748    U16(U16Vec),
1749    // HDR image data
1750    F32(F32Vec),
1751}
1752
1753impl RawImageData {
1754    #[must_use] pub const fn get_u8_vec_ref(&self) -> Option<&U8Vec> {
1755        match self {
1756            Self::U8(v) => Some(v),
1757            _ => None,
1758        }
1759    }
1760
1761    #[must_use] pub const fn get_u16_vec_ref(&self) -> Option<&U16Vec> {
1762        match self {
1763            Self::U16(v) => Some(v),
1764            _ => None,
1765        }
1766    }
1767
1768    #[must_use] pub const fn get_f32_vec_ref(&self) -> Option<&F32Vec> {
1769        match self {
1770            Self::F32(v) => Some(v),
1771            _ => None,
1772        }
1773    }
1774
1775    fn get_u8_vec(self) -> Option<U8Vec> {
1776        match self {
1777            Self::U8(v) => Some(v),
1778            _ => None,
1779        }
1780    }
1781
1782    fn get_u16_vec(self) -> Option<U16Vec> {
1783        match self {
1784            Self::U16(v) => Some(v),
1785            _ => None,
1786        }
1787    }
1788}
1789
1790#[derive(Debug, Clone, PartialEq, PartialOrd)]
1791#[repr(C)]
1792pub struct RawImage {
1793    pub pixels: RawImageData,
1794    pub width: usize,
1795    pub height: usize,
1796    pub premultiplied_alpha: bool,
1797    pub data_format: RawImageFormat,
1798    pub tag: U8Vec,
1799}
1800
1801/// A soft round brush for the painting API.
1802///
1803/// The same parameters drive the CPU
1804/// rasterizer ([`RawImage::paint_dot`]) and the GPU brush shader, so a stroke
1805/// looks identical whether it lands on a `RawImage` or a `Texture`.
1806#[repr(C)]
1807#[derive(Debug, Copy, Clone, PartialEq)]
1808pub struct Brush {
1809    /// Brush color (its alpha scales the dab opacity together with `flow`).
1810    pub color: ColorU,
1811    /// Brush radius in pixels.
1812    pub radius: f32,
1813    /// Edge hardness, `0.0` (fully feathered) .. `1.0` (hard edge). Opaque out
1814    /// to `hardness * radius`, then a smooth falloff to zero at the edge.
1815    pub hardness: f32,
1816    /// Per-dab opacity multiplier, `0.0`..`1.0`. Values < 1 let overlapping dabs
1817    /// build up smoothly (the "metaball"-like blend).
1818    pub flow: f32,
1819    /// Spacing between stamped dabs along a stroke, as a fraction of `radius`
1820    /// (e.g. `0.25` = a dab every quarter-radius). Smaller = smoother + slower.
1821    pub spacing: f32,
1822}
1823
1824impl Brush {
1825    /// A sensible default brush: medium-soft, full flow, dense spacing.
1826    #[must_use] pub const fn new(color: ColorU, radius: f32) -> Self {
1827        Self {
1828            color,
1829            radius,
1830            hardness: 0.5,
1831            flow: 1.0,
1832            spacing: 0.25,
1833        }
1834    }
1835}
1836
1837/// Brush dab coverage: `1.0` at the dab center, smoothly `0.0` at its edge.
1838///
1839/// `t` is `distance / radius` in `[0, 1]`; `hardness` in `[0, 1]`. Single source
1840/// of truth for the dab profile -- the GPU brush shader computes the identical
1841/// `1 - smoothstep(hardness, 1, t)` so CPU and GPU strokes match.
1842#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
1843#[inline]
1844#[must_use] pub fn brush_dab_coverage(t: f32, hardness: f32) -> f32 {
1845    let edge0 = hardness.clamp(0.0, 1.0);
1846    let denom = (1.0 - edge0).max(1.0e-4);
1847    let x = ((t - edge0) / denom).clamp(0.0, 1.0);
1848    1.0 - (x * x * (3.0 - 2.0 * x))
1849}
1850
1851impl RawImage {
1852    /// CPU painting: stamp one brush dab centered at (`cx`, `cy`) in pixel
1853    /// coordinates, alpha-over compositing a radial-falloff disc. Only 8-bit
1854    /// `RGBA8`/`BGRA8` images are painted (other formats are left untouched).
1855    /// This is the CPU mirror of the GPU brush shader.
1856    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
1857    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1858    #[allow(clippy::cast_possible_wrap)] // image/graphics: bounded pixel/colour casts
1859    pub fn paint_dot(&mut self, cx: f32, cy: f32, brush: Brush) {
1860        let r = brush.radius;
1861        // `!(r > 0.0)` intentionally also rejects NaN (`r <= 0.0` would not).
1862        #[allow(clippy::neg_cmp_op_on_partial_ord)]
1863        if !(r > 0.0) || self.width == 0 || self.height == 0 {
1864            return;
1865        }
1866        let bgr = match self.data_format {
1867            RawImageFormat::RGBA8 => false,
1868            RawImageFormat::BGRA8 => true,
1869            _ => return,
1870        };
1871        let (w, h) = (self.width as i32, self.height as i32);
1872        let buf: &mut [u8] = match self.pixels {
1873            RawImageData::U8(ref mut v) => v.as_mut(),
1874            _ => return,
1875        };
1876        let flow = brush.flow.clamp(0.0, 1.0) * (f32::from(brush.color.a) / 255.0);
1877        let (cr, cg, cb) = (
1878            f32::from(brush.color.r),
1879            f32::from(brush.color.g),
1880            f32::from(brush.color.b),
1881        );
1882        let x0 = (cx - r).floor().max(0.0) as i32;
1883        let y0 = (cy - r).floor().max(0.0) as i32;
1884        let x1 = ((cx + r).ceil() as i32).min(w);
1885        let y1 = ((cy + r).ceil() as i32).min(h);
1886        for y in y0..y1 {
1887            for x in x0..x1 {
1888                let dx = x as f32 + 0.5 - cx;
1889                let dy = y as f32 + 0.5 - cy;
1890                let dist = dx.hypot(dy);
1891                if dist > r {
1892                    continue;
1893                }
1894                let a = brush_dab_coverage(dist / r, brush.hardness) * flow;
1895                if a <= 0.0 {
1896                    continue;
1897                }
1898                let idx = ((y * w + x) as usize) * 4;
1899                // `width`/`height` are public and may exceed the actual buffer;
1900                // trust the buffer, not the claimed dimensions, so a mismatch
1901                // skips the pixel instead of indexing out of bounds.
1902                if idx + 4 > buf.len() {
1903                    continue;
1904                }
1905                let (ri, gi, bi, ai) = if bgr {
1906                    (idx + 2, idx + 1, idx, idx + 3)
1907                } else {
1908                    (idx, idx + 1, idx + 2, idx + 3)
1909                };
1910                let inv = 1.0 - a;
1911                buf[ri] = (cr * a + f32::from(buf[ri]) * inv).round().clamp(0.0, 255.0) as u8;
1912                buf[gi] = (cg * a + f32::from(buf[gi]) * inv).round().clamp(0.0, 255.0) as u8;
1913                buf[bi] = (cb * a + f32::from(buf[bi]) * inv).round().clamp(0.0, 255.0) as u8;
1914                buf[ai] =
1915                    ((a + (f32::from(buf[ai]) / 255.0) * inv) * 255.0).round().clamp(0.0, 255.0) as u8;
1916            }
1917        }
1918    }
1919
1920    /// CPU painting: stamp a stroke by spacing dabs along the segment
1921    /// (`x0`,`y0`)->(`x1`,`y1`). Call once per pointer move with the previous and
1922    /// current positions for a continuous line.
1923    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
1924    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1925    pub fn paint_stroke(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, brush: Brush) {
1926        let dx = x1 - x0;
1927        let dy = y1 - y0;
1928        let len = dx.hypot(dy);
1929        // A non-finite length (infinite / NaN endpoint) would make `n` saturate
1930        // to i32::MAX and the `for i in 0..=n` loop run ~2.1 billion times. Bail
1931        // rather than spin: an infinite segment has no finite dabs to stamp.
1932        if !len.is_finite() {
1933            return;
1934        }
1935        let step = (brush.radius * brush.spacing.max(0.01)).max(0.5);
1936        let n = (len / step).floor() as i32;
1937        if n <= 0 {
1938            self.paint_dot(x1, y1, brush);
1939            return;
1940        }
1941        for i in 0..=n {
1942            let t = i as f32 / n as f32;
1943            self.paint_dot(x0 + dx * t, y0 + dy * t, brush);
1944        }
1945    }
1946}
1947
1948/// Multiplies the RGB channels of a single 4-byte BGRA/RGBA pixel by its alpha.
1949///
1950/// From webrender/wrench. These are slow. Gecko's gfx/2d/Swizzle.cpp has better
1951/// versions.
1952#[inline]
1953#[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
1954fn premultiply_alpha(array: &mut [u8]) {
1955    if array.len() != 4 {
1956        return;
1957    }
1958    let a = u32::from(array[3]);
1959    array[0] = (((u32::from(array[0]) * a) + 128) / 255) as u8;
1960    array[1] = (((u32::from(array[1]) * a) + 128) / 255) as u8;
1961    array[2] = (((u32::from(array[2]) * a) + 128) / 255) as u8;
1962}
1963
1964#[inline]
1965#[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
1966#[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
1967fn normalize_u16(i: u16) -> u8 {
1968    ((f32::from(i) / f32::from(core::u16::MAX)) * f32::from(core::u8::MAX)) as u8
1969}
1970
1971const FOUR_BPP: usize = 4;
1972const TWO_CHANNELS: usize = 2;
1973const THREE_CHANNELS: usize = 3;
1974const FOUR_CHANNELS: usize = 4;
1975
1976impl RawImage {
1977    /// Returns a null / empty image
1978    #[must_use] pub fn null_image() -> Self {
1979        Self {
1980            pixels: RawImageData::U8(Vec::new().into()),
1981            width: 0,
1982            height: 0,
1983            premultiplied_alpha: true,
1984            data_format: RawImageFormat::BGRA8,
1985            tag: Vec::new().into(),
1986        }
1987    }
1988
1989    /// Allocates a width * height, single-channel mask, used for drawing CPU image masks
1990    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1991    #[must_use] pub fn allocate_mask(size: LayoutSize) -> Self {
1992        Self {
1993            pixels: RawImageData::U8(
1994                vec![0; size.width.max(0) as usize * size.height.max(0) as usize].into(),
1995            ),
1996            width: size.width as usize,
1997            height: size.height as usize,
1998            premultiplied_alpha: true,
1999            data_format: RawImageFormat::R8,
2000            tag: Vec::new().into(),
2001        }
2002    }
2003
2004    /// Encodes a `RawImage` as BGRA8 bytes and premultiplies it if the alpha is not premultiplied
2005    ///
2006    /// Returns None if the width * height * BPP does not match
2007    ///
2008    /// TODO: autovectorization fails spectacularly, need to manually optimize!
2009    #[must_use] pub fn into_loaded_image_source(self) -> Option<(ImageData, ImageDescriptor)> {
2010        let Self {
2011            width,
2012            height,
2013            pixels,
2014            data_format,
2015            premultiplied_alpha,
2016            tag,
2017        } = self;
2018
2019        // Checked: a width*height that overflows usize is not a real image; return
2020        // None rather than panicking (debug) / wrapping to a bogus length (release).
2021        let expected_len = width.checked_mul(height)?;
2022
2023        // …and neither is one whose BYTE count overflows. Every `load_*` below
2024        // scales this pixel count by its channel count (2, 3 or 4) to validate the
2025        // input buffer, and allocates a 4-byte-per-pixel BGRA output buffer, so a
2026        // pixel count that cannot survive `* 4` cannot describe a real image
2027        // either. Without this guard those multiplies wrapped in release (an empty
2028        // buffer then *validated* as a 2^31 x 2^31 image) and panicked in debug —
2029        // and because the callers are `extern "C"` widget callbacks, that panic is
2030        // a non-unwinding ABORT that `catch_unwind` cannot contain. One check here
2031        // covers all 20 multiplication sites.
2032        expected_len.checked_mul(FOUR_BPP)?;
2033
2034        let (bytes, data_format, is_opaque): (U8Vec, RawImageFormat, bool) = match data_format {
2035            RawImageFormat::R8 => {
2036                let (bytes, is_opaque) = Self::load_r8(pixels, expected_len)?;
2037                (bytes, RawImageFormat::R8, is_opaque)
2038            }
2039            RawImageFormat::RG8 => {
2040                let (bytes, is_opaque) = Self::load_rg8(pixels, expected_len, premultiplied_alpha)?;
2041                (bytes, RawImageFormat::BGRA8, is_opaque)
2042            }
2043            RawImageFormat::RGB8 => {
2044                let (bytes, is_opaque) = Self::load_rgb8(pixels, expected_len)?;
2045                (bytes, RawImageFormat::BGRA8, is_opaque)
2046            }
2047            RawImageFormat::RGBA8 => {
2048                let (bytes, is_opaque) = Self::load_rgba8(pixels, expected_len, premultiplied_alpha)?;
2049                (bytes, RawImageFormat::BGRA8, is_opaque)
2050            }
2051            RawImageFormat::R16 => {
2052                let (bytes, is_opaque) = Self::load_r16(pixels, expected_len)?;
2053                (bytes, RawImageFormat::BGRA8, is_opaque)
2054            }
2055            RawImageFormat::RG16 => {
2056                let (bytes, is_opaque) = Self::load_rg16(pixels, expected_len)?;
2057                (bytes, RawImageFormat::BGRA8, is_opaque)
2058            }
2059            RawImageFormat::RGB16 => {
2060                let (bytes, is_opaque) = Self::load_rgb16(pixels, expected_len)?;
2061                (bytes, RawImageFormat::BGRA8, is_opaque)
2062            }
2063            RawImageFormat::RGBA16 => {
2064                let (bytes, is_opaque) =
2065                    Self::load_rgba16(pixels, expected_len, premultiplied_alpha)?;
2066                (bytes, RawImageFormat::BGRA8, is_opaque)
2067            }
2068            RawImageFormat::BGR8 => {
2069                let (bytes, is_opaque) = Self::load_bgr8(pixels, expected_len)?;
2070                (bytes, RawImageFormat::BGRA8, is_opaque)
2071            }
2072            RawImageFormat::BGRA8 => {
2073                let (bytes, is_opaque) = Self::load_bgra8(pixels, expected_len, premultiplied_alpha)?;
2074                (bytes, RawImageFormat::BGRA8, is_opaque)
2075            }
2076            RawImageFormat::RGBF32 => {
2077                let (bytes, is_opaque) = Self::load_rgbf32(pixels, expected_len)?;
2078                (bytes, RawImageFormat::BGRA8, is_opaque)
2079            }
2080            RawImageFormat::RGBAF32 => {
2081                let (bytes, is_opaque) =
2082                    Self::load_rgbaf32(pixels, expected_len, premultiplied_alpha)?;
2083                (bytes, RawImageFormat::BGRA8, is_opaque)
2084            }
2085        };
2086
2087        let image_data = ImageData::Raw(SharedRawImageData::new(bytes));
2088        let image_descriptor = ImageDescriptor {
2089            format: data_format,
2090            width,
2091            height,
2092            offset: 0,
2093            stride: None.into(),
2094            flags: ImageDescriptorFlags {
2095                is_opaque,
2096                allow_mipmaps: true,
2097            },
2098        };
2099
2100        Some((image_data, image_descriptor))
2101    }
2102
2103    /// Keep R8 data as-is — `WebRender` supports R8 natively. This is important for
2104    /// image mask clips which need the single-channel data (white=visible,
2105    /// black=clipped). Stays in `R8` format; never opaque.
2106    fn load_r8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2107        let pixels = pixels.get_u8_vec()?;
2108
2109        if pixels.len() != expected_len {
2110            return None;
2111        }
2112
2113        Some((pixels, false))
2114    }
2115
2116    fn load_rg8(
2117        pixels: RawImageData,
2118        expected_len: usize,
2119        premultiplied_alpha: bool,
2120    ) -> Option<(U8Vec, bool)> {
2121        let pixels = pixels.get_u8_vec()?;
2122
2123        if pixels.len() != expected_len * TWO_CHANNELS {
2124            return None;
2125        }
2126
2127        let mut is_opaque = true;
2128        let mut px = vec![0; expected_len * FOUR_BPP];
2129
2130        // TODO: check that this function is SIMD optimized
2131        for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
2132            let grey = greyalpha[0];
2133            let alpha = greyalpha[1];
2134
2135            if alpha != 255 {
2136                is_opaque = false;
2137            }
2138
2139            px[pixel_index * FOUR_BPP] = grey;
2140            px[(pixel_index * FOUR_BPP) + 1] = grey;
2141            px[(pixel_index * FOUR_BPP) + 2] = grey;
2142            px[(pixel_index * FOUR_BPP) + 3] = alpha;
2143
2144            if !premultiplied_alpha {
2145                premultiply_alpha(
2146                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2147                );
2148            }
2149        }
2150
2151        Some((px.into(), is_opaque))
2152    }
2153
2154    fn load_rgb8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2155        let pixels = pixels.get_u8_vec()?;
2156
2157        if pixels.len() != expected_len * THREE_CHANNELS {
2158            return None;
2159        }
2160
2161        let mut px = vec![0; expected_len * FOUR_BPP];
2162
2163        // TODO: check that this function is SIMD optimized
2164        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2165            let red = rgb[0];
2166            let green = rgb[1];
2167            let blue = rgb[2];
2168
2169            px[pixel_index * FOUR_BPP] = blue;
2170            px[(pixel_index * FOUR_BPP) + 1] = green;
2171            px[(pixel_index * FOUR_BPP) + 2] = red;
2172            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2173        }
2174
2175        Some((px.into(), true))
2176    }
2177
2178    fn load_rgba8(
2179        pixels: RawImageData,
2180        expected_len: usize,
2181        premultiplied_alpha: bool,
2182    ) -> Option<(U8Vec, bool)> {
2183        let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
2184
2185        if pixels.len() != expected_len * FOUR_CHANNELS {
2186            return None;
2187        }
2188
2189        let mut is_opaque = true;
2190
2191        // TODO: check that this function is SIMD optimized
2192        // no extra allocation necessary, but swizzling
2193        if premultiplied_alpha {
2194            for rgba in pixels.chunks_exact_mut(4) {
2195                let (r, gba) = rgba.split_first_mut()?;
2196                core::mem::swap(r, gba.get_mut(1)?);
2197                let a = rgba.get_mut(3)?;
2198                if *a != 255 {
2199                    is_opaque = false;
2200                }
2201            }
2202        } else {
2203            for rgba in pixels.chunks_exact_mut(4) {
2204                // RGBA => BGRA
2205                let (r, gba) = rgba.split_first_mut()?;
2206                core::mem::swap(r, gba.get_mut(1)?);
2207                let a = rgba.get_mut(3)?;
2208                if *a != 255 {
2209                    is_opaque = false;
2210                }
2211                premultiply_alpha(rgba); // <-
2212            }
2213        }
2214
2215        Some((pixels.into(), is_opaque))
2216    }
2217
2218    fn load_r16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2219        let pixels = pixels.get_u16_vec()?;
2220
2221        if pixels.len() != expected_len {
2222            return None;
2223        }
2224
2225        let mut px = vec![0; expected_len * FOUR_BPP];
2226
2227        // TODO: check that this function is SIMD optimized
2228        for (pixel_index, grey_u16) in pixels.as_ref().iter().enumerate() {
2229            let grey_u8 = normalize_u16(*grey_u16);
2230            px[pixel_index * FOUR_BPP] = grey_u8;
2231            px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
2232            px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
2233            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2234        }
2235
2236        Some((px.into(), true))
2237    }
2238
2239    fn load_rg16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2240        let pixels = pixels.get_u16_vec()?;
2241
2242        if pixels.len() != expected_len * TWO_CHANNELS {
2243            return None;
2244        }
2245
2246        let mut is_opaque = true;
2247        let mut px = vec![0; expected_len * FOUR_BPP];
2248
2249        // TODO: check that this function is SIMD optimized
2250        for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
2251            let grey_u8 = normalize_u16(greyalpha[0]);
2252            let alpha_u8 = normalize_u16(greyalpha[1]);
2253
2254            if alpha_u8 != 255 {
2255                is_opaque = false;
2256            }
2257
2258            px[pixel_index * FOUR_BPP] = grey_u8;
2259            px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
2260            px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
2261            px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2262        }
2263
2264        Some((px.into(), is_opaque))
2265    }
2266
2267    fn load_rgb16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2268        let pixels = pixels.get_u16_vec()?;
2269
2270        if pixels.len() != expected_len * THREE_CHANNELS {
2271            return None;
2272        }
2273
2274        let mut px = vec![0; expected_len * FOUR_BPP];
2275
2276        // TODO: check that this function is SIMD optimized
2277        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2278            let red_u8 = normalize_u16(rgb[0]);
2279            let green_u8 = normalize_u16(rgb[1]);
2280            let blue_u8 = normalize_u16(rgb[2]);
2281
2282            px[pixel_index * FOUR_BPP] = blue_u8;
2283            px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2284            px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2285            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2286        }
2287
2288        Some((px.into(), true))
2289    }
2290
2291    fn load_rgba16(
2292        pixels: RawImageData,
2293        expected_len: usize,
2294        premultiplied_alpha: bool,
2295    ) -> Option<(U8Vec, bool)> {
2296        let pixels = pixels.get_u16_vec()?;
2297
2298        if pixels.len() != expected_len * FOUR_CHANNELS {
2299            return None;
2300        }
2301
2302        let mut is_opaque = true;
2303        let mut px = vec![0; expected_len * FOUR_BPP];
2304
2305        // TODO: check that this function is SIMD optimized
2306        if premultiplied_alpha {
2307            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2308                let red_u8 = normalize_u16(rgba[0]);
2309                let green_u8 = normalize_u16(rgba[1]);
2310                let blue_u8 = normalize_u16(rgba[2]);
2311                let alpha_u8 = normalize_u16(rgba[3]);
2312
2313                if alpha_u8 != 255 {
2314                    is_opaque = false;
2315                }
2316
2317                px[pixel_index * FOUR_BPP] = blue_u8;
2318                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2319                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2320                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2321            }
2322        } else {
2323            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2324                let red_u8 = normalize_u16(rgba[0]);
2325                let green_u8 = normalize_u16(rgba[1]);
2326                let blue_u8 = normalize_u16(rgba[2]);
2327                let alpha_u8 = normalize_u16(rgba[3]);
2328
2329                if alpha_u8 != 255 {
2330                    is_opaque = false;
2331                }
2332
2333                px[pixel_index * FOUR_BPP] = blue_u8;
2334                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2335                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2336                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2337                premultiply_alpha(
2338                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2339                );
2340            }
2341        }
2342
2343        Some((px.into(), is_opaque))
2344    }
2345
2346    fn load_bgr8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2347        let pixels = pixels.get_u8_vec()?;
2348
2349        if pixels.len() != expected_len * THREE_CHANNELS {
2350            return None;
2351        }
2352
2353        let mut px = vec![0; expected_len * FOUR_BPP];
2354
2355        // TODO: check that this function is SIMD optimized
2356        for (pixel_index, bgr) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2357            let blue = bgr[0];
2358            let green = bgr[1];
2359            let red = bgr[2];
2360
2361            px[pixel_index * FOUR_BPP] = blue;
2362            px[(pixel_index * FOUR_BPP) + 1] = green;
2363            px[(pixel_index * FOUR_BPP) + 2] = red;
2364            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2365        }
2366
2367        Some((px.into(), true))
2368    }
2369
2370    fn load_bgra8(
2371        pixels: RawImageData,
2372        expected_len: usize,
2373        premultiplied_alpha: bool,
2374    ) -> Option<(U8Vec, bool)> {
2375        let mut is_opaque = true;
2376
2377        let bytes: U8Vec = if premultiplied_alpha {
2378            // DO NOT CLONE THE IMAGE HERE!
2379            let pixels = pixels.get_u8_vec()?;
2380
2381            if pixels.len() != expected_len * FOUR_BPP {
2382                return None;
2383            }
2384
2385            is_opaque = pixels
2386                .as_ref()
2387                .chunks_exact(FOUR_CHANNELS)
2388                .all(|bgra| bgra[3] == 255);
2389
2390            pixels
2391        } else {
2392            let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
2393
2394            if pixels.len() != expected_len * FOUR_BPP {
2395                return None;
2396            }
2397
2398            for bgra in pixels.chunks_exact_mut(FOUR_CHANNELS) {
2399                if bgra[3] != 255 {
2400                    is_opaque = false;
2401                }
2402                premultiply_alpha(bgra);
2403            }
2404            pixels.into()
2405        };
2406
2407        Some((bytes, is_opaque))
2408    }
2409
2410    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2411    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2412    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
2413    fn load_rgbf32(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2414        let pixels = pixels.get_f32_vec_ref()?;
2415
2416        if pixels.len() != expected_len * THREE_CHANNELS {
2417            return None;
2418        }
2419
2420        let mut px = vec![0; expected_len * FOUR_BPP];
2421
2422        // TODO: check that this function is SIMD optimized
2423        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2424            let red_u8 = (rgb[0] * 255.0) as u8;
2425            let green_u8 = (rgb[1] * 255.0) as u8;
2426            let blue_u8 = (rgb[2] * 255.0) as u8;
2427
2428            px[pixel_index * FOUR_BPP] = blue_u8;
2429            px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2430            px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2431            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2432        }
2433
2434        Some((px.into(), true))
2435    }
2436
2437    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2438    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2439    #[allow(clippy::needless_pass_by_value)] // owned RawImageData taken by value (image decode entry point)
2440    fn load_rgbaf32(
2441        pixels: RawImageData,
2442        expected_len: usize,
2443        premultiplied_alpha: bool,
2444    ) -> Option<(U8Vec, bool)> {
2445        let pixels = pixels.get_f32_vec_ref()?;
2446
2447        if pixels.len() != expected_len * FOUR_CHANNELS {
2448            return None;
2449        }
2450
2451        let mut is_opaque = true;
2452        let mut px = vec![0; expected_len * FOUR_BPP];
2453
2454        // TODO: check that this function is SIMD optimized
2455        if premultiplied_alpha {
2456            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2457                let red_u8 = (rgba[0] * 255.0) as u8;
2458                let green_u8 = (rgba[1] * 255.0) as u8;
2459                let blue_u8 = (rgba[2] * 255.0) as u8;
2460                let alpha_u8 = (rgba[3] * 255.0) as u8;
2461
2462                if alpha_u8 != 255 {
2463                    is_opaque = false;
2464                }
2465
2466                px[pixel_index * FOUR_BPP] = blue_u8;
2467                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2468                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2469                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2470            }
2471        } else {
2472            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2473                let red_u8 = (rgba[0] * 255.0) as u8;
2474                let green_u8 = (rgba[1] * 255.0) as u8;
2475                let blue_u8 = (rgba[2] * 255.0) as u8;
2476                let alpha_u8 = (rgba[3] * 255.0) as u8;
2477
2478                if alpha_u8 != 255 {
2479                    is_opaque = false;
2480                }
2481
2482                px[pixel_index * FOUR_BPP] = blue_u8;
2483                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2484                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2485                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2486                premultiply_alpha(
2487                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2488                );
2489            }
2490        }
2491
2492        Some((px.into(), is_opaque))
2493    }
2494}
2495
2496impl_option!(
2497    RawImage,
2498    OptionRawImage,
2499    copy = false,
2500    [Debug, Clone, PartialEq, PartialOrd]
2501);
2502
2503#[must_use] pub fn font_size_to_au(font_size: StyleFontSize) -> Au {
2504    Au::from_px(font_size.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE))
2505}
2506
2507pub type FontInstanceFlags = u32;
2508
2509// Common flags
2510pub const FONT_INSTANCE_FLAG_SYNTHETIC_BOLD: u32 = 1 << 1;
2511pub const FONT_INSTANCE_FLAG_EMBEDDED_BITMAPS: u32 = 1 << 2;
2512pub const FONT_INSTANCE_FLAG_SUBPIXEL_BGR: u32 = 1 << 3;
2513pub const FONT_INSTANCE_FLAG_TRANSPOSE: u32 = 1 << 4;
2514pub const FONT_INSTANCE_FLAG_FLIP_X: u32 = 1 << 5;
2515pub const FONT_INSTANCE_FLAG_FLIP_Y: u32 = 1 << 6;
2516pub const FONT_INSTANCE_FLAG_SUBPIXEL_POSITION: u32 = 1 << 7;
2517
2518// Windows flags
2519pub const FONT_INSTANCE_FLAG_FORCE_GDI: u32 = 1 << 16;
2520
2521// Mac flags
2522pub const FONT_INSTANCE_FLAG_FONT_SMOOTHING: u32 = 1 << 16;
2523
2524// FreeType flags
2525pub const FONT_INSTANCE_FLAG_FORCE_AUTOHINT: u32 = 1 << 16;
2526pub const FONT_INSTANCE_FLAG_NO_AUTOHINT: u32 = 1 << 17;
2527pub const FONT_INSTANCE_FLAG_VERTICAL_LAYOUT: u32 = 1 << 18;
2528pub const FONT_INSTANCE_FLAG_LCD_VERTICAL: u32 = 1 << 19;
2529
2530#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2531pub struct GlyphOptions {
2532    pub render_mode: FontRenderMode,
2533    pub flags: FontInstanceFlags,
2534}
2535
2536#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2537pub enum FontRenderMode {
2538    Mono,
2539    Alpha,
2540    Subpixel,
2541}
2542
2543#[cfg(target_arch = "wasm32")]
2544#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2545pub struct FontInstancePlatformOptions {
2546    // empty for now
2547}
2548
2549#[cfg(target_os = "windows")]
2550#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2551pub struct FontInstancePlatformOptions {
2552    pub gamma: u16,
2553    pub contrast: u8,
2554    pub cleartype_level: u8,
2555}
2556
2557#[cfg(target_os = "macos")]
2558#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2559pub struct FontInstancePlatformOptions {
2560    pub unused: u32,
2561}
2562
2563#[cfg(target_os = "linux")]
2564#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2565pub struct FontInstancePlatformOptions {
2566    pub lcd_filter: FontLCDFilter,
2567    pub hinting: FontHinting,
2568}
2569
2570// Mobile targets — empty platform-options struct keeps the
2571// `FontInstanceOptions { platform_options: Option<...>, .. }` field
2572// well-typed without inheriting Linux's freetype-specific tunables.
2573#[cfg(any(target_os = "android", target_os = "ios"))]
2574#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2575pub struct FontInstancePlatformOptions {
2576    pub unused: u32,
2577}
2578
2579#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2580pub enum FontHinting {
2581    None,
2582    Mono,
2583    Light,
2584    Normal,
2585    LCD,
2586}
2587
2588#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2589#[derive(Default)]
2590pub enum FontLCDFilter {
2591    None,
2592    #[default]
2593    Default,
2594    Light,
2595    Legacy,
2596}
2597
2598
2599#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2600pub struct FontInstanceOptions {
2601    pub render_mode: FontRenderMode,
2602    pub flags: FontInstanceFlags,
2603    pub bg_color: ColorU,
2604    /// When `bg_color.a` is != 0 and `render_mode` is `FontRenderMode::Subpixel`,
2605    /// the text will be rendered with `bg_color.r/g/b` as an opaque estimated
2606    /// background color.
2607    pub synthetic_italics: SyntheticItalics,
2608}
2609
2610impl Default for FontInstanceOptions {
2611    fn default() -> Self {
2612        Self {
2613            render_mode: FontRenderMode::Subpixel,
2614            flags: 0,
2615            bg_color: ColorU::TRANSPARENT,
2616            synthetic_italics: SyntheticItalics::default(),
2617        }
2618    }
2619}
2620
2621#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2622#[derive(Default)]
2623pub struct SyntheticItalics {
2624    pub angle: i16,
2625}
2626
2627
2628/// Reference-counted wrapper around raw image bytes (`U8Vec`).
2629/// This allows sharing image data between azul-core and webrender without cloning.
2630///
2631/// Similar to `ImageRef` but specifically for raw byte data, avoiding the overhead
2632/// of the full `DecodedImage` enum when we just need the bytes.
2633#[derive(Debug)]
2634#[repr(C)]
2635pub struct SharedRawImageData {
2636    /// Shared pointer to the raw image bytes
2637    pub data: *const U8Vec,
2638    /// Reference counter - when it reaches 0, the data is deallocated
2639    pub copies: *const AtomicUsize,
2640    /// Whether to run the destructor (for FFI safety)
2641    pub run_destructor: bool,
2642}
2643
2644impl SharedRawImageData {
2645    /// Create a new `SharedRawImageData` from a `U8Vec`
2646    #[must_use] pub fn new(data: U8Vec) -> Self {
2647        Self {
2648            data: Box::into_raw(Box::new(data)),
2649            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
2650            run_destructor: true,
2651        }
2652    }
2653
2654    /// Get a reference to the underlying bytes
2655    #[must_use] pub fn as_ref(&self) -> &[u8] {
2656        // SAFETY: `data` is a non-null, live `Box<U8Vec>` owned by this handle (and its
2657        // clones) until the last copy drops; the borrow is tied to `&self`.
2658        unsafe { (*self.data).as_ref() }
2659    }
2660
2661    /// Alias for `as_ref()` - get the raw bytes as a slice
2662    #[must_use] pub fn get_bytes(&self) -> &[u8] {
2663        self.as_ref()
2664    }
2665
2666    /// Get a pointer to the raw bytes for hashing/identification
2667    #[must_use] pub fn as_ptr(&self) -> *const u8 {
2668        // SAFETY: `data` is a non-null, live `Box<U8Vec>` (see `as_ref`).
2669        unsafe { (*self.data).as_ref().as_ptr() }
2670    }
2671
2672    /// Get the length of the data
2673    #[must_use] pub const fn len(&self) -> usize {
2674        // SAFETY: `data` is a non-null, live `Box<U8Vec>` (see `as_ref`).
2675        unsafe { (*self.data).len() }
2676    }
2677
2678    /// Check if the data is empty
2679    #[must_use] pub const fn is_empty(&self) -> bool {
2680        self.len() == 0
2681    }
2682
2683    /// Try to extract the `U8Vec` if this is the only reference
2684    /// Returns None if there are other references
2685    #[must_use] pub fn into_inner(self) -> Option<U8Vec> {
2686        // SAFETY: `data`/`copies` are non-null heap allocations from `Box::into_raw` in
2687        // `new()`. When `copies == 1` we are the sole owner, so reclaiming both Boxes
2688        // and `forget`-ing `self` transfers ownership without a double free.
2689        unsafe {
2690            if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
2691                let data = Box::from_raw(self.data.cast_mut());
2692                drop(Box::from_raw(self.copies.cast_mut()));
2693                core::mem::forget(self); // don't run the destructor
2694                Some(*data)
2695            } else {
2696                None
2697            }
2698        }
2699    }
2700}
2701
2702// SAFETY: the raw pointers only address heap `Box`es of `Send`/`Sync` data, and all
2703// cross-thread refcount mutation goes through the `AtomicUsize` in `copies`.
2704unsafe impl Send for SharedRawImageData {}
2705unsafe impl Sync for SharedRawImageData {}
2706
2707impl Clone for SharedRawImageData {
2708    fn clone(&self) -> Self {
2709        // SAFETY: `copies` is a non-null, live `AtomicUsize` shared by all clones; the
2710        // atomic increment balances the `fetch_sub` in `Drop`.
2711        unsafe {
2712            self.copies
2713                .as_ref()
2714                .map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
2715        }
2716        Self {
2717            data: self.data,
2718            copies: self.copies,
2719            run_destructor: true,
2720        }
2721    }
2722}
2723
2724impl Drop for SharedRawImageData {
2725    fn drop(&mut self) {
2726        self.run_destructor = false;
2727        // SAFETY: `data`/`copies` are non-null, live `Box`es shared by all clones.
2728        // `fetch_sub` returns the pre-decrement count, so `== 1` means we are the last
2729        // owner; only then do we reclaim both Boxes exactly once.
2730        unsafe {
2731            let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
2732            if copies == 1 {
2733                drop(Box::from_raw(self.data.cast_mut()));
2734                drop(Box::from_raw(self.copies.cast_mut()));
2735            }
2736        }
2737    }
2738}
2739
2740impl PartialEq for SharedRawImageData {
2741    fn eq(&self, rhs: &Self) -> bool {
2742        core::ptr::eq(self.data, rhs.data)
2743    }
2744}
2745
2746impl Eq for SharedRawImageData {}
2747
2748impl PartialOrd for SharedRawImageData {
2749    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
2750        Some(self.cmp(other))
2751    }
2752}
2753
2754impl Ord for SharedRawImageData {
2755    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
2756        (self.data as usize).cmp(&(other.data as usize))
2757    }
2758}
2759
2760impl Hash for SharedRawImageData {
2761    fn hash<H>(&self, state: &mut H)
2762    where
2763        H: Hasher,
2764    {
2765        (self.data as usize).hash(state);
2766    }
2767}
2768
2769/// Represents the backing store of an arbitrary series of pixels for display by
2770/// `WebRender`. This storage can take several forms.
2771#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2772#[repr(C, u8)]
2773pub enum ImageData {
2774    /// A simple series of bytes, provided by the embedding and owned by `WebRender`.
2775    /// The format is stored out-of-band, currently in `ImageDescriptor`.
2776    Raw(SharedRawImageData),
2777    /// An image owned by the embedding, and referenced by `WebRender`. This may
2778    /// take the form of a texture or a heap-allocated buffer.
2779    External(ExternalImageData),
2780}
2781
2782/// Storage format identifier for externally-managed images.
2783#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
2784#[repr(C, u8)]
2785pub enum ExternalImageType {
2786    /// The image is texture-backed.
2787    TextureHandle(ImageBufferKind),
2788    /// The image is heap-allocated by the embedding.
2789    Buffer,
2790}
2791
2792/// An arbitrary identifier for an external image provided by the
2793/// application. It must be a unique identifier for each external
2794/// image.
2795#[repr(C)]
2796#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
2797pub struct ExternalImageId {
2798    pub inner: u64,
2799}
2800
2801static LAST_EXTERNAL_IMAGE_ID: AtomicUsize = AtomicUsize::new(0);
2802
2803impl Default for ExternalImageId {
2804    fn default() -> Self {
2805        Self::new()
2806    }
2807}
2808
2809impl ExternalImageId {
2810    /// Creates a new, unique `ExternalImageId`
2811    pub fn new() -> Self {
2812        Self {
2813            inner: LAST_EXTERNAL_IMAGE_ID.fetch_add(1, AtomicOrdering::SeqCst) as u64,
2814        }
2815    }
2816}
2817
2818#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2819#[repr(C, u8)]
2820pub enum GlyphOutlineOperation {
2821    MoveTo(OutlineMoveTo),
2822    LineTo(OutlineLineTo),
2823    QuadraticCurveTo(OutlineQuadTo),
2824    CubicCurveTo(OutlineCubicTo),
2825    ClosePath,
2826}
2827
2828impl_option!(
2829    GlyphOutlineOperation,
2830    OptionGlyphOutlineOperation,
2831    copy = false,
2832    [Debug, Clone, PartialEq, Eq, PartialOrd]
2833);
2834
2835// MoveTo in em units
2836#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2837#[repr(C)]
2838pub struct OutlineMoveTo {
2839    pub x: i16,
2840    pub y: i16,
2841}
2842
2843// LineTo in em units
2844#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2845#[repr(C)]
2846pub struct OutlineLineTo {
2847    pub x: i16,
2848    pub y: i16,
2849}
2850
2851// QuadTo in em units
2852#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2853#[repr(C)]
2854pub struct OutlineQuadTo {
2855    pub ctrl_1_x: i16,
2856    pub ctrl_1_y: i16,
2857    pub end_x: i16,
2858    pub end_y: i16,
2859}
2860
2861// CubicTo in em units
2862#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2863#[repr(C)]
2864pub struct OutlineCubicTo {
2865    pub ctrl_1_x: i16,
2866    pub ctrl_1_y: i16,
2867    pub ctrl_2_x: i16,
2868    pub ctrl_2_y: i16,
2869    pub end_x: i16,
2870    pub end_y: i16,
2871}
2872
2873#[derive(Debug, Clone, PartialEq, PartialOrd)]
2874#[repr(C)]
2875pub struct GlyphOutline {
2876    pub operations: GlyphOutlineOperationVec,
2877}
2878
2879azul_css::impl_vec!(GlyphOutlineOperation, GlyphOutlineOperationVec, GlyphOutlineOperationVecDestructor, GlyphOutlineOperationVecDestructorType, GlyphOutlineOperationVecSlice, OptionGlyphOutlineOperation);
2880azul_css::impl_vec_clone!(
2881    GlyphOutlineOperation,
2882    GlyphOutlineOperationVec,
2883    GlyphOutlineOperationVecDestructor
2884);
2885azul_css::impl_vec_debug!(GlyphOutlineOperation, GlyphOutlineOperationVec);
2886azul_css::impl_vec_partialord!(GlyphOutlineOperation, GlyphOutlineOperationVec);
2887azul_css::impl_vec_partialeq!(GlyphOutlineOperation, GlyphOutlineOperationVec);
2888
2889#[derive(Debug, Clone, Copy)]
2890#[repr(C)]
2891pub struct OwnedGlyphBoundingBox {
2892    pub max_x: i16,
2893    pub max_y: i16,
2894    pub min_x: i16,
2895    pub min_y: i16,
2896}
2897
2898/// Specifies the type of texture target in driver terms.
2899#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
2900#[repr(C)]
2901pub enum ImageBufferKind {
2902    /// Standard texture. This maps to `GL_TEXTURE_2D` in OpenGL.
2903    Texture2D = 0,
2904    /// Rectangle texture. This maps to `GL_TEXTURE_RECTANGLE` in OpenGL. This
2905    /// is similar to a standard texture, with a few subtle differences
2906    /// (no mipmaps, non-power-of-two dimensions, different coordinate space)
2907    /// that make it useful for representing the kinds of textures we use
2908    /// in `WebRender`. See <https://www.khronos.org/opengl/wiki/Rectangle_Texture>
2909    /// for background on Rectangle textures.
2910    TextureRect = 1,
2911    /// External texture. This maps to `GL_TEXTURE_EXTERNAL_OES` in OpenGL, which
2912    /// is an extension. This is used for image formats that OpenGL doesn't
2913    /// understand, particularly YUV. See
2914    /// <https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt>
2915    TextureExternal = 2,
2916}
2917
2918/// Descriptor for external image resources. See `ImageData`.
2919#[repr(C)]
2920#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
2921pub struct ExternalImageData {
2922    /// The identifier of this external image, provided by the embedding.
2923    pub id: ExternalImageId,
2924    /// For multi-plane images (i.e. YUV), indicates the plane of the
2925    /// original image that this struct represents. 0 for single-plane images.
2926    pub channel_index: u8,
2927    /// Storage format identifier.
2928    pub image_type: ExternalImageType,
2929}
2930
2931pub type TileSize = u16;
2932
2933#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
2934pub enum ImageDirtyRect {
2935    All,
2936    Partial(LayoutRect),
2937}
2938
2939#[derive(Debug, Clone, PartialEq, PartialOrd)]
2940pub enum ResourceUpdate {
2941    AddFont(AddFont),
2942    DeleteFont(FontKey),
2943    AddFontInstance(AddFontInstance),
2944    DeleteFontInstance(FontInstanceKey),
2945    AddImage(AddImage),
2946    UpdateImage(UpdateImage),
2947    DeleteImage(ImageKey),
2948}
2949
2950#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2951pub struct AddImage {
2952    pub key: ImageKey,
2953    pub descriptor: ImageDescriptor,
2954    pub data: ImageData,
2955    pub tiling: Option<TileSize>,
2956}
2957
2958#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
2959pub struct UpdateImage {
2960    pub key: ImageKey,
2961    pub descriptor: ImageDescriptor,
2962    pub data: ImageData,
2963    pub dirty_rect: ImageDirtyRect,
2964}
2965
2966/// Message to add a font to `WebRender`.
2967/// Contains a reference to the parsed font data.
2968#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2969pub struct AddFont {
2970    pub key: FontKey,
2971    pub font: FontRef,
2972}
2973
2974impl fmt::Debug for AddFont {
2975    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2976        write!(
2977            f,
2978            "AddFont {{ key: {:?}, font: {:?} }}",
2979            self.key, self.font
2980        )
2981    }
2982}
2983
2984#[derive(Debug, Clone, PartialEq, PartialOrd)]
2985pub struct AddFontInstance {
2986    pub key: FontInstanceKey,
2987    pub font_key: FontKey,
2988    pub glyph_size: (Au, DpiScaleFactor),
2989    pub options: Option<FontInstanceOptions>,
2990    pub platform_options: Option<FontInstancePlatformOptions>,
2991    pub variations: Vec<FontVariation>,
2992}
2993
2994#[repr(C)]
2995#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
2996pub struct FontVariation {
2997    pub tag: u32,
2998    pub value: f32,
2999}
3000
3001#[repr(C)]
3002#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
3003pub struct Epoch {
3004    inner: u32,
3005}
3006
3007impl fmt::Display for Epoch {
3008    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3009        write!(f, "{}", self.inner)
3010    }
3011}
3012
3013impl Default for Epoch {
3014    fn default() -> Self {
3015        Self::new()
3016    }
3017}
3018
3019impl Epoch {
3020    // prevent raw access to the .inner field so that
3021    // you can grep the codebase for .increment() to see
3022    // exactly where the epoch is being incremented
3023    #[must_use] pub const fn new() -> Self {
3024        Self { inner: 0 }
3025    }
3026    #[must_use] pub const fn from(i: u32) -> Self {
3027        Self { inner: i }
3028    }
3029    #[must_use] pub const fn into_u32(&self) -> u32 {
3030        self.inner
3031    }
3032
3033    // We don't want the epoch to increase to u32::MAX, since
3034    // u32::MAX represents an invalid epoch, which could confuse webrender
3035    pub const fn increment(&mut self) {
3036        use core::u32;
3037        const MAX_ID: u32 = u32::MAX - 1;
3038        *self = match self.inner {
3039            MAX_ID => Self { inner: 0 },
3040            other => Self {
3041                inner: other.saturating_add(1),
3042            },
3043        };
3044    }
3045}
3046
3047// App units that this font instance was registered for
3048#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)]
3049pub struct Au(pub i32);
3050
3051pub const AU_PER_PX: i32 = 60;
3052pub const MAX_AU: i32 = (1 << 30) - 1;
3053pub const MIN_AU: i32 = -(1 << 30) - 1;
3054
3055impl Au {
3056    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
3057    #[must_use] pub fn from_px(px: f32) -> Self {
3058        let target_app_units = (px * AU_PER_PX as f32) as i32;
3059        Self(target_app_units.clamp(MIN_AU, MAX_AU))
3060    }
3061    #[allow(clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
3062    #[must_use] pub fn into_px(&self) -> f32 {
3063        self.0 as f32 / AU_PER_PX as f32
3064    }
3065}
3066
3067// Debug, PartialEq, Eq, PartialOrd, Ord
3068#[derive(Debug)]
3069pub enum AddFontMsg {
3070    // add font: font key, font bytes + font index
3071    Font(FontKey, StyleFontFamilyHash, FontRef),
3072    Instance(AddFontInstance, (Au, DpiScaleFactor)),
3073}
3074
3075impl AddFontMsg {
3076    #[must_use] pub fn into_resource_update(&self) -> ResourceUpdate {
3077        use self::AddFontMsg::{Font, Instance};
3078        match self {
3079            Font(font_key, _, font_ref) => ResourceUpdate::AddFont(AddFont {
3080                key: *font_key,
3081                font: font_ref.clone(),
3082            }),
3083            Instance(fi, _) => ResourceUpdate::AddFontInstance(fi.clone()),
3084        }
3085    }
3086}
3087
3088#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
3089pub enum DeleteFontMsg {
3090    Font(FontKey),
3091    Instance(FontInstanceKey, (Au, DpiScaleFactor)),
3092}
3093
3094impl DeleteFontMsg {
3095    #[must_use] pub const fn into_resource_update(&self) -> ResourceUpdate {
3096        use self::DeleteFontMsg::{Font, Instance};
3097        match self {
3098            Font(f) => ResourceUpdate::DeleteFont(*f),
3099            Instance(fi, _) => ResourceUpdate::DeleteFontInstance(*fi),
3100        }
3101    }
3102}
3103
3104#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
3105pub struct AddImageMsg(pub AddImage);
3106
3107impl AddImageMsg {
3108    #[must_use] pub fn into_resource_update(&self) -> ResourceUpdate {
3109        ResourceUpdate::AddImage(self.0.clone())
3110    }
3111}
3112
3113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3114#[repr(C)]
3115pub struct LoadedFontSource {
3116    pub data: U8Vec,
3117    pub index: u32,
3118    pub load_outlines: bool,
3119}
3120
3121// function to load the font source from a file
3122pub type LoadFontFn = fn(&StyleFontFamily, &FcFontCache) -> Option<LoadedFontSource>;
3123
3124// function to parse the font given the loaded font source
3125pub type ParseFontFn = fn(LoadedFontSource) -> Option<FontRef>; // = Option<Box<azul_text_layout::Font>>
3126
3127pub type GlStoreImageFn = fn(DocumentId, Epoch, Texture, ExternalImageId);
3128
3129/// Compute the deterministic `ExternalImageId` that the OpenGL texture cache uses
3130/// for a texture bound to a specific DOM node.
3131///
3132/// The same `(DomId, NodeId)` always
3133/// maps to the same `ExternalImageId`, so cached display lists keep working across
3134/// frames.
3135#[must_use] pub fn texture_external_image_id(dom_id: DomId, node_id: NodeId) -> ExternalImageId {
3136    let dom = dom_id.inner as u64;
3137    let node = node_id.index() as u64;
3138    debug_assert!(u32::try_from(dom).is_ok(), "DomId exceeds 32-bit range");
3139    debug_assert!(u32::try_from(node).is_ok(), "NodeId exceeds 32-bit range");
3140    ExternalImageId {
3141        inner: (dom << 32) | (node & 0xFFFF_FFFF),
3142    }
3143}
3144
3145/// Compute the `ExternalImageId` for a static GL texture identified by its
3146/// `ImageRefHash`. Mirrors `image_ref_hash_to_image_key` so a given image hash
3147/// produces the same identifiers everywhere.
3148#[must_use] pub const fn image_ref_hash_to_external_image_id(hash: ImageRefHash) -> ExternalImageId {
3149    ExternalImageId {
3150        inner: hash.inner,
3151    }
3152}
3153
3154/// Given the fonts of the current frame, returns `AddFont` and `AddFontInstance`s of
3155/// which fonts / instances are currently not in the `current_registered_fonts` and
3156/// need to be added.
3157///
3158/// Deleting fonts can only be done after the entire frame has finished drawing,
3159/// otherwise (if removing fonts would happen after every DOM) we'd constantly
3160/// add-and-remove fonts after every `VirtualViewCallback`, which would cause a lot of
3161/// I/O waiting.
3162#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3163pub fn build_add_font_resource_updates(
3164    renderer_resources: &mut RendererResources,
3165    dpi: DpiScaleFactor,
3166    fc_cache: &FcFontCache,
3167    id_namespace: IdNamespace,
3168    fonts_in_dom: &OrderedMap<ImmediateFontId, FastBTreeSet<Au>>,
3169    font_source_load_fn: LoadFontFn,
3170    parse_font_fn: ParseFontFn,
3171) -> Vec<(StyleFontFamilyHash, AddFontMsg)> {
3172    let mut resource_updates = Vec::new();
3173    let mut font_instances_added_this_frame = FastBTreeSet::new();
3174
3175    'outer: for (im_font_id, font_sizes) in fonts_in_dom {
3176        macro_rules! insert_font_instances {
3177            ($font_family_hash:expr, $font_key:expr, $font_size:expr) => {{
3178                let font_instance_key_exists = renderer_resources
3179                    .currently_registered_fonts
3180                    .get(&$font_key)
3181                    .and_then(|(_, font_instances)| font_instances.get(&($font_size, dpi)))
3182                    .is_some()
3183                    || font_instances_added_this_frame.contains(&($font_key, ($font_size, dpi)));
3184
3185                if !font_instance_key_exists {
3186                    let font_instance_key = FontInstanceKey::unique(id_namespace);
3187
3188                    // For some reason the gamma is way to low on Windows
3189                    #[cfg(target_os = "windows")]
3190                    let platform_options = FontInstancePlatformOptions {
3191                        gamma: 300,
3192                        contrast: 100,
3193                        cleartype_level: 100,
3194                    };
3195
3196                    #[cfg(target_os = "linux")]
3197                    let platform_options = FontInstancePlatformOptions {
3198                        lcd_filter: FontLCDFilter::Default,
3199                        hinting: FontHinting::Normal,
3200                    };
3201
3202                    #[cfg(target_os = "macos")]
3203                    let platform_options = FontInstancePlatformOptions::default();
3204
3205                    #[cfg(target_arch = "wasm32")]
3206                    let platform_options = FontInstancePlatformOptions::default();
3207
3208                    #[cfg(any(target_os = "android", target_os = "ios"))]
3209                    let platform_options = FontInstancePlatformOptions::default();
3210
3211                    let options = FontInstanceOptions {
3212                        render_mode: FontRenderMode::Subpixel,
3213                        flags: FONT_INSTANCE_FLAG_NO_AUTOHINT,
3214                        ..Default::default()
3215                    };
3216
3217                    font_instances_added_this_frame.insert(($font_key, ($font_size, dpi)));
3218                    resource_updates.push((
3219                        $font_family_hash,
3220                        AddFontMsg::Instance(
3221                            AddFontInstance {
3222                                key: font_instance_key,
3223                                font_key: $font_key,
3224                                glyph_size: ($font_size, dpi),
3225                                options: Some(options),
3226                                platform_options: Some(platform_options),
3227                                variations: alloc::vec::Vec::new(),
3228                            },
3229                            ($font_size, dpi),
3230                        ),
3231                    ));
3232                }
3233            }};
3234        }
3235
3236        match im_font_id {
3237            ImmediateFontId::Resolved((font_family_hash, font_id)) => {
3238                // nothing to do, font is already added,
3239                // just insert the missing font instances
3240                for font_size in font_sizes {
3241                    insert_font_instances!(*font_family_hash, *font_id, *font_size);
3242                }
3243            }
3244            ImmediateFontId::Unresolved(style_font_families) => {
3245                // If the font is already loaded during the current frame,
3246                // do not attempt to load it again
3247                //
3248                // This prevents duplicated loading for fonts in different orders, i.e.
3249                // - vec!["Times New Roman", "serif"] and
3250                // - vec!["sans", "Times New Roman"]
3251                // ... will resolve to the same font instead of creating two fonts
3252
3253                // If there is no font key, that means there's also no font instances
3254                let mut font_family_hash = None;
3255                let font_families_hash = StyleFontFamiliesHash::new(style_font_families.as_ref());
3256
3257                // Find the first font that can be loaded and parsed
3258                'inner: for family in style_font_families.as_ref() {
3259                    let current_family_hash = StyleFontFamilyHash::new(family);
3260
3261                    if let Some(font_id) = renderer_resources.font_id_map.get(&current_family_hash)
3262                    {
3263                        // font key already exists
3264                        for font_size in font_sizes {
3265                            insert_font_instances!(current_family_hash, *font_id, *font_size);
3266                        }
3267                        continue 'outer;
3268                    }
3269
3270                    let font_ref = match family {
3271                        StyleFontFamily::Ref(r) => r.clone(), // Clone the FontRef
3272                        other => {
3273                            // Load and parse the font
3274                            let Some(font_data) = (font_source_load_fn)(other, fc_cache) else {
3275                                continue 'inner;
3276                            };
3277
3278                            
3279
3280                            match (parse_font_fn)(font_data) {
3281                                Some(s) => s,
3282                                None => continue 'inner,
3283                            }
3284                        }
3285                    };
3286
3287                    // font loaded properly
3288                    font_family_hash = Some((current_family_hash, font_ref));
3289                    break 'inner;
3290                }
3291
3292                let (font_family_hash, font_ref) = match font_family_hash {
3293                    None => continue 'outer, // No font could be loaded, try again next frame
3294                    Some(s) => s,
3295                };
3296
3297                // Generate a new font key, store the mapping between hash and font key
3298                let font_key = FontKey::unique(id_namespace);
3299                let add_font_msg = AddFontMsg::Font(font_key, font_family_hash, font_ref);
3300
3301                renderer_resources
3302                    .font_id_map
3303                    .insert(font_family_hash, font_key);
3304                renderer_resources
3305                    .font_families_map
3306                    .insert(font_families_hash, font_family_hash);
3307                resource_updates.push((font_family_hash, add_font_msg));
3308
3309                // Insert font sizes for the newly generated font key
3310                for font_size in font_sizes {
3311                    insert_font_instances!(font_family_hash, font_key, *font_size);
3312                }
3313            }
3314        }
3315    }
3316
3317    resource_updates
3318}
3319
3320/// Given the images of the current frame, returns `AddImage`s of
3321/// which image keys are currently not in the `current_registered_images` and
3322/// need to be added.
3323///
3324/// Returns Vec<(`ImageRefHash`, `AddImageMsg`)> where:
3325/// - `ImageRefHash`: Stable hash of the `ImageRef` pointer
3326/// - `AddImageMsg`: Message to add the image to `WebRender`
3327///
3328/// The `ImageKey` in `AddImageMsg` is generated directly from the `ImageRefHash` using
3329/// `image_ref_hash_to_image_key()`, so no separate mapping table is needed.
3330///
3331/// Deleting images can only be done after the entire frame has finished drawing,
3332/// otherwise (if removing images would happen after every DOM) we'd constantly
3333/// add-and-remove images after every `VirtualViewCallback`, which would cause a lot of
3334/// I/O waiting.
3335#[allow(unused_variables)]
3336pub fn build_add_image_resource_updates(
3337    renderer_resources: &RendererResources,
3338    id_namespace: IdNamespace,
3339    epoch: Epoch,
3340    document_id: &DocumentId,
3341    images_in_dom: &FastBTreeSet<ImageRef>,
3342    insert_into_active_gl_textures: GlStoreImageFn,
3343) -> Vec<(ImageRefHash, AddImageMsg)> {
3344    images_in_dom
3345        .iter()
3346        .filter_map(|image_ref| {
3347            let image_ref_hash = image_ref_get_hash(image_ref);
3348
3349            if renderer_resources
3350                .currently_registered_images
3351                .contains_key(&image_ref_hash)
3352            {
3353                return None;
3354            }
3355
3356            // NOTE: The image_ref.clone() is a shallow clone,
3357            // does not actually clone the data
3358            match image_ref.get_data() {
3359                DecodedImage::Gl(texture) => {
3360                    let descriptor = texture.get_descriptor();
3361                    let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
3362                    // The ExternalImageId is derived from the same stable hash that
3363                    // produces the ImageKey, so the GL texture cache and WebRender
3364                    // agree on a single identifier for this texture.
3365                    let external_image_id = image_ref_hash_to_external_image_id(image_ref_hash);
3366                    // NOTE: The texture is not really cloned here,
3367                    (insert_into_active_gl_textures)(
3368                        *document_id,
3369                        epoch,
3370                        texture.clone(),
3371                        external_image_id,
3372                    );
3373                    Some((
3374                        image_ref_hash,
3375                        AddImageMsg(AddImage {
3376                            key,
3377                            data: ImageData::External(ExternalImageData {
3378                                id: external_image_id,
3379                                channel_index: 0,
3380                                image_type: ExternalImageType::TextureHandle(
3381                                    ImageBufferKind::Texture2D,
3382                                ),
3383                            }),
3384                            descriptor,
3385                            tiling: None,
3386                        }),
3387                    ))
3388                }
3389                DecodedImage::Raw((descriptor, data)) => {
3390                    let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
3391                    Some((
3392                        image_ref_hash,
3393                        AddImageMsg(AddImage {
3394                            key,
3395                            data: data.clone(), // deep-copy except in the &'static case
3396                            descriptor: *descriptor, /* deep-copy, but struct is not very
3397                                                 * large */
3398                            tiling: None,
3399                        }),
3400                    ))
3401                }
3402                // NullImage has nothing to upload; texture callbacks are handled after
3403                // layout is done.
3404                DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => None,
3405            }
3406        })
3407        .collect()
3408}
3409
3410/// Submits the `AddFont`, `AddFontInstance` and `AddImage` resources to the `RenderApi`.
3411///
3412/// Extends `currently_registered_images` and `currently_registered_fonts` by the
3413/// `last_frame_image_keys` and `last_frame_font_keys`, so that we don't lose track of
3414/// what font and image keys are currently in the API.
3415#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
3416pub fn add_resources(
3417    renderer_resources: &mut RendererResources,
3418    all_resource_updates: &mut Vec<ResourceUpdate>,
3419    add_font_resources: Vec<(StyleFontFamilyHash, AddFontMsg)>,
3420    add_image_resources: Vec<(ImageRefHash, AddImageMsg)>,
3421) {
3422    all_resource_updates.extend(
3423        add_font_resources
3424            .iter()
3425            .map(|(_, f)| f.into_resource_update()),
3426    );
3427    all_resource_updates.extend(
3428        add_image_resources
3429            .iter()
3430            .map(|(_, i)| i.into_resource_update()),
3431    );
3432
3433    for (image_ref_hash, add_image_msg) in &add_image_resources {
3434        renderer_resources.currently_registered_images.insert(
3435            *image_ref_hash,
3436            ResolvedImage {
3437                key: add_image_msg.0.key,
3438                descriptor: add_image_msg.0.descriptor,
3439            },
3440        );
3441        // Keep the reverse lookup (`ImageKey` -> `ImageRefHash`) in sync with the
3442        // forward map so display-list translation can resolve keys back to hashes.
3443        renderer_resources
3444            .image_key_map
3445            .insert(add_image_msg.0.key, *image_ref_hash);
3446    }
3447
3448    for (_, add_font_msg) in add_font_resources {
3449        use self::AddFontMsg::{Font, Instance};
3450        match add_font_msg {
3451            Font(fk, font_family_hash, font_ref) => {
3452                renderer_resources
3453                    .currently_registered_fonts
3454                    .entry(fk)
3455                    .or_insert_with(|| (font_ref.clone(), OrderedMap::default()));
3456
3457                // CRITICAL: Map font_hash to FontKey so we can look it up during rendering
3458                renderer_resources
3459                    .font_hash_map
3460                    .insert(font_ref.get_hash(), fk);
3461            }
3462            Instance(fi, size) => {
3463                if let Some((_, instances)) = renderer_resources
3464                    .currently_registered_fonts
3465                    .get_mut(&fi.font_key)
3466                {
3467                    instances.insert(size, fi.key);
3468                }
3469            }
3470        }
3471    }
3472}
3473
3474#[cfg(test)]
3475#[allow(clippy::items_after_statements, clippy::redundant_clone, clippy::cast_possible_truncation, clippy::cast_sign_loss, trivial_casts, clippy::borrow_as_ptr, clippy::cast_ptr_alignment, clippy::unused_self, unused_qualifications, unreachable_pub, private_interfaces)] // pedantic lints are noise in unsafe-exercising test code
3476mod tests {
3477    use super::*;
3478
3479    #[test]
3480    fn normalize_u16_maps_full_range() {
3481        // 0 -> 0, u16::MAX -> u8::MAX, midpoint -> ~127/128, no div-by-zero.
3482        assert_eq!(normalize_u16(0), 0);
3483        assert_eq!(normalize_u16(u16::MAX), 255);
3484        // Half of u16::MAX should land at ~half of u8::MAX.
3485        let mid = normalize_u16(u16::MAX / 2);
3486        assert!((126..=128).contains(&mid), "midpoint normalized to {mid}");
3487        // Previously `(65535/i)*255` produced near-white garbage for small i;
3488        // a small input must now map to a small output.
3489        assert_eq!(normalize_u16(256), 0);
3490        assert_eq!(normalize_u16(257), 1);
3491    }
3492
3493    #[test]
3494    fn load_bgra8_rejects_wrong_length() {
3495        // premultiplied branch: buffer shorter than expected must be rejected,
3496        // not silently accepted (previously missing length guard).
3497        let short = RawImageData::U8(vec![0u8; 4 * 3].into()); // 3 px worth
3498        assert!(RawImage::load_bgra8(short, 4, true).is_none());
3499
3500        // correct length is accepted.
3501        let ok = RawImageData::U8(vec![255u8; 4 * 4].into()); // 4 px
3502        assert!(RawImage::load_bgra8(ok, 4, true).is_some());
3503
3504        // non-premultiplied branch still rejects wrong length.
3505        let short2 = RawImageData::U8(vec![0u8; 4 * 2].into());
3506        assert!(RawImage::load_bgra8(short2, 4, false).is_none());
3507    }
3508
3509    // --- unsafe-hardening tests (Miri-compatible: pure in-memory, no FFI/GL) ---
3510
3511    #[test]
3512    fn imageref_get_data_reads_backing_box() {
3513        // Exercises the `&*self.data` raw-pointer deref in `get_data`.
3514        let img = ImageRef::null_image(2, 3, RawImageFormat::RGBA8, vec![7, 8]);
3515        match img.get_data() {
3516            DecodedImage::NullImage { width, height, tag, .. } => {
3517                assert_eq!((*width, *height), (2, 3));
3518                assert_eq!(tag.as_slice(), &[7, 8]);
3519            }
3520            _ => panic!("expected NullImage"),
3521        }
3522    }
3523
3524    #[test]
3525    fn imageref_clone_shares_refcount_and_identity() {
3526        // Clone must bump the shared AtomicUsize (so `into_inner` refuses while a
3527        // second copy is alive) and preserve the never-reused identity `id`.
3528        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
3529        let c = img.clone();
3530        assert_eq!(img, c); // same id -> shallow clone
3531        // Two live copies: sole-owner extraction must fail (and `c` drops cleanly).
3532        assert!(c.into_inner().is_none());
3533        // Back to one owner: extraction now succeeds, forgetting `self` without leak.
3534        assert!(img.into_inner().is_some());
3535    }
3536
3537    #[test]
3538    fn imageref_deep_copy_has_distinct_identity() {
3539        // deep_copy allocates a fresh backing Box + fresh id -> not equal, independent
3540        // drop (Miri would flag any shared/double-freed allocation here).
3541        let img = ImageRef::null_image(4, 4, RawImageFormat::RGBA8, vec![1]);
3542        let d = img.deep_copy();
3543        assert_ne!(img, d);
3544        drop(img);
3545        // `d` still valid and independently readable after `img` freed.
3546        assert_eq!(d.get_size().width as usize, 4);
3547    }
3548
3549    #[test]
3550    fn imageref_last_drop_frees_once() {
3551        // Clone then drop both: the refcount path must free the two Boxes exactly once
3552        // on the final drop. Under Miri a double free / leak fails the test.
3553        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
3554        let c = img.clone();
3555        drop(img);
3556        drop(c);
3557    }
3558
3559    #[test]
3560    fn imageref_get_callback_none_for_non_callback_and_when_shared() {
3561        // `get_image_callback` derefs both `copies` and `data`; a NullImage yields None,
3562        // and a shared (copies != 1) handle also yields None.
3563        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
3564        assert!(img.get_image_callback().is_none());
3565        let c = img.clone();
3566        assert!(img.get_image_callback().is_none()); // shared -> not safe
3567        drop(c);
3568    }
3569
3570    #[test]
3571    fn shared_raw_image_data_read_paths() {
3572        // Exercises as_ref / len / is_empty / as_ptr raw-pointer derefs.
3573        let s = SharedRawImageData::new(vec![10u8, 20, 30].into());
3574        assert_eq!(s.as_ref(), &[10, 20, 30]);
3575        assert_eq!(s.len(), 3);
3576        assert!(!s.is_empty());
3577        assert_eq!(unsafe { *s.as_ptr() }, 10);
3578        assert!(SharedRawImageData::new(Vec::<u8>::new().into()).is_empty());
3579    }
3580
3581    #[test]
3582    fn shared_raw_image_data_clone_shares_alloc() {
3583        // Clone shares the backing Box (ptr-equal) and refcount; `into_inner` refuses
3584        // while a second copy lives, and succeeds once sole owner.
3585        let s = SharedRawImageData::new(vec![1u8, 2, 3, 4].into());
3586        let c = s.clone();
3587        assert_eq!(s, c); // ptr::eq on the shared `data`
3588        assert_eq!(s.as_ptr(), c.as_ptr());
3589        assert!(c.into_inner().is_none()); // two owners -> None, `c` drops to refcount 1
3590        let inner = s.into_inner().expect("sole owner extraction");
3591        assert_eq!(inner.as_ref(), &[1, 2, 3, 4]);
3592    }
3593
3594    #[test]
3595    fn shared_raw_image_data_last_drop_frees_once() {
3596        // Refcounted drop must free both Boxes exactly once; Miri flags UB otherwise.
3597        let s = SharedRawImageData::new(vec![0u8; 8].into());
3598        let c = s.clone();
3599        drop(s);
3600        drop(c);
3601    }
3602}
3603
3604#[cfg(test)]
3605#[allow(
3606    clippy::float_cmp,
3607    clippy::items_after_statements,
3608    clippy::redundant_clone,
3609    clippy::cast_possible_truncation,
3610    clippy::cast_precision_loss,
3611    clippy::cast_sign_loss,
3612    clippy::cast_lossless,
3613    clippy::unreadable_literal,
3614    clippy::too_many_lines,
3615    clippy::many_single_char_names,
3616    clippy::similar_names,
3617    unused_qualifications,
3618    unreachable_pub,
3619    private_interfaces
3620)] // pedantic lints are noise in adversarial test code
3621mod autotest_generated {
3622    use alloc::string::String;
3623
3624    use super::*;
3625
3626    // ---------------------------------------------------------------------
3627    // helpers
3628    // ---------------------------------------------------------------------
3629
3630    /// A `FontRef` whose `parsed` pointer addresses a `'static` byte and whose
3631    /// destructor is a no-op, so nothing is freed on drop. Sound because
3632    /// `FontRef`'s `Hash`/`get_hash` only read the never-reused `id` and never
3633    /// dereference `parsed`.
3634    fn dummy_font_ref() -> FontRef {
3635        static DUMMY_FONT_DATA: u8 = 0;
3636        extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
3637        FontRef::new(
3638            core::ptr::addr_of!(DUMMY_FONT_DATA).cast::<core::ffi::c_void>(),
3639            dummy_destructor,
3640        )
3641    }
3642
3643    /// `LoadFontFn` that never resolves a font (simulates a missing font file).
3644    fn load_font_none(_: &StyleFontFamily, _: &FcFontCache) -> Option<LoadedFontSource> {
3645        None
3646    }
3647
3648    /// `ParseFontFn` that never parses (simulates a corrupt font file).
3649    fn parse_font_none(_: LoadedFontSource) -> Option<FontRef> {
3650        None
3651    }
3652
3653    /// `GlStoreImageFn` no-op: never invoked for raw / null / callback images.
3654    fn store_gl_texture_noop(_: DocumentId, _: Epoch, _: Texture, _: ExternalImageId) {}
3655
3656    fn test_document_id() -> DocumentId {
3657        DocumentId {
3658            namespace_id: IdNamespace(7),
3659            id: 0,
3660        }
3661    }
3662
3663    /// An `RGBA8` image of `w * h` transparent-black pixels.
3664    fn rgba8_image(w: usize, h: usize) -> RawImage {
3665        RawImage {
3666            pixels: RawImageData::U8(vec![0u8; w * h * 4].into()),
3667            width: w,
3668            height: h,
3669            premultiplied_alpha: true,
3670            data_format: RawImageFormat::RGBA8,
3671            tag: Vec::new().into(),
3672        }
3673    }
3674
3675    fn opaque_red() -> ColorU {
3676        ColorU {
3677            r: 255,
3678            g: 0,
3679            b: 0,
3680            a: 255,
3681        }
3682    }
3683
3684    // =====================================================================
3685    // PARSERS: match_route / RouteMatch::get_param / AppConfig::match_route_for_path
3686    // =====================================================================
3687
3688    #[test]
3689    fn match_route_valid_minimal_positive_control() {
3690        // Documented examples must hold (positive control).
3691        let m = match_route("/user/:id", "/user/42").expect("documented example must match");
3692        assert_eq!(m.pattern.as_str(), "/user/:id");
3693        assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
3694
3695        let root = match_route("/", "/").expect("root must match root");
3696        assert!(root.params.as_ref().is_empty());
3697
3698        assert!(match_route("/about", "/settings").is_none());
3699    }
3700
3701    #[test]
3702    fn match_route_empty_input_does_not_panic() {
3703        // Empty pattern/path degrade to zero segments; the segment-count check
3704        // makes "" and "/" equivalent (both filter to no segments).
3705        let m = match_route("", "").expect("empty vs empty is a zero-segment match");
3706        assert!(m.params.as_ref().is_empty());
3707        assert!(match_route("", "/").is_some()); // "/" also has zero segments
3708        assert!(match_route("/", "").is_some());
3709        assert!(match_route("", "/a").is_none()); // 0 segments != 1 segment
3710        assert!(match_route("/a", "").is_none());
3711    }
3712
3713    #[test]
3714    fn match_route_whitespace_only_is_not_trimmed() {
3715        // Whitespace is NOT trimmed: it is an ordinary (opaque) path segment,
3716        // so it only matches itself. Deterministic, no panic.
3717        assert!(match_route("   ", "   ").is_some());
3718        assert!(match_route("   ", "\t\n").is_none());
3719        assert!(match_route("/ ", "/").is_none()); // " " is a real segment
3720        assert!(match_route("/\t\n", "/\t\n").is_some());
3721    }
3722
3723    #[test]
3724    fn match_route_garbage_never_panics() {
3725        for pat in [
3726            "\0\0\0",
3727            "///////",
3728            "::::",
3729            ":",
3730            "%%%$#@!",
3731            "\u{feff}",
3732            "/a/../../etc/passwd",
3733        ] {
3734            for path in ["", "/", "\0", "/a/b/c", "%%%$#@!", "\u{feff}"] {
3735                // Only requirement: a total function that never panics.
3736                let _ = match_route(pat, path);
3737            }
3738        }
3739        // "///////" collapses to zero segments, so it matches the root.
3740        assert!(match_route("///////", "/").is_some());
3741        // A bare ":" is a param with an EMPTY name; the value is still captured.
3742        let m = match_route("/:", "/hello").expect("empty param name still matches");
3743        assert_eq!(m.get_param("").map(AzString::as_str), Some("hello"));
3744    }
3745
3746    #[test]
3747    fn match_route_leading_trailing_junk_is_rejected_or_ignored() {
3748        // Trailing slashes produce empty segments that are filtered out, so a
3749        // trailing slash is ignored (deterministic).
3750        assert!(match_route("/user/:id/", "/user/42").is_some());
3751        assert!(match_route("/user/:id", "/user/42/").is_some());
3752        // Surrounding spaces are part of the segment -> rejected.
3753        assert!(match_route(" /about ", "/about").is_none());
3754        assert!(match_route("/about", "/about;garbage").is_none());
3755    }
3756
3757    #[test]
3758    fn match_route_boundary_number_strings_are_opaque_segments() {
3759        // Numeric-looking params are never parsed; they round-trip verbatim.
3760        for v in [
3761            "0",
3762            "-0",
3763            "9223372036854775807",
3764            "-9223372036854775808",
3765            "1e400",
3766            "NaN",
3767            "inf",
3768            "-inf",
3769            "0.0000000000000000001",
3770        ] {
3771            let path = String::from("/user/") + v;
3772            let m = match_route("/user/:id", &path).expect("any segment matches a :param");
3773            assert_eq!(m.get_param("id").map(AzString::as_str), Some(v));
3774        }
3775    }
3776
3777    #[test]
3778    fn match_route_unicode_multibyte_does_not_panic() {
3779        // Splitting on '/' is byte-safe for UTF-8; multibyte segments survive.
3780        let m = match_route("/user/:id", "/user/\u{1F600}").expect("emoji segment matches");
3781        assert_eq!(m.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
3782
3783        // Combining marks + RTL + a unicode param NAME.
3784        let m = match_route("/:é\u{0301}", "/e\u{0301}\u{202E}x").expect("unicode param name");
3785        assert_eq!(
3786            m.get_param("é\u{0301}").map(AzString::as_str),
3787            Some("e\u{0301}\u{202E}x")
3788        );
3789        // A unicode segment does not equal its NFC/NFD-different twin.
3790        assert!(match_route("/é", "/e\u{0301}").is_none());
3791    }
3792
3793    #[test]
3794    fn match_route_extremely_long_input_does_not_hang() {
3795        // 1M-char single segment: linear split, no quadratic blowup / panic.
3796        let huge = String::from("/") + &"a".repeat(1_000_000);
3797        assert!(match_route("/x", &huge).is_none());
3798        let m = match_route("/:id", &huge).expect("one long segment is still one segment");
3799        assert_eq!(m.get_param("id").map(|s| s.as_str().len()), Some(1_000_000));
3800    }
3801
3802    #[test]
3803    fn match_route_deeply_nested_input_does_not_stack_overflow() {
3804        // match_route is iterative: 10k segments and 10k nested brackets are fine.
3805        let deep = "/a".repeat(10_000);
3806        let m = match_route(&deep, &deep).expect("identical deep paths match");
3807        assert!(m.params.as_ref().is_empty());
3808
3809        let all_params = "/:p".repeat(10_000);
3810        let m = match_route(&all_params, &deep).expect("10k params extract");
3811        assert_eq!(m.params.as_ref().len(), 10_000);
3812        // Duplicate keys: get_param returns the FIRST binding.
3813        assert_eq!(m.get_param("p").map(AzString::as_str), Some("a"));
3814
3815        let brackets = String::from("/") + &"[".repeat(10_000);
3816        assert!(match_route("/:x", &brackets).is_some());
3817    }
3818
3819    #[test]
3820    fn match_route_segment_count_mismatch_is_none() {
3821        assert!(match_route("/a/:b", "/a").is_none());
3822        assert!(match_route("/a", "/a/b").is_none());
3823        assert!(match_route("/:a/:b/:c", "/1/2").is_none());
3824    }
3825
3826    #[test]
3827    fn route_match_get_param_missing_keys_return_none() {
3828        let empty = RouteMatch {
3829            pattern: AzString::from_const_str("/"),
3830            params: StringPairVec::from_vec(Vec::new()),
3831        };
3832        // Empty / whitespace / garbage / unicode / huge keys: None, never a panic.
3833        assert!(empty.get_param("").is_none());
3834        assert!(empty.get_param("   ").is_none());
3835        assert!(empty.get_param("\t\n").is_none());
3836        assert!(empty.get_param("\u{1F600}").is_none());
3837        assert!(empty.get_param("\0").is_none());
3838        assert!(empty.get_param(&"k".repeat(100_000)).is_none());
3839
3840        // Positive control + near-miss keys on a populated match.
3841        let m = match_route("/u/:id", "/u/7").expect("valid");
3842        assert_eq!(m.get_param("id").map(AzString::as_str), Some("7"));
3843        assert!(m.get_param("ID").is_none()); // case-sensitive
3844        assert!(m.get_param("i").is_none()); // no prefix matching
3845        assert!(m.get_param(":id").is_none()); // the ':' is stripped from the key
3846    }
3847
3848    #[test]
3849    fn app_config_match_route_for_path_adversarial_inputs() {
3850        let mut config = AppConfig::create();
3851        let cb: crate::callbacks::LayoutCallbackType = autotest_layout;
3852        extern "C" fn autotest_layout(
3853            _: RefAny,
3854            _: crate::callbacks::LayoutCallbackInfo,
3855        ) -> crate::dom::Dom {
3856            crate::dom::Dom::create_body()
3857        }
3858        config.add_route(AzString::from_const_str("/"), cb);
3859        config.add_route(AzString::from_const_str("/user/:id"), cb);
3860
3861        // valid_minimal (positive control)
3862        let (route, m) = config
3863            .match_route_for_path("/user/42")
3864            .expect("registered route must match");
3865        assert_eq!(route.pattern.as_str(), "/user/:id");
3866        assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
3867
3868        // "" and "/" both have zero segments -> they hit the "/" route.
3869        assert!(config.match_route_for_path("").is_some());
3870        assert!(config.match_route_for_path("/").is_some());
3871
3872        // garbage / unicode / long / whitespace: deterministic, never panics.
3873        assert!(config.match_route_for_path("/nope/nope/nope").is_none());
3874        assert!(config.match_route_for_path("\0\0").is_none());
3875        assert!(config.match_route_for_path("   ").is_none());
3876        let long = String::from("/user/") + &"9".repeat(500_000);
3877        assert!(config.match_route_for_path(&long).is_some());
3878        let m = config
3879            .match_route_for_path("/user/\u{1F600}")
3880            .expect("unicode param");
3881        assert_eq!(m.1.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
3882    }
3883
3884    #[test]
3885    fn app_config_add_route_replaces_same_pattern_and_orders_by_insertion() {
3886        extern "C" fn layout_a(_: RefAny, _: crate::callbacks::LayoutCallbackInfo) -> crate::dom::Dom {
3887            crate::dom::Dom::create_body()
3888        }
3889        let cb: crate::callbacks::LayoutCallbackType = layout_a;
3890
3891        let mut config = AppConfig::create();
3892        assert!(config.routes.as_ref().is_empty());
3893        config.add_route(AzString::from_const_str("/dup"), cb);
3894        config.add_route(AzString::from_const_str("/dup"), cb);
3895        assert_eq!(config.routes.as_ref().len(), 1, "same pattern must replace");
3896
3897        // First matching route wins: a catch-all registered first shadows later routes.
3898        let mut config = AppConfig::create();
3899        config.add_route(AzString::from_const_str("/:anything"), cb);
3900        config.add_route(AzString::from_const_str("/about"), cb);
3901        let (route, _) = config.match_route_for_path("/about").expect("matches");
3902        assert_eq!(route.pattern.as_str(), "/:anything");
3903    }
3904
3905    // =====================================================================
3906    // CONSTRUCTORS / INVARIANTS
3907    // =====================================================================
3908
3909    #[test]
3910    fn dpi_scale_factor_new_handles_nan_and_infinities() {
3911        // FloatValue::new does a saturating f32 -> isize cast (NaN -> 0).
3912        assert_eq!(DpiScaleFactor::new(0.0).inner.get(), 0.0);
3913        assert_eq!(DpiScaleFactor::new(1.0).inner.get(), 1.0);
3914        assert_eq!(DpiScaleFactor::new(f32::NAN).inner.get(), 0.0);
3915        assert!(DpiScaleFactor::new(f32::INFINITY).inner.get().is_finite());
3916        assert!(DpiScaleFactor::new(f32::NEG_INFINITY).inner.get().is_finite());
3917        assert!(DpiScaleFactor::new(f32::MAX).inner.get().is_finite());
3918        assert!(DpiScaleFactor::new(f32::MIN).inner.get().is_finite());
3919        // Sub-precision values collapse to 0 (1/1000 fixed point), not to NaN.
3920        assert_eq!(DpiScaleFactor::new(f32::MIN_POSITIVE).inner.get(), 0.0);
3921        // Eq/Hash invariant: equal inputs produce equal (hashable) keys.
3922        assert_eq!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(1.5));
3923        assert_ne!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(2.0));
3924    }
3925
3926    #[test]
3927    fn named_font_new_keeps_fields_verbatim() {
3928        let f = NamedFont::new(
3929            AzString::from_const_str(""),
3930            U8Vec::from_vec(Vec::new()),
3931        );
3932        assert_eq!(f.name.as_str(), "");
3933        assert!(f.bytes.as_ref().is_empty());
3934
3935        let bytes = vec![0u8, 255, 128];
3936        let f = NamedFont::new(AzString::from(String::from("\u{1F600}")), bytes.clone().into());
3937        assert_eq!(f.name.as_str(), "\u{1F600}");
3938        assert_eq!(f.bytes.as_ref(), bytes.as_slice());
3939    }
3940
3941    #[test]
3942    fn loaded_font_new_keeps_fields_verbatim_at_limits() {
3943        let f = LoadedFont::new(0, AzString::from_const_str(""), 0, false);
3944        assert_eq!(f.font_hash, 0);
3945        assert_eq!(f.num_glyphs, 0);
3946        assert!(!f.has_bytes);
3947
3948        let f = LoadedFont::new(u64::MAX, AzString::from_const_str("x"), u32::MAX, true);
3949        assert_eq!(f.font_hash, u64::MAX);
3950        assert_eq!(f.num_glyphs, u32::MAX);
3951        assert!(f.has_bytes);
3952    }
3953
3954    #[test]
3955    fn brush_new_defaults_and_extreme_radius() {
3956        let b = Brush::new(opaque_red(), 4.0);
3957        assert_eq!(b.radius, 4.0);
3958        assert_eq!(b.hardness, 0.5);
3959        assert_eq!(b.flow, 1.0);
3960        assert_eq!(b.spacing, 0.25);
3961        assert_eq!(b.color, opaque_red());
3962
3963        // Extreme radii are stored verbatim (validation happens in paint_dot).
3964        assert!(Brush::new(opaque_red(), f32::NAN).radius.is_nan());
3965        assert_eq!(Brush::new(opaque_red(), -0.0).radius, -0.0);
3966        assert_eq!(Brush::new(opaque_red(), f32::INFINITY).radius, f32::INFINITY);
3967    }
3968
3969    #[test]
3970    fn image_cache_new_is_empty_and_default_is_neutral() {
3971        let cache = ImageCache::new();
3972        assert!(cache.image_id_map.is_empty());
3973        assert!(ImageCache::default().image_id_map.is_empty());
3974    }
3975
3976    #[test]
3977    fn gl_texture_cache_empty_is_neutral() {
3978        let cache = GlTextureCache::empty();
3979        assert!(cache.solved_textures.is_empty());
3980        assert!(cache.hashes.is_empty());
3981        let d = GlTextureCache::default();
3982        assert!(d.solved_textures.is_empty());
3983        assert!(d.hashes.is_empty());
3984    }
3985
3986    #[test]
3987    fn external_image_id_new_is_monotonic() {
3988        let a = ExternalImageId::new();
3989        let b = ExternalImageId::new();
3990        assert!(b.inner > a.inner, "the counter must strictly increase");
3991        assert!(ExternalImageId::default().inner > b.inner);
3992    }
3993
3994    #[test]
3995    fn shared_raw_image_data_new_invariants() {
3996        let empty = SharedRawImageData::new(U8Vec::from_vec(Vec::new()));
3997        assert_eq!(empty.len(), 0);
3998        assert!(empty.is_empty());
3999        assert!(empty.as_ref().is_empty());
4000        assert!(empty.get_bytes().is_empty());
4001        // An empty Vec still yields a non-null (dangling but aligned) pointer.
4002        assert!(!empty.as_ptr().is_null());
4003
4004        let big = SharedRawImageData::new(vec![7u8; 100_000].into());
4005        assert_eq!(big.len(), 100_000);
4006        assert!(!big.is_empty());
4007        assert_eq!(big.as_ref().len(), big.len());
4008        assert_eq!(big.get_bytes(), big.as_ref());
4009        // len() must agree with the slice view (no stale-length bug).
4010        assert_eq!(big.into_inner().expect("sole owner").as_ref().len(), 100_000);
4011    }
4012
4013    #[test]
4014    fn app_config_create_registers_builtins_and_defaults() {
4015        let config = AppConfig::create();
4016        assert_eq!(config.log_level, AppLogLevel::Error);
4017        assert!(!config.enable_visual_panic_hook);
4018        assert!(config.enable_logging_on_panic);
4019        assert_eq!(config.termination_behavior, AppTerminationBehavior::EndProcess);
4020        assert!(config.routes.as_ref().is_empty());
4021        assert!(matches!(
4022            config.mock_css_environment,
4023            OptionCssMockEnvironment::None
4024        ));
4025        // create() dogfoods add_component_library -> exactly one "builtin" library.
4026        let libs = config.component_libraries.as_ref();
4027        assert_eq!(libs.len(), 1);
4028        assert_eq!(libs[0].name.as_str(), "builtin");
4029        assert!(!libs[0].components.as_ref().is_empty());
4030    }
4031
4032    #[test]
4033    fn app_config_add_component_library_replaces_same_name() {
4034        let register: crate::xml::RegisterComponentLibraryFnType =
4035            crate::xml::register_builtin_components;
4036        let mut config = AppConfig::create();
4037        let n_builtin = config.component_libraries.as_ref()[0].components.as_ref().len();
4038
4039        // Same name -> wholesale replacement, NOT a duplicate library.
4040        config.add_component_library(AzString::from_const_str("builtin"), register);
4041        assert_eq!(config.component_libraries.as_ref().len(), 1);
4042        assert_eq!(
4043            config.component_libraries.as_ref()[0].components.as_ref().len(),
4044            n_builtin
4045        );
4046
4047        // A different name (incl. empty / unicode) appends a new library.
4048        config.add_component_library(AzString::from_const_str(""), register);
4049        config.add_component_library(AzString::from_const_str("\u{1F600}"), register);
4050        assert_eq!(config.component_libraries.as_ref().len(), 3);
4051        assert_eq!(config.component_libraries.as_ref()[2].name.as_str(), "\u{1F600}");
4052    }
4053
4054    #[test]
4055    fn app_config_with_mock_environment_sets_the_option() {
4056        let config = AppConfig::create().with_mock_environment(CssMockEnvironment::dark_theme());
4057        match config.mock_css_environment {
4058            OptionCssMockEnvironment::Some(env) => {
4059                assert!(matches!(
4060                    env.theme,
4061                    azul_css::dynamic_selector::OptionThemeCondition::Some(
4062                        azul_css::dynamic_selector::ThemeCondition::Dark
4063                    )
4064                ));
4065            }
4066            OptionCssMockEnvironment::None => panic!("mock env must be Some"),
4067        }
4068        // Last call wins (the field is overwritten, not merged).
4069        let config = AppConfig::create()
4070            .with_mock_environment(CssMockEnvironment::linux())
4071            .with_mock_environment(CssMockEnvironment::windows());
4072        match config.mock_css_environment {
4073            OptionCssMockEnvironment::Some(env) => assert!(matches!(
4074                env.os,
4075                azul_css::dynamic_selector::OptionOsCondition::Some(
4076                    azul_css::dynamic_selector::OsCondition::Windows
4077                )
4078            )),
4079            OptionCssMockEnvironment::None => panic!("mock env must be Some"),
4080        }
4081    }
4082
4083    // =====================================================================
4084    // CssMockEnvironment
4085    // =====================================================================
4086
4087    #[test]
4088    fn css_mock_environment_presets_only_set_their_own_field() {
4089        use azul_css::dynamic_selector::{
4090            OptionOsCondition, OptionThemeCondition, OsCondition, ThemeCondition,
4091        };
4092
4093        for (mock, os) in [
4094            (CssMockEnvironment::linux(), OsCondition::Linux),
4095            (CssMockEnvironment::windows(), OsCondition::Windows),
4096            (CssMockEnvironment::macos(), OsCondition::MacOS),
4097        ] {
4098            assert!(matches!(mock.os, OptionOsCondition::Some(o) if o == os));
4099            // The other overrides stay unset (auto-detect).
4100            assert!(matches!(mock.theme, OptionThemeCondition::None));
4101            assert!(matches!(mock.viewport_width, azul_css::OptionF32::None));
4102        }
4103
4104        assert!(matches!(
4105            CssMockEnvironment::dark_theme().theme,
4106            OptionThemeCondition::Some(ThemeCondition::Dark)
4107        ));
4108        assert!(matches!(
4109            CssMockEnvironment::light_theme().theme,
4110            OptionThemeCondition::Some(ThemeCondition::Light)
4111        ));
4112        assert!(matches!(
4113            CssMockEnvironment::dark_theme().os,
4114            OptionOsCondition::None
4115        ));
4116    }
4117
4118    #[test]
4119    fn css_mock_environment_apply_to_overrides_only_set_fields() {
4120        use azul_css::dynamic_selector::{
4121            BoolCondition, DynamicSelectorContext, OptionOsCondition, OptionThemeCondition,
4122            OsCondition, ThemeCondition,
4123        };
4124
4125        // An all-None mock must leave the context byte-for-byte alone.
4126        let mut ctx = DynamicSelectorContext::default();
4127        let before_os = ctx.os;
4128        let before_lang = ctx.language.clone();
4129        let before_w = ctx.viewport_width;
4130        CssMockEnvironment::default().apply_to(&mut ctx);
4131        assert_eq!(ctx.os, before_os);
4132        assert_eq!(ctx.language.as_str(), before_lang.as_str());
4133        assert_eq!(ctx.viewport_width, before_w);
4134
4135        // A fully-populated mock overrides every field it sets - including
4136        // adversarial floats (NaN viewport) which must not panic.
4137        let mock = CssMockEnvironment {
4138            os: OptionOsCondition::Some(OsCondition::Windows),
4139            theme: OptionThemeCondition::Some(ThemeCondition::Dark),
4140            language: azul_css::OptionString::Some(AzString::from_const_str("de-DE")),
4141            viewport_width: azul_css::OptionF32::Some(f32::NAN),
4142            viewport_height: azul_css::OptionF32::Some(f32::INFINITY),
4143            prefers_reduced_motion: azul_css::OptionBool::Some(true),
4144            prefers_high_contrast: azul_css::OptionBool::Some(false),
4145            ..Default::default()
4146        };
4147        let mut ctx = DynamicSelectorContext::default();
4148        mock.apply_to(&mut ctx);
4149        assert_eq!(ctx.os, OsCondition::Windows);
4150        assert_eq!(ctx.theme, ThemeCondition::Dark);
4151        assert_eq!(ctx.language.as_str(), "de-DE");
4152        assert!(ctx.viewport_width.is_nan());
4153        assert_eq!(ctx.viewport_height, f32::INFINITY);
4154        assert_eq!(ctx.prefers_reduced_motion, BoolCondition::True);
4155        assert_eq!(ctx.prefers_high_contrast, BoolCondition::False);
4156
4157        // apply_to is idempotent.
4158        let mut ctx2 = ctx.clone();
4159        mock.apply_to(&mut ctx2);
4160        assert_eq!(ctx2.os, ctx.os);
4161        assert_eq!(ctx2.theme, ctx.theme);
4162    }
4163
4164    // =====================================================================
4165    // NUMERIC: brush_dab_coverage / normalize_u16 / premultiply_alpha / Au
4166    // =====================================================================
4167
4168    #[test]
4169    fn brush_dab_coverage_boundaries_and_monotonicity() {
4170        // Documented profile: 1.0 at the center, 0.0 at (and past) the edge.
4171        assert_eq!(brush_dab_coverage(0.0, 0.5), 1.0);
4172        assert_eq!(brush_dab_coverage(1.0, 0.5), 0.0);
4173        // Out-of-range t is clamped, not extrapolated.
4174        assert_eq!(brush_dab_coverage(-5.0, 0.5), 1.0);
4175        assert_eq!(brush_dab_coverage(2.0, 0.5), 0.0);
4176        assert_eq!(brush_dab_coverage(f32::INFINITY, 0.5), 0.0);
4177        assert_eq!(brush_dab_coverage(f32::NEG_INFINITY, 0.5), 1.0);
4178
4179        // Monotonically non-increasing in t, and always inside [0, 1].
4180        let mut prev = f32::INFINITY;
4181        for i in 0..=100 {
4182            let t = i as f32 / 100.0;
4183            let c = brush_dab_coverage(t, 0.5);
4184            assert!((0.0..=1.0).contains(&c), "coverage {c} out of range at t={t}");
4185            assert!(c <= prev + 1.0e-6, "not monotonic at t={t}");
4186            prev = c;
4187        }
4188    }
4189
4190    #[test]
4191    fn brush_dab_coverage_hardness_limits_never_divide_by_zero() {
4192        // hardness == 1.0 would make (1 - edge0) == 0; the 1e-4 floor prevents
4193        // a division by zero -> a hard (but finite) edge instead of inf/NaN.
4194        assert_eq!(brush_dab_coverage(0.5, 1.0), 1.0);
4195        assert!(brush_dab_coverage(1.0, 1.0).is_finite());
4196        assert_eq!(brush_dab_coverage(1.0, 1.0), 1.0); // exactly at edge0 -> x == 0
4197        assert_eq!(brush_dab_coverage(2.0, 1.0), 0.0);
4198
4199        // hardness is clamped, so out-of-range hardness behaves like 0.0 / 1.0.
4200        assert_eq!(brush_dab_coverage(0.5, -10.0), brush_dab_coverage(0.5, 0.0));
4201        assert_eq!(brush_dab_coverage(0.5, 10.0), brush_dab_coverage(0.5, 1.0));
4202        assert_eq!(
4203            brush_dab_coverage(0.5, f32::NEG_INFINITY),
4204            brush_dab_coverage(0.5, 0.0)
4205        );
4206        assert!(brush_dab_coverage(0.5, f32::INFINITY).is_finite());
4207    }
4208
4209    #[test]
4210    fn brush_dab_coverage_nan_propagates_without_panicking() {
4211        // NaN in -> NaN out (documented-by-behavior); crucially, no panic and no
4212        // hang. paint_dot's `a <= 0.0` check then skips NaN coverage entirely.
4213        assert!(brush_dab_coverage(f32::NAN, 0.5).is_nan());
4214        assert!(brush_dab_coverage(0.5, f32::NAN).is_nan());
4215        assert!(brush_dab_coverage(f32::NAN, f32::NAN).is_nan());
4216    }
4217
4218    #[test]
4219    fn normalize_u16_is_monotonic_and_saturating() {
4220        assert_eq!(normalize_u16(u16::MIN), 0);
4221        assert_eq!(normalize_u16(u16::MAX), u8::MAX);
4222        let mut prev = 0u8;
4223        for i in (0..=u16::MAX).step_by(97) {
4224            let v = normalize_u16(i);
4225            assert!(v >= prev, "normalize_u16 must be monotonic ({i} -> {v})");
4226            prev = v;
4227        }
4228    }
4229
4230    #[test]
4231    fn premultiply_alpha_ignores_non_4_byte_slices() {
4232        // Documented: only a single 4-byte pixel is touched.
4233        for len in [0usize, 1, 2, 3, 5, 8] {
4234            let mut buf = vec![200u8; len];
4235            let before = buf.clone();
4236            premultiply_alpha(&mut buf);
4237            assert_eq!(buf, before, "len {len} must be left untouched");
4238        }
4239    }
4240
4241    #[test]
4242    fn premultiply_alpha_boundary_values_never_overflow() {
4243        // a == 255 -> unchanged (rounding must not drift).
4244        let mut opaque = [255u8, 128, 0, 255];
4245        premultiply_alpha(&mut opaque);
4246        assert_eq!(opaque, [255, 128, 0, 255]);
4247
4248        // a == 0 -> fully transparent -> RGB zeroed, alpha untouched.
4249        let mut transparent = [255u8, 255, 255, 0];
4250        premultiply_alpha(&mut transparent);
4251        assert_eq!(transparent, [0, 0, 0, 0]);
4252
4253        // a == 128 -> ~half, computed with the +128/255 rounding, never > 255.
4254        let mut half = [255u8, 128, 0, 128];
4255        premultiply_alpha(&mut half);
4256        assert_eq!(half, [128, 64, 0, 128]);
4257
4258        // The u32 intermediate must not truncate at the maximum product.
4259        let mut max = [255u8, 255, 255, 255];
4260        premultiply_alpha(&mut max);
4261        assert_eq!(max, [255, 255, 255, 255]);
4262    }
4263
4264    #[test]
4265    fn au_from_px_saturates_at_limits_and_nan() {
4266        assert_eq!(Au::from_px(0.0).0, 0);
4267        assert_eq!(Au::from_px(-0.0).0, 0);
4268        assert_eq!(Au::from_px(1.0).0, AU_PER_PX);
4269        assert_eq!(Au::from_px(-1.0).0, -AU_PER_PX);
4270        // NaN -> 0 (saturating `as` cast), NOT a panic and NOT garbage.
4271        assert_eq!(Au::from_px(f32::NAN).0, 0);
4272        // Infinities / f32 extremes clamp into [MIN_AU, MAX_AU].
4273        assert_eq!(Au::from_px(f32::INFINITY).0, MAX_AU);
4274        assert_eq!(Au::from_px(f32::NEG_INFINITY).0, MIN_AU);
4275        assert_eq!(Au::from_px(f32::MAX).0, MAX_AU);
4276        assert_eq!(Au::from_px(f32::MIN).0, MIN_AU);
4277        // Anything in range stays in range.
4278        for px in [-1.0e9_f32, -1.0, 0.5, 16.0, 1.0e9] {
4279            let au = Au::from_px(px).0;
4280            assert!((MIN_AU..=MAX_AU).contains(&au), "{px} -> {au} escaped the clamp");
4281        }
4282    }
4283
4284    #[test]
4285    fn au_px_round_trip_is_stable() {
4286        // px -> Au -> px must round-trip within one app-unit (1/60 px).
4287        for px in [0.0_f32, 0.5, 1.0, 12.0, 16.0, 72.5, -3.25, 1000.0] {
4288            let back = Au::from_px(px).into_px();
4289            assert!(
4290                (back - px).abs() <= 1.0 / AU_PER_PX as f32,
4291                "{px} round-tripped to {back}"
4292            );
4293        }
4294        // Exact for whole pixels.
4295        assert_eq!(Au::from_px(16.0).into_px(), 16.0);
4296        // Extremes stay finite.
4297        assert!(Au(MAX_AU).into_px().is_finite());
4298        assert!(Au(MIN_AU).into_px().is_finite());
4299        assert!(Au(i32::MIN).into_px().is_finite());
4300        assert!(Au(i32::MAX).into_px().is_finite());
4301    }
4302
4303    #[test]
4304    fn font_size_to_au_zero_negative_and_typical() {
4305        use azul_css::props::basic::PixelValue;
4306        let au = |px: isize| {
4307            font_size_to_au(StyleFontSize {
4308                inner: PixelValue::const_px(px),
4309            })
4310            .0
4311        };
4312        assert_eq!(au(0), 0);
4313        assert_eq!(au(16), 16 * AU_PER_PX);
4314        assert_eq!(au(-10), -10 * AU_PER_PX);
4315        // Large-but-representable sizes stay inside the clamp.
4316        assert!((MIN_AU..=MAX_AU).contains(&au(1_000_000)));
4317        assert!((MIN_AU..=MAX_AU).contains(&au(-1_000_000)));
4318    }
4319
4320    // =====================================================================
4321    // NUMERIC: Epoch
4322    // =====================================================================
4323
4324    #[test]
4325    fn epoch_new_from_and_into_u32() {
4326        assert_eq!(Epoch::new().into_u32(), 0);
4327        assert_eq!(Epoch::default().into_u32(), 0);
4328        assert_eq!(Epoch::from(0).into_u32(), 0);
4329        assert_eq!(Epoch::from(1).into_u32(), 1);
4330        assert_eq!(Epoch::from(u32::MAX).into_u32(), u32::MAX);
4331        assert_eq!(Epoch::from(u32::MAX - 1).into_u32(), u32::MAX - 1);
4332    }
4333
4334    #[test]
4335    fn epoch_increment_wraps_at_max_minus_one_and_never_reaches_max() {
4336        let mut e = Epoch::new();
4337        e.increment();
4338        assert_eq!(e.into_u32(), 1);
4339
4340        // u32::MAX is reserved as "invalid", so MAX-1 wraps back to 0.
4341        let mut e = Epoch::from(u32::MAX - 1);
4342        e.increment();
4343        assert_eq!(e.into_u32(), 0, "MAX-1 must wrap to 0, never to u32::MAX");
4344
4345        // An epoch that somehow starts AT u32::MAX saturates (fixpoint) instead
4346        // of wrapping or overflow-panicking - deterministic, no UB.
4347        let mut e = Epoch::from(u32::MAX);
4348        e.increment();
4349        assert_eq!(e.into_u32(), u32::MAX);
4350
4351        // A long run of increments never yields the invalid u32::MAX.
4352        let mut e = Epoch::from(u32::MAX - 3);
4353        for _ in 0..8 {
4354            e.increment();
4355            assert_ne!(e.into_u32(), u32::MAX);
4356        }
4357    }
4358
4359    #[test]
4360    fn epoch_display_is_non_empty_for_edge_values() {
4361        assert_eq!(alloc::format!("{}", Epoch::new()), "0");
4362        assert_eq!(alloc::format!("{}", Epoch::from(42)), "42");
4363        assert_eq!(
4364            alloc::format!("{}", Epoch::from(u32::MAX)),
4365            alloc::format!("{}", u32::MAX)
4366        );
4367        assert!(!alloc::format!("{:?}", Epoch::default()).is_empty());
4368    }
4369
4370    #[test]
4371    fn id_namespace_display_and_debug_are_well_formed() {
4372        assert_eq!(alloc::format!("{}", IdNamespace(0)), "IdNamespace(0)");
4373        assert_eq!(
4374            alloc::format!("{}", IdNamespace(u32::MAX)),
4375            alloc::format!("IdNamespace({})", u32::MAX)
4376        );
4377        // Debug delegates to Display (must not recurse / be empty).
4378        assert_eq!(
4379            alloc::format!("{:?}", IdNamespace(7)),
4380            alloc::format!("{}", IdNamespace(7))
4381        );
4382    }
4383
4384    // =====================================================================
4385    // KEYS: uniqueness / namespace preservation / hash->key derivation
4386    // =====================================================================
4387
4388    #[test]
4389    fn unique_keys_are_strictly_increasing_and_keep_their_namespace() {
4390        let ns = IdNamespace(u32::MAX);
4391
4392        let a = ImageKey::unique(ns);
4393        let b = ImageKey::unique(ns);
4394        assert_eq!(a.namespace, ns);
4395        assert!(b.key > a.key, "ImageKey counter must strictly increase");
4396        // The counter starts at 1 so a live key can never collide with DUMMY.
4397        assert_eq!(ImageKey::DUMMY.key, 0);
4398        assert_ne!(a, ImageKey::DUMMY);
4399
4400        let a = FontKey::unique(ns);
4401        let b = FontKey::unique(ns);
4402        assert_eq!(a.namespace, ns);
4403        assert!(b.key > a.key);
4404
4405        let a = FontInstanceKey::unique(IdNamespace(0));
4406        let b = FontInstanceKey::unique(IdNamespace(0));
4407        assert_eq!(a.namespace, IdNamespace(0));
4408        assert!(b.key > a.key);
4409    }
4410
4411    #[test]
4412    fn image_ref_id_counter_is_monotonic_and_never_zero() {
4413        // id == 0 is reserved to flag an un-initialised handle.
4414        let a = next_image_ref_id();
4415        let b = next_image_ref_id();
4416        assert!(a > 0 && b > a);
4417    }
4418
4419    #[test]
4420    fn image_ref_hash_conversions_are_lossless_and_agree() {
4421        let img = ImageRef::null_image(1, 1, RawImageFormat::RGBA8, Vec::new());
4422        let hash = img.get_hash();
4423        assert_eq!(hash, image_ref_get_hash(&img));
4424
4425        let key = image_ref_hash_to_image_key(hash, IdNamespace(9));
4426        assert_eq!(key.namespace, IdNamespace(9));
4427        assert_eq!(key.key, hash.inner, "the u64 id must survive verbatim");
4428
4429        let ext = image_ref_hash_to_external_image_id(hash);
4430        assert_eq!(ext.inner, hash.inner);
4431
4432        // Both derivations agree for boundary hashes too.
4433        for inner in [0u64, 1, u64::MAX, u64::MAX - 1] {
4434            let h = ImageRefHash { inner };
4435            assert_eq!(image_ref_hash_to_image_key(h, IdNamespace(0)).key, inner);
4436            assert_eq!(image_ref_hash_to_external_image_id(h).inner, inner);
4437        }
4438    }
4439
4440    #[test]
4441    fn texture_external_image_id_is_deterministic_and_collision_free() {
4442        let id = |d: usize, n: usize| texture_external_image_id(DomId { inner: d }, NodeId::new(n));
4443
4444        // Same input -> same id (cached display lists depend on this).
4445        assert_eq!(id(3, 7), id(3, 7));
4446        assert_eq!(id(0, 0).inner, 0);
4447        // The dom goes in the high 32 bits, the node in the low 32.
4448        assert_eq!(id(1, 2).inner, (1u64 << 32) | 2);
4449        // (0,1) and (1,0) must not collide.
4450        assert_ne!(id(0, 1), id(1, 0));
4451        // Boundary node index inside the 32-bit range.
4452        assert_eq!(id(0, u32::MAX as usize).inner, u64::from(u32::MAX));
4453        assert_eq!(
4454            id(u32::MAX as usize, 0).inner,
4455            u64::from(u32::MAX) << 32
4456        );
4457    }
4458
4459    // =====================================================================
4460    // GETTERS / PREDICATES: RawImageData
4461    // =====================================================================
4462
4463    #[test]
4464    fn raw_image_data_typed_getters_only_match_their_own_variant() {
4465        let u8v = RawImageData::U8(vec![1u8, 2].into());
4466        let u16v = RawImageData::U16(vec![1u16, 2].into());
4467        let f32v = RawImageData::F32(vec![1.0f32, 2.0].into());
4468
4469        assert_eq!(u8v.get_u8_vec_ref().map(|v| v.len()), Some(2));
4470        assert!(u8v.get_u16_vec_ref().is_none());
4471        assert!(u8v.get_f32_vec_ref().is_none());
4472
4473        assert!(u16v.get_u8_vec_ref().is_none());
4474        assert_eq!(u16v.get_u16_vec_ref().map(|v| v.len()), Some(2));
4475        assert!(u16v.get_f32_vec_ref().is_none());
4476
4477        assert!(f32v.get_u8_vec_ref().is_none());
4478        assert!(f32v.get_u16_vec_ref().is_none());
4479        assert_eq!(f32v.get_f32_vec_ref().map(|v| v.len()), Some(2));
4480
4481        // Empty payloads are Some(empty), not None.
4482        let empty = RawImageData::U8(U8Vec::from_vec(Vec::new()));
4483        assert_eq!(empty.get_u8_vec_ref().map(|v| v.len()), Some(0));
4484
4485        // by-value variants agree with the by-ref ones
4486        assert!(RawImageData::U8(vec![9u8].into()).get_u8_vec().is_some());
4487        assert!(RawImageData::U16(vec![9u16].into()).get_u8_vec().is_none());
4488        assert!(RawImageData::U16(vec![9u16].into()).get_u16_vec().is_some());
4489        assert!(RawImageData::F32(vec![9.0f32].into()).get_u16_vec().is_none());
4490    }
4491
4492    // =====================================================================
4493    // NUMERIC / ROUND-TRIP: RawImage::load_* format conversions
4494    // =====================================================================
4495
4496    #[test]
4497    fn load_fns_reject_wrong_payload_type() {
4498        // Every loader demands a specific RawImageData variant; a mismatch is
4499        // None (never a panic / never garbage pixels).
4500        let u16_1px = || RawImageData::U16(vec![0u16; 4].into());
4501        let f32_1px = || RawImageData::F32(vec![0.0f32; 4].into());
4502        let u8_1px = || RawImageData::U8(vec![0u8; 4].into());
4503
4504        assert!(RawImage::load_r8(u16_1px(), 4).is_none());
4505        assert!(RawImage::load_rg8(f32_1px(), 2, true).is_none());
4506        assert!(RawImage::load_rgb8(u16_1px(), 1).is_none());
4507        assert!(RawImage::load_rgba8(f32_1px(), 1, true).is_none());
4508        assert!(RawImage::load_r16(u8_1px(), 4).is_none());
4509        assert!(RawImage::load_rg16(f32_1px(), 2).is_none());
4510        assert!(RawImage::load_rgb16(u8_1px(), 1).is_none());
4511        assert!(RawImage::load_rgba16(u8_1px(), 1, true).is_none());
4512        assert!(RawImage::load_bgr8(u16_1px(), 1).is_none());
4513        assert!(RawImage::load_bgra8(u16_1px(), 1, true).is_none());
4514        assert!(RawImage::load_rgbf32(u8_1px(), 1).is_none());
4515        assert!(RawImage::load_rgbaf32(u16_1px(), 1, true).is_none());
4516    }
4517
4518    #[test]
4519    fn load_fns_reject_every_wrong_length() {
4520        // One byte too few and one too many must BOTH be rejected for each format.
4521        assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 3].into()), 4).is_none());
4522        assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 5].into()), 4).is_none());
4523        assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 3].into()), 2, true).is_none());
4524        assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 5].into()), 2, true).is_none());
4525        assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
4526        assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 7].into()), 2).is_none());
4527        assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 7].into()), 2, true).is_none());
4528        assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 9].into()), 2, false).is_none());
4529        assert!(RawImage::load_r16(RawImageData::U16(vec![0u16; 3].into()), 4).is_none());
4530        assert!(RawImage::load_rg16(RawImageData::U16(vec![0u16; 3].into()), 2).is_none());
4531        assert!(RawImage::load_rgb16(RawImageData::U16(vec![0u16; 5].into()), 2).is_none());
4532        assert!(RawImage::load_rgba16(RawImageData::U16(vec![0u16; 7].into()), 2, true).is_none());
4533        assert!(RawImage::load_bgr8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
4534        assert!(RawImage::load_bgra8(RawImageData::U8(vec![0u8; 7].into()), 2, false).is_none());
4535        assert!(RawImage::load_rgbf32(RawImageData::F32(vec![0.0f32; 5].into()), 2).is_none());
4536        assert!(
4537            RawImage::load_rgbaf32(RawImageData::F32(vec![0.0f32; 7].into()), 2, true).is_none()
4538        );
4539    }
4540
4541    #[test]
4542    fn load_fns_accept_zero_pixels() {
4543        // expected_len == 0 with an empty buffer: Some(empty), no div-by-zero.
4544        let empty_u8 = || RawImageData::U8(U8Vec::from_vec(Vec::new()));
4545        let empty_u16 = || RawImageData::U16(U16Vec::from_vec(Vec::new()));
4546        let empty_f32 = || RawImageData::F32(F32Vec::from_vec(Vec::new()));
4547
4548        assert_eq!(RawImage::load_r8(empty_u8(), 0).map(|(b, o)| (b.len(), o)), Some((0, false)));
4549        assert_eq!(RawImage::load_rg8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
4550        assert_eq!(RawImage::load_rgb8(empty_u8(), 0).map(|(b, _)| b.len()), Some(0));
4551        assert_eq!(RawImage::load_rgba8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
4552        assert_eq!(RawImage::load_r16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
4553        assert_eq!(RawImage::load_rg16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
4554        assert_eq!(RawImage::load_rgb16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
4555        assert_eq!(RawImage::load_rgba16(empty_u16(), 0, true).map(|(b, _)| b.len()), Some(0));
4556        assert_eq!(RawImage::load_bgr8(empty_u8(), 0).map(|(b, _)| b.len()), Some(0));
4557        assert_eq!(RawImage::load_bgra8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
4558        assert_eq!(RawImage::load_rgbf32(empty_f32(), 0).map(|(b, _)| b.len()), Some(0));
4559        assert_eq!(RawImage::load_rgbaf32(empty_f32(), 0, true).map(|(b, _)| b.len()), Some(0));
4560    }
4561
4562    #[test]
4563    fn load_r8_passes_data_through_and_is_never_opaque() {
4564        // R8 must stay R8 (image masks depend on the single channel surviving).
4565        let (bytes, is_opaque) =
4566            RawImage::load_r8(RawImageData::U8(vec![0u8, 128, 255, 1].into()), 4)
4567                .expect("exact length");
4568        assert_eq!(bytes.as_ref(), &[0, 128, 255, 1]);
4569        assert!(!is_opaque, "R8 is documented as never opaque");
4570    }
4571
4572    #[test]
4573    fn load_rgb8_and_bgr8_swizzle_to_bgra_opaque() {
4574        // RGB8 -> BGRA8: channel order flips, alpha forced to 0xFF, always opaque.
4575        let (bytes, is_opaque) =
4576            RawImage::load_rgb8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
4577        assert_eq!(bytes.as_ref(), &[3, 2, 1, 255]);
4578        assert!(is_opaque);
4579
4580        // BGR8 -> BGRA8: order preserved, alpha appended.
4581        let (bytes, is_opaque) =
4582            RawImage::load_bgr8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
4583        assert_eq!(bytes.as_ref(), &[1, 2, 3, 255]);
4584        assert!(is_opaque);
4585    }
4586
4587    #[test]
4588    fn load_rgba8_swizzles_and_detects_transparency() {
4589        // Premultiplied: RGBA -> BGRA swizzle only.
4590        let (bytes, is_opaque) =
4591            RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 255].into()), 1, true)
4592                .expect("1 px");
4593        assert_eq!(bytes.as_ref(), &[30, 20, 10, 255]);
4594        assert!(is_opaque);
4595
4596        // A single non-255 alpha flips is_opaque to false.
4597        let (_, is_opaque) =
4598            RawImage::load_rgba8(RawImageData::U8(vec![0u8, 0, 0, 254].into()), 1, true)
4599                .expect("1 px");
4600        assert!(!is_opaque);
4601
4602        // Non-premultiplied: swizzle THEN premultiply by alpha.
4603        let (bytes, is_opaque) =
4604            RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 128].into()), 1, false)
4605                .expect("1 px");
4606        assert_eq!(bytes.as_ref(), &[15, 10, 5, 128]);
4607        assert!(!is_opaque);
4608
4609        // alpha == 0 must zero the colour (no leftover colour fringe).
4610        let (bytes, _) =
4611            RawImage::load_rgba8(RawImageData::U8(vec![255u8, 255, 255, 0].into()), 1, false)
4612                .expect("1 px");
4613        assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
4614    }
4615
4616    #[test]
4617    fn load_rg8_expands_grey_to_bgra() {
4618        // Greyscale + alpha -> BGRA with the grey replicated across B/G/R.
4619        let (bytes, is_opaque) =
4620            RawImage::load_rg8(RawImageData::U8(vec![100u8, 255].into()), 1, true).expect("1 px");
4621        assert_eq!(bytes.as_ref(), &[100, 100, 100, 255]);
4622        assert!(is_opaque);
4623
4624        let (bytes, is_opaque) =
4625            RawImage::load_rg8(RawImageData::U8(vec![100u8, 128].into()), 1, false).expect("1 px");
4626        assert_eq!(bytes.as_ref(), &[50, 50, 50, 128]);
4627        assert!(!is_opaque);
4628    }
4629
4630    #[test]
4631    fn load_16_bit_formats_normalize_to_8_bit() {
4632        // u16::MAX -> 255, 0 -> 0 (no wrap-around / no div-by-zero).
4633        let (bytes, is_opaque) =
4634            RawImage::load_r16(RawImageData::U16(vec![u16::MAX].into()), 1).expect("1 px");
4635        assert_eq!(bytes.as_ref(), &[255, 255, 255, 255]);
4636        assert!(is_opaque);
4637
4638        let (bytes, is_opaque) =
4639            RawImage::load_rg16(RawImageData::U16(vec![0u16, u16::MAX].into()), 1).expect("1 px");
4640        assert_eq!(bytes.as_ref(), &[0, 0, 0, 255]);
4641        assert!(is_opaque);
4642
4643        // RGB16 -> BGRA8 swizzle.
4644        let (bytes, _) = RawImage::load_rgb16(
4645            RawImageData::U16(vec![u16::MAX, 0, 0].into()),
4646            1,
4647        )
4648        .expect("1 px");
4649        assert_eq!(bytes.as_ref(), &[0, 0, 255, 255]);
4650
4651        // RGBA16 with a zero alpha -> not opaque; premultiply zeroes the colour.
4652        let (bytes, is_opaque) = RawImage::load_rgba16(
4653            RawImageData::U16(vec![u16::MAX, u16::MAX, u16::MAX, 0].into()),
4654            1,
4655            false,
4656        )
4657        .expect("1 px");
4658        assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
4659        assert!(!is_opaque);
4660    }
4661
4662    #[test]
4663    fn load_f32_formats_saturate_on_out_of_range_nan_and_inf() {
4664        // The f32 -> u8 cast is saturating: >1.0 -> 255, <0.0 -> 0, NaN -> 0.
4665        // (This is the whole "HDR pixel with a garbage float" attack surface.)
4666        let (bytes, is_opaque) = RawImage::load_rgbf32(
4667            RawImageData::F32(vec![2.0f32, -1.0, f32::NAN].into()),
4668            1,
4669        )
4670        .expect("1 px");
4671        assert_eq!(bytes.as_ref(), &[0, 0, 255, 255], "b=NaN->0, g=-1->0, r=2.0->255");
4672        assert!(is_opaque);
4673
4674        let (bytes, is_opaque) = RawImage::load_rgbaf32(
4675            RawImageData::F32(vec![f32::INFINITY, f32::NEG_INFINITY, 0.5, 1.0].into()),
4676            1,
4677            true,
4678        )
4679        .expect("1 px");
4680        assert_eq!(bytes.as_ref(), &[127, 0, 255, 255]);
4681        assert!(is_opaque);
4682
4683        // NaN alpha -> 0 -> not opaque (fails safe, does not claim opacity).
4684        let (_, is_opaque) = RawImage::load_rgbaf32(
4685            RawImageData::F32(vec![1.0f32, 1.0, 1.0, f32::NAN].into()),
4686            1,
4687            true,
4688        )
4689        .expect("1 px");
4690        assert!(!is_opaque);
4691    }
4692
4693    // =====================================================================
4694    // ROUND-TRIP: RawImage <-> ImageRef
4695    // =====================================================================
4696
4697    #[test]
4698    fn raw_image_null_image_encodes_to_an_empty_bgra8_descriptor() {
4699        let null = RawImage::null_image();
4700        assert_eq!(null.width, 0);
4701        assert_eq!(null.height, 0);
4702        assert_eq!(null.data_format, RawImageFormat::BGRA8);
4703        assert!(null.premultiplied_alpha);
4704
4705        let (data, descriptor) = null
4706            .into_loaded_image_source()
4707            .expect("a 0x0 image is still a valid (empty) source");
4708        assert_eq!(descriptor.width, 0);
4709        assert_eq!(descriptor.height, 0);
4710        assert_eq!(descriptor.format, RawImageFormat::BGRA8);
4711        assert_eq!(descriptor.offset, 0);
4712        match data {
4713            ImageData::Raw(bytes) => assert!(bytes.is_empty()),
4714            ImageData::External(_) => panic!("a RawImage must never encode to External"),
4715        }
4716    }
4717
4718    #[test]
4719    fn raw_image_allocate_mask_zero_and_negative_sizes() {
4720        let mask = RawImage::allocate_mask(LayoutSize::zero());
4721        assert_eq!(mask.data_format, RawImageFormat::R8);
4722        assert_eq!(mask.width, 0);
4723        assert_eq!(mask.height, 0);
4724        assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
4725
4726        let mask = RawImage::allocate_mask(LayoutSize::new(4, 4));
4727        assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(16));
4728        assert!(mask
4729            .pixels
4730            .get_u8_vec_ref()
4731            .expect("u8")
4732            .as_ref()
4733            .iter()
4734            .all(|b| *b == 0));
4735
4736        // Negative sizes: the BUFFER is clamped to 0 (no huge alloc, no panic),
4737        // but the width/height FIELDS keep the wrapped `as usize` value, so the
4738        // returned RawImage is internally inconsistent. Callers must not feed a
4739        // negative LayoutSize in. (Buffer-side safety is what matters here.)
4740        let mask = RawImage::allocate_mask(LayoutSize::new(-4, 4));
4741        assert_eq!(
4742            mask.pixels.get_u8_vec_ref().map(|v| v.len()),
4743            Some(0),
4744            "a negative extent must never allocate"
4745        );
4746        assert!(mask.width > 1_000_000, "negative width wraps via `as usize`");
4747    }
4748
4749    #[test]
4750    fn raw_image_mask_round_trips_as_r8() {
4751        // A mask must stay single-channel R8 through the encoder (clip masks
4752        // break if it silently becomes BGRA8).
4753        let mask = RawImage::allocate_mask(LayoutSize::new(2, 2));
4754        let (data, descriptor) = mask.into_loaded_image_source().expect("consistent mask");
4755        assert_eq!(descriptor.format, RawImageFormat::R8);
4756        assert_eq!((descriptor.width, descriptor.height), (2, 2));
4757        assert!(!descriptor.flags.is_opaque, "R8 is never opaque");
4758        match data {
4759            ImageData::Raw(bytes) => assert_eq!(bytes.len(), 4),
4760            ImageData::External(_) => panic!("expected raw bytes"),
4761        }
4762    }
4763
4764    #[test]
4765    fn raw_image_rgba8_encode_decode_round_trip() {
4766        // encode: RGBA8 -> BGRA8 bytes; decode: ImageRef::get_rawimage gives the
4767        // encoded (BGRA8) pixels back verbatim.
4768        let raw = RawImage {
4769            pixels: RawImageData::U8(vec![10u8, 20, 30, 255].into()),
4770            width: 1,
4771            height: 1,
4772            premultiplied_alpha: true,
4773            data_format: RawImageFormat::RGBA8,
4774            tag: Vec::new().into(),
4775        };
4776        let img = ImageRef::new_rawimage(raw).expect("1x1 RGBA8 with 4 bytes is valid");
4777
4778        assert!(img.is_raw_image());
4779        assert!(!img.is_null_image());
4780        assert!(!img.is_gl_texture());
4781        assert!(!img.is_callback());
4782        assert_eq!(img.get_size(), LogicalSize::new(1.0, 1.0));
4783        assert_eq!(img.get_bytes(), Some(&[30u8, 20, 10, 255][..]));
4784        assert!(!img.get_bytes_ptr().is_null());
4785
4786        let decoded = img.get_rawimage().expect("raw image round-trips");
4787        assert_eq!(decoded.width, 1);
4788        assert_eq!(decoded.height, 1);
4789        assert_eq!(decoded.data_format, RawImageFormat::BGRA8);
4790        assert!(decoded.premultiplied_alpha);
4791        assert_eq!(
4792            decoded.pixels.get_u8_vec_ref().map(|v| v.as_ref().to_vec()),
4793            Some(vec![30, 20, 10, 255])
4794        );
4795    }
4796
4797    #[test]
4798    fn image_ref_new_rawimage_rejects_dimension_mismatch() {
4799        // 2x2 RGBA8 needs 16 bytes; 4 bytes must be rejected (None, not a crash).
4800        let too_small = RawImage {
4801            pixels: RawImageData::U8(vec![0u8; 4].into()),
4802            width: 2,
4803            height: 2,
4804            premultiplied_alpha: true,
4805            data_format: RawImageFormat::RGBA8,
4806            tag: Vec::new().into(),
4807        };
4808        assert!(ImageRef::new_rawimage(too_small).is_none());
4809
4810        // Too MANY bytes is equally invalid.
4811        let too_big = RawImage {
4812            pixels: RawImageData::U8(vec![0u8; 64].into()),
4813            width: 2,
4814            height: 2,
4815            premultiplied_alpha: true,
4816            data_format: RawImageFormat::RGBA8,
4817            tag: Vec::new().into(),
4818        };
4819        assert!(ImageRef::new_rawimage(too_big).is_none());
4820
4821        // Right byte count, wrong payload type -> None.
4822        let wrong_type = RawImage {
4823            pixels: RawImageData::U16(vec![0u16; 16].into()),
4824            width: 2,
4825            height: 2,
4826            premultiplied_alpha: true,
4827            data_format: RawImageFormat::RGBA8,
4828            tag: Vec::new().into(),
4829        };
4830        assert!(ImageRef::new_rawimage(wrong_type).is_none());
4831    }
4832
4833    // =====================================================================
4834    // GETTERS / PREDICATES: ImageRef
4835    // =====================================================================
4836
4837    #[test]
4838    fn image_ref_null_image_predicates_and_accessors() {
4839        let img = ImageRef::null_image(0, 0, RawImageFormat::BGRA8, Vec::new());
4840        assert!(img.is_null_image());
4841        assert!(!img.is_raw_image());
4842        assert!(!img.is_gl_texture());
4843        assert!(!img.is_callback());
4844        assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
4845        assert!(img.get_bytes().is_none());
4846        assert!(img.get_rawimage().is_none());
4847        assert!(img.get_bytes_ptr().is_null());
4848        assert!(img.get_image_callback().is_none());
4849        assert!(matches!(img.get_data(), DecodedImage::NullImage { .. }));
4850    }
4851
4852    #[test]
4853    fn image_ref_null_image_at_usize_max_reports_a_finite_size() {
4854        // usize::MAX as f32 must not produce NaN/inf (it saturates to ~1.8e19).
4855        let img = ImageRef::null_image(usize::MAX, usize::MAX, RawImageFormat::R8, Vec::new());
4856        let size = img.get_size();
4857        assert!(size.width.is_finite() && size.height.is_finite());
4858        assert!(size.width > 0.0 && size.height > 0.0);
4859        assert!(img.is_null_image());
4860
4861        // A large tag survives verbatim.
4862        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, vec![9u8; 10_000]);
4863        match img.get_data() {
4864            DecodedImage::NullImage { tag, .. } => assert_eq!(tag.len(), 10_000),
4865            _ => panic!("expected NullImage"),
4866        }
4867    }
4868
4869    #[test]
4870    fn image_ref_hash_identity_rules() {
4871        let a = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
4872        let b = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
4873        // Two structurally identical images are still DIFFERENT images.
4874        assert_ne!(a.get_hash(), b.get_hash());
4875        assert_ne!(a, b);
4876
4877        // A shallow clone is the SAME image.
4878        let a2 = a.clone();
4879        assert_eq!(a.get_hash(), a2.get_hash());
4880        assert_eq!(a, a2);
4881
4882        // A deep copy is a NEW image with a fresh identity.
4883        let deep = a.deep_copy();
4884        assert_ne!(a.get_hash(), deep.get_hash());
4885        assert!(deep.is_null_image());
4886        assert_eq!(deep.get_size(), a.get_size());
4887    }
4888
4889    #[test]
4890    fn image_ref_callback_accessors() {
4891        // CoreRenderImageCallbackType is a usize placeholder, so 0 is a valid
4892        // (if inert) callback token.
4893        let mut img = ImageRef::callback(0usize, RefAny::new(123u32));
4894        assert!(img.is_callback());
4895        assert!(!img.is_null_image());
4896        assert!(!img.is_raw_image());
4897        // Documented: a Callback reports a (0, 0) size.
4898        assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
4899        assert!(img.get_bytes().is_none());
4900        assert!(img.get_bytes_ptr().is_null());
4901        assert!(img.get_rawimage().is_none());
4902
4903        // Sole owner -> the callback is reachable (shared / mutable).
4904        assert!(img.get_image_callback().is_some());
4905        assert!(img.get_image_callback_mut().is_some());
4906
4907        // While a second handle is alive, aliasing &mut would be unsound, so
4908        // BOTH accessors must refuse.
4909        let clone = img.clone();
4910        assert!(img.get_image_callback().is_none());
4911        assert!(img.get_image_callback_mut().is_none());
4912        drop(clone);
4913        assert!(img.get_image_callback().is_some());
4914    }
4915
4916    #[test]
4917    fn image_ref_deep_copy_of_a_callback_keeps_it_a_callback() {
4918        let img = ImageRef::callback(0usize, RefAny::new(1u8));
4919        let deep = img.deep_copy();
4920        assert!(deep.is_callback());
4921        assert_ne!(img.get_hash(), deep.get_hash());
4922    }
4923
4924    #[test]
4925    fn image_ref_into_inner_only_when_sole_owner() {
4926        let img = ImageRef::null_image(2, 2, RawImageFormat::RGBA8, vec![1, 2, 3]);
4927        let clone = img.clone();
4928        assert!(clone.into_inner().is_none(), "shared -> must refuse");
4929
4930        let inner = img.into_inner().expect("sole owner -> takes ownership");
4931        match inner {
4932            DecodedImage::NullImage {
4933                width,
4934                height,
4935                format,
4936                tag,
4937            } => {
4938                assert_eq!((width, height), (2, 2));
4939                assert_eq!(format, RawImageFormat::RGBA8);
4940                assert_eq!(tag, vec![1, 2, 3]);
4941            }
4942            _ => panic!("expected NullImage"),
4943        }
4944    }
4945
4946    // =====================================================================
4947    // ImageCache
4948    // =====================================================================
4949
4950    #[test]
4951    fn image_cache_add_get_delete_round_trip() {
4952        let mut cache = ImageCache::new();
4953        let key = AzString::from_const_str("my_image");
4954        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
4955        let hash = img.get_hash();
4956
4957        assert!(cache.get_css_image_id(&key).is_none());
4958        cache.add_css_image_id(key.clone(), img);
4959        assert_eq!(cache.get_css_image_id(&key).map(ImageRef::get_hash), Some(hash));
4960
4961        // Re-inserting the same id replaces (does not duplicate).
4962        let img2 = ImageRef::null_image(2, 2, RawImageFormat::R8, Vec::new());
4963        let hash2 = img2.get_hash();
4964        cache.add_css_image_id(key.clone(), img2);
4965        assert_eq!(cache.image_id_map.len(), 1);
4966        assert_eq!(cache.get_css_image_id(&key).map(ImageRef::get_hash), Some(hash2));
4967
4968        cache.delete_css_image_id(&key);
4969        assert!(cache.get_css_image_id(&key).is_none());
4970        assert!(cache.image_id_map.is_empty());
4971        // Deleting a missing id is a no-op, not a panic.
4972        cache.delete_css_image_id(&key);
4973        cache.delete_css_image_id(&AzString::from_const_str("never-existed"));
4974    }
4975
4976    #[test]
4977    fn image_cache_handles_empty_and_unicode_keys() {
4978        let mut cache = ImageCache::new();
4979        let empty = AzString::from_const_str("");
4980        let unicode = AzString::from(String::from("\u{1F600}\u{0301}"));
4981        let long = AzString::from("k".repeat(100_000));
4982
4983        cache.add_css_image_id(
4984            empty.clone(),
4985            ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
4986        );
4987        cache.add_css_image_id(
4988            unicode.clone(),
4989            ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
4990        );
4991        cache.add_css_image_id(
4992            long.clone(),
4993            ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
4994        );
4995
4996        assert_eq!(cache.image_id_map.len(), 3);
4997        assert!(cache.get_css_image_id(&empty).is_some());
4998        assert!(cache.get_css_image_id(&unicode).is_some());
4999        assert!(cache.get_css_image_id(&long).is_some());
5000        // Distinct keys must not alias each other.
5001        assert!(cache
5002            .get_css_image_id(&AzString::from_const_str("\u{1F600}"))
5003            .is_none());
5004    }
5005
5006    // =====================================================================
5007    // RendererResources
5008    // =====================================================================
5009
5010    #[test]
5011    fn renderer_resources_lookups_on_an_empty_registry_are_none() {
5012        let rr = RendererResources::default();
5013        let ns = IdNamespace(1);
5014        assert!(rr
5015            .get_renderable_font_data(&FontInstanceKey::unique(ns))
5016            .is_none());
5017        let families = StyleFontFamiliesHash::new(&[]);
5018        assert!(rr
5019            .get_font_instance_key(&families, Au(0), DpiScaleFactor::new(1.0))
5020            .is_none());
5021        assert!(rr
5022            .get_font_instance_key(&families, Au(MAX_AU), DpiScaleFactor::new(f32::NAN))
5023            .is_none());
5024        assert!(rr.get_image(&ImageRefHash { inner: 0 }).is_none());
5025        assert!(rr.get_font_key(&StyleFontFamilyHash::new(&StyleFontFamily::System(
5026            AzString::from_const_str("Arial")
5027        ))).is_none());
5028    }
5029
5030    #[test]
5031    fn renderer_resources_gc_helper_is_a_noop_on_empty_maps() {
5032        // The private helper must not panic (or wrongly prune) on empty maps.
5033        let mut rr = RendererResources::default();
5034        rr.remove_font_families_with_zero_references();
5035        assert!(rr.font_id_map.is_empty());
5036        assert!(rr.font_families_map.is_empty());
5037
5038        // A family whose FontKey is NOT registered gets pruned; the families
5039        // map entry pointing at it is pruned too.
5040        let family = StyleFontFamily::System(AzString::from_const_str("Arial"));
5041        let family_hash = StyleFontFamilyHash::new(&family);
5042        let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
5043        rr.font_id_map.insert(family_hash, FontKey::unique(IdNamespace(1)));
5044        rr.font_families_map.insert(families_hash, family_hash);
5045        rr.remove_font_families_with_zero_references();
5046        assert!(rr.font_id_map.is_empty(), "dangling font key must be pruned");
5047        assert!(rr.font_families_map.is_empty());
5048    }
5049
5050    #[test]
5051    fn get_font_instance_key_for_text_is_none_on_empty_resources_for_all_sane_sizes() {
5052        // No fonts registered -> every lookup misses, whatever the size / DPI.
5053        // (0, negative and NaN sizes must not panic on the way to the miss.)
5054        let rr = RendererResources::default();
5055        let cache = CssPropertyCache::default();
5056        let node = NodeData::default();
5057        let node_id = NodeId::new(0);
5058        let state = StyledNodeState::default();
5059
5060        for size in [0.0_f32, -0.0, 1.0, -12.0, f32::NAN, 1.0e6, -1.0e6] {
5061            for dpi in [1.0_f32, 0.0, -1.0, f32::NAN, f32::INFINITY] {
5062                assert!(
5063                    rr.get_font_instance_key_for_text(size, &cache, &node, &node_id, &state, dpi)
5064                        .is_none(),
5065                    "size={size} dpi={dpi} must miss cleanly"
5066                );
5067            }
5068        }
5069    }
5070
5071    #[test]
5072    fn bug_get_font_instance_key_for_text_overflow_panics_on_infinite_font_size() {
5073        // `font_size_px as isize` saturates to isize::MAX for +inf / f32::MAX,
5074        // and FloatValue::const_new then computes `isize::MAX * 1000`, which
5075        // panics with "attempt to multiply with overflow" under the (default)
5076        // dev-profile overflow checks. A miss (None) is the correct behaviour.
5077        let rr = RendererResources::default();
5078        let cache = CssPropertyCache::default();
5079        let node = NodeData::default();
5080        let node_id = NodeId::new(0);
5081        let state = StyledNodeState::default();
5082        assert!(rr
5083            .get_font_instance_key_for_text(
5084                f32::INFINITY,
5085                &cache,
5086                &node,
5087                &node_id,
5088                &state,
5089                1.0
5090            )
5091            .is_none());
5092    }
5093
5094    // =====================================================================
5095    // Resource-update builders (end-to-end)
5096    // =====================================================================
5097
5098    #[test]
5099    fn font_ref_get_hash_is_stable_per_font_and_distinct_across_fonts() {
5100        let a = dummy_font_ref();
5101        let b = dummy_font_ref();
5102        assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a));
5103        assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a.clone()));
5104        assert_ne!(
5105            font_ref_get_hash(&a),
5106            font_ref_get_hash(&b),
5107            "two distinct FontRefs must not share a hash"
5108        );
5109    }
5110
5111    #[test]
5112    fn build_add_font_resource_updates_on_empty_input_is_empty() {
5113        let mut rr = RendererResources::default();
5114        let fonts = OrderedMap::new();
5115        let updates = build_add_font_resource_updates(
5116            &mut rr,
5117            DpiScaleFactor::new(1.0),
5118            &FcFontCache::default(),
5119            IdNamespace(1),
5120            &fonts,
5121            load_font_none,
5122            parse_font_none,
5123        );
5124        assert!(updates.is_empty());
5125        assert!(rr.font_id_map.is_empty());
5126    }
5127
5128    #[test]
5129    fn build_add_font_resource_updates_skips_unloadable_fonts() {
5130        // A family that cannot be loaded (missing file) must not register
5131        // anything - it is retried next frame, not half-registered.
5132        let mut rr = RendererResources::default();
5133        let mut fonts = OrderedMap::new();
5134        let mut sizes = FastBTreeSet::new();
5135        sizes.insert(Au::from_px(16.0));
5136        fonts.insert(
5137            ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![
5138                StyleFontFamily::System(AzString::from_const_str("DoesNotExist")),
5139            ])),
5140            sizes,
5141        );
5142
5143        let updates = build_add_font_resource_updates(
5144            &mut rr,
5145            DpiScaleFactor::new(1.0),
5146            &FcFontCache::default(),
5147            IdNamespace(1),
5148            &fonts,
5149            load_font_none,
5150            parse_font_none,
5151        );
5152        assert!(updates.is_empty(), "an unloadable font must add no resources");
5153        assert!(rr.font_id_map.is_empty());
5154        assert!(rr.font_families_map.is_empty());
5155    }
5156
5157    #[test]
5158    fn build_add_font_resource_updates_registers_a_font_and_deduplicates_sizes() {
5159        // A StyleFontFamily::Ref resolves without touching the loader, so this
5160        // exercises the whole add-font path deterministically.
5161        let mut rr = RendererResources::default();
5162        let font = dummy_font_ref();
5163        let family = StyleFontFamily::Ref(font.clone());
5164        let dpi = DpiScaleFactor::new(1.0);
5165
5166        let mut sizes = FastBTreeSet::new();
5167        sizes.insert(Au::from_px(16.0));
5168        sizes.insert(Au::from_px(24.0));
5169        sizes.insert(Au::from_px(16.0)); // duplicate -> set dedups it
5170        assert_eq!(sizes.len(), 2);
5171
5172        let mut fonts = OrderedMap::new();
5173        fonts.insert(
5174            ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![family.clone()])),
5175            sizes,
5176        );
5177
5178        let updates = build_add_font_resource_updates(
5179            &mut rr,
5180            dpi,
5181            &FcFontCache::default(),
5182            IdNamespace(1),
5183            &fonts,
5184            load_font_none,
5185            parse_font_none,
5186        );
5187        // 1 AddFont + 2 AddFontInstance
5188        assert_eq!(updates.len(), 3);
5189        assert_eq!(
5190            updates
5191                .iter()
5192                .filter(|(_, m)| matches!(m, AddFontMsg::Font(..)))
5193                .count(),
5194            1
5195        );
5196        assert_eq!(
5197            updates
5198                .iter()
5199                .filter(|(_, m)| matches!(m, AddFontMsg::Instance(..)))
5200                .count(),
5201            2
5202        );
5203        assert_eq!(rr.font_id_map.len(), 1);
5204        assert_eq!(rr.font_families_map.len(), 1);
5205
5206        // add_resources then makes the instances findable by (families, size, dpi).
5207        let mut all_updates = Vec::new();
5208        add_resources(&mut rr, &mut all_updates, updates, Vec::new());
5209        assert_eq!(all_updates.len(), 3);
5210
5211        let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
5212        assert!(rr
5213            .get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
5214            .is_some());
5215        assert!(rr
5216            .get_font_instance_key(&families_hash, Au::from_px(24.0), dpi)
5217            .is_some());
5218        // A size that was never registered misses; so does a different DPI.
5219        assert!(rr
5220            .get_font_instance_key(&families_hash, Au::from_px(99.0), dpi)
5221            .is_none());
5222        assert!(rr
5223            .get_font_instance_key(&families_hash, Au::from_px(16.0), DpiScaleFactor::new(2.0))
5224            .is_none());
5225
5226        // The instance key resolves back to the font (reverse lookup invariant).
5227        let key = rr
5228            .get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
5229            .expect("registered");
5230        let (font_ref, au, got_dpi) = rr
5231            .get_renderable_font_data(&key)
5232            .expect("registered instance must be renderable");
5233        assert_eq!(font_ref.get_hash(), font.get_hash());
5234        assert_eq!(au, Au::from_px(16.0));
5235        assert_eq!(got_dpi, dpi);
5236
5237        // Rebuilding with the same font must not add anything a second time.
5238        let again = build_add_font_resource_updates(
5239            &mut rr,
5240            dpi,
5241            &FcFontCache::default(),
5242            IdNamespace(1),
5243            &fonts,
5244            load_font_none,
5245            parse_font_none,
5246        );
5247        assert!(again.is_empty(), "already-registered fonts must not be re-added");
5248    }
5249
5250    #[test]
5251    fn add_font_msg_into_resource_update_preserves_keys() {
5252        let font = dummy_font_ref();
5253        let key = FontKey::unique(IdNamespace(3));
5254        let family_hash = StyleFontFamilyHash::new(&StyleFontFamily::Ref(font.clone()));
5255        let msg = AddFontMsg::Font(key, family_hash, font.clone());
5256        match msg.into_resource_update() {
5257            ResourceUpdate::AddFont(add) => {
5258                assert_eq!(add.key, key);
5259                assert_eq!(add.font.get_hash(), font.get_hash());
5260            }
5261            other => panic!("expected AddFont, got {other:?}"),
5262        }
5263    }
5264
5265    #[test]
5266    fn delete_font_msg_into_resource_update_preserves_keys() {
5267        let fk = FontKey::unique(IdNamespace(1));
5268        match DeleteFontMsg::Font(fk).into_resource_update() {
5269            ResourceUpdate::DeleteFont(k) => assert_eq!(k, fk),
5270            other => panic!("expected DeleteFont, got {other:?}"),
5271        }
5272        let fik = FontInstanceKey::unique(IdNamespace(1));
5273        let size = (Au::from_px(16.0), DpiScaleFactor::new(1.0));
5274        match DeleteFontMsg::Instance(fik, size).into_resource_update() {
5275            ResourceUpdate::DeleteFontInstance(k) => assert_eq!(k, fik),
5276            other => panic!("expected DeleteFontInstance, got {other:?}"),
5277        }
5278    }
5279
5280    #[test]
5281    fn add_image_msg_into_resource_update_preserves_the_key_and_descriptor() {
5282        let key = ImageKey::unique(IdNamespace(2));
5283        let descriptor = ImageDescriptor {
5284            format: RawImageFormat::BGRA8,
5285            width: 3,
5286            height: 5,
5287            stride: None.into(),
5288            offset: 0,
5289            flags: ImageDescriptorFlags {
5290                is_opaque: false,
5291                allow_mipmaps: true,
5292            },
5293        };
5294        let msg = AddImageMsg(AddImage {
5295            key,
5296            descriptor,
5297            data: ImageData::Raw(SharedRawImageData::new(vec![0u8; 60].into())),
5298            tiling: None,
5299        });
5300        match msg.into_resource_update() {
5301            ResourceUpdate::AddImage(add) => {
5302                assert_eq!(add.key, key);
5303                assert_eq!(add.descriptor, descriptor);
5304                assert!(add.tiling.is_none());
5305            }
5306            other => panic!("expected AddImage, got {other:?}"),
5307        }
5308    }
5309
5310    #[test]
5311    fn build_add_image_resource_updates_skips_null_and_callback_images() {
5312        // NullImage has nothing to upload, Callback runs after layout.
5313        let rr = RendererResources::default();
5314        let mut images = FastBTreeSet::new();
5315        images.insert(ImageRef::null_image(4, 4, RawImageFormat::RGBA8, Vec::new()));
5316        images.insert(ImageRef::callback(0usize, RefAny::new(0u8)));
5317
5318        let updates = build_add_image_resource_updates(
5319            &rr,
5320            IdNamespace(1),
5321            Epoch::new(),
5322            &test_document_id(),
5323            &images,
5324            store_gl_texture_noop,
5325        );
5326        assert!(updates.is_empty());
5327
5328        // ... and an empty DOM produces no updates at all.
5329        let empty = FastBTreeSet::new();
5330        assert!(build_add_image_resource_updates(
5331            &rr,
5332            IdNamespace(1),
5333            Epoch::new(),
5334            &test_document_id(),
5335            &empty,
5336            store_gl_texture_noop,
5337        )
5338        .is_empty());
5339    }
5340
5341    #[test]
5342    fn build_add_image_resource_updates_then_add_resources_round_trip() {
5343        let mut rr = RendererResources::default();
5344        let img = ImageRef::new_rawimage(rgba8_image(2, 2)).expect("valid 2x2");
5345        let hash = img.get_hash();
5346        let ns = IdNamespace(11);
5347
5348        let mut images = FastBTreeSet::new();
5349        images.insert(img.clone());
5350
5351        let updates = build_add_image_resource_updates(
5352            &rr,
5353            ns,
5354            Epoch::new(),
5355            &test_document_id(),
5356            &images,
5357            store_gl_texture_noop,
5358        );
5359        assert_eq!(updates.len(), 1);
5360        assert_eq!(updates[0].0, hash);
5361        // The ImageKey is derived from the hash (no separate mapping table).
5362        assert_eq!(updates[0].1 .0.key, image_ref_hash_to_image_key(hash, ns));
5363        assert_eq!(updates[0].1 .0.descriptor.width, 2);
5364        assert_eq!(updates[0].1 .0.descriptor.height, 2);
5365
5366        let key = updates[0].1 .0.key;
5367        let mut all_updates = Vec::new();
5368        add_resources(&mut rr, &mut all_updates, Vec::new(), updates);
5369        assert_eq!(all_updates.len(), 1);
5370        assert!(matches!(all_updates[0], ResourceUpdate::AddImage(_)));
5371
5372        // Forward and reverse maps must agree after registration.
5373        assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
5374        assert_eq!(rr.image_key_map.get(&key), Some(&hash));
5375
5376        // An already-registered image is never re-uploaded.
5377        let again = build_add_image_resource_updates(
5378            &rr,
5379            ns,
5380            Epoch::new(),
5381            &test_document_id(),
5382            &images,
5383            store_gl_texture_noop,
5384        );
5385        assert!(again.is_empty());
5386
5387        // update_image mutates the descriptor in place, keeping the key.
5388        let new_descriptor = ImageDescriptor {
5389            format: RawImageFormat::BGRA8,
5390            width: 8,
5391            height: 8,
5392            stride: None.into(),
5393            offset: 0,
5394            flags: ImageDescriptorFlags {
5395                is_opaque: true,
5396                allow_mipmaps: true,
5397            },
5398        };
5399        rr.update_image(&hash, new_descriptor);
5400        assert_eq!(rr.get_image(&hash).map(|r| r.descriptor.width), Some(8));
5401        assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
5402        // Updating an unknown hash is a silent no-op, not a panic.
5403        rr.update_image(&ImageRefHash { inner: u64::MAX }, new_descriptor);
5404    }
5405
5406    #[test]
5407    fn add_resources_with_empty_input_changes_nothing() {
5408        let mut rr = RendererResources::default();
5409        let mut updates = Vec::new();
5410        add_resources(&mut rr, &mut updates, Vec::new(), Vec::new());
5411        assert!(updates.is_empty());
5412        assert!(rr.currently_registered_images.is_empty());
5413        assert!(rr.currently_registered_fonts.is_empty());
5414        assert!(rr.image_key_map.is_empty());
5415    }
5416
5417    // =====================================================================
5418    // NUMERIC: CPU painting (paint_dot / paint_stroke)
5419    // =====================================================================
5420
5421    #[test]
5422    fn paint_dot_composites_at_the_center_and_leaves_far_pixels_alone() {
5423        let mut img = rgba8_image(4, 4);
5424        img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
5425        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5426
5427        // Pixel (1,1) is 0.71 px from the center -> inside the hard core.
5428        let idx = (4 + 1) * 4;
5429        assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255], "center pixel must be opaque red");
5430        // Pixel (0,0) is 2.12 px away -> outside the radius -> untouched.
5431        assert_eq!(&px[0..4], &[0, 0, 0, 0], "pixels beyond the radius stay untouched");
5432    }
5433
5434    #[test]
5435    fn paint_dot_honours_bgra_channel_order() {
5436        let mut img = rgba8_image(4, 4);
5437        img.data_format = RawImageFormat::BGRA8;
5438        img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
5439        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5440        let idx = (4 + 1) * 4;
5441        // BGRA: red lands in byte 2, blue in byte 0.
5442        assert_eq!(&px[idx..idx + 4], &[0, 0, 255, 255]);
5443    }
5444
5445    #[test]
5446    fn paint_dot_rejects_degenerate_radii_and_sizes() {
5447        let untouched = |img: &RawImage| {
5448            img.pixels
5449                .get_u8_vec_ref()
5450                .expect("u8")
5451                .as_ref()
5452                .iter()
5453                .all(|b| *b == 0)
5454        };
5455
5456        // radius <= 0 and NaN radius are no-ops (the `!(r > 0.0)` guard).
5457        for r in [0.0_f32, -1.0, -0.0, f32::NAN, f32::NEG_INFINITY] {
5458            let mut img = rgba8_image(4, 4);
5459            img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), r));
5460            assert!(untouched(&img), "radius {r} must not paint");
5461        }
5462
5463        // Zero-sized images are no-ops (and must not index out of bounds).
5464        let mut img = rgba8_image(0, 0);
5465        img.paint_dot(0.0, 0.0, Brush::new(opaque_red(), 4.0));
5466        assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
5467
5468        // Non-8-bit-RGBA formats are documented as left untouched.
5469        for format in [
5470            RawImageFormat::R8,
5471            RawImageFormat::RGB8,
5472            RawImageFormat::RGBA16,
5473            RawImageFormat::RGBAF32,
5474        ] {
5475            let mut img = rgba8_image(4, 4);
5476            img.data_format = format;
5477            img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
5478            assert!(untouched(&img), "format {format:?} must not be painted");
5479        }
5480    }
5481
5482    #[test]
5483    fn paint_dot_with_nan_and_infinite_coordinates_is_a_safe_noop() {
5484        // NaN / +-inf centers collapse the scan range to nothing instead of
5485        // producing an out-of-bounds index.
5486        for (cx, cy) in [
5487            (f32::NAN, 2.0_f32),
5488            (2.0, f32::NAN),
5489            (f32::NAN, f32::NAN),
5490            (f32::INFINITY, 2.0),
5491            (f32::NEG_INFINITY, 2.0),
5492            (2.0, f32::INFINITY),
5493            (1.0e30, 1.0e30),
5494            (-1.0e30, -1.0e30),
5495        ] {
5496            let mut img = rgba8_image(4, 4);
5497            img.paint_dot(cx, cy, Brush::new(opaque_red(), 2.0));
5498            let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5499            assert!(
5500                px.iter().all(|b| *b == 0),
5501                "({cx}, {cy}) must not paint anything"
5502            );
5503        }
5504    }
5505
5506    #[test]
5507    fn paint_dot_alpha_saturates_and_never_overflows() {
5508        // Repeatedly stamping an opaque dab must clamp at 255, never wrap.
5509        let mut img = rgba8_image(4, 4);
5510        let brush = Brush::new(opaque_red(), 2.0);
5511        for _ in 0..50 {
5512            img.paint_dot(2.0, 2.0, brush);
5513        }
5514        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5515        let idx = (4 + 1) * 4;
5516        assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
5517
5518        // A NaN hardness makes the coverage NaN; the `a <= 0.0` check is false
5519        // for NaN, so the blend runs with a NaN alpha -- the `.clamp(0, 255)`
5520        // on the result keeps every channel a valid u8 (NaN clamps to 0 here),
5521        // i.e. garbage-in stays in-range instead of wrapping.
5522        let mut img = rgba8_image(4, 4);
5523        let mut nan_brush = Brush::new(opaque_red(), 2.0);
5524        nan_brush.hardness = f32::NAN;
5525        img.paint_dot(2.0, 2.0, nan_brush);
5526        // No assertion on the exact value: the point is that it did not panic
5527        // and every byte is (trivially) a valid u8.
5528        assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
5529    }
5530
5531    #[test]
5532    fn paint_dot_zero_flow_and_transparent_color_do_not_paint() {
5533        let mut img = rgba8_image(4, 4);
5534        let mut brush = Brush::new(opaque_red(), 2.0);
5535        brush.flow = 0.0;
5536        img.paint_dot(2.0, 2.0, brush);
5537        assert!(img
5538            .pixels
5539            .get_u8_vec_ref()
5540            .expect("u8")
5541            .as_ref()
5542            .iter()
5543            .all(|b| *b == 0));
5544
5545        let mut img = rgba8_image(4, 4);
5546        let transparent = ColorU {
5547            r: 255,
5548            g: 0,
5549            b: 0,
5550            a: 0,
5551        };
5552        img.paint_dot(2.0, 2.0, Brush::new(transparent, 2.0));
5553        assert!(img
5554            .pixels
5555            .get_u8_vec_ref()
5556            .expect("u8")
5557            .as_ref()
5558            .iter()
5559            .all(|b| *b == 0));
5560
5561        // A negative / >1 flow is clamped, not extrapolated.
5562        let mut img = rgba8_image(4, 4);
5563        let mut brush = Brush::new(opaque_red(), 2.0);
5564        brush.flow = -5.0;
5565        img.paint_dot(2.0, 2.0, brush);
5566        assert!(img
5567            .pixels
5568            .get_u8_vec_ref()
5569            .expect("u8")
5570            .as_ref()
5571            .iter()
5572            .all(|b| *b == 0));
5573    }
5574
5575    #[test]
5576    fn paint_stroke_paints_both_endpoints() {
5577        let mut img = rgba8_image(8, 8);
5578        img.paint_stroke(1.5, 1.5, 6.5, 6.5, Brush::new(opaque_red(), 1.5));
5579        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5580        let alpha_at = |x: usize, y: usize| px[(y * 8 + x) * 4 + 3];
5581        assert!(alpha_at(1, 1) > 0, "start of the stroke must be painted");
5582        assert!(alpha_at(6, 6) > 0, "end of the stroke must be painted");
5583        assert_eq!(alpha_at(7, 0), 0, "off-line pixels stay untouched");
5584    }
5585
5586    #[test]
5587    fn paint_stroke_zero_length_stamps_a_single_dab() {
5588        // len == 0 -> n == 0 -> the `n <= 0` branch stamps one dab at (x1, y1).
5589        let mut img = rgba8_image(4, 4);
5590        img.paint_stroke(2.0, 2.0, 2.0, 2.0, Brush::new(opaque_red(), 2.0));
5591        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5592        let idx = (4 + 1) * 4;
5593        assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
5594    }
5595
5596    #[test]
5597    fn paint_stroke_degenerate_brush_params_do_not_divide_by_zero_or_hang() {
5598        // spacing == 0 / negative: the `.max(0.01)` and `.max(0.5)` floors keep
5599        // the step positive, so the dab count stays finite.
5600        for spacing in [0.0_f32, -1.0, f32::NAN] {
5601            let mut img = rgba8_image(8, 8);
5602            let mut brush = Brush::new(opaque_red(), 2.0);
5603            brush.spacing = spacing;
5604            img.paint_stroke(0.0, 0.0, 7.0, 7.0, brush);
5605            assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(8 * 8 * 4));
5606        }
5607
5608        // A NaN endpoint yields a NaN length -> n == 0 -> one (no-op) dab.
5609        let mut img = rgba8_image(4, 4);
5610        img.paint_stroke(f32::NAN, 0.0, 1.0, 1.0, Brush::new(opaque_red(), 1.0));
5611        assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
5612
5613        // radius == 0 -> every dab is a no-op, and the loop still terminates.
5614        let mut img = rgba8_image(4, 4);
5615        img.paint_stroke(0.0, 0.0, 3.0, 3.0, Brush::new(opaque_red(), 0.0));
5616        assert!(img
5617            .pixels
5618            .get_u8_vec_ref()
5619            .expect("u8")
5620            .as_ref()
5621            .iter()
5622            .all(|b| *b == 0));
5623    }
5624
5625    #[test]
5626    fn bug_paint_stroke_with_infinite_endpoint_loops_2_billion_times() {
5627        // `len` is +inf, `step` is finite, so `n = (inf / step).floor() as i32`
5628        // saturates to i32::MAX and the `for i in 0..=n` loop runs 2^31 times.
5629        // paint_stroke should clamp `n` (or bail on a non-finite length).
5630        let mut img = rgba8_image(4, 4);
5631        img.paint_stroke(0.0, 0.0, f32::INFINITY, 0.0, Brush::new(opaque_red(), 2.0));
5632    }
5633
5634    #[test]
5635    fn bug_paint_dot_indexes_out_of_bounds_when_dims_exceed_the_buffer() {
5636        // RawImage's fields are all public, so a caller can hand paint_dot a
5637        // 100x100 image backed by 4 bytes. paint_dot computes the index from
5638        // width/height and indexes `buf` unchecked -> panic. It should clamp the
5639        // scan rect to the buffer length (or bail).
5640        let mut img = RawImage {
5641            pixels: RawImageData::U8(vec![0u8; 4].into()),
5642            width: 100,
5643            height: 100,
5644            premultiplied_alpha: true,
5645            data_format: RawImageFormat::RGBA8,
5646            tag: Vec::new().into(),
5647        };
5648        img.paint_dot(50.0, 50.0, Brush::new(opaque_red(), 4.0));
5649    }
5650
5651    #[test]
5652    fn bug_into_loaded_image_source_overflows_on_huge_dimensions() {
5653        // Documented contract: "Returns None if the width * height * BPP does not
5654        // match". With width == usize::MAX the multiplication overflows and
5655        // panics under the dev-profile overflow checks instead.
5656        let img = RawImage {
5657            pixels: RawImageData::U8(vec![0u8; 4].into()),
5658            width: usize::MAX,
5659            height: 2,
5660            premultiplied_alpha: true,
5661            data_format: RawImageFormat::RGBA8,
5662            tag: Vec::new().into(),
5663        };
5664        assert!(img.into_loaded_image_source().is_none());
5665    }
5666}