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