Skip to main content

azul_core/
resources.rs

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