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        let font_size = StyleFontSize {
1400            inner: azul_css::props::basic::PixelValue::const_px(font_size_px as isize),
1401        };
1402
1403        // Convert to application units
1404        let font_size_au = font_size_to_au(font_size);
1405
1406        // Create DPI scale factor
1407        let dpi_scale_factor = DpiScaleFactor {
1408            inner: FloatValue::new(dpi_scale),
1409        };
1410
1411        // Get font family
1412        let font_family =
1413            css_property_cache.get_font_id_or_default(node_data, node_id, styled_node_state);
1414
1415        // Calculate hash and lookup font instance key
1416        let font_families_hash = StyleFontFamiliesHash::new(font_family.as_ref());
1417
1418        self.get_font_instance_key(&font_families_hash, font_size_au, dpi_scale_factor)
1419    }
1420
1421    #[must_use] pub fn get_font_instance_key(
1422        &self,
1423        font_families_hash: &StyleFontFamiliesHash,
1424        font_size_au: Au,
1425        dpi_scale: DpiScaleFactor,
1426    ) -> Option<FontInstanceKey> {
1427        let font_family_hash = self.get_font_family(font_families_hash)?;
1428        let font_key = self.get_font_key(font_family_hash)?;
1429        let (_, instances) = self.get_registered_font(font_key)?;
1430        instances.get(&(font_size_au, dpi_scale)).copied()
1431    }
1432
1433    // Delete all font family hashes that do not have a font key anymore
1434    //
1435    // AUDIT-TODO (font GC, resources.rs font leak — 2026-07-08):
1436    // Fonts and font instances are currently NEVER garbage-collected. This helper
1437    // only prunes `font_id_map` / `font_families_map` entries whose `FontKey` has
1438    // *already* vanished from `currently_registered_fonts` — but nothing ever
1439    // removes fonts from `currently_registered_fonts` in the first place, and this
1440    // helper itself has no callers. No `DeleteFont` / `DeleteFontInstance`
1441    // `ResourceUpdate` is ever emitted, so WebRender font memory grows unbounded
1442    // when an app cycles fonts (font pickers, editors, live CSS).
1443    //
1444    // To wire a real font GC mirroring the image GC (see `dll/.../wr_translate2.rs`
1445    // `garbage_collect_images` + `image_last_seen_epoch`), the following are needed
1446    // and MUST be done together (do not half-implement):
1447    //   1. Add `font_last_seen_epoch: OrderedMap<FontKey, u32>` (and, if instance-
1448    //      level GC is wanted, per-`FontInstanceKey` epochs) to `RendererResources`.
1449    //   2. In the display-list build (dll crate), after resolving each glyph run's
1450    //      `FontInstanceKey`, mark the owning `FontKey` (and instance) seen at the
1451    //      current epoch — exactly as images are marked in the image GC.
1452    //   3. Add a `garbage_collect_fonts(&mut self, now, keep_epochs, updates)` that,
1453    //      for every `FontKey` unseen for > keep_epochs frames, emits
1454    //      `DeleteFontInstance` for each of its instances then `DeleteFont`, and
1455    //      evicts the key from `currently_registered_fonts`, `font_hash_map`,
1456    //      `last_frame_registered_fonts`, and `font_id_map`/`font_families_map`
1457    //      (via this helper). Respect the "delete on current frame + 1" rule already
1458    //      documented on `last_frame_registered_fonts`.
1459    //   4. Call it once per frame from the same site as the image GC.
1460    // Left as a TODO because steps 2 and 4 are cross-crate (dll) and cannot be
1461    // implemented from `azul-core` alone; adding a GC method here without a caller
1462    // would just be more dead code.
1463    #[allow(dead_code)]
1464    fn remove_font_families_with_zero_references(&mut self) {
1465        let font_family_to_delete = self
1466            .font_id_map
1467            .iter()
1468            .filter_map(|(font_family, font_key)| {
1469                if self.currently_registered_fonts.contains_key(font_key) {
1470                    None
1471                } else {
1472                    Some(*font_family)
1473                }
1474            })
1475            .collect::<Vec<_>>();
1476
1477        for f in font_family_to_delete {
1478            self.font_id_map.remove(&f); // font key does not exist anymore
1479        }
1480
1481        let font_families_to_delete = self
1482            .font_families_map
1483            .iter()
1484            .filter_map(|(font_families, font_family)| {
1485                if self.font_id_map.contains_key(font_family) {
1486                    None
1487                } else {
1488                    Some(*font_families)
1489                }
1490            })
1491            .collect::<Vec<_>>();
1492
1493        for f in font_families_to_delete {
1494            self.font_families_map.remove(&f); // font family does not exist anymore
1495        }
1496    }
1497}
1498
1499// Result returned from rerender_image_callback() - should be used as:
1500//
1501// ```rust
1502// txn.update_image(
1503//     wr_translate_image_key(key),
1504//     wr_translate_image_descriptor(descriptor),
1505//     wr_translate_image_data(data),
1506//     &WrImageDirtyRect::All,
1507// );
1508// ```
1509#[derive(Debug, Clone)]
1510pub struct UpdateImageResult {
1511    pub key_to_update: ImageKey,
1512    pub new_descriptor: ImageDescriptor,
1513    pub new_image_data: ImageData,
1514}
1515
1516#[derive(Debug, Default)]
1517pub struct GlTextureCache {
1518    pub solved_textures:
1519        BTreeMap<DomId, BTreeMap<NodeId, (ImageKey, ImageDescriptor, ExternalImageId)>>,
1520    pub hashes: BTreeMap<(DomId, NodeId, ImageRefHash), ImageRefHash>,
1521}
1522
1523// necessary so the display list can be built in parallel
1524// SAFETY: only the raw pointers inside the contained `ImageRefHash`/key maps are
1525// non-`Send`-inferring; every stored value is a plain POD id/descriptor with no
1526// interior aliasing, so moving the cache to another thread is sound.
1527unsafe impl Send for GlTextureCache {}
1528
1529impl GlTextureCache {
1530    /// Initializes an empty cache
1531    #[must_use] pub const fn empty() -> Self {
1532        Self {
1533            solved_textures: BTreeMap::new(),
1534            hashes: BTreeMap::new(),
1535        }
1536    }
1537
1538    /// Updates a given texture
1539    ///
1540    /// This is called when a texture needs to be re-rendered (e.g., on resize or animation frame).
1541    /// It updates the texture in the `WebRender` external image cache and updates the internal
1542    /// descriptor to reflect the new size.
1543    ///
1544    /// # Arguments
1545    ///
1546    /// * `dom_id` - The DOM ID containing the texture
1547    /// * `node_id` - The node ID of the image element
1548    /// * `document_id` - The `WebRender` document ID
1549    /// * `epoch` - The current frame epoch
1550    /// * `new_texture` - The new texture to use
1551    /// * `insert_into_active_gl_textures_fn` - Function to insert the texture into the cache
1552    ///
1553    /// # Returns
1554    ///
1555    /// The `ExternalImageId` if successful, None if the texture wasn't found in the cache
1556    pub fn update_texture(
1557        &mut self,
1558        dom_id: DomId,
1559        node_id: NodeId,
1560        document_id: DocumentId,
1561        epoch: Epoch,
1562        new_texture: Texture,
1563        insert_into_active_gl_textures_fn: &GlStoreImageFn,
1564    ) -> Option<ExternalImageId> {
1565        let new_descriptor = new_texture.get_descriptor();
1566        let di_map = self.solved_textures.get_mut(&dom_id)?;
1567        let entry = di_map.get_mut(&node_id)?;
1568
1569        // Update the descriptor
1570        entry.1 = new_descriptor;
1571
1572        // The ExternalImageId is deterministic from (dom_id, node_id), so the cache
1573        // entry can keep referencing the same id across re-renders.
1574        let external_image_id = texture_external_image_id(dom_id, node_id);
1575        (insert_into_active_gl_textures_fn)(document_id, epoch, new_texture, external_image_id);
1576        entry.2 = external_image_id;
1577
1578        Some(external_image_id)
1579    }
1580}
1581
1582macro_rules! unique_id {
1583    ($struct_name:ident, $counter_name:ident) => {
1584        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
1585        #[repr(C)]
1586        pub struct $struct_name {
1587            pub id: usize,
1588        }
1589
1590        impl $struct_name {
1591            pub fn unique() -> Self {
1592                Self {
1593                    id: $counter_name.fetch_add(1, AtomicOrdering::SeqCst),
1594                }
1595            }
1596        }
1597    };
1598}
1599
1600// NOTE: the property key is unique across transform, color and opacity properties
1601static PROPERTY_KEY_COUNTER: AtomicUsize = AtomicUsize::new(0);
1602unique_id!(TransformKey, PROPERTY_KEY_COUNTER);
1603unique_id!(ColorKey, PROPERTY_KEY_COUNTER);
1604unique_id!(OpacityKey, PROPERTY_KEY_COUNTER);
1605
1606static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
1607unique_id!(ImageId, IMAGE_ID_COUNTER);
1608static FONT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
1609unique_id!(FontId, FONT_ID_COUNTER);
1610
1611#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1612#[repr(C)]
1613pub struct ImageMask {
1614    pub image: ImageRef,
1615    pub rect: LogicalRect,
1616    pub repeat: bool,
1617}
1618
1619impl_option!(
1620    ImageMask,
1621    OptionImageMask,
1622    copy = false,
1623    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
1624);
1625
1626#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1627pub enum ImmediateFontId {
1628    Resolved((StyleFontFamilyHash, FontKey)),
1629    Unresolved(StyleFontFamilyVec),
1630}
1631
1632#[derive(Debug, Clone, PartialEq, PartialOrd)]
1633#[repr(C, u8)]
1634pub enum RawImageData {
1635    // 8-bit image data
1636    U8(U8Vec),
1637    // 16-bit image data
1638    U16(U16Vec),
1639    // HDR image data
1640    F32(F32Vec),
1641}
1642
1643impl RawImageData {
1644    #[must_use] pub const fn get_u8_vec_ref(&self) -> Option<&U8Vec> {
1645        match self {
1646            Self::U8(v) => Some(v),
1647            _ => None,
1648        }
1649    }
1650
1651    #[must_use] pub const fn get_u16_vec_ref(&self) -> Option<&U16Vec> {
1652        match self {
1653            Self::U16(v) => Some(v),
1654            _ => None,
1655        }
1656    }
1657
1658    #[must_use] pub const fn get_f32_vec_ref(&self) -> Option<&F32Vec> {
1659        match self {
1660            Self::F32(v) => Some(v),
1661            _ => None,
1662        }
1663    }
1664
1665    fn get_u8_vec(self) -> Option<U8Vec> {
1666        match self {
1667            Self::U8(v) => Some(v),
1668            _ => None,
1669        }
1670    }
1671
1672    fn get_u16_vec(self) -> Option<U16Vec> {
1673        match self {
1674            Self::U16(v) => Some(v),
1675            _ => None,
1676        }
1677    }
1678}
1679
1680#[derive(Debug, Clone, PartialEq, PartialOrd)]
1681#[repr(C)]
1682pub struct RawImage {
1683    pub pixels: RawImageData,
1684    pub width: usize,
1685    pub height: usize,
1686    pub premultiplied_alpha: bool,
1687    pub data_format: RawImageFormat,
1688    pub tag: U8Vec,
1689}
1690
1691/// A soft round brush for the painting API.
1692///
1693/// The same parameters drive the CPU
1694/// rasterizer ([`RawImage::paint_dot`]) and the GPU brush shader, so a stroke
1695/// looks identical whether it lands on a `RawImage` or a `Texture`.
1696#[repr(C)]
1697#[derive(Debug, Copy, Clone, PartialEq)]
1698pub struct Brush {
1699    /// Brush color (its alpha scales the dab opacity together with `flow`).
1700    pub color: ColorU,
1701    /// Brush radius in pixels.
1702    pub radius: f32,
1703    /// Edge hardness, `0.0` (fully feathered) .. `1.0` (hard edge). Opaque out
1704    /// to `hardness * radius`, then a smooth falloff to zero at the edge.
1705    pub hardness: f32,
1706    /// Per-dab opacity multiplier, `0.0`..`1.0`. Values < 1 let overlapping dabs
1707    /// build up smoothly (the "metaball"-like blend).
1708    pub flow: f32,
1709    /// Spacing between stamped dabs along a stroke, as a fraction of `radius`
1710    /// (e.g. `0.25` = a dab every quarter-radius). Smaller = smoother + slower.
1711    pub spacing: f32,
1712}
1713
1714impl Brush {
1715    /// A sensible default brush: medium-soft, full flow, dense spacing.
1716    #[must_use] pub const fn new(color: ColorU, radius: f32) -> Self {
1717        Self {
1718            color,
1719            radius,
1720            hardness: 0.5,
1721            flow: 1.0,
1722            spacing: 0.25,
1723        }
1724    }
1725}
1726
1727/// Brush dab coverage: `1.0` at the dab center, smoothly `0.0` at its edge.
1728///
1729/// `t` is `distance / radius` in `[0, 1]`; `hardness` in `[0, 1]`. Single source
1730/// of truth for the dab profile -- the GPU brush shader computes the identical
1731/// `1 - smoothstep(hardness, 1, t)` so CPU and GPU strokes match.
1732#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
1733#[inline]
1734#[must_use] pub fn brush_dab_coverage(t: f32, hardness: f32) -> f32 {
1735    let edge0 = hardness.clamp(0.0, 1.0);
1736    let denom = (1.0 - edge0).max(1.0e-4);
1737    let x = ((t - edge0) / denom).clamp(0.0, 1.0);
1738    1.0 - (x * x * (3.0 - 2.0 * x))
1739}
1740
1741impl RawImage {
1742    /// CPU painting: stamp one brush dab centered at (`cx`, `cy`) in pixel
1743    /// coordinates, alpha-over compositing a radial-falloff disc. Only 8-bit
1744    /// `RGBA8`/`BGRA8` images are painted (other formats are left untouched).
1745    /// This is the CPU mirror of the GPU brush shader.
1746    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
1747    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1748    #[allow(clippy::cast_possible_wrap)] // image/graphics: bounded pixel/colour casts
1749    pub fn paint_dot(&mut self, cx: f32, cy: f32, brush: Brush) {
1750        let r = brush.radius;
1751        // `!(r > 0.0)` intentionally also rejects NaN (`r <= 0.0` would not).
1752        #[allow(clippy::neg_cmp_op_on_partial_ord)]
1753        if !(r > 0.0) || self.width == 0 || self.height == 0 {
1754            return;
1755        }
1756        let bgr = match self.data_format {
1757            RawImageFormat::RGBA8 => false,
1758            RawImageFormat::BGRA8 => true,
1759            _ => return,
1760        };
1761        let (w, h) = (self.width as i32, self.height as i32);
1762        let buf: &mut [u8] = match self.pixels {
1763            RawImageData::U8(ref mut v) => v.as_mut(),
1764            _ => return,
1765        };
1766        let flow = brush.flow.clamp(0.0, 1.0) * (f32::from(brush.color.a) / 255.0);
1767        let (cr, cg, cb) = (
1768            f32::from(brush.color.r),
1769            f32::from(brush.color.g),
1770            f32::from(brush.color.b),
1771        );
1772        let x0 = (cx - r).floor().max(0.0) as i32;
1773        let y0 = (cy - r).floor().max(0.0) as i32;
1774        let x1 = ((cx + r).ceil() as i32).min(w);
1775        let y1 = ((cy + r).ceil() as i32).min(h);
1776        for y in y0..y1 {
1777            for x in x0..x1 {
1778                let dx = x as f32 + 0.5 - cx;
1779                let dy = y as f32 + 0.5 - cy;
1780                let dist = dx.hypot(dy);
1781                if dist > r {
1782                    continue;
1783                }
1784                let a = brush_dab_coverage(dist / r, brush.hardness) * flow;
1785                if a <= 0.0 {
1786                    continue;
1787                }
1788                let idx = ((y * w + x) as usize) * 4;
1789                let (ri, gi, bi, ai) = if bgr {
1790                    (idx + 2, idx + 1, idx, idx + 3)
1791                } else {
1792                    (idx, idx + 1, idx + 2, idx + 3)
1793                };
1794                let inv = 1.0 - a;
1795                buf[ri] = (cr * a + f32::from(buf[ri]) * inv).round().clamp(0.0, 255.0) as u8;
1796                buf[gi] = (cg * a + f32::from(buf[gi]) * inv).round().clamp(0.0, 255.0) as u8;
1797                buf[bi] = (cb * a + f32::from(buf[bi]) * inv).round().clamp(0.0, 255.0) as u8;
1798                buf[ai] =
1799                    ((a + (f32::from(buf[ai]) / 255.0) * inv) * 255.0).round().clamp(0.0, 255.0) as u8;
1800            }
1801        }
1802    }
1803
1804    /// CPU painting: stamp a stroke by spacing dabs along the segment
1805    /// (`x0`,`y0`)->(`x1`,`y1`). Call once per pointer move with the previous and
1806    /// current positions for a continuous line.
1807    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
1808    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1809    pub fn paint_stroke(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, brush: Brush) {
1810        let dx = x1 - x0;
1811        let dy = y1 - y0;
1812        let len = dx.hypot(dy);
1813        let step = (brush.radius * brush.spacing.max(0.01)).max(0.5);
1814        let n = (len / step).floor() as i32;
1815        if n <= 0 {
1816            self.paint_dot(x1, y1, brush);
1817            return;
1818        }
1819        for i in 0..=n {
1820            let t = i as f32 / n as f32;
1821            self.paint_dot(x0 + dx * t, y0 + dy * t, brush);
1822        }
1823    }
1824}
1825
1826/// Multiplies the RGB channels of a single 4-byte BGRA/RGBA pixel by its alpha.
1827///
1828/// From webrender/wrench. These are slow. Gecko's gfx/2d/Swizzle.cpp has better
1829/// versions.
1830#[inline]
1831#[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
1832fn premultiply_alpha(array: &mut [u8]) {
1833    if array.len() != 4 {
1834        return;
1835    }
1836    let a = u32::from(array[3]);
1837    array[0] = (((u32::from(array[0]) * a) + 128) / 255) as u8;
1838    array[1] = (((u32::from(array[1]) * a) + 128) / 255) as u8;
1839    array[2] = (((u32::from(array[2]) * a) + 128) / 255) as u8;
1840}
1841
1842#[inline]
1843#[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
1844#[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
1845fn normalize_u16(i: u16) -> u8 {
1846    ((f32::from(i) / f32::from(core::u16::MAX)) * f32::from(core::u8::MAX)) as u8
1847}
1848
1849const FOUR_BPP: usize = 4;
1850const TWO_CHANNELS: usize = 2;
1851const THREE_CHANNELS: usize = 3;
1852const FOUR_CHANNELS: usize = 4;
1853
1854impl RawImage {
1855    /// Returns a null / empty image
1856    #[must_use] pub fn null_image() -> Self {
1857        Self {
1858            pixels: RawImageData::U8(Vec::new().into()),
1859            width: 0,
1860            height: 0,
1861            premultiplied_alpha: true,
1862            data_format: RawImageFormat::BGRA8,
1863            tag: Vec::new().into(),
1864        }
1865    }
1866
1867    /// Allocates a width * height, single-channel mask, used for drawing CPU image masks
1868    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1869    #[must_use] pub fn allocate_mask(size: LayoutSize) -> Self {
1870        Self {
1871            pixels: RawImageData::U8(
1872                vec![0; size.width.max(0) as usize * size.height.max(0) as usize].into(),
1873            ),
1874            width: size.width as usize,
1875            height: size.height as usize,
1876            premultiplied_alpha: true,
1877            data_format: RawImageFormat::R8,
1878            tag: Vec::new().into(),
1879        }
1880    }
1881
1882    /// Encodes a `RawImage` as BGRA8 bytes and premultiplies it if the alpha is not premultiplied
1883    ///
1884    /// Returns None if the width * height * BPP does not match
1885    ///
1886    /// TODO: autovectorization fails spectacularly, need to manually optimize!
1887    #[must_use] pub fn into_loaded_image_source(self) -> Option<(ImageData, ImageDescriptor)> {
1888        let Self {
1889            width,
1890            height,
1891            pixels,
1892            data_format,
1893            premultiplied_alpha,
1894            tag,
1895        } = self;
1896
1897        let expected_len = width * height;
1898
1899        let (bytes, data_format, is_opaque): (U8Vec, RawImageFormat, bool) = match data_format {
1900            RawImageFormat::R8 => {
1901                let (bytes, is_opaque) = Self::load_r8(pixels, expected_len)?;
1902                (bytes, RawImageFormat::R8, is_opaque)
1903            }
1904            RawImageFormat::RG8 => {
1905                let (bytes, is_opaque) = Self::load_rg8(pixels, expected_len, premultiplied_alpha)?;
1906                (bytes, RawImageFormat::BGRA8, is_opaque)
1907            }
1908            RawImageFormat::RGB8 => {
1909                let (bytes, is_opaque) = Self::load_rgb8(pixels, expected_len)?;
1910                (bytes, RawImageFormat::BGRA8, is_opaque)
1911            }
1912            RawImageFormat::RGBA8 => {
1913                let (bytes, is_opaque) = Self::load_rgba8(pixels, expected_len, premultiplied_alpha)?;
1914                (bytes, RawImageFormat::BGRA8, is_opaque)
1915            }
1916            RawImageFormat::R16 => {
1917                let (bytes, is_opaque) = Self::load_r16(pixels, expected_len)?;
1918                (bytes, RawImageFormat::BGRA8, is_opaque)
1919            }
1920            RawImageFormat::RG16 => {
1921                let (bytes, is_opaque) = Self::load_rg16(pixels, expected_len)?;
1922                (bytes, RawImageFormat::BGRA8, is_opaque)
1923            }
1924            RawImageFormat::RGB16 => {
1925                let (bytes, is_opaque) = Self::load_rgb16(pixels, expected_len)?;
1926                (bytes, RawImageFormat::BGRA8, is_opaque)
1927            }
1928            RawImageFormat::RGBA16 => {
1929                let (bytes, is_opaque) =
1930                    Self::load_rgba16(pixels, expected_len, premultiplied_alpha)?;
1931                (bytes, RawImageFormat::BGRA8, is_opaque)
1932            }
1933            RawImageFormat::BGR8 => {
1934                let (bytes, is_opaque) = Self::load_bgr8(pixels, expected_len)?;
1935                (bytes, RawImageFormat::BGRA8, is_opaque)
1936            }
1937            RawImageFormat::BGRA8 => {
1938                let (bytes, is_opaque) = Self::load_bgra8(pixels, expected_len, premultiplied_alpha)?;
1939                (bytes, RawImageFormat::BGRA8, is_opaque)
1940            }
1941            RawImageFormat::RGBF32 => {
1942                let (bytes, is_opaque) = Self::load_rgbf32(pixels, expected_len)?;
1943                (bytes, RawImageFormat::BGRA8, is_opaque)
1944            }
1945            RawImageFormat::RGBAF32 => {
1946                let (bytes, is_opaque) =
1947                    Self::load_rgbaf32(pixels, expected_len, premultiplied_alpha)?;
1948                (bytes, RawImageFormat::BGRA8, is_opaque)
1949            }
1950        };
1951
1952        let image_data = ImageData::Raw(SharedRawImageData::new(bytes));
1953        let image_descriptor = ImageDescriptor {
1954            format: data_format,
1955            width,
1956            height,
1957            offset: 0,
1958            stride: None.into(),
1959            flags: ImageDescriptorFlags {
1960                is_opaque,
1961                allow_mipmaps: true,
1962            },
1963        };
1964
1965        Some((image_data, image_descriptor))
1966    }
1967
1968    /// Keep R8 data as-is — `WebRender` supports R8 natively. This is important for
1969    /// image mask clips which need the single-channel data (white=visible,
1970    /// black=clipped). Stays in `R8` format; never opaque.
1971    fn load_r8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
1972        let pixels = pixels.get_u8_vec()?;
1973
1974        if pixels.len() != expected_len {
1975            return None;
1976        }
1977
1978        Some((pixels, false))
1979    }
1980
1981    fn load_rg8(
1982        pixels: RawImageData,
1983        expected_len: usize,
1984        premultiplied_alpha: bool,
1985    ) -> Option<(U8Vec, bool)> {
1986        let pixels = pixels.get_u8_vec()?;
1987
1988        if pixels.len() != expected_len * TWO_CHANNELS {
1989            return None;
1990        }
1991
1992        let mut is_opaque = true;
1993        let mut px = vec![0; expected_len * FOUR_BPP];
1994
1995        // TODO: check that this function is SIMD optimized
1996        for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
1997            let grey = greyalpha[0];
1998            let alpha = greyalpha[1];
1999
2000            if alpha != 255 {
2001                is_opaque = false;
2002            }
2003
2004            px[pixel_index * FOUR_BPP] = grey;
2005            px[(pixel_index * FOUR_BPP) + 1] = grey;
2006            px[(pixel_index * FOUR_BPP) + 2] = grey;
2007            px[(pixel_index * FOUR_BPP) + 3] = alpha;
2008
2009            if !premultiplied_alpha {
2010                premultiply_alpha(
2011                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2012                );
2013            }
2014        }
2015
2016        Some((px.into(), is_opaque))
2017    }
2018
2019    fn load_rgb8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2020        let pixels = pixels.get_u8_vec()?;
2021
2022        if pixels.len() != expected_len * THREE_CHANNELS {
2023            return None;
2024        }
2025
2026        let mut px = vec![0; expected_len * FOUR_BPP];
2027
2028        // TODO: check that this function is SIMD optimized
2029        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2030            let red = rgb[0];
2031            let green = rgb[1];
2032            let blue = rgb[2];
2033
2034            px[pixel_index * FOUR_BPP] = blue;
2035            px[(pixel_index * FOUR_BPP) + 1] = green;
2036            px[(pixel_index * FOUR_BPP) + 2] = red;
2037            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2038        }
2039
2040        Some((px.into(), true))
2041    }
2042
2043    fn load_rgba8(
2044        pixels: RawImageData,
2045        expected_len: usize,
2046        premultiplied_alpha: bool,
2047    ) -> Option<(U8Vec, bool)> {
2048        let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
2049
2050        if pixels.len() != expected_len * FOUR_CHANNELS {
2051            return None;
2052        }
2053
2054        let mut is_opaque = true;
2055
2056        // TODO: check that this function is SIMD optimized
2057        // no extra allocation necessary, but swizzling
2058        if premultiplied_alpha {
2059            for rgba in pixels.chunks_exact_mut(4) {
2060                let (r, gba) = rgba.split_first_mut()?;
2061                core::mem::swap(r, gba.get_mut(1)?);
2062                let a = rgba.get_mut(3)?;
2063                if *a != 255 {
2064                    is_opaque = false;
2065                }
2066            }
2067        } else {
2068            for rgba in pixels.chunks_exact_mut(4) {
2069                // RGBA => BGRA
2070                let (r, gba) = rgba.split_first_mut()?;
2071                core::mem::swap(r, gba.get_mut(1)?);
2072                let a = rgba.get_mut(3)?;
2073                if *a != 255 {
2074                    is_opaque = false;
2075                }
2076                premultiply_alpha(rgba); // <-
2077            }
2078        }
2079
2080        Some((pixels.into(), is_opaque))
2081    }
2082
2083    fn load_r16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2084        let pixels = pixels.get_u16_vec()?;
2085
2086        if pixels.len() != expected_len {
2087            return None;
2088        }
2089
2090        let mut px = vec![0; expected_len * FOUR_BPP];
2091
2092        // TODO: check that this function is SIMD optimized
2093        for (pixel_index, grey_u16) in pixels.as_ref().iter().enumerate() {
2094            let grey_u8 = normalize_u16(*grey_u16);
2095            px[pixel_index * FOUR_BPP] = grey_u8;
2096            px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
2097            px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
2098            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2099        }
2100
2101        Some((px.into(), true))
2102    }
2103
2104    fn load_rg16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2105        let pixels = pixels.get_u16_vec()?;
2106
2107        if pixels.len() != expected_len * TWO_CHANNELS {
2108            return None;
2109        }
2110
2111        let mut is_opaque = true;
2112        let mut px = vec![0; expected_len * FOUR_BPP];
2113
2114        // TODO: check that this function is SIMD optimized
2115        for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
2116            let grey_u8 = normalize_u16(greyalpha[0]);
2117            let alpha_u8 = normalize_u16(greyalpha[1]);
2118
2119            if alpha_u8 != 255 {
2120                is_opaque = false;
2121            }
2122
2123            px[pixel_index * FOUR_BPP] = grey_u8;
2124            px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
2125            px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
2126            px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2127        }
2128
2129        Some((px.into(), is_opaque))
2130    }
2131
2132    fn load_rgb16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2133        let pixels = pixels.get_u16_vec()?;
2134
2135        if pixels.len() != expected_len * THREE_CHANNELS {
2136            return None;
2137        }
2138
2139        let mut px = vec![0; expected_len * FOUR_BPP];
2140
2141        // TODO: check that this function is SIMD optimized
2142        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2143            let red_u8 = normalize_u16(rgb[0]);
2144            let green_u8 = normalize_u16(rgb[1]);
2145            let blue_u8 = normalize_u16(rgb[2]);
2146
2147            px[pixel_index * FOUR_BPP] = blue_u8;
2148            px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2149            px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2150            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2151        }
2152
2153        Some((px.into(), true))
2154    }
2155
2156    fn load_rgba16(
2157        pixels: RawImageData,
2158        expected_len: usize,
2159        premultiplied_alpha: bool,
2160    ) -> Option<(U8Vec, bool)> {
2161        let pixels = pixels.get_u16_vec()?;
2162
2163        if pixels.len() != expected_len * FOUR_CHANNELS {
2164            return None;
2165        }
2166
2167        let mut is_opaque = true;
2168        let mut px = vec![0; expected_len * FOUR_BPP];
2169
2170        // TODO: check that this function is SIMD optimized
2171        if premultiplied_alpha {
2172            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2173                let red_u8 = normalize_u16(rgba[0]);
2174                let green_u8 = normalize_u16(rgba[1]);
2175                let blue_u8 = normalize_u16(rgba[2]);
2176                let alpha_u8 = normalize_u16(rgba[3]);
2177
2178                if alpha_u8 != 255 {
2179                    is_opaque = false;
2180                }
2181
2182                px[pixel_index * FOUR_BPP] = blue_u8;
2183                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2184                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2185                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2186            }
2187        } else {
2188            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2189                let red_u8 = normalize_u16(rgba[0]);
2190                let green_u8 = normalize_u16(rgba[1]);
2191                let blue_u8 = normalize_u16(rgba[2]);
2192                let alpha_u8 = normalize_u16(rgba[3]);
2193
2194                if alpha_u8 != 255 {
2195                    is_opaque = false;
2196                }
2197
2198                px[pixel_index * FOUR_BPP] = blue_u8;
2199                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2200                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2201                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2202                premultiply_alpha(
2203                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2204                );
2205            }
2206        }
2207
2208        Some((px.into(), is_opaque))
2209    }
2210
2211    fn load_bgr8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2212        let pixels = pixels.get_u8_vec()?;
2213
2214        if pixels.len() != expected_len * THREE_CHANNELS {
2215            return None;
2216        }
2217
2218        let mut px = vec![0; expected_len * FOUR_BPP];
2219
2220        // TODO: check that this function is SIMD optimized
2221        for (pixel_index, bgr) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2222            let blue = bgr[0];
2223            let green = bgr[1];
2224            let red = bgr[2];
2225
2226            px[pixel_index * FOUR_BPP] = blue;
2227            px[(pixel_index * FOUR_BPP) + 1] = green;
2228            px[(pixel_index * FOUR_BPP) + 2] = red;
2229            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2230        }
2231
2232        Some((px.into(), true))
2233    }
2234
2235    fn load_bgra8(
2236        pixels: RawImageData,
2237        expected_len: usize,
2238        premultiplied_alpha: bool,
2239    ) -> Option<(U8Vec, bool)> {
2240        let mut is_opaque = true;
2241
2242        let bytes: U8Vec = if premultiplied_alpha {
2243            // DO NOT CLONE THE IMAGE HERE!
2244            let pixels = pixels.get_u8_vec()?;
2245
2246            if pixels.len() != expected_len * FOUR_BPP {
2247                return None;
2248            }
2249
2250            is_opaque = pixels
2251                .as_ref()
2252                .chunks_exact(FOUR_CHANNELS)
2253                .all(|bgra| bgra[3] == 255);
2254
2255            pixels
2256        } else {
2257            let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
2258
2259            if pixels.len() != expected_len * FOUR_BPP {
2260                return None;
2261            }
2262
2263            for bgra in pixels.chunks_exact_mut(FOUR_CHANNELS) {
2264                if bgra[3] != 255 {
2265                    is_opaque = false;
2266                }
2267                premultiply_alpha(bgra);
2268            }
2269            pixels.into()
2270        };
2271
2272        Some((bytes, is_opaque))
2273    }
2274
2275    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2276    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2277    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
2278    fn load_rgbf32(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2279        let pixels = pixels.get_f32_vec_ref()?;
2280
2281        if pixels.len() != expected_len * THREE_CHANNELS {
2282            return None;
2283        }
2284
2285        let mut px = vec![0; expected_len * FOUR_BPP];
2286
2287        // TODO: check that this function is SIMD optimized
2288        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2289            let red_u8 = (rgb[0] * 255.0) as u8;
2290            let green_u8 = (rgb[1] * 255.0) as u8;
2291            let blue_u8 = (rgb[2] * 255.0) as u8;
2292
2293            px[pixel_index * FOUR_BPP] = blue_u8;
2294            px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2295            px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2296            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2297        }
2298
2299        Some((px.into(), true))
2300    }
2301
2302    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2303    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2304    #[allow(clippy::needless_pass_by_value)] // owned RawImageData taken by value (image decode entry point)
2305    fn load_rgbaf32(
2306        pixels: RawImageData,
2307        expected_len: usize,
2308        premultiplied_alpha: bool,
2309    ) -> Option<(U8Vec, bool)> {
2310        let pixels = pixels.get_f32_vec_ref()?;
2311
2312        if pixels.len() != expected_len * FOUR_CHANNELS {
2313            return None;
2314        }
2315
2316        let mut is_opaque = true;
2317        let mut px = vec![0; expected_len * FOUR_BPP];
2318
2319        // TODO: check that this function is SIMD optimized
2320        if premultiplied_alpha {
2321            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2322                let red_u8 = (rgba[0] * 255.0) as u8;
2323                let green_u8 = (rgba[1] * 255.0) as u8;
2324                let blue_u8 = (rgba[2] * 255.0) as u8;
2325                let alpha_u8 = (rgba[3] * 255.0) as u8;
2326
2327                if alpha_u8 != 255 {
2328                    is_opaque = false;
2329                }
2330
2331                px[pixel_index * FOUR_BPP] = blue_u8;
2332                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2333                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2334                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2335            }
2336        } else {
2337            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2338                let red_u8 = (rgba[0] * 255.0) as u8;
2339                let green_u8 = (rgba[1] * 255.0) as u8;
2340                let blue_u8 = (rgba[2] * 255.0) as u8;
2341                let alpha_u8 = (rgba[3] * 255.0) as u8;
2342
2343                if alpha_u8 != 255 {
2344                    is_opaque = false;
2345                }
2346
2347                px[pixel_index * FOUR_BPP] = blue_u8;
2348                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2349                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2350                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2351                premultiply_alpha(
2352                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2353                );
2354            }
2355        }
2356
2357        Some((px.into(), is_opaque))
2358    }
2359}
2360
2361impl_option!(
2362    RawImage,
2363    OptionRawImage,
2364    copy = false,
2365    [Debug, Clone, PartialEq, PartialOrd]
2366);
2367
2368#[must_use] pub fn font_size_to_au(font_size: StyleFontSize) -> Au {
2369    Au::from_px(font_size.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE))
2370}
2371
2372pub type FontInstanceFlags = u32;
2373
2374// Common flags
2375pub const FONT_INSTANCE_FLAG_SYNTHETIC_BOLD: u32 = 1 << 1;
2376pub const FONT_INSTANCE_FLAG_EMBEDDED_BITMAPS: u32 = 1 << 2;
2377pub const FONT_INSTANCE_FLAG_SUBPIXEL_BGR: u32 = 1 << 3;
2378pub const FONT_INSTANCE_FLAG_TRANSPOSE: u32 = 1 << 4;
2379pub const FONT_INSTANCE_FLAG_FLIP_X: u32 = 1 << 5;
2380pub const FONT_INSTANCE_FLAG_FLIP_Y: u32 = 1 << 6;
2381pub const FONT_INSTANCE_FLAG_SUBPIXEL_POSITION: u32 = 1 << 7;
2382
2383// Windows flags
2384pub const FONT_INSTANCE_FLAG_FORCE_GDI: u32 = 1 << 16;
2385
2386// Mac flags
2387pub const FONT_INSTANCE_FLAG_FONT_SMOOTHING: u32 = 1 << 16;
2388
2389// FreeType flags
2390pub const FONT_INSTANCE_FLAG_FORCE_AUTOHINT: u32 = 1 << 16;
2391pub const FONT_INSTANCE_FLAG_NO_AUTOHINT: u32 = 1 << 17;
2392pub const FONT_INSTANCE_FLAG_VERTICAL_LAYOUT: u32 = 1 << 18;
2393pub const FONT_INSTANCE_FLAG_LCD_VERTICAL: u32 = 1 << 19;
2394
2395#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2396pub struct GlyphOptions {
2397    pub render_mode: FontRenderMode,
2398    pub flags: FontInstanceFlags,
2399}
2400
2401#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2402pub enum FontRenderMode {
2403    Mono,
2404    Alpha,
2405    Subpixel,
2406}
2407
2408#[cfg(target_arch = "wasm32")]
2409#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2410pub struct FontInstancePlatformOptions {
2411    // empty for now
2412}
2413
2414#[cfg(target_os = "windows")]
2415#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2416pub struct FontInstancePlatformOptions {
2417    pub gamma: u16,
2418    pub contrast: u8,
2419    pub cleartype_level: u8,
2420}
2421
2422#[cfg(target_os = "macos")]
2423#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2424pub struct FontInstancePlatformOptions {
2425    pub unused: u32,
2426}
2427
2428#[cfg(target_os = "linux")]
2429#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2430pub struct FontInstancePlatformOptions {
2431    pub lcd_filter: FontLCDFilter,
2432    pub hinting: FontHinting,
2433}
2434
2435// Mobile targets — empty platform-options struct keeps the
2436// `FontInstanceOptions { platform_options: Option<...>, .. }` field
2437// well-typed without inheriting Linux's freetype-specific tunables.
2438#[cfg(any(target_os = "android", target_os = "ios"))]
2439#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2440pub struct FontInstancePlatformOptions {
2441    pub unused: u32,
2442}
2443
2444#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2445pub enum FontHinting {
2446    None,
2447    Mono,
2448    Light,
2449    Normal,
2450    LCD,
2451}
2452
2453#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2454#[derive(Default)]
2455pub enum FontLCDFilter {
2456    None,
2457    #[default]
2458    Default,
2459    Light,
2460    Legacy,
2461}
2462
2463
2464#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2465pub struct FontInstanceOptions {
2466    pub render_mode: FontRenderMode,
2467    pub flags: FontInstanceFlags,
2468    pub bg_color: ColorU,
2469    /// When `bg_color.a` is != 0 and `render_mode` is `FontRenderMode::Subpixel`,
2470    /// the text will be rendered with `bg_color.r/g/b` as an opaque estimated
2471    /// background color.
2472    pub synthetic_italics: SyntheticItalics,
2473}
2474
2475impl Default for FontInstanceOptions {
2476    fn default() -> Self {
2477        Self {
2478            render_mode: FontRenderMode::Subpixel,
2479            flags: 0,
2480            bg_color: ColorU::TRANSPARENT,
2481            synthetic_italics: SyntheticItalics::default(),
2482        }
2483    }
2484}
2485
2486#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2487#[derive(Default)]
2488pub struct SyntheticItalics {
2489    pub angle: i16,
2490}
2491
2492
2493/// Reference-counted wrapper around raw image bytes (`U8Vec`).
2494/// This allows sharing image data between azul-core and webrender without cloning.
2495///
2496/// Similar to `ImageRef` but specifically for raw byte data, avoiding the overhead
2497/// of the full `DecodedImage` enum when we just need the bytes.
2498#[derive(Debug)]
2499#[repr(C)]
2500pub struct SharedRawImageData {
2501    /// Shared pointer to the raw image bytes
2502    pub data: *const U8Vec,
2503    /// Reference counter - when it reaches 0, the data is deallocated
2504    pub copies: *const AtomicUsize,
2505    /// Whether to run the destructor (for FFI safety)
2506    pub run_destructor: bool,
2507}
2508
2509impl SharedRawImageData {
2510    /// Create a new `SharedRawImageData` from a `U8Vec`
2511    #[must_use] pub fn new(data: U8Vec) -> Self {
2512        Self {
2513            data: Box::into_raw(Box::new(data)),
2514            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
2515            run_destructor: true,
2516        }
2517    }
2518
2519    /// Get a reference to the underlying bytes
2520    #[must_use] pub fn as_ref(&self) -> &[u8] {
2521        // SAFETY: `data` is a non-null, live `Box<U8Vec>` owned by this handle (and its
2522        // clones) until the last copy drops; the borrow is tied to `&self`.
2523        unsafe { (*self.data).as_ref() }
2524    }
2525
2526    /// Alias for `as_ref()` - get the raw bytes as a slice
2527    #[must_use] pub fn get_bytes(&self) -> &[u8] {
2528        self.as_ref()
2529    }
2530
2531    /// Get a pointer to the raw bytes for hashing/identification
2532    #[must_use] pub fn as_ptr(&self) -> *const u8 {
2533        // SAFETY: `data` is a non-null, live `Box<U8Vec>` (see `as_ref`).
2534        unsafe { (*self.data).as_ref().as_ptr() }
2535    }
2536
2537    /// Get the length of the data
2538    #[must_use] pub const fn len(&self) -> usize {
2539        // SAFETY: `data` is a non-null, live `Box<U8Vec>` (see `as_ref`).
2540        unsafe { (*self.data).len() }
2541    }
2542
2543    /// Check if the data is empty
2544    #[must_use] pub const fn is_empty(&self) -> bool {
2545        self.len() == 0
2546    }
2547
2548    /// Try to extract the `U8Vec` if this is the only reference
2549    /// Returns None if there are other references
2550    #[must_use] pub fn into_inner(self) -> Option<U8Vec> {
2551        // SAFETY: `data`/`copies` are non-null heap allocations from `Box::into_raw` in
2552        // `new()`. When `copies == 1` we are the sole owner, so reclaiming both Boxes
2553        // and `forget`-ing `self` transfers ownership without a double free.
2554        unsafe {
2555            if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
2556                let data = Box::from_raw(self.data.cast_mut());
2557                drop(Box::from_raw(self.copies.cast_mut()));
2558                core::mem::forget(self); // don't run the destructor
2559                Some(*data)
2560            } else {
2561                None
2562            }
2563        }
2564    }
2565}
2566
2567// SAFETY: the raw pointers only address heap `Box`es of `Send`/`Sync` data, and all
2568// cross-thread refcount mutation goes through the `AtomicUsize` in `copies`.
2569unsafe impl Send for SharedRawImageData {}
2570unsafe impl Sync for SharedRawImageData {}
2571
2572impl Clone for SharedRawImageData {
2573    fn clone(&self) -> Self {
2574        // SAFETY: `copies` is a non-null, live `AtomicUsize` shared by all clones; the
2575        // atomic increment balances the `fetch_sub` in `Drop`.
2576        unsafe {
2577            self.copies
2578                .as_ref()
2579                .map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
2580        }
2581        Self {
2582            data: self.data,
2583            copies: self.copies,
2584            run_destructor: true,
2585        }
2586    }
2587}
2588
2589impl Drop for SharedRawImageData {
2590    fn drop(&mut self) {
2591        self.run_destructor = false;
2592        // SAFETY: `data`/`copies` are non-null, live `Box`es shared by all clones.
2593        // `fetch_sub` returns the pre-decrement count, so `== 1` means we are the last
2594        // owner; only then do we reclaim both Boxes exactly once.
2595        unsafe {
2596            let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
2597            if copies == 1 {
2598                drop(Box::from_raw(self.data.cast_mut()));
2599                drop(Box::from_raw(self.copies.cast_mut()));
2600            }
2601        }
2602    }
2603}
2604
2605impl PartialEq for SharedRawImageData {
2606    fn eq(&self, rhs: &Self) -> bool {
2607        core::ptr::eq(self.data, rhs.data)
2608    }
2609}
2610
2611impl Eq for SharedRawImageData {}
2612
2613impl PartialOrd for SharedRawImageData {
2614    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
2615        Some(self.cmp(other))
2616    }
2617}
2618
2619impl Ord for SharedRawImageData {
2620    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
2621        (self.data as usize).cmp(&(other.data as usize))
2622    }
2623}
2624
2625impl Hash for SharedRawImageData {
2626    fn hash<H>(&self, state: &mut H)
2627    where
2628        H: Hasher,
2629    {
2630        (self.data as usize).hash(state);
2631    }
2632}
2633
2634/// Represents the backing store of an arbitrary series of pixels for display by
2635/// `WebRender`. This storage can take several forms.
2636#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2637#[repr(C, u8)]
2638pub enum ImageData {
2639    /// A simple series of bytes, provided by the embedding and owned by `WebRender`.
2640    /// The format is stored out-of-band, currently in `ImageDescriptor`.
2641    Raw(SharedRawImageData),
2642    /// An image owned by the embedding, and referenced by `WebRender`. This may
2643    /// take the form of a texture or a heap-allocated buffer.
2644    External(ExternalImageData),
2645}
2646
2647/// Storage format identifier for externally-managed images.
2648#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
2649#[repr(C, u8)]
2650pub enum ExternalImageType {
2651    /// The image is texture-backed.
2652    TextureHandle(ImageBufferKind),
2653    /// The image is heap-allocated by the embedding.
2654    Buffer,
2655}
2656
2657/// An arbitrary identifier for an external image provided by the
2658/// application. It must be a unique identifier for each external
2659/// image.
2660#[repr(C)]
2661#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
2662pub struct ExternalImageId {
2663    pub inner: u64,
2664}
2665
2666static LAST_EXTERNAL_IMAGE_ID: AtomicUsize = AtomicUsize::new(0);
2667
2668impl Default for ExternalImageId {
2669    fn default() -> Self {
2670        Self::new()
2671    }
2672}
2673
2674impl ExternalImageId {
2675    /// Creates a new, unique `ExternalImageId`
2676    pub fn new() -> Self {
2677        Self {
2678            inner: LAST_EXTERNAL_IMAGE_ID.fetch_add(1, AtomicOrdering::SeqCst) as u64,
2679        }
2680    }
2681}
2682
2683#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2684#[repr(C, u8)]
2685pub enum GlyphOutlineOperation {
2686    MoveTo(OutlineMoveTo),
2687    LineTo(OutlineLineTo),
2688    QuadraticCurveTo(OutlineQuadTo),
2689    CubicCurveTo(OutlineCubicTo),
2690    ClosePath,
2691}
2692
2693impl_option!(
2694    GlyphOutlineOperation,
2695    OptionGlyphOutlineOperation,
2696    copy = false,
2697    [Debug, Clone, PartialEq, Eq, PartialOrd]
2698);
2699
2700// MoveTo in em units
2701#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2702#[repr(C)]
2703pub struct OutlineMoveTo {
2704    pub x: i16,
2705    pub y: i16,
2706}
2707
2708// LineTo in em units
2709#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2710#[repr(C)]
2711pub struct OutlineLineTo {
2712    pub x: i16,
2713    pub y: i16,
2714}
2715
2716// QuadTo in em units
2717#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2718#[repr(C)]
2719pub struct OutlineQuadTo {
2720    pub ctrl_1_x: i16,
2721    pub ctrl_1_y: i16,
2722    pub end_x: i16,
2723    pub end_y: i16,
2724}
2725
2726// CubicTo in em units
2727#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
2728#[repr(C)]
2729pub struct OutlineCubicTo {
2730    pub ctrl_1_x: i16,
2731    pub ctrl_1_y: i16,
2732    pub ctrl_2_x: i16,
2733    pub ctrl_2_y: i16,
2734    pub end_x: i16,
2735    pub end_y: i16,
2736}
2737
2738#[derive(Debug, Clone, PartialEq, PartialOrd)]
2739#[repr(C)]
2740pub struct GlyphOutline {
2741    pub operations: GlyphOutlineOperationVec,
2742}
2743
2744azul_css::impl_vec!(GlyphOutlineOperation, GlyphOutlineOperationVec, GlyphOutlineOperationVecDestructor, GlyphOutlineOperationVecDestructorType, GlyphOutlineOperationVecSlice, OptionGlyphOutlineOperation);
2745azul_css::impl_vec_clone!(
2746    GlyphOutlineOperation,
2747    GlyphOutlineOperationVec,
2748    GlyphOutlineOperationVecDestructor
2749);
2750azul_css::impl_vec_debug!(GlyphOutlineOperation, GlyphOutlineOperationVec);
2751azul_css::impl_vec_partialord!(GlyphOutlineOperation, GlyphOutlineOperationVec);
2752azul_css::impl_vec_partialeq!(GlyphOutlineOperation, GlyphOutlineOperationVec);
2753
2754#[derive(Debug, Clone, Copy)]
2755#[repr(C)]
2756pub struct OwnedGlyphBoundingBox {
2757    pub max_x: i16,
2758    pub max_y: i16,
2759    pub min_x: i16,
2760    pub min_y: i16,
2761}
2762
2763/// Specifies the type of texture target in driver terms.
2764#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
2765#[repr(C)]
2766pub enum ImageBufferKind {
2767    /// Standard texture. This maps to `GL_TEXTURE_2D` in OpenGL.
2768    Texture2D = 0,
2769    /// Rectangle texture. This maps to `GL_TEXTURE_RECTANGLE` in OpenGL. This
2770    /// is similar to a standard texture, with a few subtle differences
2771    /// (no mipmaps, non-power-of-two dimensions, different coordinate space)
2772    /// that make it useful for representing the kinds of textures we use
2773    /// in `WebRender`. See <https://www.khronos.org/opengl/wiki/Rectangle_Texture>
2774    /// for background on Rectangle textures.
2775    TextureRect = 1,
2776    /// External texture. This maps to `GL_TEXTURE_EXTERNAL_OES` in OpenGL, which
2777    /// is an extension. This is used for image formats that OpenGL doesn't
2778    /// understand, particularly YUV. See
2779    /// <https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt>
2780    TextureExternal = 2,
2781}
2782
2783/// Descriptor for external image resources. See `ImageData`.
2784#[repr(C)]
2785#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
2786pub struct ExternalImageData {
2787    /// The identifier of this external image, provided by the embedding.
2788    pub id: ExternalImageId,
2789    /// For multi-plane images (i.e. YUV), indicates the plane of the
2790    /// original image that this struct represents. 0 for single-plane images.
2791    pub channel_index: u8,
2792    /// Storage format identifier.
2793    pub image_type: ExternalImageType,
2794}
2795
2796pub type TileSize = u16;
2797
2798#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
2799pub enum ImageDirtyRect {
2800    All,
2801    Partial(LayoutRect),
2802}
2803
2804#[derive(Debug, Clone, PartialEq, PartialOrd)]
2805pub enum ResourceUpdate {
2806    AddFont(AddFont),
2807    DeleteFont(FontKey),
2808    AddFontInstance(AddFontInstance),
2809    DeleteFontInstance(FontInstanceKey),
2810    AddImage(AddImage),
2811    UpdateImage(UpdateImage),
2812    DeleteImage(ImageKey),
2813}
2814
2815#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2816pub struct AddImage {
2817    pub key: ImageKey,
2818    pub descriptor: ImageDescriptor,
2819    pub data: ImageData,
2820    pub tiling: Option<TileSize>,
2821}
2822
2823#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
2824pub struct UpdateImage {
2825    pub key: ImageKey,
2826    pub descriptor: ImageDescriptor,
2827    pub data: ImageData,
2828    pub dirty_rect: ImageDirtyRect,
2829}
2830
2831/// Message to add a font to `WebRender`.
2832/// Contains a reference to the parsed font data.
2833#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
2834pub struct AddFont {
2835    pub key: FontKey,
2836    pub font: FontRef,
2837}
2838
2839impl fmt::Debug for AddFont {
2840    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2841        write!(
2842            f,
2843            "AddFont {{ key: {:?}, font: {:?} }}",
2844            self.key, self.font
2845        )
2846    }
2847}
2848
2849#[derive(Debug, Clone, PartialEq, PartialOrd)]
2850pub struct AddFontInstance {
2851    pub key: FontInstanceKey,
2852    pub font_key: FontKey,
2853    pub glyph_size: (Au, DpiScaleFactor),
2854    pub options: Option<FontInstanceOptions>,
2855    pub platform_options: Option<FontInstancePlatformOptions>,
2856    pub variations: Vec<FontVariation>,
2857}
2858
2859#[repr(C)]
2860#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
2861pub struct FontVariation {
2862    pub tag: u32,
2863    pub value: f32,
2864}
2865
2866#[repr(C)]
2867#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
2868pub struct Epoch {
2869    inner: u32,
2870}
2871
2872impl fmt::Display for Epoch {
2873    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2874        write!(f, "{}", self.inner)
2875    }
2876}
2877
2878impl Default for Epoch {
2879    fn default() -> Self {
2880        Self::new()
2881    }
2882}
2883
2884impl Epoch {
2885    // prevent raw access to the .inner field so that
2886    // you can grep the codebase for .increment() to see
2887    // exactly where the epoch is being incremented
2888    #[must_use] pub const fn new() -> Self {
2889        Self { inner: 0 }
2890    }
2891    #[must_use] pub const fn from(i: u32) -> Self {
2892        Self { inner: i }
2893    }
2894    #[must_use] pub const fn into_u32(&self) -> u32 {
2895        self.inner
2896    }
2897
2898    // We don't want the epoch to increase to u32::MAX, since
2899    // u32::MAX represents an invalid epoch, which could confuse webrender
2900    pub const fn increment(&mut self) {
2901        use core::u32;
2902        const MAX_ID: u32 = u32::MAX - 1;
2903        *self = match self.inner {
2904            MAX_ID => Self { inner: 0 },
2905            other => Self {
2906                inner: other.saturating_add(1),
2907            },
2908        };
2909    }
2910}
2911
2912// App units that this font instance was registered for
2913#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)]
2914pub struct Au(pub i32);
2915
2916pub const AU_PER_PX: i32 = 60;
2917pub const MAX_AU: i32 = (1 << 30) - 1;
2918pub const MIN_AU: i32 = -(1 << 30) - 1;
2919
2920impl Au {
2921    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
2922    #[must_use] pub fn from_px(px: f32) -> Self {
2923        let target_app_units = (px * AU_PER_PX as f32) as i32;
2924        Self(target_app_units.clamp(MIN_AU, MAX_AU))
2925    }
2926    #[allow(clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
2927    #[must_use] pub fn into_px(&self) -> f32 {
2928        self.0 as f32 / AU_PER_PX as f32
2929    }
2930}
2931
2932// Debug, PartialEq, Eq, PartialOrd, Ord
2933#[derive(Debug)]
2934pub enum AddFontMsg {
2935    // add font: font key, font bytes + font index
2936    Font(FontKey, StyleFontFamilyHash, FontRef),
2937    Instance(AddFontInstance, (Au, DpiScaleFactor)),
2938}
2939
2940impl AddFontMsg {
2941    #[must_use] pub fn into_resource_update(&self) -> ResourceUpdate {
2942        use self::AddFontMsg::{Font, Instance};
2943        match self {
2944            Font(font_key, _, font_ref) => ResourceUpdate::AddFont(AddFont {
2945                key: *font_key,
2946                font: font_ref.clone(),
2947            }),
2948            Instance(fi, _) => ResourceUpdate::AddFontInstance(fi.clone()),
2949        }
2950    }
2951}
2952
2953#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
2954pub enum DeleteFontMsg {
2955    Font(FontKey),
2956    Instance(FontInstanceKey, (Au, DpiScaleFactor)),
2957}
2958
2959impl DeleteFontMsg {
2960    #[must_use] pub const fn into_resource_update(&self) -> ResourceUpdate {
2961        use self::DeleteFontMsg::{Font, Instance};
2962        match self {
2963            Font(f) => ResourceUpdate::DeleteFont(*f),
2964            Instance(fi, _) => ResourceUpdate::DeleteFontInstance(*fi),
2965        }
2966    }
2967}
2968
2969#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
2970pub struct AddImageMsg(pub AddImage);
2971
2972impl AddImageMsg {
2973    #[must_use] pub fn into_resource_update(&self) -> ResourceUpdate {
2974        ResourceUpdate::AddImage(self.0.clone())
2975    }
2976}
2977
2978#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2979#[repr(C)]
2980pub struct LoadedFontSource {
2981    pub data: U8Vec,
2982    pub index: u32,
2983    pub load_outlines: bool,
2984}
2985
2986// function to load the font source from a file
2987pub type LoadFontFn = fn(&StyleFontFamily, &FcFontCache) -> Option<LoadedFontSource>;
2988
2989// function to parse the font given the loaded font source
2990pub type ParseFontFn = fn(LoadedFontSource) -> Option<FontRef>; // = Option<Box<azul_text_layout::Font>>
2991
2992pub type GlStoreImageFn = fn(DocumentId, Epoch, Texture, ExternalImageId);
2993
2994/// Compute the deterministic `ExternalImageId` that the OpenGL texture cache uses
2995/// for a texture bound to a specific DOM node.
2996///
2997/// The same `(DomId, NodeId)` always
2998/// maps to the same `ExternalImageId`, so cached display lists keep working across
2999/// frames.
3000#[must_use] pub fn texture_external_image_id(dom_id: DomId, node_id: NodeId) -> ExternalImageId {
3001    let dom = dom_id.inner as u64;
3002    let node = node_id.index() as u64;
3003    debug_assert!(u32::try_from(dom).is_ok(), "DomId exceeds 32-bit range");
3004    debug_assert!(u32::try_from(node).is_ok(), "NodeId exceeds 32-bit range");
3005    ExternalImageId {
3006        inner: (dom << 32) | (node & 0xFFFF_FFFF),
3007    }
3008}
3009
3010/// Compute the `ExternalImageId` for a static GL texture identified by its
3011/// `ImageRefHash`. Mirrors `image_ref_hash_to_image_key` so a given image hash
3012/// produces the same identifiers everywhere.
3013#[must_use] pub const fn image_ref_hash_to_external_image_id(hash: ImageRefHash) -> ExternalImageId {
3014    ExternalImageId {
3015        inner: hash.inner,
3016    }
3017}
3018
3019/// Given the fonts of the current frame, returns `AddFont` and `AddFontInstance`s of
3020/// which fonts / instances are currently not in the `current_registered_fonts` and
3021/// need to be added.
3022///
3023/// Deleting fonts can only be done after the entire frame has finished drawing,
3024/// otherwise (if removing fonts would happen after every DOM) we'd constantly
3025/// add-and-remove fonts after every `VirtualViewCallback`, which would cause a lot of
3026/// I/O waiting.
3027#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3028pub fn build_add_font_resource_updates(
3029    renderer_resources: &mut RendererResources,
3030    dpi: DpiScaleFactor,
3031    fc_cache: &FcFontCache,
3032    id_namespace: IdNamespace,
3033    fonts_in_dom: &OrderedMap<ImmediateFontId, FastBTreeSet<Au>>,
3034    font_source_load_fn: LoadFontFn,
3035    parse_font_fn: ParseFontFn,
3036) -> Vec<(StyleFontFamilyHash, AddFontMsg)> {
3037    let mut resource_updates = Vec::new();
3038    let mut font_instances_added_this_frame = FastBTreeSet::new();
3039
3040    'outer: for (im_font_id, font_sizes) in fonts_in_dom {
3041        macro_rules! insert_font_instances {
3042            ($font_family_hash:expr, $font_key:expr, $font_size:expr) => {{
3043                let font_instance_key_exists = renderer_resources
3044                    .currently_registered_fonts
3045                    .get(&$font_key)
3046                    .and_then(|(_, font_instances)| font_instances.get(&($font_size, dpi)))
3047                    .is_some()
3048                    || font_instances_added_this_frame.contains(&($font_key, ($font_size, dpi)));
3049
3050                if !font_instance_key_exists {
3051                    let font_instance_key = FontInstanceKey::unique(id_namespace);
3052
3053                    // For some reason the gamma is way to low on Windows
3054                    #[cfg(target_os = "windows")]
3055                    let platform_options = FontInstancePlatformOptions {
3056                        gamma: 300,
3057                        contrast: 100,
3058                        cleartype_level: 100,
3059                    };
3060
3061                    #[cfg(target_os = "linux")]
3062                    let platform_options = FontInstancePlatformOptions {
3063                        lcd_filter: FontLCDFilter::Default,
3064                        hinting: FontHinting::Normal,
3065                    };
3066
3067                    #[cfg(target_os = "macos")]
3068                    let platform_options = FontInstancePlatformOptions::default();
3069
3070                    #[cfg(target_arch = "wasm32")]
3071                    let platform_options = FontInstancePlatformOptions::default();
3072
3073                    #[cfg(any(target_os = "android", target_os = "ios"))]
3074                    let platform_options = FontInstancePlatformOptions::default();
3075
3076                    let options = FontInstanceOptions {
3077                        render_mode: FontRenderMode::Subpixel,
3078                        flags: FONT_INSTANCE_FLAG_NO_AUTOHINT,
3079                        ..Default::default()
3080                    };
3081
3082                    font_instances_added_this_frame.insert(($font_key, ($font_size, dpi)));
3083                    resource_updates.push((
3084                        $font_family_hash,
3085                        AddFontMsg::Instance(
3086                            AddFontInstance {
3087                                key: font_instance_key,
3088                                font_key: $font_key,
3089                                glyph_size: ($font_size, dpi),
3090                                options: Some(options),
3091                                platform_options: Some(platform_options),
3092                                variations: alloc::vec::Vec::new(),
3093                            },
3094                            ($font_size, dpi),
3095                        ),
3096                    ));
3097                }
3098            }};
3099        }
3100
3101        match im_font_id {
3102            ImmediateFontId::Resolved((font_family_hash, font_id)) => {
3103                // nothing to do, font is already added,
3104                // just insert the missing font instances
3105                for font_size in font_sizes {
3106                    insert_font_instances!(*font_family_hash, *font_id, *font_size);
3107                }
3108            }
3109            ImmediateFontId::Unresolved(style_font_families) => {
3110                // If the font is already loaded during the current frame,
3111                // do not attempt to load it again
3112                //
3113                // This prevents duplicated loading for fonts in different orders, i.e.
3114                // - vec!["Times New Roman", "serif"] and
3115                // - vec!["sans", "Times New Roman"]
3116                // ... will resolve to the same font instead of creating two fonts
3117
3118                // If there is no font key, that means there's also no font instances
3119                let mut font_family_hash = None;
3120                let font_families_hash = StyleFontFamiliesHash::new(style_font_families.as_ref());
3121
3122                // Find the first font that can be loaded and parsed
3123                'inner: for family in style_font_families.as_ref() {
3124                    let current_family_hash = StyleFontFamilyHash::new(family);
3125
3126                    if let Some(font_id) = renderer_resources.font_id_map.get(&current_family_hash)
3127                    {
3128                        // font key already exists
3129                        for font_size in font_sizes {
3130                            insert_font_instances!(current_family_hash, *font_id, *font_size);
3131                        }
3132                        continue 'outer;
3133                    }
3134
3135                    let font_ref = match family {
3136                        StyleFontFamily::Ref(r) => r.clone(), // Clone the FontRef
3137                        other => {
3138                            // Load and parse the font
3139                            let Some(font_data) = (font_source_load_fn)(other, fc_cache) else {
3140                                continue 'inner;
3141                            };
3142
3143                            
3144
3145                            match (parse_font_fn)(font_data) {
3146                                Some(s) => s,
3147                                None => continue 'inner,
3148                            }
3149                        }
3150                    };
3151
3152                    // font loaded properly
3153                    font_family_hash = Some((current_family_hash, font_ref));
3154                    break 'inner;
3155                }
3156
3157                let (font_family_hash, font_ref) = match font_family_hash {
3158                    None => continue 'outer, // No font could be loaded, try again next frame
3159                    Some(s) => s,
3160                };
3161
3162                // Generate a new font key, store the mapping between hash and font key
3163                let font_key = FontKey::unique(id_namespace);
3164                let add_font_msg = AddFontMsg::Font(font_key, font_family_hash, font_ref);
3165
3166                renderer_resources
3167                    .font_id_map
3168                    .insert(font_family_hash, font_key);
3169                renderer_resources
3170                    .font_families_map
3171                    .insert(font_families_hash, font_family_hash);
3172                resource_updates.push((font_family_hash, add_font_msg));
3173
3174                // Insert font sizes for the newly generated font key
3175                for font_size in font_sizes {
3176                    insert_font_instances!(font_family_hash, font_key, *font_size);
3177                }
3178            }
3179        }
3180    }
3181
3182    resource_updates
3183}
3184
3185/// Given the images of the current frame, returns `AddImage`s of
3186/// which image keys are currently not in the `current_registered_images` and
3187/// need to be added.
3188///
3189/// Returns Vec<(`ImageRefHash`, `AddImageMsg`)> where:
3190/// - `ImageRefHash`: Stable hash of the `ImageRef` pointer
3191/// - `AddImageMsg`: Message to add the image to `WebRender`
3192///
3193/// The `ImageKey` in `AddImageMsg` is generated directly from the `ImageRefHash` using
3194/// `image_ref_hash_to_image_key()`, so no separate mapping table is needed.
3195///
3196/// Deleting images can only be done after the entire frame has finished drawing,
3197/// otherwise (if removing images would happen after every DOM) we'd constantly
3198/// add-and-remove images after every `VirtualViewCallback`, which would cause a lot of
3199/// I/O waiting.
3200#[allow(unused_variables)]
3201pub fn build_add_image_resource_updates(
3202    renderer_resources: &RendererResources,
3203    id_namespace: IdNamespace,
3204    epoch: Epoch,
3205    document_id: &DocumentId,
3206    images_in_dom: &FastBTreeSet<ImageRef>,
3207    insert_into_active_gl_textures: GlStoreImageFn,
3208) -> Vec<(ImageRefHash, AddImageMsg)> {
3209    images_in_dom
3210        .iter()
3211        .filter_map(|image_ref| {
3212            let image_ref_hash = image_ref_get_hash(image_ref);
3213
3214            if renderer_resources
3215                .currently_registered_images
3216                .contains_key(&image_ref_hash)
3217            {
3218                return None;
3219            }
3220
3221            // NOTE: The image_ref.clone() is a shallow clone,
3222            // does not actually clone the data
3223            match image_ref.get_data() {
3224                DecodedImage::Gl(texture) => {
3225                    let descriptor = texture.get_descriptor();
3226                    let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
3227                    // The ExternalImageId is derived from the same stable hash that
3228                    // produces the ImageKey, so the GL texture cache and WebRender
3229                    // agree on a single identifier for this texture.
3230                    let external_image_id = image_ref_hash_to_external_image_id(image_ref_hash);
3231                    // NOTE: The texture is not really cloned here,
3232                    (insert_into_active_gl_textures)(
3233                        *document_id,
3234                        epoch,
3235                        texture.clone(),
3236                        external_image_id,
3237                    );
3238                    Some((
3239                        image_ref_hash,
3240                        AddImageMsg(AddImage {
3241                            key,
3242                            data: ImageData::External(ExternalImageData {
3243                                id: external_image_id,
3244                                channel_index: 0,
3245                                image_type: ExternalImageType::TextureHandle(
3246                                    ImageBufferKind::Texture2D,
3247                                ),
3248                            }),
3249                            descriptor,
3250                            tiling: None,
3251                        }),
3252                    ))
3253                }
3254                DecodedImage::Raw((descriptor, data)) => {
3255                    let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
3256                    Some((
3257                        image_ref_hash,
3258                        AddImageMsg(AddImage {
3259                            key,
3260                            data: data.clone(), // deep-copy except in the &'static case
3261                            descriptor: *descriptor, /* deep-copy, but struct is not very
3262                                                 * large */
3263                            tiling: None,
3264                        }),
3265                    ))
3266                }
3267                // NullImage has nothing to upload; texture callbacks are handled after
3268                // layout is done.
3269                DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => None,
3270            }
3271        })
3272        .collect()
3273}
3274
3275/// Submits the `AddFont`, `AddFontInstance` and `AddImage` resources to the `RenderApi`.
3276///
3277/// Extends `currently_registered_images` and `currently_registered_fonts` by the
3278/// `last_frame_image_keys` and `last_frame_font_keys`, so that we don't lose track of
3279/// what font and image keys are currently in the API.
3280#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
3281pub fn add_resources(
3282    renderer_resources: &mut RendererResources,
3283    all_resource_updates: &mut Vec<ResourceUpdate>,
3284    add_font_resources: Vec<(StyleFontFamilyHash, AddFontMsg)>,
3285    add_image_resources: Vec<(ImageRefHash, AddImageMsg)>,
3286) {
3287    all_resource_updates.extend(
3288        add_font_resources
3289            .iter()
3290            .map(|(_, f)| f.into_resource_update()),
3291    );
3292    all_resource_updates.extend(
3293        add_image_resources
3294            .iter()
3295            .map(|(_, i)| i.into_resource_update()),
3296    );
3297
3298    for (image_ref_hash, add_image_msg) in &add_image_resources {
3299        renderer_resources.currently_registered_images.insert(
3300            *image_ref_hash,
3301            ResolvedImage {
3302                key: add_image_msg.0.key,
3303                descriptor: add_image_msg.0.descriptor,
3304            },
3305        );
3306        // Keep the reverse lookup (`ImageKey` -> `ImageRefHash`) in sync with the
3307        // forward map so display-list translation can resolve keys back to hashes.
3308        renderer_resources
3309            .image_key_map
3310            .insert(add_image_msg.0.key, *image_ref_hash);
3311    }
3312
3313    for (_, add_font_msg) in add_font_resources {
3314        use self::AddFontMsg::{Font, Instance};
3315        match add_font_msg {
3316            Font(fk, font_family_hash, font_ref) => {
3317                renderer_resources
3318                    .currently_registered_fonts
3319                    .entry(fk)
3320                    .or_insert_with(|| (font_ref.clone(), OrderedMap::default()));
3321
3322                // CRITICAL: Map font_hash to FontKey so we can look it up during rendering
3323                renderer_resources
3324                    .font_hash_map
3325                    .insert(font_ref.get_hash(), fk);
3326            }
3327            Instance(fi, size) => {
3328                if let Some((_, instances)) = renderer_resources
3329                    .currently_registered_fonts
3330                    .get_mut(&fi.font_key)
3331                {
3332                    instances.insert(size, fi.key);
3333                }
3334            }
3335        }
3336    }
3337}
3338
3339#[cfg(test)]
3340#[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
3341mod tests {
3342    use super::*;
3343
3344    #[test]
3345    fn normalize_u16_maps_full_range() {
3346        // 0 -> 0, u16::MAX -> u8::MAX, midpoint -> ~127/128, no div-by-zero.
3347        assert_eq!(normalize_u16(0), 0);
3348        assert_eq!(normalize_u16(u16::MAX), 255);
3349        // Half of u16::MAX should land at ~half of u8::MAX.
3350        let mid = normalize_u16(u16::MAX / 2);
3351        assert!((126..=128).contains(&mid), "midpoint normalized to {mid}");
3352        // Previously `(65535/i)*255` produced near-white garbage for small i;
3353        // a small input must now map to a small output.
3354        assert_eq!(normalize_u16(256), 0);
3355        assert_eq!(normalize_u16(257), 1);
3356    }
3357
3358    #[test]
3359    fn load_bgra8_rejects_wrong_length() {
3360        // premultiplied branch: buffer shorter than expected must be rejected,
3361        // not silently accepted (previously missing length guard).
3362        let short = RawImageData::U8(vec![0u8; 4 * 3].into()); // 3 px worth
3363        assert!(RawImage::load_bgra8(short, 4, true).is_none());
3364
3365        // correct length is accepted.
3366        let ok = RawImageData::U8(vec![255u8; 4 * 4].into()); // 4 px
3367        assert!(RawImage::load_bgra8(ok, 4, true).is_some());
3368
3369        // non-premultiplied branch still rejects wrong length.
3370        let short2 = RawImageData::U8(vec![0u8; 4 * 2].into());
3371        assert!(RawImage::load_bgra8(short2, 4, false).is_none());
3372    }
3373
3374    // --- unsafe-hardening tests (Miri-compatible: pure in-memory, no FFI/GL) ---
3375
3376    #[test]
3377    fn imageref_get_data_reads_backing_box() {
3378        // Exercises the `&*self.data` raw-pointer deref in `get_data`.
3379        let img = ImageRef::null_image(2, 3, RawImageFormat::RGBA8, vec![7, 8]);
3380        match img.get_data() {
3381            DecodedImage::NullImage { width, height, tag, .. } => {
3382                assert_eq!((*width, *height), (2, 3));
3383                assert_eq!(tag.as_slice(), &[7, 8]);
3384            }
3385            _ => panic!("expected NullImage"),
3386        }
3387    }
3388
3389    #[test]
3390    fn imageref_clone_shares_refcount_and_identity() {
3391        // Clone must bump the shared AtomicUsize (so `into_inner` refuses while a
3392        // second copy is alive) and preserve the never-reused identity `id`.
3393        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
3394        let c = img.clone();
3395        assert_eq!(img, c); // same id -> shallow clone
3396        // Two live copies: sole-owner extraction must fail (and `c` drops cleanly).
3397        assert!(c.into_inner().is_none());
3398        // Back to one owner: extraction now succeeds, forgetting `self` without leak.
3399        assert!(img.into_inner().is_some());
3400    }
3401
3402    #[test]
3403    fn imageref_deep_copy_has_distinct_identity() {
3404        // deep_copy allocates a fresh backing Box + fresh id -> not equal, independent
3405        // drop (Miri would flag any shared/double-freed allocation here).
3406        let img = ImageRef::null_image(4, 4, RawImageFormat::RGBA8, vec![1]);
3407        let d = img.deep_copy();
3408        assert_ne!(img, d);
3409        drop(img);
3410        // `d` still valid and independently readable after `img` freed.
3411        assert_eq!(d.get_size().width as usize, 4);
3412    }
3413
3414    #[test]
3415    fn imageref_last_drop_frees_once() {
3416        // Clone then drop both: the refcount path must free the two Boxes exactly once
3417        // on the final drop. Under Miri a double free / leak fails the test.
3418        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
3419        let c = img.clone();
3420        drop(img);
3421        drop(c);
3422    }
3423
3424    #[test]
3425    fn imageref_get_callback_none_for_non_callback_and_when_shared() {
3426        // `get_image_callback` derefs both `copies` and `data`; a NullImage yields None,
3427        // and a shared (copies != 1) handle also yields None.
3428        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
3429        assert!(img.get_image_callback().is_none());
3430        let c = img.clone();
3431        assert!(img.get_image_callback().is_none()); // shared -> not safe
3432        drop(c);
3433    }
3434
3435    #[test]
3436    fn shared_raw_image_data_read_paths() {
3437        // Exercises as_ref / len / is_empty / as_ptr raw-pointer derefs.
3438        let s = SharedRawImageData::new(vec![10u8, 20, 30].into());
3439        assert_eq!(s.as_ref(), &[10, 20, 30]);
3440        assert_eq!(s.len(), 3);
3441        assert!(!s.is_empty());
3442        assert_eq!(unsafe { *s.as_ptr() }, 10);
3443        assert!(SharedRawImageData::new(Vec::<u8>::new().into()).is_empty());
3444    }
3445
3446    #[test]
3447    fn shared_raw_image_data_clone_shares_alloc() {
3448        // Clone shares the backing Box (ptr-equal) and refcount; `into_inner` refuses
3449        // while a second copy lives, and succeeds once sole owner.
3450        let s = SharedRawImageData::new(vec![1u8, 2, 3, 4].into());
3451        let c = s.clone();
3452        assert_eq!(s, c); // ptr::eq on the shared `data`
3453        assert_eq!(s.as_ptr(), c.as_ptr());
3454        assert!(c.into_inner().is_none()); // two owners -> None, `c` drops to refcount 1
3455        let inner = s.into_inner().expect("sole owner extraction");
3456        assert_eq!(inner.as_ref(), &[1, 2, 3, 4]);
3457    }
3458
3459    #[test]
3460    fn shared_raw_image_data_last_drop_frees_once() {
3461        // Refcounted drop must free both Boxes exactly once; Miri flags UB otherwise.
3462        let s = SharedRawImageData::new(vec![0u8; 8].into());
3463        let c = s.clone();
3464        drop(s);
3465        drop(c);
3466    }
3467}
3468
3469#[cfg(test)]
3470#[allow(
3471    clippy::float_cmp,
3472    clippy::items_after_statements,
3473    clippy::redundant_clone,
3474    clippy::cast_possible_truncation,
3475    clippy::cast_precision_loss,
3476    clippy::cast_sign_loss,
3477    clippy::cast_lossless,
3478    clippy::unreadable_literal,
3479    clippy::too_many_lines,
3480    clippy::many_single_char_names,
3481    clippy::similar_names,
3482    unused_qualifications,
3483    unreachable_pub,
3484    private_interfaces
3485)] // pedantic lints are noise in adversarial test code
3486mod autotest_generated {
3487    use alloc::string::String;
3488
3489    use super::*;
3490
3491    // ---------------------------------------------------------------------
3492    // helpers
3493    // ---------------------------------------------------------------------
3494
3495    /// A `FontRef` whose `parsed` pointer addresses a `'static` byte and whose
3496    /// destructor is a no-op, so nothing is freed on drop. Sound because
3497    /// `FontRef`'s `Hash`/`get_hash` only read the never-reused `id` and never
3498    /// dereference `parsed`.
3499    fn dummy_font_ref() -> FontRef {
3500        static DUMMY_FONT_DATA: u8 = 0;
3501        extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
3502        FontRef::new(
3503            core::ptr::addr_of!(DUMMY_FONT_DATA).cast::<core::ffi::c_void>(),
3504            dummy_destructor,
3505        )
3506    }
3507
3508    /// `LoadFontFn` that never resolves a font (simulates a missing font file).
3509    fn load_font_none(_: &StyleFontFamily, _: &FcFontCache) -> Option<LoadedFontSource> {
3510        None
3511    }
3512
3513    /// `ParseFontFn` that never parses (simulates a corrupt font file).
3514    fn parse_font_none(_: LoadedFontSource) -> Option<FontRef> {
3515        None
3516    }
3517
3518    /// `GlStoreImageFn` no-op: never invoked for raw / null / callback images.
3519    fn store_gl_texture_noop(_: DocumentId, _: Epoch, _: Texture, _: ExternalImageId) {}
3520
3521    fn test_document_id() -> DocumentId {
3522        DocumentId {
3523            namespace_id: IdNamespace(7),
3524            id: 0,
3525        }
3526    }
3527
3528    /// An `RGBA8` image of `w * h` transparent-black pixels.
3529    fn rgba8_image(w: usize, h: usize) -> RawImage {
3530        RawImage {
3531            pixels: RawImageData::U8(vec![0u8; w * h * 4].into()),
3532            width: w,
3533            height: h,
3534            premultiplied_alpha: true,
3535            data_format: RawImageFormat::RGBA8,
3536            tag: Vec::new().into(),
3537        }
3538    }
3539
3540    fn opaque_red() -> ColorU {
3541        ColorU {
3542            r: 255,
3543            g: 0,
3544            b: 0,
3545            a: 255,
3546        }
3547    }
3548
3549    // =====================================================================
3550    // PARSERS: match_route / RouteMatch::get_param / AppConfig::match_route_for_path
3551    // =====================================================================
3552
3553    #[test]
3554    fn match_route_valid_minimal_positive_control() {
3555        // Documented examples must hold (positive control).
3556        let m = match_route("/user/:id", "/user/42").expect("documented example must match");
3557        assert_eq!(m.pattern.as_str(), "/user/:id");
3558        assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
3559
3560        let root = match_route("/", "/").expect("root must match root");
3561        assert!(root.params.as_ref().is_empty());
3562
3563        assert!(match_route("/about", "/settings").is_none());
3564    }
3565
3566    #[test]
3567    fn match_route_empty_input_does_not_panic() {
3568        // Empty pattern/path degrade to zero segments; the segment-count check
3569        // makes "" and "/" equivalent (both filter to no segments).
3570        let m = match_route("", "").expect("empty vs empty is a zero-segment match");
3571        assert!(m.params.as_ref().is_empty());
3572        assert!(match_route("", "/").is_some()); // "/" also has zero segments
3573        assert!(match_route("/", "").is_some());
3574        assert!(match_route("", "/a").is_none()); // 0 segments != 1 segment
3575        assert!(match_route("/a", "").is_none());
3576    }
3577
3578    #[test]
3579    fn match_route_whitespace_only_is_not_trimmed() {
3580        // Whitespace is NOT trimmed: it is an ordinary (opaque) path segment,
3581        // so it only matches itself. Deterministic, no panic.
3582        assert!(match_route("   ", "   ").is_some());
3583        assert!(match_route("   ", "\t\n").is_none());
3584        assert!(match_route("/ ", "/").is_none()); // " " is a real segment
3585        assert!(match_route("/\t\n", "/\t\n").is_some());
3586    }
3587
3588    #[test]
3589    fn match_route_garbage_never_panics() {
3590        for pat in [
3591            "\0\0\0",
3592            "///////",
3593            "::::",
3594            ":",
3595            "%%%$#@!",
3596            "\u{feff}",
3597            "/a/../../etc/passwd",
3598        ] {
3599            for path in ["", "/", "\0", "/a/b/c", "%%%$#@!", "\u{feff}"] {
3600                // Only requirement: a total function that never panics.
3601                let _ = match_route(pat, path);
3602            }
3603        }
3604        // "///////" collapses to zero segments, so it matches the root.
3605        assert!(match_route("///////", "/").is_some());
3606        // A bare ":" is a param with an EMPTY name; the value is still captured.
3607        let m = match_route("/:", "/hello").expect("empty param name still matches");
3608        assert_eq!(m.get_param("").map(AzString::as_str), Some("hello"));
3609    }
3610
3611    #[test]
3612    fn match_route_leading_trailing_junk_is_rejected_or_ignored() {
3613        // Trailing slashes produce empty segments that are filtered out, so a
3614        // trailing slash is ignored (deterministic).
3615        assert!(match_route("/user/:id/", "/user/42").is_some());
3616        assert!(match_route("/user/:id", "/user/42/").is_some());
3617        // Surrounding spaces are part of the segment -> rejected.
3618        assert!(match_route(" /about ", "/about").is_none());
3619        assert!(match_route("/about", "/about;garbage").is_none());
3620    }
3621
3622    #[test]
3623    fn match_route_boundary_number_strings_are_opaque_segments() {
3624        // Numeric-looking params are never parsed; they round-trip verbatim.
3625        for v in [
3626            "0",
3627            "-0",
3628            "9223372036854775807",
3629            "-9223372036854775808",
3630            "1e400",
3631            "NaN",
3632            "inf",
3633            "-inf",
3634            "0.0000000000000000001",
3635        ] {
3636            let path = String::from("/user/") + v;
3637            let m = match_route("/user/:id", &path).expect("any segment matches a :param");
3638            assert_eq!(m.get_param("id").map(AzString::as_str), Some(v));
3639        }
3640    }
3641
3642    #[test]
3643    fn match_route_unicode_multibyte_does_not_panic() {
3644        // Splitting on '/' is byte-safe for UTF-8; multibyte segments survive.
3645        let m = match_route("/user/:id", "/user/\u{1F600}").expect("emoji segment matches");
3646        assert_eq!(m.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
3647
3648        // Combining marks + RTL + a unicode param NAME.
3649        let m = match_route("/:é\u{0301}", "/e\u{0301}\u{202E}x").expect("unicode param name");
3650        assert_eq!(
3651            m.get_param("é\u{0301}").map(AzString::as_str),
3652            Some("e\u{0301}\u{202E}x")
3653        );
3654        // A unicode segment does not equal its NFC/NFD-different twin.
3655        assert!(match_route("/é", "/e\u{0301}").is_none());
3656    }
3657
3658    #[test]
3659    fn match_route_extremely_long_input_does_not_hang() {
3660        // 1M-char single segment: linear split, no quadratic blowup / panic.
3661        let huge = String::from("/") + &"a".repeat(1_000_000);
3662        assert!(match_route("/x", &huge).is_none());
3663        let m = match_route("/:id", &huge).expect("one long segment is still one segment");
3664        assert_eq!(m.get_param("id").map(|s| s.as_str().len()), Some(1_000_000));
3665    }
3666
3667    #[test]
3668    fn match_route_deeply_nested_input_does_not_stack_overflow() {
3669        // match_route is iterative: 10k segments and 10k nested brackets are fine.
3670        let deep = "/a".repeat(10_000);
3671        let m = match_route(&deep, &deep).expect("identical deep paths match");
3672        assert!(m.params.as_ref().is_empty());
3673
3674        let all_params = "/:p".repeat(10_000);
3675        let m = match_route(&all_params, &deep).expect("10k params extract");
3676        assert_eq!(m.params.as_ref().len(), 10_000);
3677        // Duplicate keys: get_param returns the FIRST binding.
3678        assert_eq!(m.get_param("p").map(AzString::as_str), Some("a"));
3679
3680        let brackets = String::from("/") + &"[".repeat(10_000);
3681        assert!(match_route("/:x", &brackets).is_some());
3682    }
3683
3684    #[test]
3685    fn match_route_segment_count_mismatch_is_none() {
3686        assert!(match_route("/a/:b", "/a").is_none());
3687        assert!(match_route("/a", "/a/b").is_none());
3688        assert!(match_route("/:a/:b/:c", "/1/2").is_none());
3689    }
3690
3691    #[test]
3692    fn route_match_get_param_missing_keys_return_none() {
3693        let empty = RouteMatch {
3694            pattern: AzString::from_const_str("/"),
3695            params: StringPairVec::from_vec(Vec::new()),
3696        };
3697        // Empty / whitespace / garbage / unicode / huge keys: None, never a panic.
3698        assert!(empty.get_param("").is_none());
3699        assert!(empty.get_param("   ").is_none());
3700        assert!(empty.get_param("\t\n").is_none());
3701        assert!(empty.get_param("\u{1F600}").is_none());
3702        assert!(empty.get_param("\0").is_none());
3703        assert!(empty.get_param(&"k".repeat(100_000)).is_none());
3704
3705        // Positive control + near-miss keys on a populated match.
3706        let m = match_route("/u/:id", "/u/7").expect("valid");
3707        assert_eq!(m.get_param("id").map(AzString::as_str), Some("7"));
3708        assert!(m.get_param("ID").is_none()); // case-sensitive
3709        assert!(m.get_param("i").is_none()); // no prefix matching
3710        assert!(m.get_param(":id").is_none()); // the ':' is stripped from the key
3711    }
3712
3713    #[test]
3714    fn app_config_match_route_for_path_adversarial_inputs() {
3715        let mut config = AppConfig::create();
3716        let cb: crate::callbacks::LayoutCallbackType = autotest_layout;
3717        extern "C" fn autotest_layout(
3718            _: RefAny,
3719            _: crate::callbacks::LayoutCallbackInfo,
3720        ) -> crate::dom::Dom {
3721            crate::dom::Dom::create_body()
3722        }
3723        config.add_route(AzString::from_const_str("/"), cb);
3724        config.add_route(AzString::from_const_str("/user/:id"), cb);
3725
3726        // valid_minimal (positive control)
3727        let (route, m) = config
3728            .match_route_for_path("/user/42")
3729            .expect("registered route must match");
3730        assert_eq!(route.pattern.as_str(), "/user/:id");
3731        assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
3732
3733        // "" and "/" both have zero segments -> they hit the "/" route.
3734        assert!(config.match_route_for_path("").is_some());
3735        assert!(config.match_route_for_path("/").is_some());
3736
3737        // garbage / unicode / long / whitespace: deterministic, never panics.
3738        assert!(config.match_route_for_path("/nope/nope/nope").is_none());
3739        assert!(config.match_route_for_path("\0\0").is_none());
3740        assert!(config.match_route_for_path("   ").is_none());
3741        let long = String::from("/user/") + &"9".repeat(500_000);
3742        assert!(config.match_route_for_path(&long).is_some());
3743        let m = config
3744            .match_route_for_path("/user/\u{1F600}")
3745            .expect("unicode param");
3746        assert_eq!(m.1.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
3747    }
3748
3749    #[test]
3750    fn app_config_add_route_replaces_same_pattern_and_orders_by_insertion() {
3751        extern "C" fn layout_a(_: RefAny, _: crate::callbacks::LayoutCallbackInfo) -> crate::dom::Dom {
3752            crate::dom::Dom::create_body()
3753        }
3754        let cb: crate::callbacks::LayoutCallbackType = layout_a;
3755
3756        let mut config = AppConfig::create();
3757        assert!(config.routes.as_ref().is_empty());
3758        config.add_route(AzString::from_const_str("/dup"), cb);
3759        config.add_route(AzString::from_const_str("/dup"), cb);
3760        assert_eq!(config.routes.as_ref().len(), 1, "same pattern must replace");
3761
3762        // First matching route wins: a catch-all registered first shadows later routes.
3763        let mut config = AppConfig::create();
3764        config.add_route(AzString::from_const_str("/:anything"), cb);
3765        config.add_route(AzString::from_const_str("/about"), cb);
3766        let (route, _) = config.match_route_for_path("/about").expect("matches");
3767        assert_eq!(route.pattern.as_str(), "/:anything");
3768    }
3769
3770    // =====================================================================
3771    // CONSTRUCTORS / INVARIANTS
3772    // =====================================================================
3773
3774    #[test]
3775    fn dpi_scale_factor_new_handles_nan_and_infinities() {
3776        // FloatValue::new does a saturating f32 -> isize cast (NaN -> 0).
3777        assert_eq!(DpiScaleFactor::new(0.0).inner.get(), 0.0);
3778        assert_eq!(DpiScaleFactor::new(1.0).inner.get(), 1.0);
3779        assert_eq!(DpiScaleFactor::new(f32::NAN).inner.get(), 0.0);
3780        assert!(DpiScaleFactor::new(f32::INFINITY).inner.get().is_finite());
3781        assert!(DpiScaleFactor::new(f32::NEG_INFINITY).inner.get().is_finite());
3782        assert!(DpiScaleFactor::new(f32::MAX).inner.get().is_finite());
3783        assert!(DpiScaleFactor::new(f32::MIN).inner.get().is_finite());
3784        // Sub-precision values collapse to 0 (1/1000 fixed point), not to NaN.
3785        assert_eq!(DpiScaleFactor::new(f32::MIN_POSITIVE).inner.get(), 0.0);
3786        // Eq/Hash invariant: equal inputs produce equal (hashable) keys.
3787        assert_eq!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(1.5));
3788        assert_ne!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(2.0));
3789    }
3790
3791    #[test]
3792    fn named_font_new_keeps_fields_verbatim() {
3793        let f = NamedFont::new(
3794            AzString::from_const_str(""),
3795            U8Vec::from_vec(Vec::new()),
3796        );
3797        assert_eq!(f.name.as_str(), "");
3798        assert!(f.bytes.as_ref().is_empty());
3799
3800        let bytes = vec![0u8, 255, 128];
3801        let f = NamedFont::new(AzString::from(String::from("\u{1F600}")), bytes.clone().into());
3802        assert_eq!(f.name.as_str(), "\u{1F600}");
3803        assert_eq!(f.bytes.as_ref(), bytes.as_slice());
3804    }
3805
3806    #[test]
3807    fn loaded_font_new_keeps_fields_verbatim_at_limits() {
3808        let f = LoadedFont::new(0, AzString::from_const_str(""), 0, false);
3809        assert_eq!(f.font_hash, 0);
3810        assert_eq!(f.num_glyphs, 0);
3811        assert!(!f.has_bytes);
3812
3813        let f = LoadedFont::new(u64::MAX, AzString::from_const_str("x"), u32::MAX, true);
3814        assert_eq!(f.font_hash, u64::MAX);
3815        assert_eq!(f.num_glyphs, u32::MAX);
3816        assert!(f.has_bytes);
3817    }
3818
3819    #[test]
3820    fn brush_new_defaults_and_extreme_radius() {
3821        let b = Brush::new(opaque_red(), 4.0);
3822        assert_eq!(b.radius, 4.0);
3823        assert_eq!(b.hardness, 0.5);
3824        assert_eq!(b.flow, 1.0);
3825        assert_eq!(b.spacing, 0.25);
3826        assert_eq!(b.color, opaque_red());
3827
3828        // Extreme radii are stored verbatim (validation happens in paint_dot).
3829        assert!(Brush::new(opaque_red(), f32::NAN).radius.is_nan());
3830        assert_eq!(Brush::new(opaque_red(), -0.0).radius, -0.0);
3831        assert_eq!(Brush::new(opaque_red(), f32::INFINITY).radius, f32::INFINITY);
3832    }
3833
3834    #[test]
3835    fn image_cache_new_is_empty_and_default_is_neutral() {
3836        let cache = ImageCache::new();
3837        assert!(cache.image_id_map.is_empty());
3838        assert!(ImageCache::default().image_id_map.is_empty());
3839    }
3840
3841    #[test]
3842    fn gl_texture_cache_empty_is_neutral() {
3843        let cache = GlTextureCache::empty();
3844        assert!(cache.solved_textures.is_empty());
3845        assert!(cache.hashes.is_empty());
3846        let d = GlTextureCache::default();
3847        assert!(d.solved_textures.is_empty());
3848        assert!(d.hashes.is_empty());
3849    }
3850
3851    #[test]
3852    fn external_image_id_new_is_monotonic() {
3853        let a = ExternalImageId::new();
3854        let b = ExternalImageId::new();
3855        assert!(b.inner > a.inner, "the counter must strictly increase");
3856        assert!(ExternalImageId::default().inner > b.inner);
3857    }
3858
3859    #[test]
3860    fn shared_raw_image_data_new_invariants() {
3861        let empty = SharedRawImageData::new(U8Vec::from_vec(Vec::new()));
3862        assert_eq!(empty.len(), 0);
3863        assert!(empty.is_empty());
3864        assert!(empty.as_ref().is_empty());
3865        assert!(empty.get_bytes().is_empty());
3866        // An empty Vec still yields a non-null (dangling but aligned) pointer.
3867        assert!(!empty.as_ptr().is_null());
3868
3869        let big = SharedRawImageData::new(vec![7u8; 100_000].into());
3870        assert_eq!(big.len(), 100_000);
3871        assert!(!big.is_empty());
3872        assert_eq!(big.as_ref().len(), big.len());
3873        assert_eq!(big.get_bytes(), big.as_ref());
3874        // len() must agree with the slice view (no stale-length bug).
3875        assert_eq!(big.into_inner().expect("sole owner").as_ref().len(), 100_000);
3876    }
3877
3878    #[test]
3879    fn app_config_create_registers_builtins_and_defaults() {
3880        let config = AppConfig::create();
3881        assert_eq!(config.log_level, AppLogLevel::Error);
3882        assert!(!config.enable_visual_panic_hook);
3883        assert!(config.enable_logging_on_panic);
3884        assert_eq!(config.termination_behavior, AppTerminationBehavior::EndProcess);
3885        assert!(config.routes.as_ref().is_empty());
3886        assert!(matches!(
3887            config.mock_css_environment,
3888            OptionCssMockEnvironment::None
3889        ));
3890        // create() dogfoods add_component_library -> exactly one "builtin" library.
3891        let libs = config.component_libraries.as_ref();
3892        assert_eq!(libs.len(), 1);
3893        assert_eq!(libs[0].name.as_str(), "builtin");
3894        assert!(!libs[0].components.as_ref().is_empty());
3895    }
3896
3897    #[test]
3898    fn app_config_add_component_library_replaces_same_name() {
3899        let register: crate::xml::RegisterComponentLibraryFnType =
3900            crate::xml::register_builtin_components;
3901        let mut config = AppConfig::create();
3902        let n_builtin = config.component_libraries.as_ref()[0].components.as_ref().len();
3903
3904        // Same name -> wholesale replacement, NOT a duplicate library.
3905        config.add_component_library(AzString::from_const_str("builtin"), register);
3906        assert_eq!(config.component_libraries.as_ref().len(), 1);
3907        assert_eq!(
3908            config.component_libraries.as_ref()[0].components.as_ref().len(),
3909            n_builtin
3910        );
3911
3912        // A different name (incl. empty / unicode) appends a new library.
3913        config.add_component_library(AzString::from_const_str(""), register);
3914        config.add_component_library(AzString::from_const_str("\u{1F600}"), register);
3915        assert_eq!(config.component_libraries.as_ref().len(), 3);
3916        assert_eq!(config.component_libraries.as_ref()[2].name.as_str(), "\u{1F600}");
3917    }
3918
3919    #[test]
3920    fn app_config_with_mock_environment_sets_the_option() {
3921        let config = AppConfig::create().with_mock_environment(CssMockEnvironment::dark_theme());
3922        match config.mock_css_environment {
3923            OptionCssMockEnvironment::Some(env) => {
3924                assert!(matches!(
3925                    env.theme,
3926                    azul_css::dynamic_selector::OptionThemeCondition::Some(
3927                        azul_css::dynamic_selector::ThemeCondition::Dark
3928                    )
3929                ));
3930            }
3931            OptionCssMockEnvironment::None => panic!("mock env must be Some"),
3932        }
3933        // Last call wins (the field is overwritten, not merged).
3934        let config = AppConfig::create()
3935            .with_mock_environment(CssMockEnvironment::linux())
3936            .with_mock_environment(CssMockEnvironment::windows());
3937        match config.mock_css_environment {
3938            OptionCssMockEnvironment::Some(env) => assert!(matches!(
3939                env.os,
3940                azul_css::dynamic_selector::OptionOsCondition::Some(
3941                    azul_css::dynamic_selector::OsCondition::Windows
3942                )
3943            )),
3944            OptionCssMockEnvironment::None => panic!("mock env must be Some"),
3945        }
3946    }
3947
3948    // =====================================================================
3949    // CssMockEnvironment
3950    // =====================================================================
3951
3952    #[test]
3953    fn css_mock_environment_presets_only_set_their_own_field() {
3954        use azul_css::dynamic_selector::{
3955            OptionOsCondition, OptionThemeCondition, OsCondition, ThemeCondition,
3956        };
3957
3958        for (mock, os) in [
3959            (CssMockEnvironment::linux(), OsCondition::Linux),
3960            (CssMockEnvironment::windows(), OsCondition::Windows),
3961            (CssMockEnvironment::macos(), OsCondition::MacOS),
3962        ] {
3963            assert!(matches!(mock.os, OptionOsCondition::Some(o) if o == os));
3964            // The other overrides stay unset (auto-detect).
3965            assert!(matches!(mock.theme, OptionThemeCondition::None));
3966            assert!(matches!(mock.viewport_width, azul_css::OptionF32::None));
3967        }
3968
3969        assert!(matches!(
3970            CssMockEnvironment::dark_theme().theme,
3971            OptionThemeCondition::Some(ThemeCondition::Dark)
3972        ));
3973        assert!(matches!(
3974            CssMockEnvironment::light_theme().theme,
3975            OptionThemeCondition::Some(ThemeCondition::Light)
3976        ));
3977        assert!(matches!(
3978            CssMockEnvironment::dark_theme().os,
3979            OptionOsCondition::None
3980        ));
3981    }
3982
3983    #[test]
3984    fn css_mock_environment_apply_to_overrides_only_set_fields() {
3985        use azul_css::dynamic_selector::{
3986            BoolCondition, DynamicSelectorContext, OptionOsCondition, OptionThemeCondition,
3987            OsCondition, ThemeCondition,
3988        };
3989
3990        // An all-None mock must leave the context byte-for-byte alone.
3991        let mut ctx = DynamicSelectorContext::default();
3992        let before_os = ctx.os;
3993        let before_lang = ctx.language.clone();
3994        let before_w = ctx.viewport_width;
3995        CssMockEnvironment::default().apply_to(&mut ctx);
3996        assert_eq!(ctx.os, before_os);
3997        assert_eq!(ctx.language.as_str(), before_lang.as_str());
3998        assert_eq!(ctx.viewport_width, before_w);
3999
4000        // A fully-populated mock overrides every field it sets - including
4001        // adversarial floats (NaN viewport) which must not panic.
4002        let mock = CssMockEnvironment {
4003            os: OptionOsCondition::Some(OsCondition::Windows),
4004            theme: OptionThemeCondition::Some(ThemeCondition::Dark),
4005            language: azul_css::OptionString::Some(AzString::from_const_str("de-DE")),
4006            viewport_width: azul_css::OptionF32::Some(f32::NAN),
4007            viewport_height: azul_css::OptionF32::Some(f32::INFINITY),
4008            prefers_reduced_motion: azul_css::OptionBool::Some(true),
4009            prefers_high_contrast: azul_css::OptionBool::Some(false),
4010            ..Default::default()
4011        };
4012        let mut ctx = DynamicSelectorContext::default();
4013        mock.apply_to(&mut ctx);
4014        assert_eq!(ctx.os, OsCondition::Windows);
4015        assert_eq!(ctx.theme, ThemeCondition::Dark);
4016        assert_eq!(ctx.language.as_str(), "de-DE");
4017        assert!(ctx.viewport_width.is_nan());
4018        assert_eq!(ctx.viewport_height, f32::INFINITY);
4019        assert_eq!(ctx.prefers_reduced_motion, BoolCondition::True);
4020        assert_eq!(ctx.prefers_high_contrast, BoolCondition::False);
4021
4022        // apply_to is idempotent.
4023        let mut ctx2 = ctx.clone();
4024        mock.apply_to(&mut ctx2);
4025        assert_eq!(ctx2.os, ctx.os);
4026        assert_eq!(ctx2.theme, ctx.theme);
4027    }
4028
4029    // =====================================================================
4030    // NUMERIC: brush_dab_coverage / normalize_u16 / premultiply_alpha / Au
4031    // =====================================================================
4032
4033    #[test]
4034    fn brush_dab_coverage_boundaries_and_monotonicity() {
4035        // Documented profile: 1.0 at the center, 0.0 at (and past) the edge.
4036        assert_eq!(brush_dab_coverage(0.0, 0.5), 1.0);
4037        assert_eq!(brush_dab_coverage(1.0, 0.5), 0.0);
4038        // Out-of-range t is clamped, not extrapolated.
4039        assert_eq!(brush_dab_coverage(-5.0, 0.5), 1.0);
4040        assert_eq!(brush_dab_coverage(2.0, 0.5), 0.0);
4041        assert_eq!(brush_dab_coverage(f32::INFINITY, 0.5), 0.0);
4042        assert_eq!(brush_dab_coverage(f32::NEG_INFINITY, 0.5), 1.0);
4043
4044        // Monotonically non-increasing in t, and always inside [0, 1].
4045        let mut prev = f32::INFINITY;
4046        for i in 0..=100 {
4047            let t = i as f32 / 100.0;
4048            let c = brush_dab_coverage(t, 0.5);
4049            assert!((0.0..=1.0).contains(&c), "coverage {c} out of range at t={t}");
4050            assert!(c <= prev + 1.0e-6, "not monotonic at t={t}");
4051            prev = c;
4052        }
4053    }
4054
4055    #[test]
4056    fn brush_dab_coverage_hardness_limits_never_divide_by_zero() {
4057        // hardness == 1.0 would make (1 - edge0) == 0; the 1e-4 floor prevents
4058        // a division by zero -> a hard (but finite) edge instead of inf/NaN.
4059        assert_eq!(brush_dab_coverage(0.5, 1.0), 1.0);
4060        assert!(brush_dab_coverage(1.0, 1.0).is_finite());
4061        assert_eq!(brush_dab_coverage(1.0, 1.0), 1.0); // exactly at edge0 -> x == 0
4062        assert_eq!(brush_dab_coverage(2.0, 1.0), 0.0);
4063
4064        // hardness is clamped, so out-of-range hardness behaves like 0.0 / 1.0.
4065        assert_eq!(brush_dab_coverage(0.5, -10.0), brush_dab_coverage(0.5, 0.0));
4066        assert_eq!(brush_dab_coverage(0.5, 10.0), brush_dab_coverage(0.5, 1.0));
4067        assert_eq!(
4068            brush_dab_coverage(0.5, f32::NEG_INFINITY),
4069            brush_dab_coverage(0.5, 0.0)
4070        );
4071        assert!(brush_dab_coverage(0.5, f32::INFINITY).is_finite());
4072    }
4073
4074    #[test]
4075    fn brush_dab_coverage_nan_propagates_without_panicking() {
4076        // NaN in -> NaN out (documented-by-behavior); crucially, no panic and no
4077        // hang. paint_dot's `a <= 0.0` check then skips NaN coverage entirely.
4078        assert!(brush_dab_coverage(f32::NAN, 0.5).is_nan());
4079        assert!(brush_dab_coverage(0.5, f32::NAN).is_nan());
4080        assert!(brush_dab_coverage(f32::NAN, f32::NAN).is_nan());
4081    }
4082
4083    #[test]
4084    fn normalize_u16_is_monotonic_and_saturating() {
4085        assert_eq!(normalize_u16(u16::MIN), 0);
4086        assert_eq!(normalize_u16(u16::MAX), u8::MAX);
4087        let mut prev = 0u8;
4088        for i in (0..=u16::MAX).step_by(97) {
4089            let v = normalize_u16(i);
4090            assert!(v >= prev, "normalize_u16 must be monotonic ({i} -> {v})");
4091            prev = v;
4092        }
4093        // Never wraps: the value is always <= u8::MAX by construction.
4094        assert!(normalize_u16(u16::MAX - 1) <= u8::MAX);
4095    }
4096
4097    #[test]
4098    fn premultiply_alpha_ignores_non_4_byte_slices() {
4099        // Documented: only a single 4-byte pixel is touched.
4100        for len in [0usize, 1, 2, 3, 5, 8] {
4101            let mut buf = vec![200u8; len];
4102            let before = buf.clone();
4103            premultiply_alpha(&mut buf);
4104            assert_eq!(buf, before, "len {len} must be left untouched");
4105        }
4106    }
4107
4108    #[test]
4109    fn premultiply_alpha_boundary_values_never_overflow() {
4110        // a == 255 -> unchanged (rounding must not drift).
4111        let mut opaque = [255u8, 128, 0, 255];
4112        premultiply_alpha(&mut opaque);
4113        assert_eq!(opaque, [255, 128, 0, 255]);
4114
4115        // a == 0 -> fully transparent -> RGB zeroed, alpha untouched.
4116        let mut transparent = [255u8, 255, 255, 0];
4117        premultiply_alpha(&mut transparent);
4118        assert_eq!(transparent, [0, 0, 0, 0]);
4119
4120        // a == 128 -> ~half, computed with the +128/255 rounding, never > 255.
4121        let mut half = [255u8, 128, 0, 128];
4122        premultiply_alpha(&mut half);
4123        assert_eq!(half, [128, 64, 0, 128]);
4124
4125        // The u32 intermediate must not truncate at the maximum product.
4126        let mut max = [255u8, 255, 255, 255];
4127        premultiply_alpha(&mut max);
4128        assert_eq!(max, [255, 255, 255, 255]);
4129    }
4130
4131    #[test]
4132    fn au_from_px_saturates_at_limits_and_nan() {
4133        assert_eq!(Au::from_px(0.0).0, 0);
4134        assert_eq!(Au::from_px(-0.0).0, 0);
4135        assert_eq!(Au::from_px(1.0).0, AU_PER_PX);
4136        assert_eq!(Au::from_px(-1.0).0, -AU_PER_PX);
4137        // NaN -> 0 (saturating `as` cast), NOT a panic and NOT garbage.
4138        assert_eq!(Au::from_px(f32::NAN).0, 0);
4139        // Infinities / f32 extremes clamp into [MIN_AU, MAX_AU].
4140        assert_eq!(Au::from_px(f32::INFINITY).0, MAX_AU);
4141        assert_eq!(Au::from_px(f32::NEG_INFINITY).0, MIN_AU);
4142        assert_eq!(Au::from_px(f32::MAX).0, MAX_AU);
4143        assert_eq!(Au::from_px(f32::MIN).0, MIN_AU);
4144        // Anything in range stays in range.
4145        for px in [-1.0e9_f32, -1.0, 0.5, 16.0, 1.0e9] {
4146            let au = Au::from_px(px).0;
4147            assert!((MIN_AU..=MAX_AU).contains(&au), "{px} -> {au} escaped the clamp");
4148        }
4149    }
4150
4151    #[test]
4152    fn au_px_round_trip_is_stable() {
4153        // px -> Au -> px must round-trip within one app-unit (1/60 px).
4154        for px in [0.0_f32, 0.5, 1.0, 12.0, 16.0, 72.5, -3.25, 1000.0] {
4155            let back = Au::from_px(px).into_px();
4156            assert!(
4157                (back - px).abs() <= 1.0 / AU_PER_PX as f32,
4158                "{px} round-tripped to {back}"
4159            );
4160        }
4161        // Exact for whole pixels.
4162        assert_eq!(Au::from_px(16.0).into_px(), 16.0);
4163        // Extremes stay finite.
4164        assert!(Au(MAX_AU).into_px().is_finite());
4165        assert!(Au(MIN_AU).into_px().is_finite());
4166        assert!(Au(i32::MIN).into_px().is_finite());
4167        assert!(Au(i32::MAX).into_px().is_finite());
4168    }
4169
4170    #[test]
4171    fn font_size_to_au_zero_negative_and_typical() {
4172        use azul_css::props::basic::PixelValue;
4173        let au = |px: isize| {
4174            font_size_to_au(StyleFontSize {
4175                inner: PixelValue::const_px(px),
4176            })
4177            .0
4178        };
4179        assert_eq!(au(0), 0);
4180        assert_eq!(au(16), 16 * AU_PER_PX);
4181        assert_eq!(au(-10), -10 * AU_PER_PX);
4182        // Large-but-representable sizes stay inside the clamp.
4183        assert!((MIN_AU..=MAX_AU).contains(&au(1_000_000)));
4184        assert!((MIN_AU..=MAX_AU).contains(&au(-1_000_000)));
4185    }
4186
4187    // =====================================================================
4188    // NUMERIC: Epoch
4189    // =====================================================================
4190
4191    #[test]
4192    fn epoch_new_from_and_into_u32() {
4193        assert_eq!(Epoch::new().into_u32(), 0);
4194        assert_eq!(Epoch::default().into_u32(), 0);
4195        assert_eq!(Epoch::from(0).into_u32(), 0);
4196        assert_eq!(Epoch::from(1).into_u32(), 1);
4197        assert_eq!(Epoch::from(u32::MAX).into_u32(), u32::MAX);
4198        assert_eq!(Epoch::from(u32::MAX - 1).into_u32(), u32::MAX - 1);
4199    }
4200
4201    #[test]
4202    fn epoch_increment_wraps_at_max_minus_one_and_never_reaches_max() {
4203        let mut e = Epoch::new();
4204        e.increment();
4205        assert_eq!(e.into_u32(), 1);
4206
4207        // u32::MAX is reserved as "invalid", so MAX-1 wraps back to 0.
4208        let mut e = Epoch::from(u32::MAX - 1);
4209        e.increment();
4210        assert_eq!(e.into_u32(), 0, "MAX-1 must wrap to 0, never to u32::MAX");
4211
4212        // An epoch that somehow starts AT u32::MAX saturates (fixpoint) instead
4213        // of wrapping or overflow-panicking - deterministic, no UB.
4214        let mut e = Epoch::from(u32::MAX);
4215        e.increment();
4216        assert_eq!(e.into_u32(), u32::MAX);
4217
4218        // A long run of increments never yields the invalid u32::MAX.
4219        let mut e = Epoch::from(u32::MAX - 3);
4220        for _ in 0..8 {
4221            e.increment();
4222            assert_ne!(e.into_u32(), u32::MAX);
4223        }
4224    }
4225
4226    #[test]
4227    fn epoch_display_is_non_empty_for_edge_values() {
4228        assert_eq!(alloc::format!("{}", Epoch::new()), "0");
4229        assert_eq!(alloc::format!("{}", Epoch::from(42)), "42");
4230        assert_eq!(
4231            alloc::format!("{}", Epoch::from(u32::MAX)),
4232            alloc::format!("{}", u32::MAX)
4233        );
4234        assert!(!alloc::format!("{:?}", Epoch::default()).is_empty());
4235    }
4236
4237    #[test]
4238    fn id_namespace_display_and_debug_are_well_formed() {
4239        assert_eq!(alloc::format!("{}", IdNamespace(0)), "IdNamespace(0)");
4240        assert_eq!(
4241            alloc::format!("{}", IdNamespace(u32::MAX)),
4242            alloc::format!("IdNamespace({})", u32::MAX)
4243        );
4244        // Debug delegates to Display (must not recurse / be empty).
4245        assert_eq!(
4246            alloc::format!("{:?}", IdNamespace(7)),
4247            alloc::format!("{}", IdNamespace(7))
4248        );
4249    }
4250
4251    // =====================================================================
4252    // KEYS: uniqueness / namespace preservation / hash->key derivation
4253    // =====================================================================
4254
4255    #[test]
4256    fn unique_keys_are_strictly_increasing_and_keep_their_namespace() {
4257        let ns = IdNamespace(u32::MAX);
4258
4259        let a = ImageKey::unique(ns);
4260        let b = ImageKey::unique(ns);
4261        assert_eq!(a.namespace, ns);
4262        assert!(b.key > a.key, "ImageKey counter must strictly increase");
4263        // The counter starts at 1 so a live key can never collide with DUMMY.
4264        assert_eq!(ImageKey::DUMMY.key, 0);
4265        assert_ne!(a, ImageKey::DUMMY);
4266
4267        let a = FontKey::unique(ns);
4268        let b = FontKey::unique(ns);
4269        assert_eq!(a.namespace, ns);
4270        assert!(b.key > a.key);
4271
4272        let a = FontInstanceKey::unique(IdNamespace(0));
4273        let b = FontInstanceKey::unique(IdNamespace(0));
4274        assert_eq!(a.namespace, IdNamespace(0));
4275        assert!(b.key > a.key);
4276    }
4277
4278    #[test]
4279    fn image_ref_id_counter_is_monotonic_and_never_zero() {
4280        // id == 0 is reserved to flag an un-initialised handle.
4281        let a = next_image_ref_id();
4282        let b = next_image_ref_id();
4283        assert!(a > 0 && b > a);
4284    }
4285
4286    #[test]
4287    fn image_ref_hash_conversions_are_lossless_and_agree() {
4288        let img = ImageRef::null_image(1, 1, RawImageFormat::RGBA8, Vec::new());
4289        let hash = img.get_hash();
4290        assert_eq!(hash, image_ref_get_hash(&img));
4291
4292        let key = image_ref_hash_to_image_key(hash, IdNamespace(9));
4293        assert_eq!(key.namespace, IdNamespace(9));
4294        assert_eq!(key.key, hash.inner, "the u64 id must survive verbatim");
4295
4296        let ext = image_ref_hash_to_external_image_id(hash);
4297        assert_eq!(ext.inner, hash.inner);
4298
4299        // Both derivations agree for boundary hashes too.
4300        for inner in [0u64, 1, u64::MAX, u64::MAX - 1] {
4301            let h = ImageRefHash { inner };
4302            assert_eq!(image_ref_hash_to_image_key(h, IdNamespace(0)).key, inner);
4303            assert_eq!(image_ref_hash_to_external_image_id(h).inner, inner);
4304        }
4305    }
4306
4307    #[test]
4308    fn texture_external_image_id_is_deterministic_and_collision_free() {
4309        let id = |d: usize, n: usize| texture_external_image_id(DomId { inner: d }, NodeId::new(n));
4310
4311        // Same input -> same id (cached display lists depend on this).
4312        assert_eq!(id(3, 7), id(3, 7));
4313        assert_eq!(id(0, 0).inner, 0);
4314        // The dom goes in the high 32 bits, the node in the low 32.
4315        assert_eq!(id(1, 2).inner, (1u64 << 32) | 2);
4316        // (0,1) and (1,0) must not collide.
4317        assert_ne!(id(0, 1), id(1, 0));
4318        // Boundary node index inside the 32-bit range.
4319        assert_eq!(id(0, u32::MAX as usize).inner, u64::from(u32::MAX));
4320        assert_eq!(
4321            id(u32::MAX as usize, 0).inner,
4322            u64::from(u32::MAX) << 32
4323        );
4324    }
4325
4326    // =====================================================================
4327    // GETTERS / PREDICATES: RawImageData
4328    // =====================================================================
4329
4330    #[test]
4331    fn raw_image_data_typed_getters_only_match_their_own_variant() {
4332        let u8v = RawImageData::U8(vec![1u8, 2].into());
4333        let u16v = RawImageData::U16(vec![1u16, 2].into());
4334        let f32v = RawImageData::F32(vec![1.0f32, 2.0].into());
4335
4336        assert_eq!(u8v.get_u8_vec_ref().map(|v| v.len()), Some(2));
4337        assert!(u8v.get_u16_vec_ref().is_none());
4338        assert!(u8v.get_f32_vec_ref().is_none());
4339
4340        assert!(u16v.get_u8_vec_ref().is_none());
4341        assert_eq!(u16v.get_u16_vec_ref().map(|v| v.len()), Some(2));
4342        assert!(u16v.get_f32_vec_ref().is_none());
4343
4344        assert!(f32v.get_u8_vec_ref().is_none());
4345        assert!(f32v.get_u16_vec_ref().is_none());
4346        assert_eq!(f32v.get_f32_vec_ref().map(|v| v.len()), Some(2));
4347
4348        // Empty payloads are Some(empty), not None.
4349        let empty = RawImageData::U8(U8Vec::from_vec(Vec::new()));
4350        assert_eq!(empty.get_u8_vec_ref().map(|v| v.len()), Some(0));
4351
4352        // by-value variants agree with the by-ref ones
4353        assert!(RawImageData::U8(vec![9u8].into()).get_u8_vec().is_some());
4354        assert!(RawImageData::U16(vec![9u16].into()).get_u8_vec().is_none());
4355        assert!(RawImageData::U16(vec![9u16].into()).get_u16_vec().is_some());
4356        assert!(RawImageData::F32(vec![9.0f32].into()).get_u16_vec().is_none());
4357    }
4358
4359    // =====================================================================
4360    // NUMERIC / ROUND-TRIP: RawImage::load_* format conversions
4361    // =====================================================================
4362
4363    #[test]
4364    fn load_fns_reject_wrong_payload_type() {
4365        // Every loader demands a specific RawImageData variant; a mismatch is
4366        // None (never a panic / never garbage pixels).
4367        let u16_1px = || RawImageData::U16(vec![0u16; 4].into());
4368        let f32_1px = || RawImageData::F32(vec![0.0f32; 4].into());
4369        let u8_1px = || RawImageData::U8(vec![0u8; 4].into());
4370
4371        assert!(RawImage::load_r8(u16_1px(), 4).is_none());
4372        assert!(RawImage::load_rg8(f32_1px(), 2, true).is_none());
4373        assert!(RawImage::load_rgb8(u16_1px(), 1).is_none());
4374        assert!(RawImage::load_rgba8(f32_1px(), 1, true).is_none());
4375        assert!(RawImage::load_r16(u8_1px(), 4).is_none());
4376        assert!(RawImage::load_rg16(f32_1px(), 2).is_none());
4377        assert!(RawImage::load_rgb16(u8_1px(), 1).is_none());
4378        assert!(RawImage::load_rgba16(u8_1px(), 1, true).is_none());
4379        assert!(RawImage::load_bgr8(u16_1px(), 1).is_none());
4380        assert!(RawImage::load_bgra8(u16_1px(), 1, true).is_none());
4381        assert!(RawImage::load_rgbf32(u8_1px(), 1).is_none());
4382        assert!(RawImage::load_rgbaf32(u16_1px(), 1, true).is_none());
4383    }
4384
4385    #[test]
4386    fn load_fns_reject_every_wrong_length() {
4387        // One byte too few and one too many must BOTH be rejected for each format.
4388        assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 3].into()), 4).is_none());
4389        assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 5].into()), 4).is_none());
4390        assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 3].into()), 2, true).is_none());
4391        assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 5].into()), 2, true).is_none());
4392        assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
4393        assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 7].into()), 2).is_none());
4394        assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 7].into()), 2, true).is_none());
4395        assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 9].into()), 2, false).is_none());
4396        assert!(RawImage::load_r16(RawImageData::U16(vec![0u16; 3].into()), 4).is_none());
4397        assert!(RawImage::load_rg16(RawImageData::U16(vec![0u16; 3].into()), 2).is_none());
4398        assert!(RawImage::load_rgb16(RawImageData::U16(vec![0u16; 5].into()), 2).is_none());
4399        assert!(RawImage::load_rgba16(RawImageData::U16(vec![0u16; 7].into()), 2, true).is_none());
4400        assert!(RawImage::load_bgr8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
4401        assert!(RawImage::load_bgra8(RawImageData::U8(vec![0u8; 7].into()), 2, false).is_none());
4402        assert!(RawImage::load_rgbf32(RawImageData::F32(vec![0.0f32; 5].into()), 2).is_none());
4403        assert!(
4404            RawImage::load_rgbaf32(RawImageData::F32(vec![0.0f32; 7].into()), 2, true).is_none()
4405        );
4406    }
4407
4408    #[test]
4409    fn load_fns_accept_zero_pixels() {
4410        // expected_len == 0 with an empty buffer: Some(empty), no div-by-zero.
4411        let empty_u8 = || RawImageData::U8(U8Vec::from_vec(Vec::new()));
4412        let empty_u16 = || RawImageData::U16(U16Vec::from_vec(Vec::new()));
4413        let empty_f32 = || RawImageData::F32(F32Vec::from_vec(Vec::new()));
4414
4415        assert_eq!(RawImage::load_r8(empty_u8(), 0).map(|(b, o)| (b.len(), o)), Some((0, false)));
4416        assert_eq!(RawImage::load_rg8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
4417        assert_eq!(RawImage::load_rgb8(empty_u8(), 0).map(|(b, _)| b.len()), Some(0));
4418        assert_eq!(RawImage::load_rgba8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
4419        assert_eq!(RawImage::load_r16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
4420        assert_eq!(RawImage::load_rg16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
4421        assert_eq!(RawImage::load_rgb16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
4422        assert_eq!(RawImage::load_rgba16(empty_u16(), 0, true).map(|(b, _)| b.len()), Some(0));
4423        assert_eq!(RawImage::load_bgr8(empty_u8(), 0).map(|(b, _)| b.len()), Some(0));
4424        assert_eq!(RawImage::load_bgra8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
4425        assert_eq!(RawImage::load_rgbf32(empty_f32(), 0).map(|(b, _)| b.len()), Some(0));
4426        assert_eq!(RawImage::load_rgbaf32(empty_f32(), 0, true).map(|(b, _)| b.len()), Some(0));
4427    }
4428
4429    #[test]
4430    fn load_r8_passes_data_through_and_is_never_opaque() {
4431        // R8 must stay R8 (image masks depend on the single channel surviving).
4432        let (bytes, is_opaque) =
4433            RawImage::load_r8(RawImageData::U8(vec![0u8, 128, 255, 1].into()), 4)
4434                .expect("exact length");
4435        assert_eq!(bytes.as_ref(), &[0, 128, 255, 1]);
4436        assert!(!is_opaque, "R8 is documented as never opaque");
4437    }
4438
4439    #[test]
4440    fn load_rgb8_and_bgr8_swizzle_to_bgra_opaque() {
4441        // RGB8 -> BGRA8: channel order flips, alpha forced to 0xFF, always opaque.
4442        let (bytes, is_opaque) =
4443            RawImage::load_rgb8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
4444        assert_eq!(bytes.as_ref(), &[3, 2, 1, 255]);
4445        assert!(is_opaque);
4446
4447        // BGR8 -> BGRA8: order preserved, alpha appended.
4448        let (bytes, is_opaque) =
4449            RawImage::load_bgr8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
4450        assert_eq!(bytes.as_ref(), &[1, 2, 3, 255]);
4451        assert!(is_opaque);
4452    }
4453
4454    #[test]
4455    fn load_rgba8_swizzles_and_detects_transparency() {
4456        // Premultiplied: RGBA -> BGRA swizzle only.
4457        let (bytes, is_opaque) =
4458            RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 255].into()), 1, true)
4459                .expect("1 px");
4460        assert_eq!(bytes.as_ref(), &[30, 20, 10, 255]);
4461        assert!(is_opaque);
4462
4463        // A single non-255 alpha flips is_opaque to false.
4464        let (_, is_opaque) =
4465            RawImage::load_rgba8(RawImageData::U8(vec![0u8, 0, 0, 254].into()), 1, true)
4466                .expect("1 px");
4467        assert!(!is_opaque);
4468
4469        // Non-premultiplied: swizzle THEN premultiply by alpha.
4470        let (bytes, is_opaque) =
4471            RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 128].into()), 1, false)
4472                .expect("1 px");
4473        assert_eq!(bytes.as_ref(), &[15, 10, 5, 128]);
4474        assert!(!is_opaque);
4475
4476        // alpha == 0 must zero the colour (no leftover colour fringe).
4477        let (bytes, _) =
4478            RawImage::load_rgba8(RawImageData::U8(vec![255u8, 255, 255, 0].into()), 1, false)
4479                .expect("1 px");
4480        assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
4481    }
4482
4483    #[test]
4484    fn load_rg8_expands_grey_to_bgra() {
4485        // Greyscale + alpha -> BGRA with the grey replicated across B/G/R.
4486        let (bytes, is_opaque) =
4487            RawImage::load_rg8(RawImageData::U8(vec![100u8, 255].into()), 1, true).expect("1 px");
4488        assert_eq!(bytes.as_ref(), &[100, 100, 100, 255]);
4489        assert!(is_opaque);
4490
4491        let (bytes, is_opaque) =
4492            RawImage::load_rg8(RawImageData::U8(vec![100u8, 128].into()), 1, false).expect("1 px");
4493        assert_eq!(bytes.as_ref(), &[50, 50, 50, 128]);
4494        assert!(!is_opaque);
4495    }
4496
4497    #[test]
4498    fn load_16_bit_formats_normalize_to_8_bit() {
4499        // u16::MAX -> 255, 0 -> 0 (no wrap-around / no div-by-zero).
4500        let (bytes, is_opaque) =
4501            RawImage::load_r16(RawImageData::U16(vec![u16::MAX].into()), 1).expect("1 px");
4502        assert_eq!(bytes.as_ref(), &[255, 255, 255, 255]);
4503        assert!(is_opaque);
4504
4505        let (bytes, is_opaque) =
4506            RawImage::load_rg16(RawImageData::U16(vec![0u16, u16::MAX].into()), 1).expect("1 px");
4507        assert_eq!(bytes.as_ref(), &[0, 0, 0, 255]);
4508        assert!(is_opaque);
4509
4510        // RGB16 -> BGRA8 swizzle.
4511        let (bytes, _) = RawImage::load_rgb16(
4512            RawImageData::U16(vec![u16::MAX, 0, 0].into()),
4513            1,
4514        )
4515        .expect("1 px");
4516        assert_eq!(bytes.as_ref(), &[0, 0, 255, 255]);
4517
4518        // RGBA16 with a zero alpha -> not opaque; premultiply zeroes the colour.
4519        let (bytes, is_opaque) = RawImage::load_rgba16(
4520            RawImageData::U16(vec![u16::MAX, u16::MAX, u16::MAX, 0].into()),
4521            1,
4522            false,
4523        )
4524        .expect("1 px");
4525        assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
4526        assert!(!is_opaque);
4527    }
4528
4529    #[test]
4530    fn load_f32_formats_saturate_on_out_of_range_nan_and_inf() {
4531        // The f32 -> u8 cast is saturating: >1.0 -> 255, <0.0 -> 0, NaN -> 0.
4532        // (This is the whole "HDR pixel with a garbage float" attack surface.)
4533        let (bytes, is_opaque) = RawImage::load_rgbf32(
4534            RawImageData::F32(vec![2.0f32, -1.0, f32::NAN].into()),
4535            1,
4536        )
4537        .expect("1 px");
4538        assert_eq!(bytes.as_ref(), &[0, 0, 255, 255], "b=NaN->0, g=-1->0, r=2.0->255");
4539        assert!(is_opaque);
4540
4541        let (bytes, is_opaque) = RawImage::load_rgbaf32(
4542            RawImageData::F32(vec![f32::INFINITY, f32::NEG_INFINITY, 0.5, 1.0].into()),
4543            1,
4544            true,
4545        )
4546        .expect("1 px");
4547        assert_eq!(bytes.as_ref(), &[127, 0, 255, 255]);
4548        assert!(is_opaque);
4549
4550        // NaN alpha -> 0 -> not opaque (fails safe, does not claim opacity).
4551        let (_, is_opaque) = RawImage::load_rgbaf32(
4552            RawImageData::F32(vec![1.0f32, 1.0, 1.0, f32::NAN].into()),
4553            1,
4554            true,
4555        )
4556        .expect("1 px");
4557        assert!(!is_opaque);
4558    }
4559
4560    // =====================================================================
4561    // ROUND-TRIP: RawImage <-> ImageRef
4562    // =====================================================================
4563
4564    #[test]
4565    fn raw_image_null_image_encodes_to_an_empty_bgra8_descriptor() {
4566        let null = RawImage::null_image();
4567        assert_eq!(null.width, 0);
4568        assert_eq!(null.height, 0);
4569        assert_eq!(null.data_format, RawImageFormat::BGRA8);
4570        assert!(null.premultiplied_alpha);
4571
4572        let (data, descriptor) = null
4573            .into_loaded_image_source()
4574            .expect("a 0x0 image is still a valid (empty) source");
4575        assert_eq!(descriptor.width, 0);
4576        assert_eq!(descriptor.height, 0);
4577        assert_eq!(descriptor.format, RawImageFormat::BGRA8);
4578        assert_eq!(descriptor.offset, 0);
4579        match data {
4580            ImageData::Raw(bytes) => assert!(bytes.is_empty()),
4581            ImageData::External(_) => panic!("a RawImage must never encode to External"),
4582        }
4583    }
4584
4585    #[test]
4586    fn raw_image_allocate_mask_zero_and_negative_sizes() {
4587        let mask = RawImage::allocate_mask(LayoutSize::zero());
4588        assert_eq!(mask.data_format, RawImageFormat::R8);
4589        assert_eq!(mask.width, 0);
4590        assert_eq!(mask.height, 0);
4591        assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
4592
4593        let mask = RawImage::allocate_mask(LayoutSize::new(4, 4));
4594        assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(16));
4595        assert!(mask
4596            .pixels
4597            .get_u8_vec_ref()
4598            .expect("u8")
4599            .as_ref()
4600            .iter()
4601            .all(|b| *b == 0));
4602
4603        // Negative sizes: the BUFFER is clamped to 0 (no huge alloc, no panic),
4604        // but the width/height FIELDS keep the wrapped `as usize` value, so the
4605        // returned RawImage is internally inconsistent. Callers must not feed a
4606        // negative LayoutSize in. (Buffer-side safety is what matters here.)
4607        let mask = RawImage::allocate_mask(LayoutSize::new(-4, 4));
4608        assert_eq!(
4609            mask.pixels.get_u8_vec_ref().map(|v| v.len()),
4610            Some(0),
4611            "a negative extent must never allocate"
4612        );
4613        assert!(mask.width > 1_000_000, "negative width wraps via `as usize`");
4614    }
4615
4616    #[test]
4617    fn raw_image_mask_round_trips_as_r8() {
4618        // A mask must stay single-channel R8 through the encoder (clip masks
4619        // break if it silently becomes BGRA8).
4620        let mask = RawImage::allocate_mask(LayoutSize::new(2, 2));
4621        let (data, descriptor) = mask.into_loaded_image_source().expect("consistent mask");
4622        assert_eq!(descriptor.format, RawImageFormat::R8);
4623        assert_eq!((descriptor.width, descriptor.height), (2, 2));
4624        assert!(!descriptor.flags.is_opaque, "R8 is never opaque");
4625        match data {
4626            ImageData::Raw(bytes) => assert_eq!(bytes.len(), 4),
4627            ImageData::External(_) => panic!("expected raw bytes"),
4628        }
4629    }
4630
4631    #[test]
4632    fn raw_image_rgba8_encode_decode_round_trip() {
4633        // encode: RGBA8 -> BGRA8 bytes; decode: ImageRef::get_rawimage gives the
4634        // encoded (BGRA8) pixels back verbatim.
4635        let raw = RawImage {
4636            pixels: RawImageData::U8(vec![10u8, 20, 30, 255].into()),
4637            width: 1,
4638            height: 1,
4639            premultiplied_alpha: true,
4640            data_format: RawImageFormat::RGBA8,
4641            tag: Vec::new().into(),
4642        };
4643        let img = ImageRef::new_rawimage(raw).expect("1x1 RGBA8 with 4 bytes is valid");
4644
4645        assert!(img.is_raw_image());
4646        assert!(!img.is_null_image());
4647        assert!(!img.is_gl_texture());
4648        assert!(!img.is_callback());
4649        assert_eq!(img.get_size(), LogicalSize::new(1.0, 1.0));
4650        assert_eq!(img.get_bytes(), Some(&[30u8, 20, 10, 255][..]));
4651        assert!(!img.get_bytes_ptr().is_null());
4652
4653        let decoded = img.get_rawimage().expect("raw image round-trips");
4654        assert_eq!(decoded.width, 1);
4655        assert_eq!(decoded.height, 1);
4656        assert_eq!(decoded.data_format, RawImageFormat::BGRA8);
4657        assert!(decoded.premultiplied_alpha);
4658        assert_eq!(
4659            decoded.pixels.get_u8_vec_ref().map(|v| v.as_ref().to_vec()),
4660            Some(vec![30, 20, 10, 255])
4661        );
4662    }
4663
4664    #[test]
4665    fn image_ref_new_rawimage_rejects_dimension_mismatch() {
4666        // 2x2 RGBA8 needs 16 bytes; 4 bytes must be rejected (None, not a crash).
4667        let too_small = RawImage {
4668            pixels: RawImageData::U8(vec![0u8; 4].into()),
4669            width: 2,
4670            height: 2,
4671            premultiplied_alpha: true,
4672            data_format: RawImageFormat::RGBA8,
4673            tag: Vec::new().into(),
4674        };
4675        assert!(ImageRef::new_rawimage(too_small).is_none());
4676
4677        // Too MANY bytes is equally invalid.
4678        let too_big = RawImage {
4679            pixels: RawImageData::U8(vec![0u8; 64].into()),
4680            width: 2,
4681            height: 2,
4682            premultiplied_alpha: true,
4683            data_format: RawImageFormat::RGBA8,
4684            tag: Vec::new().into(),
4685        };
4686        assert!(ImageRef::new_rawimage(too_big).is_none());
4687
4688        // Right byte count, wrong payload type -> None.
4689        let wrong_type = RawImage {
4690            pixels: RawImageData::U16(vec![0u16; 16].into()),
4691            width: 2,
4692            height: 2,
4693            premultiplied_alpha: true,
4694            data_format: RawImageFormat::RGBA8,
4695            tag: Vec::new().into(),
4696        };
4697        assert!(ImageRef::new_rawimage(wrong_type).is_none());
4698    }
4699
4700    // =====================================================================
4701    // GETTERS / PREDICATES: ImageRef
4702    // =====================================================================
4703
4704    #[test]
4705    fn image_ref_null_image_predicates_and_accessors() {
4706        let img = ImageRef::null_image(0, 0, RawImageFormat::BGRA8, Vec::new());
4707        assert!(img.is_null_image());
4708        assert!(!img.is_raw_image());
4709        assert!(!img.is_gl_texture());
4710        assert!(!img.is_callback());
4711        assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
4712        assert!(img.get_bytes().is_none());
4713        assert!(img.get_rawimage().is_none());
4714        assert!(img.get_bytes_ptr().is_null());
4715        assert!(img.get_image_callback().is_none());
4716        assert!(matches!(img.get_data(), DecodedImage::NullImage { .. }));
4717    }
4718
4719    #[test]
4720    fn image_ref_null_image_at_usize_max_reports_a_finite_size() {
4721        // usize::MAX as f32 must not produce NaN/inf (it saturates to ~1.8e19).
4722        let img = ImageRef::null_image(usize::MAX, usize::MAX, RawImageFormat::R8, Vec::new());
4723        let size = img.get_size();
4724        assert!(size.width.is_finite() && size.height.is_finite());
4725        assert!(size.width > 0.0 && size.height > 0.0);
4726        assert!(img.is_null_image());
4727
4728        // A large tag survives verbatim.
4729        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, vec![9u8; 10_000]);
4730        match img.get_data() {
4731            DecodedImage::NullImage { tag, .. } => assert_eq!(tag.len(), 10_000),
4732            _ => panic!("expected NullImage"),
4733        }
4734    }
4735
4736    #[test]
4737    fn image_ref_hash_identity_rules() {
4738        let a = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
4739        let b = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
4740        // Two structurally identical images are still DIFFERENT images.
4741        assert_ne!(a.get_hash(), b.get_hash());
4742        assert_ne!(a, b);
4743
4744        // A shallow clone is the SAME image.
4745        let a2 = a.clone();
4746        assert_eq!(a.get_hash(), a2.get_hash());
4747        assert_eq!(a, a2);
4748
4749        // A deep copy is a NEW image with a fresh identity.
4750        let deep = a.deep_copy();
4751        assert_ne!(a.get_hash(), deep.get_hash());
4752        assert!(deep.is_null_image());
4753        assert_eq!(deep.get_size(), a.get_size());
4754    }
4755
4756    #[test]
4757    fn image_ref_callback_accessors() {
4758        // CoreRenderImageCallbackType is a usize placeholder, so 0 is a valid
4759        // (if inert) callback token.
4760        let mut img = ImageRef::callback(0usize, RefAny::new(123u32));
4761        assert!(img.is_callback());
4762        assert!(!img.is_null_image());
4763        assert!(!img.is_raw_image());
4764        // Documented: a Callback reports a (0, 0) size.
4765        assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
4766        assert!(img.get_bytes().is_none());
4767        assert!(img.get_bytes_ptr().is_null());
4768        assert!(img.get_rawimage().is_none());
4769
4770        // Sole owner -> the callback is reachable (shared / mutable).
4771        assert!(img.get_image_callback().is_some());
4772        assert!(img.get_image_callback_mut().is_some());
4773
4774        // While a second handle is alive, aliasing &mut would be unsound, so
4775        // BOTH accessors must refuse.
4776        let clone = img.clone();
4777        assert!(img.get_image_callback().is_none());
4778        assert!(img.get_image_callback_mut().is_none());
4779        drop(clone);
4780        assert!(img.get_image_callback().is_some());
4781    }
4782
4783    #[test]
4784    fn image_ref_deep_copy_of_a_callback_keeps_it_a_callback() {
4785        let img = ImageRef::callback(0usize, RefAny::new(1u8));
4786        let deep = img.deep_copy();
4787        assert!(deep.is_callback());
4788        assert_ne!(img.get_hash(), deep.get_hash());
4789    }
4790
4791    #[test]
4792    fn image_ref_into_inner_only_when_sole_owner() {
4793        let img = ImageRef::null_image(2, 2, RawImageFormat::RGBA8, vec![1, 2, 3]);
4794        let clone = img.clone();
4795        assert!(clone.into_inner().is_none(), "shared -> must refuse");
4796
4797        let inner = img.into_inner().expect("sole owner -> takes ownership");
4798        match inner {
4799            DecodedImage::NullImage {
4800                width,
4801                height,
4802                format,
4803                tag,
4804            } => {
4805                assert_eq!((width, height), (2, 2));
4806                assert_eq!(format, RawImageFormat::RGBA8);
4807                assert_eq!(tag, vec![1, 2, 3]);
4808            }
4809            _ => panic!("expected NullImage"),
4810        }
4811    }
4812
4813    // =====================================================================
4814    // ImageCache
4815    // =====================================================================
4816
4817    #[test]
4818    fn image_cache_add_get_delete_round_trip() {
4819        let mut cache = ImageCache::new();
4820        let key = AzString::from_const_str("my_image");
4821        let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
4822        let hash = img.get_hash();
4823
4824        assert!(cache.get_css_image_id(&key).is_none());
4825        cache.add_css_image_id(key.clone(), img);
4826        assert_eq!(cache.get_css_image_id(&key).map(ImageRef::get_hash), Some(hash));
4827
4828        // Re-inserting the same id replaces (does not duplicate).
4829        let img2 = ImageRef::null_image(2, 2, RawImageFormat::R8, Vec::new());
4830        let hash2 = img2.get_hash();
4831        cache.add_css_image_id(key.clone(), img2);
4832        assert_eq!(cache.image_id_map.len(), 1);
4833        assert_eq!(cache.get_css_image_id(&key).map(ImageRef::get_hash), Some(hash2));
4834
4835        cache.delete_css_image_id(&key);
4836        assert!(cache.get_css_image_id(&key).is_none());
4837        assert!(cache.image_id_map.is_empty());
4838        // Deleting a missing id is a no-op, not a panic.
4839        cache.delete_css_image_id(&key);
4840        cache.delete_css_image_id(&AzString::from_const_str("never-existed"));
4841    }
4842
4843    #[test]
4844    fn image_cache_handles_empty_and_unicode_keys() {
4845        let mut cache = ImageCache::new();
4846        let empty = AzString::from_const_str("");
4847        let unicode = AzString::from(String::from("\u{1F600}\u{0301}"));
4848        let long = AzString::from("k".repeat(100_000));
4849
4850        cache.add_css_image_id(
4851            empty.clone(),
4852            ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
4853        );
4854        cache.add_css_image_id(
4855            unicode.clone(),
4856            ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
4857        );
4858        cache.add_css_image_id(
4859            long.clone(),
4860            ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
4861        );
4862
4863        assert_eq!(cache.image_id_map.len(), 3);
4864        assert!(cache.get_css_image_id(&empty).is_some());
4865        assert!(cache.get_css_image_id(&unicode).is_some());
4866        assert!(cache.get_css_image_id(&long).is_some());
4867        // Distinct keys must not alias each other.
4868        assert!(cache
4869            .get_css_image_id(&AzString::from_const_str("\u{1F600}"))
4870            .is_none());
4871    }
4872
4873    // =====================================================================
4874    // RendererResources
4875    // =====================================================================
4876
4877    #[test]
4878    fn renderer_resources_lookups_on_an_empty_registry_are_none() {
4879        let rr = RendererResources::default();
4880        let ns = IdNamespace(1);
4881        assert!(rr
4882            .get_renderable_font_data(&FontInstanceKey::unique(ns))
4883            .is_none());
4884        let families = StyleFontFamiliesHash::new(&[]);
4885        assert!(rr
4886            .get_font_instance_key(&families, Au(0), DpiScaleFactor::new(1.0))
4887            .is_none());
4888        assert!(rr
4889            .get_font_instance_key(&families, Au(MAX_AU), DpiScaleFactor::new(f32::NAN))
4890            .is_none());
4891        assert!(rr.get_image(&ImageRefHash { inner: 0 }).is_none());
4892        assert!(rr.get_font_key(&StyleFontFamilyHash::new(&StyleFontFamily::System(
4893            AzString::from_const_str("Arial")
4894        ))).is_none());
4895    }
4896
4897    #[test]
4898    fn renderer_resources_gc_helper_is_a_noop_on_empty_maps() {
4899        // The private helper must not panic (or wrongly prune) on empty maps.
4900        let mut rr = RendererResources::default();
4901        rr.remove_font_families_with_zero_references();
4902        assert!(rr.font_id_map.is_empty());
4903        assert!(rr.font_families_map.is_empty());
4904
4905        // A family whose FontKey is NOT registered gets pruned; the families
4906        // map entry pointing at it is pruned too.
4907        let family = StyleFontFamily::System(AzString::from_const_str("Arial"));
4908        let family_hash = StyleFontFamilyHash::new(&family);
4909        let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
4910        rr.font_id_map.insert(family_hash, FontKey::unique(IdNamespace(1)));
4911        rr.font_families_map.insert(families_hash, family_hash);
4912        rr.remove_font_families_with_zero_references();
4913        assert!(rr.font_id_map.is_empty(), "dangling font key must be pruned");
4914        assert!(rr.font_families_map.is_empty());
4915    }
4916
4917    #[test]
4918    fn get_font_instance_key_for_text_is_none_on_empty_resources_for_all_sane_sizes() {
4919        // No fonts registered -> every lookup misses, whatever the size / DPI.
4920        // (0, negative and NaN sizes must not panic on the way to the miss.)
4921        let rr = RendererResources::default();
4922        let cache = CssPropertyCache::default();
4923        let node = NodeData::default();
4924        let node_id = NodeId::new(0);
4925        let state = StyledNodeState::default();
4926
4927        for size in [0.0_f32, -0.0, 1.0, -12.0, f32::NAN, 1.0e6, -1.0e6] {
4928            for dpi in [1.0_f32, 0.0, -1.0, f32::NAN, f32::INFINITY] {
4929                assert!(
4930                    rr.get_font_instance_key_for_text(size, &cache, &node, &node_id, &state, dpi)
4931                        .is_none(),
4932                    "size={size} dpi={dpi} must miss cleanly"
4933                );
4934            }
4935        }
4936    }
4937
4938    #[test]
4939    #[ignore = "BUG: font_size_px = inf/f32::MAX overflows `isize * 1000` inside \
4940                PixelValue::const_px -> panics instead of saturating"]
4941    fn bug_get_font_instance_key_for_text_overflow_panics_on_infinite_font_size() {
4942        // `font_size_px as isize` saturates to isize::MAX for +inf / f32::MAX,
4943        // and FloatValue::const_new then computes `isize::MAX * 1000`, which
4944        // panics with "attempt to multiply with overflow" under the (default)
4945        // dev-profile overflow checks. A miss (None) is the correct behaviour.
4946        let rr = RendererResources::default();
4947        let cache = CssPropertyCache::default();
4948        let node = NodeData::default();
4949        let node_id = NodeId::new(0);
4950        let state = StyledNodeState::default();
4951        assert!(rr
4952            .get_font_instance_key_for_text(
4953                f32::INFINITY,
4954                &cache,
4955                &node,
4956                &node_id,
4957                &state,
4958                1.0
4959            )
4960            .is_none());
4961    }
4962
4963    // =====================================================================
4964    // Resource-update builders (end-to-end)
4965    // =====================================================================
4966
4967    #[test]
4968    fn font_ref_get_hash_is_stable_per_font_and_distinct_across_fonts() {
4969        let a = dummy_font_ref();
4970        let b = dummy_font_ref();
4971        assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a));
4972        assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a.clone()));
4973        assert_ne!(
4974            font_ref_get_hash(&a),
4975            font_ref_get_hash(&b),
4976            "two distinct FontRefs must not share a hash"
4977        );
4978    }
4979
4980    #[test]
4981    fn build_add_font_resource_updates_on_empty_input_is_empty() {
4982        let mut rr = RendererResources::default();
4983        let fonts = OrderedMap::new();
4984        let updates = build_add_font_resource_updates(
4985            &mut rr,
4986            DpiScaleFactor::new(1.0),
4987            &FcFontCache::default(),
4988            IdNamespace(1),
4989            &fonts,
4990            load_font_none,
4991            parse_font_none,
4992        );
4993        assert!(updates.is_empty());
4994        assert!(rr.font_id_map.is_empty());
4995    }
4996
4997    #[test]
4998    fn build_add_font_resource_updates_skips_unloadable_fonts() {
4999        // A family that cannot be loaded (missing file) must not register
5000        // anything - it is retried next frame, not half-registered.
5001        let mut rr = RendererResources::default();
5002        let mut fonts = OrderedMap::new();
5003        let mut sizes = FastBTreeSet::new();
5004        sizes.insert(Au::from_px(16.0));
5005        fonts.insert(
5006            ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![
5007                StyleFontFamily::System(AzString::from_const_str("DoesNotExist")),
5008            ])),
5009            sizes,
5010        );
5011
5012        let updates = build_add_font_resource_updates(
5013            &mut rr,
5014            DpiScaleFactor::new(1.0),
5015            &FcFontCache::default(),
5016            IdNamespace(1),
5017            &fonts,
5018            load_font_none,
5019            parse_font_none,
5020        );
5021        assert!(updates.is_empty(), "an unloadable font must add no resources");
5022        assert!(rr.font_id_map.is_empty());
5023        assert!(rr.font_families_map.is_empty());
5024    }
5025
5026    #[test]
5027    fn build_add_font_resource_updates_registers_a_font_and_deduplicates_sizes() {
5028        // A StyleFontFamily::Ref resolves without touching the loader, so this
5029        // exercises the whole add-font path deterministically.
5030        let mut rr = RendererResources::default();
5031        let font = dummy_font_ref();
5032        let family = StyleFontFamily::Ref(font.clone());
5033        let dpi = DpiScaleFactor::new(1.0);
5034
5035        let mut sizes = FastBTreeSet::new();
5036        sizes.insert(Au::from_px(16.0));
5037        sizes.insert(Au::from_px(24.0));
5038        sizes.insert(Au::from_px(16.0)); // duplicate -> set dedups it
5039        assert_eq!(sizes.len(), 2);
5040
5041        let mut fonts = OrderedMap::new();
5042        fonts.insert(
5043            ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![family.clone()])),
5044            sizes,
5045        );
5046
5047        let updates = build_add_font_resource_updates(
5048            &mut rr,
5049            dpi,
5050            &FcFontCache::default(),
5051            IdNamespace(1),
5052            &fonts,
5053            load_font_none,
5054            parse_font_none,
5055        );
5056        // 1 AddFont + 2 AddFontInstance
5057        assert_eq!(updates.len(), 3);
5058        assert_eq!(
5059            updates
5060                .iter()
5061                .filter(|(_, m)| matches!(m, AddFontMsg::Font(..)))
5062                .count(),
5063            1
5064        );
5065        assert_eq!(
5066            updates
5067                .iter()
5068                .filter(|(_, m)| matches!(m, AddFontMsg::Instance(..)))
5069                .count(),
5070            2
5071        );
5072        assert_eq!(rr.font_id_map.len(), 1);
5073        assert_eq!(rr.font_families_map.len(), 1);
5074
5075        // add_resources then makes the instances findable by (families, size, dpi).
5076        let mut all_updates = Vec::new();
5077        add_resources(&mut rr, &mut all_updates, updates, Vec::new());
5078        assert_eq!(all_updates.len(), 3);
5079
5080        let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
5081        assert!(rr
5082            .get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
5083            .is_some());
5084        assert!(rr
5085            .get_font_instance_key(&families_hash, Au::from_px(24.0), dpi)
5086            .is_some());
5087        // A size that was never registered misses; so does a different DPI.
5088        assert!(rr
5089            .get_font_instance_key(&families_hash, Au::from_px(99.0), dpi)
5090            .is_none());
5091        assert!(rr
5092            .get_font_instance_key(&families_hash, Au::from_px(16.0), DpiScaleFactor::new(2.0))
5093            .is_none());
5094
5095        // The instance key resolves back to the font (reverse lookup invariant).
5096        let key = rr
5097            .get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
5098            .expect("registered");
5099        let (font_ref, au, got_dpi) = rr
5100            .get_renderable_font_data(&key)
5101            .expect("registered instance must be renderable");
5102        assert_eq!(font_ref.get_hash(), font.get_hash());
5103        assert_eq!(au, Au::from_px(16.0));
5104        assert_eq!(got_dpi, dpi);
5105
5106        // Rebuilding with the same font must not add anything a second time.
5107        let again = build_add_font_resource_updates(
5108            &mut rr,
5109            dpi,
5110            &FcFontCache::default(),
5111            IdNamespace(1),
5112            &fonts,
5113            load_font_none,
5114            parse_font_none,
5115        );
5116        assert!(again.is_empty(), "already-registered fonts must not be re-added");
5117    }
5118
5119    #[test]
5120    fn add_font_msg_into_resource_update_preserves_keys() {
5121        let font = dummy_font_ref();
5122        let key = FontKey::unique(IdNamespace(3));
5123        let family_hash = StyleFontFamilyHash::new(&StyleFontFamily::Ref(font.clone()));
5124        let msg = AddFontMsg::Font(key, family_hash, font.clone());
5125        match msg.into_resource_update() {
5126            ResourceUpdate::AddFont(add) => {
5127                assert_eq!(add.key, key);
5128                assert_eq!(add.font.get_hash(), font.get_hash());
5129            }
5130            other => panic!("expected AddFont, got {other:?}"),
5131        }
5132    }
5133
5134    #[test]
5135    fn delete_font_msg_into_resource_update_preserves_keys() {
5136        let fk = FontKey::unique(IdNamespace(1));
5137        match DeleteFontMsg::Font(fk).into_resource_update() {
5138            ResourceUpdate::DeleteFont(k) => assert_eq!(k, fk),
5139            other => panic!("expected DeleteFont, got {other:?}"),
5140        }
5141        let fik = FontInstanceKey::unique(IdNamespace(1));
5142        let size = (Au::from_px(16.0), DpiScaleFactor::new(1.0));
5143        match DeleteFontMsg::Instance(fik, size).into_resource_update() {
5144            ResourceUpdate::DeleteFontInstance(k) => assert_eq!(k, fik),
5145            other => panic!("expected DeleteFontInstance, got {other:?}"),
5146        }
5147    }
5148
5149    #[test]
5150    fn add_image_msg_into_resource_update_preserves_the_key_and_descriptor() {
5151        let key = ImageKey::unique(IdNamespace(2));
5152        let descriptor = ImageDescriptor {
5153            format: RawImageFormat::BGRA8,
5154            width: 3,
5155            height: 5,
5156            stride: None.into(),
5157            offset: 0,
5158            flags: ImageDescriptorFlags {
5159                is_opaque: false,
5160                allow_mipmaps: true,
5161            },
5162        };
5163        let msg = AddImageMsg(AddImage {
5164            key,
5165            descriptor,
5166            data: ImageData::Raw(SharedRawImageData::new(vec![0u8; 60].into())),
5167            tiling: None,
5168        });
5169        match msg.into_resource_update() {
5170            ResourceUpdate::AddImage(add) => {
5171                assert_eq!(add.key, key);
5172                assert_eq!(add.descriptor, descriptor);
5173                assert!(add.tiling.is_none());
5174            }
5175            other => panic!("expected AddImage, got {other:?}"),
5176        }
5177    }
5178
5179    #[test]
5180    fn build_add_image_resource_updates_skips_null_and_callback_images() {
5181        // NullImage has nothing to upload, Callback runs after layout.
5182        let rr = RendererResources::default();
5183        let mut images = FastBTreeSet::new();
5184        images.insert(ImageRef::null_image(4, 4, RawImageFormat::RGBA8, Vec::new()));
5185        images.insert(ImageRef::callback(0usize, RefAny::new(0u8)));
5186
5187        let updates = build_add_image_resource_updates(
5188            &rr,
5189            IdNamespace(1),
5190            Epoch::new(),
5191            &test_document_id(),
5192            &images,
5193            store_gl_texture_noop,
5194        );
5195        assert!(updates.is_empty());
5196
5197        // ... and an empty DOM produces no updates at all.
5198        let empty = FastBTreeSet::new();
5199        assert!(build_add_image_resource_updates(
5200            &rr,
5201            IdNamespace(1),
5202            Epoch::new(),
5203            &test_document_id(),
5204            &empty,
5205            store_gl_texture_noop,
5206        )
5207        .is_empty());
5208    }
5209
5210    #[test]
5211    fn build_add_image_resource_updates_then_add_resources_round_trip() {
5212        let mut rr = RendererResources::default();
5213        let img = ImageRef::new_rawimage(rgba8_image(2, 2)).expect("valid 2x2");
5214        let hash = img.get_hash();
5215        let ns = IdNamespace(11);
5216
5217        let mut images = FastBTreeSet::new();
5218        images.insert(img.clone());
5219
5220        let updates = build_add_image_resource_updates(
5221            &rr,
5222            ns,
5223            Epoch::new(),
5224            &test_document_id(),
5225            &images,
5226            store_gl_texture_noop,
5227        );
5228        assert_eq!(updates.len(), 1);
5229        assert_eq!(updates[0].0, hash);
5230        // The ImageKey is derived from the hash (no separate mapping table).
5231        assert_eq!(updates[0].1 .0.key, image_ref_hash_to_image_key(hash, ns));
5232        assert_eq!(updates[0].1 .0.descriptor.width, 2);
5233        assert_eq!(updates[0].1 .0.descriptor.height, 2);
5234
5235        let key = updates[0].1 .0.key;
5236        let mut all_updates = Vec::new();
5237        add_resources(&mut rr, &mut all_updates, Vec::new(), updates);
5238        assert_eq!(all_updates.len(), 1);
5239        assert!(matches!(all_updates[0], ResourceUpdate::AddImage(_)));
5240
5241        // Forward and reverse maps must agree after registration.
5242        assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
5243        assert_eq!(rr.image_key_map.get(&key), Some(&hash));
5244
5245        // An already-registered image is never re-uploaded.
5246        let again = build_add_image_resource_updates(
5247            &rr,
5248            ns,
5249            Epoch::new(),
5250            &test_document_id(),
5251            &images,
5252            store_gl_texture_noop,
5253        );
5254        assert!(again.is_empty());
5255
5256        // update_image mutates the descriptor in place, keeping the key.
5257        let new_descriptor = ImageDescriptor {
5258            format: RawImageFormat::BGRA8,
5259            width: 8,
5260            height: 8,
5261            stride: None.into(),
5262            offset: 0,
5263            flags: ImageDescriptorFlags {
5264                is_opaque: true,
5265                allow_mipmaps: true,
5266            },
5267        };
5268        rr.update_image(&hash, new_descriptor);
5269        assert_eq!(rr.get_image(&hash).map(|r| r.descriptor.width), Some(8));
5270        assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
5271        // Updating an unknown hash is a silent no-op, not a panic.
5272        rr.update_image(&ImageRefHash { inner: u64::MAX }, new_descriptor);
5273    }
5274
5275    #[test]
5276    fn add_resources_with_empty_input_changes_nothing() {
5277        let mut rr = RendererResources::default();
5278        let mut updates = Vec::new();
5279        add_resources(&mut rr, &mut updates, Vec::new(), Vec::new());
5280        assert!(updates.is_empty());
5281        assert!(rr.currently_registered_images.is_empty());
5282        assert!(rr.currently_registered_fonts.is_empty());
5283        assert!(rr.image_key_map.is_empty());
5284    }
5285
5286    // =====================================================================
5287    // NUMERIC: CPU painting (paint_dot / paint_stroke)
5288    // =====================================================================
5289
5290    #[test]
5291    fn paint_dot_composites_at_the_center_and_leaves_far_pixels_alone() {
5292        let mut img = rgba8_image(4, 4);
5293        img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
5294        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5295
5296        // Pixel (1,1) is 0.71 px from the center -> inside the hard core.
5297        let idx = (1 * 4 + 1) * 4;
5298        assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255], "center pixel must be opaque red");
5299        // Pixel (0,0) is 2.12 px away -> outside the radius -> untouched.
5300        assert_eq!(&px[0..4], &[0, 0, 0, 0], "pixels beyond the radius stay untouched");
5301    }
5302
5303    #[test]
5304    fn paint_dot_honours_bgra_channel_order() {
5305        let mut img = rgba8_image(4, 4);
5306        img.data_format = RawImageFormat::BGRA8;
5307        img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
5308        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5309        let idx = (1 * 4 + 1) * 4;
5310        // BGRA: red lands in byte 2, blue in byte 0.
5311        assert_eq!(&px[idx..idx + 4], &[0, 0, 255, 255]);
5312    }
5313
5314    #[test]
5315    fn paint_dot_rejects_degenerate_radii_and_sizes() {
5316        let untouched = |img: &RawImage| {
5317            img.pixels
5318                .get_u8_vec_ref()
5319                .expect("u8")
5320                .as_ref()
5321                .iter()
5322                .all(|b| *b == 0)
5323        };
5324
5325        // radius <= 0 and NaN radius are no-ops (the `!(r > 0.0)` guard).
5326        for r in [0.0_f32, -1.0, -0.0, f32::NAN, f32::NEG_INFINITY] {
5327            let mut img = rgba8_image(4, 4);
5328            img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), r));
5329            assert!(untouched(&img), "radius {r} must not paint");
5330        }
5331
5332        // Zero-sized images are no-ops (and must not index out of bounds).
5333        let mut img = rgba8_image(0, 0);
5334        img.paint_dot(0.0, 0.0, Brush::new(opaque_red(), 4.0));
5335        assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
5336
5337        // Non-8-bit-RGBA formats are documented as left untouched.
5338        for format in [
5339            RawImageFormat::R8,
5340            RawImageFormat::RGB8,
5341            RawImageFormat::RGBA16,
5342            RawImageFormat::RGBAF32,
5343        ] {
5344            let mut img = rgba8_image(4, 4);
5345            img.data_format = format;
5346            img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
5347            assert!(untouched(&img), "format {format:?} must not be painted");
5348        }
5349    }
5350
5351    #[test]
5352    fn paint_dot_with_nan_and_infinite_coordinates_is_a_safe_noop() {
5353        // NaN / +-inf centers collapse the scan range to nothing instead of
5354        // producing an out-of-bounds index.
5355        for (cx, cy) in [
5356            (f32::NAN, 2.0_f32),
5357            (2.0, f32::NAN),
5358            (f32::NAN, f32::NAN),
5359            (f32::INFINITY, 2.0),
5360            (f32::NEG_INFINITY, 2.0),
5361            (2.0, f32::INFINITY),
5362            (1.0e30, 1.0e30),
5363            (-1.0e30, -1.0e30),
5364        ] {
5365            let mut img = rgba8_image(4, 4);
5366            img.paint_dot(cx, cy, Brush::new(opaque_red(), 2.0));
5367            let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5368            assert!(
5369                px.iter().all(|b| *b == 0),
5370                "({cx}, {cy}) must not paint anything"
5371            );
5372        }
5373    }
5374
5375    #[test]
5376    fn paint_dot_alpha_saturates_and_never_overflows() {
5377        // Repeatedly stamping an opaque dab must clamp at 255, never wrap.
5378        let mut img = rgba8_image(4, 4);
5379        let brush = Brush::new(opaque_red(), 2.0);
5380        for _ in 0..50 {
5381            img.paint_dot(2.0, 2.0, brush);
5382        }
5383        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5384        let idx = (1 * 4 + 1) * 4;
5385        assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
5386
5387        // A NaN hardness makes the coverage NaN; the `a <= 0.0` check is false
5388        // for NaN, so the blend runs with a NaN alpha -- the `.clamp(0, 255)`
5389        // on the result keeps every channel a valid u8 (NaN clamps to 0 here),
5390        // i.e. garbage-in stays in-range instead of wrapping.
5391        let mut img = rgba8_image(4, 4);
5392        let mut nan_brush = Brush::new(opaque_red(), 2.0);
5393        nan_brush.hardness = f32::NAN;
5394        img.paint_dot(2.0, 2.0, nan_brush);
5395        // No assertion on the exact value: the point is that it did not panic
5396        // and every byte is (trivially) a valid u8.
5397        assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
5398    }
5399
5400    #[test]
5401    fn paint_dot_zero_flow_and_transparent_color_do_not_paint() {
5402        let mut img = rgba8_image(4, 4);
5403        let mut brush = Brush::new(opaque_red(), 2.0);
5404        brush.flow = 0.0;
5405        img.paint_dot(2.0, 2.0, brush);
5406        assert!(img
5407            .pixels
5408            .get_u8_vec_ref()
5409            .expect("u8")
5410            .as_ref()
5411            .iter()
5412            .all(|b| *b == 0));
5413
5414        let mut img = rgba8_image(4, 4);
5415        let transparent = ColorU {
5416            r: 255,
5417            g: 0,
5418            b: 0,
5419            a: 0,
5420        };
5421        img.paint_dot(2.0, 2.0, Brush::new(transparent, 2.0));
5422        assert!(img
5423            .pixels
5424            .get_u8_vec_ref()
5425            .expect("u8")
5426            .as_ref()
5427            .iter()
5428            .all(|b| *b == 0));
5429
5430        // A negative / >1 flow is clamped, not extrapolated.
5431        let mut img = rgba8_image(4, 4);
5432        let mut brush = Brush::new(opaque_red(), 2.0);
5433        brush.flow = -5.0;
5434        img.paint_dot(2.0, 2.0, brush);
5435        assert!(img
5436            .pixels
5437            .get_u8_vec_ref()
5438            .expect("u8")
5439            .as_ref()
5440            .iter()
5441            .all(|b| *b == 0));
5442    }
5443
5444    #[test]
5445    fn paint_stroke_paints_both_endpoints() {
5446        let mut img = rgba8_image(8, 8);
5447        img.paint_stroke(1.5, 1.5, 6.5, 6.5, Brush::new(opaque_red(), 1.5));
5448        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5449        let alpha_at = |x: usize, y: usize| px[(y * 8 + x) * 4 + 3];
5450        assert!(alpha_at(1, 1) > 0, "start of the stroke must be painted");
5451        assert!(alpha_at(6, 6) > 0, "end of the stroke must be painted");
5452        assert_eq!(alpha_at(7, 0), 0, "off-line pixels stay untouched");
5453    }
5454
5455    #[test]
5456    fn paint_stroke_zero_length_stamps_a_single_dab() {
5457        // len == 0 -> n == 0 -> the `n <= 0` branch stamps one dab at (x1, y1).
5458        let mut img = rgba8_image(4, 4);
5459        img.paint_stroke(2.0, 2.0, 2.0, 2.0, Brush::new(opaque_red(), 2.0));
5460        let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
5461        let idx = (1 * 4 + 1) * 4;
5462        assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
5463    }
5464
5465    #[test]
5466    fn paint_stroke_degenerate_brush_params_do_not_divide_by_zero_or_hang() {
5467        // spacing == 0 / negative: the `.max(0.01)` and `.max(0.5)` floors keep
5468        // the step positive, so the dab count stays finite.
5469        for spacing in [0.0_f32, -1.0, f32::NAN] {
5470            let mut img = rgba8_image(8, 8);
5471            let mut brush = Brush::new(opaque_red(), 2.0);
5472            brush.spacing = spacing;
5473            img.paint_stroke(0.0, 0.0, 7.0, 7.0, brush);
5474            assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(8 * 8 * 4));
5475        }
5476
5477        // A NaN endpoint yields a NaN length -> n == 0 -> one (no-op) dab.
5478        let mut img = rgba8_image(4, 4);
5479        img.paint_stroke(f32::NAN, 0.0, 1.0, 1.0, Brush::new(opaque_red(), 1.0));
5480        assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
5481
5482        // radius == 0 -> every dab is a no-op, and the loop still terminates.
5483        let mut img = rgba8_image(4, 4);
5484        img.paint_stroke(0.0, 0.0, 3.0, 3.0, Brush::new(opaque_red(), 0.0));
5485        assert!(img
5486            .pixels
5487            .get_u8_vec_ref()
5488            .expect("u8")
5489            .as_ref()
5490            .iter()
5491            .all(|b| *b == 0));
5492    }
5493
5494    #[test]
5495    #[ignore = "BUG: an infinite stroke length makes n = i32::MAX -> ~2.1 billion \
5496                paint_dot calls (effectively an infinite loop). DO NOT un-ignore \
5497                without a length/step guard in paint_stroke."]
5498    fn bug_paint_stroke_with_infinite_endpoint_loops_2_billion_times() {
5499        // `len` is +inf, `step` is finite, so `n = (inf / step).floor() as i32`
5500        // saturates to i32::MAX and the `for i in 0..=n` loop runs 2^31 times.
5501        // paint_stroke should clamp `n` (or bail on a non-finite length).
5502        let mut img = rgba8_image(4, 4);
5503        img.paint_stroke(0.0, 0.0, f32::INFINITY, 0.0, Brush::new(opaque_red(), 2.0));
5504    }
5505
5506    #[test]
5507    #[ignore = "BUG: paint_dot trusts self.width/height over the pixel buffer -> \
5508                index-out-of-bounds panic when they disagree"]
5509    fn bug_paint_dot_indexes_out_of_bounds_when_dims_exceed_the_buffer() {
5510        // RawImage's fields are all public, so a caller can hand paint_dot a
5511        // 100x100 image backed by 4 bytes. paint_dot computes the index from
5512        // width/height and indexes `buf` unchecked -> panic. It should clamp the
5513        // scan rect to the buffer length (or bail).
5514        let mut img = RawImage {
5515            pixels: RawImageData::U8(vec![0u8; 4].into()),
5516            width: 100,
5517            height: 100,
5518            premultiplied_alpha: true,
5519            data_format: RawImageFormat::RGBA8,
5520            tag: Vec::new().into(),
5521        };
5522        img.paint_dot(50.0, 50.0, Brush::new(opaque_red(), 4.0));
5523    }
5524
5525    #[test]
5526    #[ignore = "BUG: into_loaded_image_source computes `width * height` (and then \
5527                `expected_len * channels`) without checked arithmetic -> overflow \
5528                panic instead of the documented None"]
5529    fn bug_into_loaded_image_source_overflows_on_huge_dimensions() {
5530        // Documented contract: "Returns None if the width * height * BPP does not
5531        // match". With width == usize::MAX the multiplication overflows and
5532        // panics under the dev-profile overflow checks instead.
5533        let img = RawImage {
5534            pixels: RawImageData::U8(vec![0u8; 4].into()),
5535            width: usize::MAX,
5536            height: 2,
5537            premultiplied_alpha: true,
5538            data_format: RawImageFormat::RGBA8,
5539            tag: Vec::new().into(),
5540        };
5541        assert!(img.into_loaded_image_source().is_none());
5542    }
5543}