Skip to main content

azul_core/
resources.rs

1//! Resource management types for the application.
2//!
3//! This module contains the core types for managing application resources:
4//! - `AppConfig`: application-level configuration (logging, fonts, routes, components)
5//! - `ImageRef` / `ImageRefHash`: reference-counted decoded image handles
6//! - `FontKey` / `FontInstanceKey` / `ImageKey`: renderer-scoped resource keys
7//! - `RendererResources`: per-window font/image registry with frame-based GC
8//! - `RawImage`: CPU-side pixel data with format conversion to BGRA8
9//! - `build_add_font_resource_updates` / `build_add_image_resource_updates`:
10//!   diff current frame against registered resources and produce WebRender updates
11
12#[cfg(not(feature = "std"))]
13use alloc::string::ToString;
14use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
15use core::{
16    fmt,
17    hash::{Hash, Hasher},
18    sync::atomic::{AtomicU64, AtomicUsize, Ordering as AtomicOrdering},
19};
20
21use azul_css::{
22    codegen::format::GetHash,
23    props::basic::{
24        pixel::DEFAULT_FONT_SIZE, ColorU, FloatValue, FontRef, LayoutRect, LayoutSize,
25        StyleFontFamily, StyleFontFamilyVec, StyleFontSize,
26    },
27    props::style::scrollbar::OptionScrollPhysics,
28    system::SystemStyle,
29    AzString, F32Vec, LayoutDebugMessage, OptionI32, StringVec, U16Vec, U32Vec, U8Vec,
30};
31use rust_fontconfig::FcFontCache;
32
33// Re-export Core* callback types for public use
34pub use crate::callbacks::{
35    CoreImageCallback, CoreRenderImageCallback, CoreRenderImageCallbackType,
36};
37use crate::{
38    callbacks::{LayoutCallback, VirtualViewCallback},
39    dom::{DomId, NodeData, NodeType},
40    geom::{LogicalPosition, LogicalRect, LogicalSize},
41    gl::{OptionGlContextPtr, Texture},
42    hit_test::DocumentId,
43    id::NodeId,
44    prop_cache::CssPropertyCache,
45    refany::RefAny,
46    styled_dom::{
47        NodeHierarchyItemId, StyleFontFamiliesHash, StyleFontFamilyHash, StyledDom, StyledNodeState,
48    },
49    ui_solver::GlyphInstance,
50    window::{AzStringPair, OptionChar, StringPairVec},
51    xml::{
52        ComponentDef, ComponentDefVec, ComponentId, ComponentLibrary, ComponentLibraryVec,
53        ComponentSource, RegisterComponentFn, RegisterComponentLibraryFn,
54    },
55    FastBTreeSet, OrderedMap,
56};
57
58/// Selects which image layer of an element a node-image update applies to.
59///
60/// Used by `CallbackInfo::change_node_image` to distinguish between replacing an
61/// element's CSS `background` image and replacing its main content image (e.g. an
62/// animated GL texture re-rendered on resize).
63#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
64#[repr(C)]
65pub enum UpdateImageType {
66    /// The update targets the element's background.
67    Background,
68    /// The update targets the element's main content.
69    Content,
70}
71
72#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73#[repr(C)]
74pub struct DpiScaleFactor {
75    pub inner: FloatValue,
76}
77
78impl DpiScaleFactor {
79    #[must_use]
80    pub fn new(f: f32) -> Self {
81        Self {
82            inner: FloatValue::new(f),
83        }
84    }
85}
86
87/// Determines what happens when all application windows are closed
88#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
89#[repr(C)]
90#[derive(Default)]
91pub enum AppTerminationBehavior {
92    /// Return control to `main()` when all windows are closed (if platform supports it).
93    /// On macOS, this exits the `NSApplication` run loop and returns to `main()`.
94    /// This is useful if you want to clean up resources or restart the event loop.
95    ReturnToMain,
96    /// Keep the application running even when all windows are closed.
97    /// This is the standard macOS behavior (app stays in dock until explicitly quit).
98    RunForever,
99    /// Immediately terminate the process when all windows are closed.
100    /// Calls `std::process::exit(0)`.
101    #[default]
102    EndProcess,
103}
104
105/// An email address, e.g. a support mailbox problem reports go to.
106///
107/// Deliberately a thin wrapper (no RFC 5322 validation): the address is
108/// app-configured, not user input, and the SMTP layer reports a bad one
109/// loudly at send time.
110#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
111#[repr(C)]
112pub struct EmailAddress {
113    /// The address, e.g. `support@myapp.example`.
114    pub address: AzString,
115}
116
117impl_option!(
118    EmailAddress,
119    OptionEmailAddress,
120    copy = false,
121    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
122);
123
124impl EmailAddress {
125    #[must_use]
126    pub const fn new(address: AzString) -> Self {
127        Self { address }
128    }
129}
130
131/// Requested update behaviour.
132///
133/// The EFFECTIVE behaviour is this clamped by what the installation permits:
134/// a package-managed binary (dpkg-owned, `/usr`, snap, flatpak,
135/// `WindowsApps`) NEVER self-updates — `SelfUpdate` degrades to
136/// `NotifyOnly` there ("update via your package manager").
137#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
138#[repr(C)]
139pub enum UpdateMode {
140    /// Check, notify, and — after the user consents — download and swap the
141    /// binary. Staging may happen in the background; INSTALLING never does.
142    SelfUpdate,
143    /// Check and notify only ("new version available"); installing is the
144    /// user's / packager's job. The mode packagers should ship.
145    #[default]
146    NotifyOnly,
147    /// Never check for updates.
148    Disabled,
149}
150
151/// Update configuration, part of [`AppConfig`]. With `manifest_url` unset
152/// every check reports an error naming this field — nothing phones home
153/// unless the app points it somewhere.
154#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
155#[repr(C)]
156pub struct UpdateSettings {
157    /// Requested behaviour; clamped by the detected install kind at runtime.
158    pub mode: UpdateMode,
159    /// URL of the update manifest (JSON: `{"latest": {"version", "download_url",
160    /// "changelog_md", "digest"}}`). None = update checks disabled.
161    pub manifest_url: azul_css::OptionString,
162    /// The RUNNING version, compared against the manifest's `latest.version`
163    /// (dotted-numeric compare). Apps typically pass `env!("CARGO_PKG_VERSION")`.
164    pub current_version: AzString,
165    /// Directory-safe application name; keys the updater's state directory
166    /// (`{data_dir}/{app_name}/update-state.json`) and the staging area.
167    pub app_name: AzString,
168    /// RESERVED: the build's date, compiled in by the APP (e.g. a build
169    /// script stamping `env!("BUILD_DATE")`). The engine only carries it —
170    /// into telemetry resources, crash dumps and update checks.
171    pub build_date: AzString,
172    /// RESERVED: the build's VCS tag (or commit), compiled in by the APP.
173    /// Same carriage as `build_date`.
174    pub build_tag: AzString,
175    /// The RELEASE CHANNEL this binary follows: `stable` (the default when
176    /// empty), `beta`, `nightly`, or whatever names the publisher uses.
177    /// Compiled in, so a nightly build cannot be talked onto the stable
178    /// track by a manifest: the binary decides which channel it reads, the
179    /// publisher decides what is in it.
180    pub channel: AzString,
181    /// Base64 minisign ROOT public key, compiled in by the APP. Non-empty
182    /// arms the update SIGNATURE CHAIN: the manifest must then carry a
183    /// root-signed signing-key statement and an artifact signature, and an
184    /// unsigned release is a hard error instead of a fallback. Empty (the
185    /// default) = digest verification only.
186    pub root_public_key: AzString,
187}
188
189impl Default for UpdateSettings {
190    fn default() -> Self {
191        Self {
192            mode: UpdateMode::default(),
193            manifest_url: azul_css::OptionString::None,
194            current_version: AzString::from_const_str("0.0.0"),
195            app_name: AzString::from_const_str("azul-app"),
196            build_date: AzString::from_const_str(""),
197            build_tag: AzString::from_const_str(""),
198            channel: AzString::from_const_str(""),
199            root_public_key: AzString::from_const_str(""),
200        }
201    }
202}
203
204/// A named font bundled with the application (name + raw bytes).
205/// The name is used to reference the font in CSS (e.g. `font-family: "MyFont"`).
206#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
207#[repr(C)]
208pub struct NamedFont {
209    /// The font family name to use in CSS (e.g. "Roboto", "`MyCustomFont`")
210    pub name: AzString,
211    /// Raw font file bytes (TTF, OTF, etc.)
212    pub bytes: U8Vec,
213}
214
215impl_option!(
216    NamedFont,
217    OptionNamedFont,
218    copy = false,
219    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
220);
221
222impl NamedFont {
223    #[must_use]
224    pub const fn new(name: AzString, bytes: U8Vec) -> Self {
225        Self { name, bytes }
226    }
227}
228
229impl_vec!(
230    NamedFont,
231    NamedFontVec,
232    NamedFontVecDestructor,
233    NamedFontVecDestructorType,
234    NamedFontVecSlice,
235    OptionNamedFont
236);
237impl_vec_mut!(NamedFont, NamedFontVec);
238impl_vec_debug!(NamedFont, NamedFontVec);
239impl_vec_partialeq!(NamedFont, NamedFontVec);
240impl_vec_eq!(NamedFont, NamedFontVec);
241impl_vec_partialord!(NamedFont, NamedFontVec);
242impl_vec_ord!(NamedFont, NamedFontVec);
243impl_vec_hash!(NamedFont, NamedFontVec);
244impl_vec_clone!(NamedFont, NamedFontVec, NamedFontVecDestructor);
245
246/// Descriptor for a font that the layout engine currently has loaded in its
247/// font cache.
248///
249/// Returned by `CallbackInfo::get_loaded_fonts()`. The `font_hash` field is
250/// the same `u64` carried by `DisplayListItem::Text` glyph runs, so a callback
251/// can correlate a loaded font with the text runs that use it and then fetch
252/// the raw bytes via `CallbackInfo::get_loaded_font_bytes(font_hash)` (e.g. to
253/// embed every font the layout actually used into a generated PDF).
254#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
255#[repr(C)]
256pub struct LoadedFont {
257    /// Stable hash of the parsed font, identical to the `font_hash` stored on
258    /// `DisplayListItem::Text` glyph runs. Use this to look up the bytes with
259    /// `CallbackInfo::get_loaded_font_bytes`.
260    pub font_hash: u64,
261    /// PostScript / family name from the font's `name` table, or an empty
262    /// string if the font did not provide one.
263    pub family_name: AzString,
264    /// Total number of glyphs in the font (from the `maxp` table).
265    pub num_glyphs: u32,
266    /// `true` if the source font bytes are retained and can be retrieved with
267    /// `CallbackInfo::get_loaded_font_bytes(font_hash)`. Fonts loaded on the
268    /// production (lazy mmap) path retain their bytes; some test-only fonts do
269    /// not.
270    pub has_bytes: bool,
271}
272
273impl_option!(
274    LoadedFont,
275    OptionLoadedFont,
276    copy = false,
277    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
278);
279
280impl LoadedFont {
281    #[must_use]
282    pub const fn new(
283        font_hash: u64,
284        family_name: AzString,
285        num_glyphs: u32,
286        has_bytes: bool,
287    ) -> Self {
288        Self {
289            font_hash,
290            family_name,
291            num_glyphs,
292            has_bytes,
293        }
294    }
295}
296
297impl_vec!(
298    LoadedFont,
299    LoadedFontVec,
300    LoadedFontVecDestructor,
301    LoadedFontVecDestructorType,
302    LoadedFontVecSlice,
303    OptionLoadedFont
304);
305impl_vec_mut!(LoadedFont, LoadedFontVec);
306impl_vec_debug!(LoadedFont, LoadedFontVec);
307impl_vec_partialeq!(LoadedFont, LoadedFontVec);
308impl_vec_eq!(LoadedFont, LoadedFontVec);
309impl_vec_partialord!(LoadedFont, LoadedFontVec);
310impl_vec_ord!(LoadedFont, LoadedFontVec);
311impl_vec_hash!(LoadedFont, LoadedFontVec);
312impl_vec_clone!(LoadedFont, LoadedFontVec, LoadedFontVecDestructor);
313#[allow(variant_size_differences)]
314// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
315/// Configuration for how fonts should be loaded at app startup.
316#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
317#[repr(C, u8)]
318#[derive(Default)]
319pub enum FontLoadingConfig {
320    /// Load all system fonts (default behavior, can be slow on systems with many fonts)
321    #[default]
322    LoadAllSystemFonts,
323    /// Only load fonts for specific families (faster startup).
324    /// Generic families like "sans-serif" are automatically expanded to OS-specific fonts.
325    LoadOnlyFamilies(StringVec),
326    /// Don't load any system fonts, only use bundled fonts
327    BundledFontsOnly,
328}
329
330/// Mock environment for CSS evaluation.
331///
332/// Allows overriding auto-detected system properties for testing and development.
333/// Any field set to `None` will use the auto-detected value.
334/// Any field set to `Some(...)` will override the auto-detected value.
335///
336/// # Example
337/// ```rust
338/// # use azul_core::resources::CssMockEnvironment;
339/// use azul_css::dynamic_selector::{
340///     OsCondition, ThemeCondition, OsVersion,
341///     OptionOsCondition, OptionThemeCondition, OptionOsVersion,
342/// };
343///
344/// // Mock a Linux dark theme environment on any platform
345/// let mock = CssMockEnvironment {
346///     os: OptionOsCondition::Some(OsCondition::Linux),
347///     theme: OptionThemeCondition::Some(ThemeCondition::Dark),
348///     ..Default::default()
349/// };
350///
351/// // Mock Windows XP for retro testing
352/// let mock = CssMockEnvironment {
353///     os: OptionOsCondition::Some(OsCondition::Windows),
354///     os_version: OptionOsVersion::Some(OsVersion::WIN_XP),
355///     ..Default::default()
356/// };
357/// ```
358#[derive(Debug, Clone, Default)]
359#[repr(C)]
360pub struct CssMockEnvironment {
361    /// Override the current theme (light/dark)
362    pub theme: azul_css::dynamic_selector::OptionThemeCondition,
363    /// Override the current language (BCP 47 tag, e.g., "de-DE", "en-US")
364    pub language: azul_css::OptionString,
365    /// Override the detected OS version
366    pub os_version: azul_css::dynamic_selector::OptionOsVersion,
367    /// Override the detected operating system
368    pub os: azul_css::dynamic_selector::OptionOsCondition,
369    /// Override the Linux desktop environment (only applies when os = Linux)
370    pub desktop_env: azul_css::dynamic_selector::OptionLinuxDesktopEnv,
371    /// Override viewport dimensions (for @media queries)
372    /// Only use for testing - normally set by window size
373    pub viewport_width: azul_css::OptionF32,
374    pub viewport_height: azul_css::OptionF32,
375    /// Override the reduced motion preference
376    pub prefers_reduced_motion: azul_css::OptionBool,
377    /// Override the high contrast preference
378    pub prefers_high_contrast: azul_css::OptionBool,
379}
380
381impl CssMockEnvironment {
382    /// Create a mock for Linux environment
383    #[must_use]
384    pub fn linux() -> Self {
385        Self {
386            os: azul_css::dynamic_selector::OptionOsCondition::Some(
387                azul_css::dynamic_selector::OsCondition::Linux,
388            ),
389            ..Default::default()
390        }
391    }
392
393    /// Create a mock for Windows environment
394    #[must_use]
395    pub fn windows() -> Self {
396        Self {
397            os: azul_css::dynamic_selector::OptionOsCondition::Some(
398                azul_css::dynamic_selector::OsCondition::Windows,
399            ),
400            ..Default::default()
401        }
402    }
403
404    /// Create a mock for macOS environment
405    #[must_use]
406    pub fn macos() -> Self {
407        Self {
408            os: azul_css::dynamic_selector::OptionOsCondition::Some(
409                azul_css::dynamic_selector::OsCondition::MacOS,
410            ),
411            ..Default::default()
412        }
413    }
414
415    /// Create a mock for dark theme
416    #[must_use]
417    pub fn dark_theme() -> Self {
418        Self {
419            theme: azul_css::dynamic_selector::OptionThemeCondition::Some(
420                azul_css::dynamic_selector::ThemeCondition::Dark,
421            ),
422            ..Default::default()
423        }
424    }
425
426    /// Create a mock for light theme
427    #[must_use]
428    pub fn light_theme() -> Self {
429        Self {
430            theme: azul_css::dynamic_selector::OptionThemeCondition::Some(
431                azul_css::dynamic_selector::ThemeCondition::Light,
432            ),
433            ..Default::default()
434        }
435    }
436
437    /// Apply this mock to a `DynamicSelectorContext`
438    pub fn apply_to(&self, ctx: &mut azul_css::dynamic_selector::DynamicSelectorContext) {
439        if let azul_css::dynamic_selector::OptionOsCondition::Some(os) = self.os {
440            ctx.os = os;
441        }
442        if let azul_css::dynamic_selector::OptionOsVersion::Some(os_version) = self.os_version {
443            ctx.os_version = os_version;
444        }
445        if let azul_css::dynamic_selector::OptionLinuxDesktopEnv::Some(de) = self.desktop_env {
446            ctx.desktop_env = azul_css::dynamic_selector::OptionLinuxDesktopEnv::Some(de);
447        }
448        if let azul_css::dynamic_selector::OptionThemeCondition::Some(ref theme) = self.theme {
449            ctx.theme = theme.clone();
450        }
451        if let azul_css::OptionString::Some(ref lang) = self.language {
452            ctx.language = lang.clone();
453        }
454        if let azul_css::OptionBool::Some(reduced) = self.prefers_reduced_motion {
455            ctx.prefers_reduced_motion = if reduced {
456                azul_css::dynamic_selector::BoolCondition::True
457            } else {
458                azul_css::dynamic_selector::BoolCondition::False
459            };
460        }
461        if let azul_css::OptionBool::Some(high_contrast) = self.prefers_high_contrast {
462            ctx.prefers_high_contrast = if high_contrast {
463                azul_css::dynamic_selector::BoolCondition::True
464            } else {
465                azul_css::dynamic_selector::BoolCondition::False
466            };
467        }
468        if let azul_css::OptionF32::Some(w) = self.viewport_width {
469            ctx.viewport_width = w;
470        }
471        if let azul_css::OptionF32::Some(h) = self.viewport_height {
472            ctx.viewport_height = h;
473        }
474    }
475}
476
477impl_option!(
478    CssMockEnvironment,
479    OptionCssMockEnvironment,
480    copy = false,
481    [Debug, Clone]
482);
483
484/// A route mapping a URL pattern to a layout callback.
485///
486/// Routes are cross-platform: on desktop, switching routes swaps the
487/// active layout callback and triggers `RefreshDom`. On web, it also
488/// calls `history.pushState()` for browser navigation.
489///
490/// # Pattern syntax
491///
492/// - `"/"` — exact root
493/// - `"/about"` — exact path
494/// - `"/user/:id"` — parameterized segment, `/user/42` yields `id = "42"`
495///
496/// # C API
497/// ```c
498/// AzAppConfig_addRoute(&config, AzString_fromConstStr("/user/:id"), layout_user);
499/// ```
500#[repr(C)]
501pub struct Route {
502    /// URL pattern (e.g. `"/"`, `"/about"`, `"/user/:id"`)
503    pub pattern: AzString,
504    /// Layout callback invoked when this route is active
505    pub layout_callback: LayoutCallback,
506}
507
508impl Clone for Route {
509    fn clone(&self) -> Self {
510        Self {
511            pattern: self.pattern.clone(),
512            layout_callback: self.layout_callback.clone(),
513        }
514    }
515}
516impl fmt::Debug for Route {
517    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518        f.debug_struct("Route")
519            .field("pattern", &self.pattern)
520            .field("layout_callback", &self.layout_callback)
521            .finish()
522    }
523}
524impl PartialEq for Route {
525    fn eq(&self, o: &Self) -> bool {
526        self.pattern == o.pattern && self.layout_callback == o.layout_callback
527    }
528}
529impl Eq for Route {}
530impl PartialOrd for Route {
531    fn partial_cmp(&self, o: &Self) -> Option<core::cmp::Ordering> {
532        Some(self.cmp(o))
533    }
534}
535impl Ord for Route {
536    fn cmp(&self, o: &Self) -> core::cmp::Ordering {
537        self.pattern
538            .cmp(&o.pattern)
539            .then_with(|| self.layout_callback.cmp(&o.layout_callback))
540    }
541}
542impl Hash for Route {
543    fn hash<H: Hasher>(&self, state: &mut H) {
544        self.pattern.hash(state);
545        self.layout_callback.hash(state);
546    }
547}
548
549impl_option!(
550    Route,
551    OptionRoute,
552    copy = false,
553    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
554);
555impl_vec!(
556    Route,
557    RouteVec,
558    RouteVecDestructor,
559    RouteVecDestructorType,
560    RouteVecSlice,
561    OptionRoute
562);
563impl_vec_mut!(Route, RouteVec);
564impl_vec_debug!(Route, RouteVec);
565impl_vec_clone!(Route, RouteVec, RouteVecDestructor);
566impl_vec_partialeq!(Route, RouteVec);
567impl_vec_eq!(Route, RouteVec);
568impl_vec_partialord!(Route, RouteVec);
569impl_vec_ord!(Route, RouteVec);
570impl_vec_hash!(Route, RouteVec);
571
572/// Result of matching a URL against a route pattern.
573///
574/// Stores the matched pattern and any extracted parameters.
575/// Available to layout callbacks via `LayoutCallbackInfo::get_route_param()`.
576#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
577#[repr(C)]
578pub struct RouteMatch {
579    /// The matched route pattern (e.g. `"/user/:id"`)
580    pub pattern: AzString,
581    /// Extracted parameters (e.g. `[("id", "42")]`)
582    pub params: StringPairVec,
583}
584
585impl RouteMatch {
586    /// Get a route parameter by key.
587    #[must_use]
588    pub fn get_param(&self, key: &str) -> Option<&AzString> {
589        self.params.get_key(key)
590    }
591}
592
593impl_option!(
594    RouteMatch,
595    OptionRouteMatch,
596    copy = false,
597    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
598);
599
600/// Match a URL path against a route pattern, extracting parameters.
601///
602/// Returns `Some(RouteMatch)` with extracted params on match, `None` otherwise.
603///
604/// # Examples
605/// - pattern `"/user/:id"`, path `"/user/42"` → `Some(RouteMatch { params: [("id","42")] })`
606/// - pattern `"/"`, path `"/"` → `Some(RouteMatch { params: [] })`
607/// - pattern `"/about"`, path `"/settings"` → `None`
608#[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
609#[must_use]
610pub fn match_route(pattern: &str, path: &str) -> Option<RouteMatch> {
611    let pat_segs: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
612    let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
613
614    if pat_segs.len() != path_segs.len() {
615        return None;
616    }
617
618    let mut params = Vec::new();
619    for (pat, val) in pat_segs.iter().zip(path_segs.iter()) {
620        if let Some(param_name) = pat.strip_prefix(':') {
621            params.push(AzStringPair {
622                key: AzString::from(param_name.to_string()),
623                value: AzString::from((*val).to_string()),
624            });
625        } else if pat != val {
626            return None;
627        }
628    }
629
630    Some(RouteMatch {
631        pattern: AzString::from(pattern.to_string()),
632        params: StringPairVec::from_vec(params),
633    })
634}
635
636/// The ZOMBIE-SPECIFIC half of what a native animation function receives.
637///
638/// The other half is a full [`TimerCallbackInfo`] (USER ruling 2026-08-17),
639/// so a callback can also reach the LIVE dom, queue changes, read momentum,
640/// measure any node… full customizability, not a keyhole.
641///
642/// The pointers here are BORROWED for the duration of the call — a function
643/// may walk the retained tree (e.g. measure its own component's text
644/// mid-exit) but must not store them: for an exit the tree is the retained
645/// zombie frame, freed when the animation completes.
646#[derive(Debug)]
647#[repr(C)]
648pub struct ZombieAnimInfo {
649    /// The `StyledDom` the ANIMATED node lives in — the retained tree for
650    /// exits, the live tree for enters. Never null during a call. (The live
651    /// dom is separately reachable through the `TimerCallbackInfo`.)
652    pub styled_dom: *const crate::styled_dom::StyledDom,
653    /// The animated node's index in THAT tree.
654    pub node_id: u64,
655    /// The node's rect in logical px: the retained rect for exits, the
656    /// solved rect for enters.
657    pub rect: LogicalRect,
658    /// The viewport the tree was laid out in.
659    pub viewport: LogicalRect,
660    pub dpi_factor: f32,
661    /// RAW LINEAR progress 0..=1. The engine does NOT pre-apply easing for
662    /// native functions: the DECLARED timing arrives in `timing` below, and
663    /// the callback owns the math — apply it via `AnimationTiming::evaluate`
664    /// or substitute its own curve entirely.
665    pub t: f32,
666    /// The timing the CSS requested (`ease`, `spring`, a
667    /// `cubic-bezier(...)` point list, …).
668    pub timing: azul_css::props::basic::animation::AnimationTiming,
669    /// The animation's velocity ENTERING this frame, logical px/s — the
670    /// derivative of the previous two samples (one-frame lag; zero on the
671    /// first frame and for enter/live tracks today). The read half of the
672    /// momentum API for native functions: reversing with continuity means
673    /// producing frames that start at this speed.
674    pub velocity_x: f32,
675    pub velocity_y: f32,
676}
677
678/// One frame of a native presence animation, returned by a
679/// the zombie animation callback. Absolute values, not deltas.
680#[derive(Debug, Clone, Copy, PartialEq)]
681#[repr(C)]
682pub struct ZombieFrame {
683    /// Translation in logical px, applied about the node's origin.
684    pub translate_x: f32,
685    pub translate_y: f32,
686    /// 0.0 skips the node entirely this frame.
687    pub opacity: f32,
688    /// Absolute painted width in logical px (left-anchored narrowing for
689    /// exits; the live layout already owns the vacated space). `None` keeps
690    /// the full width. Ignored for enters (live width is layout's job).
691    pub width: azul_css::OptionF32,
692    /// Clip the exit to its frozen rect so the motion cannot paint over
693    /// neighbouring components. Ignored for enters.
694    pub clip_to_frozen_rect: bool,
695}
696
697impl Default for ZombieFrame {
698    fn default() -> Self {
699        Self {
700            translate_x: 0.0,
701            translate_y: 0.0,
702            opacity: 1.0,
703            width: azul_css::OptionF32::None,
704            clip_to_frozen_rect: true,
705        }
706    }
707}
708
709// TYPE-ERASED `extern "C"` entry point of a native presence animation:
710// stored as `usize` for the same reason as `crate::callbacks::CoreCallback` —
711// the REAL signature takes a `&mut TimerCallbackInfo` (full live-dom access,
712// USER ruling 2026-08-17), and that type lives in `azul-layout`, above this
713// crate. azul-layout defines the typed alias and casts on invocation:
714// `extern "C" fn(&mut RefAny, &mut TimerCallbackInfo, &ZombieAnimInfo) -> ZombieFrame`
715// (Erased alias removed: the PUBLIC `ZombieAnimCallbackType` name now
716// belongs to the GENERATED typed fn-pointer emitted from `ZombieAnimCallback`'s
717// callback_typedef in api.json — two same-name exports collided in codegen.
718// Internally the erasure is just `usize`.)
719
720/// The type-erased component animation callback (a fn pointer as `usize`).
721#[repr(C)]
722#[derive(Clone, Copy)]
723pub struct ZombieAnimCallback {
724    pub cb: usize,
725}
726
727impl fmt::Debug for ZombieAnimCallback {
728    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729        write!(f, "ZombieAnimCallback @ 0x{:x}", self.cb)
730    }
731}
732impl Hash for ZombieAnimCallback {
733    fn hash<H: Hasher>(&self, state: &mut H) {
734        state.write_usize(self.cb);
735    }
736}
737impl PartialEq for ZombieAnimCallback {
738    fn eq(&self, other: &Self) -> bool {
739        self.cb == other.cb
740    }
741}
742impl Eq for ZombieAnimCallback {}
743impl PartialOrd for ZombieAnimCallback {
744    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
745        Some(self.cmp(other))
746    }
747}
748impl Ord for ZombieAnimCallback {
749    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
750        self.cb.cmp(&other.cb)
751    }
752}
753
754/// A COMPONENT-ATTACHED native animation function.
755///
756/// Lives on the node's own `NodeData` (USER ruling 2026-08-17 — a sidebar
757/// widget ships its fly-out next to its own DOM, not in app-global state;
758/// the global `AppConfig` registry this replaced was "a bit unclean").
759/// Resolvable by NAME from that node's `-azul-animation-in` /
760/// `-azul-animation-out`, AFTER stylesheet `@keyframes` — the web mechanism
761/// stays the only default name source.
762#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
763#[repr(C)]
764pub struct AnimationFunction {
765    pub name: AzString,
766    pub callback: ZombieAnimCallback,
767    /// Passed to `callback` on every invocation.
768    pub data: RefAny,
769}
770
771/// Configuration of the SYSTEM-driven animations: physics-based scrolling
772/// and the caret / selection tweens. Lives on [`AppConfig`] so a platform or
773/// application can tune the feel without rebuilding azul.
774///
775/// The scroll physics override is applied ON TOP of the platform-discovered
776/// [`SystemStyle`] at `App::create` time (`None` keeps the per-platform
777/// preset). The tween slots always apply; set a duration to `0` to disable
778/// that tween (the caret / selection then jumps, the classic behavior).
779#[derive(Debug, Clone)]
780#[repr(C)]
781pub struct SystemAnimations {
782    // Field order: decreasing alignment (8-aligned callbacks/RefAny first,
783    // then the 4-aligned option/durations, bool last) — the autofix padding
784    // lint enforces this, and the api.json struct_fields order must match
785    // (the field-order lint enforces THAT).
786    /// The caret tween MATH: called every animation frame with the past /
787    /// current caret rectangles and linear progress `t`; returns the
788    /// rectangle to render. Default: ease-out cubic lerp.
789    pub caret_tween: crate::callbacks::CaretTweenCallback,
790    /// The selection tween MATH: called every animation frame with the
791    /// past / current selection band rectangles and linear progress `t`;
792    /// returns the rectangles to render (must match the current count).
793    /// Default: ease-out cubic lerp, rectangles paired by index.
794    pub selection_tween: crate::callbacks::SelectionTweenCallback,
795    /// User data passed to `caret_tween` on every invocation.
796    pub caret_tween_data: RefAny,
797    /// User data passed to `selection_tween` on every invocation.
798    pub selection_tween_data: RefAny,
799
800    /// Overrides `SystemStyle.scroll_physics` (momentum, overscroll /
801    /// rubber-band, wheel-vs-trackpad curves). `None` = platform default.
802    pub scroll_physics: OptionScrollPhysics,
803    /// Duration of the caret-move tween in ms. `0` disables the tween.
804    /// While the tween runs, caret blinking is suppressed (caret stays
805    /// solid while it moves).
806    pub caret_tween_duration_ms: u32,
807    /// Duration of the selection tween in ms. `0` disables the tween.
808    pub selection_tween_duration_ms: u32,
809    /// Focus-ring glide duration in ms (ledger #29). `0` (the DEFAULT)
810    /// disables the ring entirely — no visual change for existing apps;
811    /// an app that opts in gets a focus outline that GLIDES between
812    /// focused elements using the `caret_tween` interpolator (the ring is
813    /// suppressed while a text-editing session owns focus — there the
814    /// caret is the indicator).
815    pub focus_ring_duration_ms: u32,
816    /// Whether "scroll the caret into view" glides via the scroll-physics
817    /// spring (true, the Word feel) or jumps instantly (false — also what
818    /// [`Self::disabled`] sets, keeping e2e screenshots deterministic).
819    pub caret_scroll_glide: bool,
820}
821
822impl SystemAnimations {
823    /// All system animations disabled: no scroll-physics override, tween
824    /// durations 0 (caret / selection jump). Test drivers and deterministic
825    /// harnesses use this so screenshots never catch geometry mid-glide.
826    #[must_use]
827    pub fn disabled() -> Self {
828        Self {
829            caret_tween_duration_ms: 0,
830            selection_tween_duration_ms: 0,
831            caret_scroll_glide: false,
832            focus_ring_duration_ms: 0,
833            ..Self::default()
834        }
835    }
836}
837
838impl Default for SystemAnimations {
839    fn default() -> Self {
840        Self {
841            scroll_physics: OptionScrollPhysics::None,
842            // Barely noticeable by design (user directive): a short glide,
843            // not an animation the eye waits for.
844            caret_tween_duration_ms: 60,
845            caret_tween: crate::callbacks::CaretTweenCallback::create(
846                crate::callbacks::default_caret_tween,
847            ),
848            caret_tween_data: RefAny::new(()),
849            selection_tween_duration_ms: 60,
850            selection_tween: crate::callbacks::SelectionTweenCallback::create(
851                crate::callbacks::default_selection_tween,
852            ),
853            selection_tween_data: RefAny::new(()),
854            caret_scroll_glide: true,
855            // ON by default: keyboard focus MUST be visible, or Tab-navigating
856            // an app is indistinguishable from Tab doing nothing (device
857            // report, 2026-08-31 - focus and Enter/Space activation both
858            // worked, but nothing on screen said which control had focus).
859            // W3C/UA behaviour is to draw a focus indicator even with no
860            // author CSS. A short glide, like the caret tween; `disabled()`
861            // still zeroes it so e2e screenshots stay deterministic.
862            focus_ring_duration_ms: 60,
863        }
864    }
865}
866
867/// Configuration for optional features, such as whether to enable logging or panic hooks
868#[derive(Debug, Clone)]
869#[repr(C)]
870pub struct AppConfig {
871    /// If enabled, logs error and info messages.
872    ///
873    /// Default is `LevelFilter::Error` to log all errors by default
874    pub log_level: AppLogLevel,
875    /// NATURAL SCROLLING (9b-ii-b-i-a; USER RULING 2026-09-04: a field here,
876    /// default off, the app enables it or loads the system's setting).
877    ///
878    /// The engine's own scroll sign: `Disabled` never flips a delta, `Enabled`
879    /// flips every wheel / trackpad delta (in-app natural scrolling regardless
880    /// of the OS), `System` reads the platform's preference at startup and
881    /// keeps it readable (`CallbackInfo::get_natural_scroll`) - WITHOUT a
882    /// second flip, because every desktop platform already applies the user's
883    /// preference to the deltas it hands over (macOS, the Windows precision
884    /// touchpad, libinput on Wayland and X11); flipping again would undo it.
885    /// Where the platform reports nothing the answer is unknown and `System`
886    /// behaves as `Disabled`.
887    pub natural_scroll: NaturalScroll,
888    /// If the app crashes / panics, a window with a message box pops up.
889    /// Setting this to `false` disables the popup box.
890    pub enable_visual_panic_hook: bool,
891    /// If this is set to `true` (the default), a backtrace + error information
892    /// gets logged to stdout and the logging file (only if logging is enabled).
893    pub enable_logging_on_panic: bool,
894    /// Whether Ctrl+wheel is synthesized into a pinch gesture. Default `true`.
895    ///
896    /// A Windows PRECISION TOUCHPAD does not deliver pinch through
897    /// `WM_GESTURE` - that message is the touchSCREEN path. A touchpad reports
898    /// pinch as Ctrl+`WM_MOUSEWHEEL`, which is the same thing every browser
899    /// zooms on, so synthesizing a pinch from it is what makes pinch-to-zoom
900    /// work on the overwhelming majority of Windows laptops.
901    ///
902    /// The cost of that is a real MOUSE with a real Ctrl key produces the same
903    /// message, and cannot be told apart from a touchpad at this layer - so an
904    /// app where Ctrl+wheel means something else (a CAD zoom step, a font-size
905    /// nudge) receives a pinch it did not want. Setting this to `false` turns
906    /// the synthesis off and leaves Ctrl+wheel as a plain wheel event with the
907    /// Ctrl modifier set, which such an app can read directly.
908    ///
909    /// Ignored on every platform but Windows: macOS and Wayland report real
910    /// pinch gestures, so nothing has to be inferred there.
911    pub synthesize_pinch_from_ctrl_wheel: bool,
912    /// Whether the app publishes itself to the OS as a media player.
913    /// Default `false`.
914    ///
915    /// On Linux the desktop environment usually GRABS the media keys, so
916    /// `XF86AudioPlay` and friends never reach the application as keysyms at
917    /// all (the 9h-i table only sees them when nothing grabbed them). The
918    /// transport in that case is MPRIS over D-Bus: the desktop calls
919    /// `Play`/`Pause`/`Next` on whatever players are registered, and azul
920    /// turns those calls back into ordinary `VirtualKeyCode` presses.
921    ///
922    /// OFF by default because registering has a VISIBLE side effect: the app
923    /// appears in the desktop's media controls (GNOME's system menu, KDE's
924    /// media applet) as a player. That is correct for a music app and wrong
925    /// for a text editor, and no engine-side signal distinguishes them - so
926    /// the app says which it is.
927    ///
928    /// macOS is the same bargain under a different name: `MPRemoteCommandCenter`
929    /// delivers the media keys, but only to the app the system considers "now
930    /// playing", so registering puts the app in Control Center and the Now
931    /// Playing widget.
932    ///
933    /// Ignored on Windows, which delivers media keys as `WM_APPCOMMAND` to the
934    /// focused window and publishes nothing.
935    pub expose_system_media_controls: bool,
936    /// Determines what happens when all windows are closed.
937    /// Default: `EndProcess` (terminate when last window closes).
938    pub termination_behavior: AppTerminationBehavior,
939    /// Icon provider for the application.
940    /// Register icons here before calling `App::run()`.
941    /// Each window will clone this provider (cheap, Arc-based).
942    pub icon_provider: crate::icon::IconProviderHandle,
943    /// Fonts bundled with the application.
944    /// These fonts are loaded into memory and take priority over system fonts.
945    pub bundled_fonts: NamedFontVec,
946    /// Configuration for how system fonts should be loaded.
947    /// Default: `LoadAllSystemFonts` (scan all system fonts at startup)
948    pub font_loading: FontLoadingConfig,
949    /// Optional mock environment for CSS evaluation.
950    ///
951    /// When set, this overrides the auto-detected system properties (OS, theme, etc.)
952    /// for CSS @-rules and dynamic selectors. This is useful for:
953    /// - Testing OS-specific styles on a different platform
954    /// - Screenshot testing with consistent environment
955    /// - Previewing how the app looks on different systems
956    ///
957    /// Default: None (use auto-detected system properties)
958    pub mock_css_environment: OptionCssMockEnvironment,
959    /// System style detected at startup (theme, colors, fonts, etc.)
960    ///
961    /// This is detected once at `AppConfig::create()` and passed to all windows.
962    /// You can override this after creation to use a custom system style,
963    /// for example to test how your app looks on a different platform.
964    pub system_style: SystemStyle,
965    /// Component libraries registered at startup.
966    ///
967    /// Use `add_component()` to register individual components, or
968    /// `add_component_library()` to register entire libraries.
969    /// User-registered (and built-in) component libraries.
970    ///
971    /// The 52 built-in HTML elements are automatically registered by
972    /// `AppConfig::create()` via `register_builtin_components`.
973    /// Additional libraries can be added with `add_component_library`.
974    pub component_libraries: ComponentLibraryVec,
975    /// Registered routes mapping URL patterns to layout callbacks.
976    ///
977    /// Cross-platform: on desktop, the active route determines which layout
978    /// callback runs. On web, routes map to HTTP endpoints and browser URLs.
979    ///
980    /// The first route (or `"/"`) is the default. Use `add_route()` to register.
981    pub routes: RouteVec,
982    /// System-animation configuration (scroll physics override, caret /
983    /// selection tween hooks). See [`SystemAnimations`].
984    pub system_animations: SystemAnimations,
985    /// Handler for E2E ops the engine does not implement, letting a scenario
986    /// drive application-level actions ("now load the document") that the
987    /// engine cannot express on the app's behalf.
988    ///
989    /// The default recognises nothing, so a scenario naming a custom op fails
990    /// unless the app installed a handler.
991    pub custom_e2e_op: crate::events::CustomE2eOpCallback,
992    /// Update configuration: manifest URL, requested mode, the running
993    /// version. Drives `CallbackInfo::check_for_updates` and the
994    /// `SysDialogType::UpdateVersion` dialog. Default: no manifest (checks
995    /// disabled), `NotifyOnly`.
996    pub updates: UpdateSettings,
997    /// URL of the app's changelog in Markdown. The `UpdateVersion` dialog
998    /// shows it before installing when a release carries no changelog link
999    /// of its own.
1000    pub changelog_md: azul_css::OptionString,
1001    /// Support mailbox that problem reports (`SysDialogType::ReportProblem`)
1002    /// and manual crash reports go to. None = the `ReportProblem` dialog saves
1003    /// reports to disk instead of mailing them.
1004    pub report_problem: OptionEmailAddress,
1005}
1006
1007impl AppConfig {
1008    #[must_use]
1009    pub fn create() -> Self {
1010        let log_level = AppLogLevel::Error;
1011        let icon_provider = crate::icon::IconProviderHandle::new();
1012        let bundled_fonts = NamedFontVec::from_const_slice(&[]);
1013        let font_loading = FontLoadingConfig::default();
1014        let system_style = SystemStyle::detect();
1015        let mut s = Self {
1016            log_level,
1017            enable_visual_panic_hook: true,
1018            enable_logging_on_panic: true,
1019            termination_behavior: AppTerminationBehavior::default(),
1020            icon_provider,
1021            bundled_fonts,
1022            font_loading,
1023            mock_css_environment: OptionCssMockEnvironment::None,
1024            system_style,
1025            component_libraries: ComponentLibraryVec::from_const_slice(&[]),
1026            routes: RouteVec::from_const_slice(&[]),
1027            system_animations: SystemAnimations::default(),
1028            // ON by default: a precision touchpad is how most Windows laptops
1029            // pinch, and without this they cannot pinch at all.
1030            synthesize_pinch_from_ctrl_wheel: true,
1031            // OFF (user ruling): the app enables it or asks for the system's.
1032            natural_scroll: NaturalScroll::Disabled,
1033            // OFF: publishing a media player is visible in the desktop UI.
1034            expose_system_media_controls: false,
1035            custom_e2e_op: crate::events::CustomE2eOpCallback::default(),
1036            updates: UpdateSettings::default(),
1037            changelog_md: azul_css::OptionString::None,
1038            report_problem: OptionEmailAddress::None,
1039        };
1040        // Dogfood: register the 52 built-in HTML elements via the
1041        // same `add_component_library` API that users call.
1042        // Annotated binding coerces the fn item to the fn-pointer type that
1043        // `Into<RegisterComponentLibraryFn>` is implemented for (no `as` cast).
1044        let register_builtin: crate::xml::RegisterComponentLibraryFnType =
1045            crate::xml::register_builtin_components;
1046        s.add_component_library(AzString::from_const_str("builtin"), register_builtin);
1047        s
1048    }
1049
1050    /// Create config with a mock CSS environment for testing
1051    ///
1052    /// This allows you to simulate how your app would look on a different OS,
1053    /// with a different theme, language, or accessibility settings.
1054    ///
1055    /// # Example
1056    /// ```rust
1057    /// # use azul_core::resources::{AppConfig, CssMockEnvironment};
1058    /// # use azul_css::dynamic_selector::{OsCondition, OptionOsCondition, ThemeCondition, OptionThemeCondition};
1059    /// let config = AppConfig::create()
1060    ///     .with_mock_environment(CssMockEnvironment {
1061    ///         os: OptionOsCondition::Some(OsCondition::Linux),
1062    ///         theme: OptionThemeCondition::Some(ThemeCondition::Dark),
1063    ///         ..Default::default()
1064    ///     });
1065    /// ```
1066    #[must_use]
1067    pub fn with_mock_environment(mut self, env: CssMockEnvironment) -> Self {
1068        self.mock_css_environment = OptionCssMockEnvironment::Some(env);
1069        self
1070    }
1071
1072    /// Register a single component into a named library.
1073    ///
1074    /// Calls `register_fn` immediately and adds the returned `ComponentDef`
1075    /// to the library named `library`. If no library with that name exists,
1076    /// a new one is created. If a component with the same `id.name` already
1077    /// exists in the library, it is replaced.
1078    ///
1079    /// # C API
1080    /// ```c
1081    /// AzAppConfig_addComponent(&config, AzString_fromConstStr("mylib"), my_register_fn);
1082    /// ```
1083    pub fn add_component<R: Into<RegisterComponentFn>>(
1084        &mut self,
1085        library: AzString,
1086        register_fn: R,
1087    ) {
1088        let register_fn = register_fn.into();
1089        let component = (register_fn.cb)();
1090        let empty_libs = ComponentLibraryVec::from_const_slice(&[]);
1091        let mut libs =
1092            core::mem::replace(&mut self.component_libraries, empty_libs).into_library_owned_vec();
1093
1094        if let Some(existing_lib) = libs
1095            .iter_mut()
1096            .find(|l| l.name.as_str() == library.as_str())
1097        {
1098            let empty_comps = ComponentDefVec::from_const_slice(&[]);
1099            let mut comps = core::mem::replace(&mut existing_lib.components, empty_comps)
1100                .into_library_owned_vec();
1101            if let Some(ec) = comps
1102                .iter_mut()
1103                .find(|c| c.id.name.as_str() == component.id.name.as_str())
1104            {
1105                *ec = component;
1106            } else {
1107                comps.push(component);
1108            }
1109            existing_lib.components = ComponentDefVec::from_vec(comps);
1110        } else {
1111            libs.push(ComponentLibrary {
1112                name: library,
1113                version: AzString::from_const_str("1.0.0"),
1114                description: AzString::from_const_str(""),
1115                components: ComponentDefVec::from_vec(alloc::vec![component]),
1116                exportable: true,
1117                modifiable: true,
1118                data_models: crate::xml::ComponentDataModelVec::from_const_slice(&[]),
1119                enum_models: crate::xml::ComponentEnumModelVec::from_const_slice(&[]),
1120            });
1121        }
1122
1123        self.component_libraries = ComponentLibraryVec::from_vec(libs);
1124    }
1125
1126    /// Register an entire component library.
1127    ///
1128    /// Calls `register_fn` immediately and adds the returned
1129    /// `ComponentLibrary` to the config. Uses `name` as the library name
1130    /// (overriding whatever the function sets). If a library with the same
1131    /// name already exists, it is replaced wholesale.
1132    ///
1133    /// # C API
1134    /// ```c
1135    /// AzAppConfig_addComponentLibrary(&config, AzString_fromConstStr("vendor"), my_lib_fn);
1136    /// ```
1137    pub fn add_component_library<R: Into<RegisterComponentLibraryFn>>(
1138        &mut self,
1139        name: AzString,
1140        register_fn: R,
1141    ) {
1142        let register_fn = register_fn.into();
1143        let mut library = (register_fn.cb)();
1144        library.name = name;
1145
1146        let empty_libs = ComponentLibraryVec::from_const_slice(&[]);
1147        let mut libs =
1148            core::mem::replace(&mut self.component_libraries, empty_libs).into_library_owned_vec();
1149        if let Some(existing) = libs
1150            .iter_mut()
1151            .find(|l| l.name.as_str() == library.name.as_str())
1152        {
1153            *existing = library;
1154        } else {
1155            libs.push(library);
1156        }
1157
1158        self.component_libraries = ComponentLibraryVec::from_vec(libs);
1159    }
1160
1161    /// Register a route mapping a URL pattern to a layout callback.
1162    ///
1163    /// On web: each route becomes an HTTP endpoint. On desktop: the first
1164    /// route (or `"/"`) is the initial layout, and `CallbackInfo::switch_route()`
1165    /// swaps the active callback.
1166    ///
1167    /// # C API
1168    /// ```c
1169    /// AzAppConfig_addRoute(&config, AzString_fromConstStr("/user/:id"), layout_user);
1170    /// ```
1171    pub fn add_route<P: Into<AzString>, L: Into<LayoutCallback>>(
1172        &mut self,
1173        pattern: P,
1174        layout_fn: L,
1175    ) {
1176        let route = Route {
1177            pattern: pattern.into(),
1178            layout_callback: layout_fn.into(),
1179        };
1180        let empty = RouteVec::from_const_slice(&[]);
1181        let mut routes = core::mem::replace(&mut self.routes, empty).into_library_owned_vec();
1182        // Replace existing route with the same pattern
1183        if let Some(existing) = routes
1184            .iter_mut()
1185            .find(|r| r.pattern.as_str() == route.pattern.as_str())
1186        {
1187            *existing = route;
1188        } else {
1189            routes.push(route);
1190        }
1191        self.routes = RouteVec::from_vec(routes);
1192    }
1193
1194    /// Find the route matching a given URL path.
1195    ///
1196    /// Returns the matched `Route` and a `RouteMatch` with extracted parameters.
1197    #[must_use]
1198    pub fn match_route_for_path(&self, path: &str) -> Option<(&Route, RouteMatch)> {
1199        for route in self.routes.as_ref() {
1200            if let Some(m) = match_route(route.pattern.as_str(), path) {
1201                return Some((route, m));
1202            }
1203        }
1204        None
1205    }
1206}
1207
1208impl Default for AppConfig {
1209    fn default() -> Self {
1210        Self::create()
1211    }
1212}
1213
1214/// How the engine treats scroll direction (9b-ii-b-i-a) - see
1215/// `AppConfig::natural_scroll`.
1216#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
1217#[repr(C)]
1218pub enum NaturalScroll {
1219    /// Never flip a delta in the engine. The OS's own preference, if the user
1220    /// set one, still reaches the app in the deltas.
1221    #[default]
1222    Disabled,
1223    /// Flip every wheel and trackpad delta in the engine.
1224    Enabled,
1225    /// Read the platform's preference at startup and keep it readable; no
1226    /// engine flip, since the platform already applied it to the deltas.
1227    System,
1228}
1229
1230#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1231#[repr(C)]
1232pub enum AppLogLevel {
1233    Off,
1234    Error,
1235    Warn,
1236    Info,
1237    Debug,
1238    Trace,
1239}
1240
1241/// Metadata (but not storage) describing an image In `WebRender`.
1242#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1243#[repr(C)]
1244pub struct ImageDescriptor {
1245    /// Format of the image data.
1246    pub format: RawImageFormat,
1247    /// Width and height of the image data, in pixels.
1248    pub width: usize,
1249    pub height: usize,
1250    /// The number of bytes from the start of one row to the next. If non-None,
1251    /// `compute_stride` will return this value, otherwise it returns
1252    /// `width * bpp`. Different source of images have different alignment
1253    /// constraints for rows, so the stride isn't always equal to width * bpp.
1254    pub stride: OptionI32,
1255    /// Offset in bytes of the first pixel of this image in its backing buffer.
1256    /// This is used for tiling, wherein `WebRender` extracts chunks of input images
1257    /// in order to cache, manipulate, and render them individually. This offset
1258    /// tells the texture upload machinery where to find the bytes to upload for
1259    /// this tile. Non-tiled images generally set this to zero.
1260    pub offset: i32,
1261    /// Various bool flags related to this descriptor.
1262    pub flags: ImageDescriptorFlags,
1263}
1264
1265/// Various flags that are part of an image descriptor.
1266#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1267#[repr(C)]
1268pub struct ImageDescriptorFlags {
1269    /// Whether this image is opaque, or has an alpha channel. Avoiding blending
1270    /// for opaque surfaces is an important optimization.
1271    pub is_opaque: bool,
1272    /// Whether to allow the driver to automatically generate mipmaps. If images
1273    /// are already downscaled appropriately, mipmap generation can be wasted
1274    /// work, and cause performance problems on some cards/drivers.
1275    ///
1276    /// See <https://github.com/servo/webrender/pull/2555>/
1277    pub allow_mipmaps: bool,
1278}
1279
1280#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1281pub struct IdNamespace(pub u32);
1282
1283impl ::core::fmt::Display for IdNamespace {
1284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1285        write!(f, "IdNamespace({})", self.0)
1286    }
1287}
1288
1289impl ::core::fmt::Debug for IdNamespace {
1290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291        write!(f, "{self}")
1292    }
1293}
1294
1295#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1296#[repr(C)]
1297pub enum RawImageFormat {
1298    R8,
1299    RG8,
1300    RGB8,
1301    RGBA8,
1302    R16,
1303    RG16,
1304    RGB16,
1305    RGBA16,
1306    BGR8,
1307    BGRA8,
1308    RGBF32,
1309    RGBAF32,
1310}
1311
1312// NOTE: starts at 1 (0 = DUMMY)
1313static IMAGE_KEY: AtomicU64 = AtomicU64::new(1);
1314static FONT_KEY: AtomicU64 = AtomicU64::new(0);
1315static FONT_INSTANCE_KEY: AtomicU64 = AtomicU64::new(0);
1316
1317#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1318pub struct ImageKey {
1319    pub namespace: IdNamespace,
1320    pub key: u64,
1321}
1322
1323impl ImageKey {
1324    pub const DUMMY: Self = Self {
1325        namespace: IdNamespace(0),
1326        key: 0,
1327    };
1328
1329    pub fn unique(render_api_namespace: IdNamespace) -> Self {
1330        Self {
1331            namespace: render_api_namespace,
1332            key: IMAGE_KEY.fetch_add(1, AtomicOrdering::SeqCst),
1333        }
1334    }
1335}
1336
1337#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1338pub struct FontKey {
1339    pub namespace: IdNamespace,
1340    pub key: u64,
1341}
1342
1343impl FontKey {
1344    pub fn unique(render_api_namespace: IdNamespace) -> Self {
1345        Self {
1346            namespace: render_api_namespace,
1347            key: FONT_KEY.fetch_add(1, AtomicOrdering::SeqCst),
1348        }
1349    }
1350}
1351
1352#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1353pub struct FontInstanceKey {
1354    pub namespace: IdNamespace,
1355    pub key: u64,
1356}
1357
1358impl FontInstanceKey {
1359    pub fn unique(render_api_namespace: IdNamespace) -> Self {
1360        Self {
1361            namespace: render_api_namespace,
1362            key: FONT_INSTANCE_KEY.fetch_add(1, AtomicOrdering::SeqCst),
1363        }
1364    }
1365}
1366
1367// NOTE: This type should NOT be exposed in the API!
1368// The only public functions are the constructors
1369#[derive(Debug)]
1370pub enum DecodedImage {
1371    /// Image that has a reserved key, but no data, i.e it is not yet rendered
1372    /// or there was an error during rendering
1373    NullImage {
1374        width: usize,
1375        height: usize,
1376        format: RawImageFormat,
1377        /// Sometimes images need to be tagged with extra data
1378        tag: Vec<u8>,
1379    },
1380    // OpenGl texture
1381    Gl(Texture),
1382    // Image backed by CPU-rendered pixels
1383    Raw((ImageDescriptor, ImageData)),
1384    // Same as `Texture`, but rendered AFTER the layout has been done
1385    Callback(CoreImageCallback),
1386    // YUVImage(...)
1387    // VulkanSurface(...)
1388    // MetalSurface(...),
1389    // DirectXSurface(...)
1390}
1391
1392#[derive(Debug)]
1393#[repr(C)]
1394pub struct ImageRef {
1395    /// Shared pointer to an opaque implementation of the decoded image
1396    pub data: *const DecodedImage,
1397    /// How many copies does this image have (if 0, the font data will be deleted on drop)
1398    pub copies: *const AtomicUsize,
1399    /// Process-unique, monotonically-assigned identity of the *decoded image*
1400    /// (see [`ImageRefHash`]). Shared by shallow clones (they are the same
1401    /// image), fresh for [`ImageRef::deep_copy`] and every `new_*` (a
1402    /// different image). Unlike the old `data`-pointer identity this is drawn
1403    /// from a never-reused counter, so freeing an image and reusing its heap
1404    /// address can never make a *new* image collide with a stale key — the
1405    /// prerequisite for image GC (see resources.rs `image_ref_get_hash`).
1406    pub id: u64,
1407    pub run_destructor: bool,
1408}
1409
1410/// Never-reused source of [`ImageRef::id`]. Starts at 1 so `id == 0` can flag
1411/// an un-initialised / raw-reconstructed handle.
1412static IMAGE_REF_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
1413
1414#[must_use]
1415fn next_image_ref_id() -> u64 {
1416    IMAGE_REF_ID_COUNTER.fetch_add(1, AtomicOrdering::SeqCst)
1417}
1418
1419impl ImageRef {
1420    #[must_use]
1421    pub const fn get_hash(&self) -> ImageRefHash {
1422        image_ref_get_hash(self)
1423    }
1424}
1425
1426#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Ord, Eq)]
1427#[repr(C)]
1428pub struct ImageRefHash {
1429    pub inner: u64,
1430}
1431
1432impl_option!(
1433    ImageRef,
1434    OptionImageRef,
1435    copy = false,
1436    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1437);
1438
1439impl ImageRef {
1440    /// If *copies = 1, returns the internal image data
1441    #[must_use]
1442    pub fn into_inner(self) -> Option<DecodedImage> {
1443        // SAFETY: `data`/`copies` are non-null heap allocations from `Box::into_raw`
1444        // in `new()` (never mutated afterwards). When `copies == 1` we are the sole
1445        // owner, so reclaiming both Boxes and `forget`-ing `self` transfers ownership
1446        // without a double free / running the destructor twice.
1447        unsafe {
1448            if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
1449                let data = Box::from_raw(self.data.cast_mut());
1450                drop(Box::from_raw(self.copies.cast_mut()));
1451                core::mem::forget(self); // do not run the destructor
1452                Some(*data)
1453            } else {
1454                None
1455            }
1456        }
1457    }
1458
1459    #[must_use]
1460    pub const fn get_data(&self) -> &DecodedImage {
1461        // SAFETY: `data` is a non-null, live `Box` allocation owned by this handle
1462        // (and its shallow clones) until the last copy drops; the returned borrow is
1463        // tied to `&self`, so it cannot outlive the allocation.
1464        unsafe { &*self.data }
1465    }
1466
1467    #[must_use]
1468    pub fn get_image_callback(&self) -> Option<&CoreImageCallback> {
1469        // SAFETY: `copies` is a non-null, live allocation for the lifetime of `&self`.
1470        if unsafe { self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) != Some(1) } {
1471            return None; // not safe: shared, so no exclusive borrow of the data
1472        }
1473
1474        // SAFETY: `data` is a non-null, live `Box` allocation; borrow tied to `&self`.
1475        match unsafe { &*self.data } {
1476            DecodedImage::Callback(gl_texture_callback) => Some(gl_texture_callback),
1477            _ => None,
1478        }
1479    }
1480
1481    pub fn get_image_callback_mut(&mut self) -> Option<&mut CoreImageCallback> {
1482        // SAFETY: `copies` is a non-null, live allocation for the lifetime of `&self`.
1483        if unsafe { self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) != Some(1) } {
1484            return None; // not safe: shared, so a &mut would alias other clones' data
1485        }
1486
1487        // SAFETY: `copies == 1` proven above, so `&mut self` is the unique owner of
1488        // the `data` allocation; the exclusive borrow is tied to `&mut self`.
1489        match unsafe { &mut *self.data.cast_mut() } {
1490            DecodedImage::Callback(gl_texture_callback) => Some(gl_texture_callback),
1491            _ => None,
1492        }
1493    }
1494
1495    /// In difference to the default shallow copy, creates a new image ref
1496    #[must_use]
1497    pub fn deep_copy(&self) -> Self {
1498        let new_data = match self.get_data() {
1499            DecodedImage::NullImage {
1500                width,
1501                height,
1502                format,
1503                tag,
1504            } => DecodedImage::NullImage {
1505                width: *width,
1506                height: *height,
1507                format: *format,
1508                tag: tag.clone(),
1509            },
1510            // NOTE: textures cannot be deep-copied yet (since the OpenGL calls for that
1511            // are missing from the trait), so calling clone() on a GL texture will result in an
1512            // empty image
1513            DecodedImage::Gl(tex) => DecodedImage::NullImage {
1514                width: tex.size.width as usize,
1515                height: tex.size.height as usize,
1516                format: tex.format,
1517                tag: Vec::new(),
1518            },
1519            // WARNING: the data may still be a U8Vec<'static> - the data may still not be
1520            // actually cloned. The data only gets cloned on a write operation
1521            DecodedImage::Raw((descriptor, data)) => DecodedImage::Raw((*descriptor, data.clone())),
1522            DecodedImage::Callback(cb) => DecodedImage::Callback(cb.clone()),
1523        };
1524
1525        Self::new(new_data)
1526    }
1527
1528    #[must_use]
1529    pub const fn is_null_image(&self) -> bool {
1530        matches!(self.get_data(), DecodedImage::NullImage { .. })
1531    }
1532
1533    #[must_use]
1534    pub const fn is_gl_texture(&self) -> bool {
1535        matches!(self.get_data(), DecodedImage::Gl(_))
1536    }
1537
1538    #[must_use]
1539    pub const fn is_raw_image(&self) -> bool {
1540        matches!(self.get_data(), DecodedImage::Raw((_, _)))
1541    }
1542
1543    #[must_use]
1544    pub const fn is_callback(&self) -> bool {
1545        matches!(self.get_data(), DecodedImage::Callback(_))
1546    }
1547
1548    // OptionRawImage
1549    #[must_use]
1550    pub fn get_rawimage(&self) -> Option<RawImage> {
1551        match self.get_data() {
1552            DecodedImage::Raw((image_descriptor, image_data)) => Some(RawImage {
1553                pixels: match image_data {
1554                    ImageData::Raw(shared_data) => {
1555                        // Clone the SharedRawImageData (increments ref count),
1556                        // then try to extract or convert to U8Vec
1557                        let data_clone = shared_data.clone();
1558                        data_clone.into_inner().map_or_else(
1559                            || RawImageData::U8(shared_data.as_ref().to_vec().into()),
1560                            RawImageData::U8,
1561                        )
1562                    }
1563                    ImageData::External(_) => return None,
1564                },
1565                width: image_descriptor.width,
1566                height: image_descriptor.height,
1567                premultiplied_alpha: true,
1568                data_format: image_descriptor.format,
1569                tag: Vec::new().into(),
1570            }),
1571            _ => None,
1572        }
1573    }
1574
1575    /// Get raw bytes from the image as a slice
1576    /// Returns None if this is not a Raw image or if it's an External image
1577    #[must_use]
1578    pub fn get_bytes(&self) -> Option<&[u8]> {
1579        match self.get_data() {
1580            DecodedImage::Raw((_, image_data)) => match image_data {
1581                ImageData::Raw(shared_data) => Some(shared_data.as_ref()),
1582                ImageData::External(_) => None,
1583            },
1584            _ => None,
1585        }
1586    }
1587
1588    /// Get a pointer to the raw bytes for debugging/profiling purposes
1589    /// Returns a unique pointer for this `ImageRef`'s data
1590    #[must_use]
1591    pub fn get_bytes_ptr(&self) -> *const u8 {
1592        match self.get_data() {
1593            DecodedImage::Raw((_, image_data)) => match image_data {
1594                ImageData::Raw(shared_data) => shared_data.as_ptr(),
1595                ImageData::External(_) => core::ptr::null(),
1596            },
1597            _ => core::ptr::null(),
1598        }
1599    }
1600
1601    /// NOTE: returns (0, 0) for a Callback
1602    #[allow(clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
1603    #[must_use]
1604    pub const fn get_size(&self) -> LogicalSize {
1605        match self.get_data() {
1606            DecodedImage::NullImage { width, height, .. } => {
1607                LogicalSize::new(*width as f32, *height as f32)
1608            }
1609            DecodedImage::Gl(tex) => {
1610                LogicalSize::new(tex.size.width as f32, tex.size.height as f32)
1611            }
1612            DecodedImage::Raw((image_descriptor, _)) => LogicalSize::new(
1613                image_descriptor.width as f32,
1614                image_descriptor.height as f32,
1615            ),
1616            DecodedImage::Callback(_) => LogicalSize::new(0.0, 0.0),
1617        }
1618    }
1619
1620    #[must_use]
1621    pub fn null_image(width: usize, height: usize, format: RawImageFormat, tag: Vec<u8>) -> Self {
1622        Self::new(DecodedImage::NullImage {
1623            width,
1624            height,
1625            format,
1626            tag,
1627        })
1628    }
1629
1630    pub fn callback<C: Into<CoreRenderImageCallback>>(callback: C, data: RefAny) -> Self {
1631        Self::new(DecodedImage::Callback(CoreImageCallback {
1632            callback: callback.into(),
1633            refany: data,
1634        }))
1635    }
1636
1637    #[must_use]
1638    pub fn new_rawimage(image_data: RawImage) -> Option<Self> {
1639        let (image_data, image_descriptor) = image_data.into_loaded_image_source()?;
1640        Some(Self::new(DecodedImage::Raw((image_descriptor, image_data))))
1641    }
1642
1643    #[must_use]
1644    pub fn new_gltexture(texture: Texture) -> Self {
1645        Self::new(DecodedImage::Gl(texture))
1646    }
1647
1648    fn new(data: DecodedImage) -> Self {
1649        Self {
1650            data: Box::into_raw(Box::new(data)),
1651            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
1652            id: next_image_ref_id(),
1653            run_destructor: true,
1654        }
1655    }
1656
1657    // pub fn new_vulkan(...) -> Self
1658}
1659
1660// SAFETY: the raw pointers only ever address heap `Box`es whose contents are
1661// themselves `Send`/`Sync`, and all cross-thread refcount mutation goes through the
1662// `AtomicUsize` in `copies`, so sharing/moving a handle across threads is sound.
1663unsafe impl Send for ImageRef {}
1664unsafe impl Sync for ImageRef {}
1665
1666// Identity is the never-reused `id`, NOT the `data` pointer: two shallow
1667// clones of one image share an `id` (equal); distinct images (incl. a
1668// `deep_copy`) get distinct ids; a freed image's id is never handed to a
1669// later image, so a reused heap address can't forge equality.
1670impl PartialEq for ImageRef {
1671    fn eq(&self, rhs: &Self) -> bool {
1672        self.id == rhs.id
1673    }
1674}
1675
1676impl PartialOrd for ImageRef {
1677    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
1678        Some(self.id.cmp(&other.id))
1679    }
1680}
1681
1682impl Ord for ImageRef {
1683    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1684        self.id.cmp(&other.id)
1685    }
1686}
1687
1688impl Eq for ImageRef {}
1689
1690impl Hash for ImageRef {
1691    fn hash<H>(&self, state: &mut H)
1692    where
1693        H: Hasher,
1694    {
1695        self.id.hash(state);
1696    }
1697}
1698
1699impl Clone for ImageRef {
1700    fn clone(&self) -> Self {
1701        // SAFETY: `copies` is a non-null, live `AtomicUsize` allocation shared by all
1702        // clones; the atomic increment balances the `fetch_sub` in `Drop`.
1703        unsafe {
1704            self.copies
1705                .as_ref()
1706                .map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
1707        }
1708        Self {
1709            data: self.data,     // copy the pointer
1710            copies: self.copies, // copy the pointer
1711            id: self.id,         // same image → same identity
1712            run_destructor: true,
1713        }
1714    }
1715}
1716
1717impl Drop for ImageRef {
1718    fn drop(&mut self) {
1719        self.run_destructor = false;
1720        // SAFETY: `data`/`copies` are non-null, live `Box` allocations shared by all
1721        // clones. `fetch_sub` returns the pre-decrement count, so `== 1` means this is
1722        // the last owner; only then do we reclaim both Boxes exactly once.
1723        unsafe {
1724            let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
1725            if copies == 1 {
1726                drop(Box::from_raw(self.data.cast_mut()));
1727                drop(Box::from_raw(self.copies.cast_mut()));
1728            }
1729        }
1730    }
1731}
1732
1733#[must_use]
1734pub const fn image_ref_get_hash(ir: &ImageRef) -> ImageRefHash {
1735    // The identity is the never-reused `id`, not the freeable `data` pointer
1736    // (see the `id` field docs). This is what makes an ImageKey safe to
1737    // DeleteImage: once an image is dropped its id is retired forever, so a
1738    // future image that reuses the same heap address gets a *different* key
1739    // and is registered/uploaded correctly instead of aliasing the stale one.
1740    ImageRefHash { inner: ir.id }
1741}
1742
1743/// Convert a stable `ImageRefHash` directly to an `ImageKey`.
1744///
1745/// `ImageKey.key` is a `u64` and `ImageRefHash.inner` is the `ImageRef` `id`
1746/// (a `u64` counter) stored in a `usize`; on a 32-bit host that truncates the
1747/// top 32 bits, which is fine — a run would need 4 billion live images for the
1748/// low 32 bits to collide.
1749#[must_use]
1750pub const fn image_ref_hash_to_image_key(hash: ImageRefHash, namespace: IdNamespace) -> ImageKey {
1751    ImageKey {
1752        namespace,
1753        key: hash.inner,
1754    }
1755}
1756
1757#[must_use]
1758pub fn font_ref_get_hash(fr: &FontRef) -> u64 {
1759    fr.get_hash()
1760}
1761
1762/// Stores the resources for the application, such as fonts, images and cached
1763/// texts, also clipboard strings
1764///
1765/// Images and fonts can be references across window contexts (not yet tested,
1766/// but should work).
1767#[derive(Debug, Default)]
1768pub struct ImageCache {
1769    /// The `AzString` is the string used in the CSS, i.e. `url("my_image`") = "`my_image`" -> ImageId(4)
1770    ///
1771    /// NOTE: This is the only map that is modifiable by the user and that has to be manually
1772    /// managed all other maps are library-internal only and automatically delete their
1773    /// resources once they aren't needed anymore
1774    pub image_id_map: OrderedMap<AzString, ImageRef>,
1775}
1776
1777impl ImageCache {
1778    #[must_use]
1779    pub fn new() -> Self {
1780        Self::default()
1781    }
1782
1783    // -- ImageId cache
1784
1785    pub fn add_css_image_id(&mut self, css_id: AzString, image: ImageRef) {
1786        self.image_id_map.insert(css_id, image);
1787    }
1788
1789    #[must_use]
1790    pub fn get_css_image_id(&self, css_id: &AzString) -> Option<&ImageRef> {
1791        self.image_id_map.get(css_id)
1792    }
1793
1794    pub fn delete_css_image_id(&mut self, css_id: &AzString) {
1795        self.image_id_map.remove(css_id);
1796    }
1797}
1798
1799#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1800pub struct ResolvedImage {
1801    pub key: ImageKey,
1802    pub descriptor: ImageDescriptor,
1803}
1804
1805/// Trait for accessing font resources
1806pub trait RendererResourcesTrait: fmt::Debug {
1807    /// Get a font family hash from a font families hash
1808    fn get_font_family(
1809        &self,
1810        style_font_families_hash: &StyleFontFamiliesHash,
1811    ) -> Option<&StyleFontFamilyHash>;
1812
1813    /// Get a font key from a font family hash
1814    fn get_font_key(&self, style_font_family_hash: &StyleFontFamilyHash) -> Option<&FontKey>;
1815
1816    /// Get a registered font and its instances from a font key
1817    fn get_registered_font(
1818        &self,
1819        font_key: &FontKey,
1820    ) -> Option<&(FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)>;
1821
1822    /// Get image information from an image hash
1823    fn get_image(&self, hash: &ImageRefHash) -> Option<&ResolvedImage>;
1824
1825    /// Update an image descriptor for an existing image hash
1826    fn update_image(&mut self, image_ref_hash: &ImageRefHash, descriptor: ImageDescriptor);
1827}
1828
1829// Implementation for the original RendererResources struct
1830impl RendererResourcesTrait for RendererResources {
1831    fn get_font_family(
1832        &self,
1833        style_font_families_hash: &StyleFontFamiliesHash,
1834    ) -> Option<&StyleFontFamilyHash> {
1835        self.font_families_map.get(style_font_families_hash)
1836    }
1837
1838    fn get_font_key(&self, style_font_family_hash: &StyleFontFamilyHash) -> Option<&FontKey> {
1839        self.font_id_map.get(style_font_family_hash)
1840    }
1841
1842    fn get_registered_font(
1843        &self,
1844        font_key: &FontKey,
1845    ) -> Option<&(FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)> {
1846        self.currently_registered_fonts.get(font_key)
1847    }
1848
1849    fn get_image(&self, hash: &ImageRefHash) -> Option<&ResolvedImage> {
1850        self.currently_registered_images.get(hash)
1851    }
1852
1853    fn update_image(&mut self, image_ref_hash: &ImageRefHash, descriptor: ImageDescriptor) {
1854        if let Some(s) = self.currently_registered_images.get_mut(image_ref_hash) {
1855            s.descriptor = descriptor;
1856        }
1857    }
1858}
1859
1860/// Renderer resources that manage font, image and font instance keys.
1861/// `RendererResources` are local to each renderer / window, since the
1862/// keys are not shared across renderers
1863///
1864/// The resources are automatically managed, meaning that they each new frame
1865/// (signified by `start_frame_gc` and `end_frame_gc`)
1866#[derive(Default)]
1867pub struct RendererResources {
1868    /// All image keys currently active in the `RenderApi`
1869    pub currently_registered_images: OrderedMap<ImageRefHash, ResolvedImage>,
1870    /// Reverse lookup: `ImageKey` -> `ImageRefHash` for display list translation
1871    pub image_key_map: OrderedMap<ImageKey, ImageRefHash>,
1872    /// Image GC bookkeeping: last epoch (as `u32`) each registered image was
1873    /// seen referenced by a display list. An image absent for more than
1874    /// `IMAGE_GC_KEEP_EPOCHS` frames is `DeleteImage`d and evicted — this is
1875    /// what stops the unbounded texture growth of a window that swaps images
1876    /// every frame (video / capture / animated charts). Safe because
1877    /// `ImageRefHash` is now a never-reused id, not a freeable pointer.
1878    pub image_last_seen_epoch: OrderedMap<ImageRefHash, u32>,
1879    /// All font keys currently active in the `RenderApi`
1880    pub currently_registered_fonts:
1881        OrderedMap<FontKey, (FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)>,
1882    /// Fonts registered on the last frame
1883    ///
1884    /// Fonts differ from images in that regard that we can't immediately
1885    /// delete them on a new frame, instead we have to delete them on "current frame + 1"
1886    /// This is because when the frame is being built, we do not know
1887    /// whether the font will actually be successfully loaded
1888    pub last_frame_registered_fonts:
1889        OrderedMap<FontKey, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>>,
1890    /// Map from the calculated families vec (`["Arial", "Helvetica"]`)
1891    /// to the final loaded font that could be loaded
1892    /// (in this case "Arial" on Windows and "Helvetica" on Mac,
1893    /// because the fonts are loaded in fallback-order)
1894    pub font_families_map: OrderedMap<StyleFontFamiliesHash, StyleFontFamilyHash>,
1895    /// Same as `AzString` -> `ImageId`, but for fonts, i.e. "Roboto" -> FontId(9)
1896    pub font_id_map: OrderedMap<StyleFontFamilyHash, FontKey>,
1897    /// Direct mapping from font hash (from `FontRef`) to `FontKey`
1898    /// TODO: This should become part of `SharedFontRegistry`
1899    pub font_hash_map: OrderedMap<u64, FontKey>,
1900}
1901
1902impl fmt::Debug for RendererResources {
1903    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1904        write!(
1905            f,
1906            "RendererResources {{
1907                currently_registered_images: {:#?},
1908                currently_registered_fonts: {:#?},
1909                font_families_map: {:#?},
1910                font_id_map: {:#?},
1911            }}",
1912            self.currently_registered_images.keys().collect::<Vec<_>>(),
1913            self.currently_registered_fonts.keys().collect::<Vec<_>>(),
1914            self.font_families_map.keys().collect::<Vec<_>>(),
1915            self.font_id_map.keys().collect::<Vec<_>>(),
1916        )
1917    }
1918}
1919
1920impl RendererResources {
1921    #[must_use]
1922    pub fn get_renderable_font_data(
1923        &self,
1924        font_instance_key: &FontInstanceKey,
1925    ) -> Option<(&FontRef, Au, DpiScaleFactor)> {
1926        self.currently_registered_fonts
1927            .iter()
1928            .find_map(|(font_key, (font_ref, instances))| {
1929                instances.iter().find_map(|((au, dpi), instance_key)| {
1930                    if *instance_key == *font_instance_key {
1931                        Some((font_ref, *au, *dpi))
1932                    } else {
1933                        None
1934                    }
1935                })
1936            })
1937    }
1938
1939    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
1940    pub fn get_font_instance_key_for_text(
1941        &self,
1942        font_size_px: f32,
1943        css_property_cache: &CssPropertyCache,
1944        node_data: &NodeData,
1945        node_id: &NodeId,
1946        styled_node_state: &StyledNodeState,
1947        dpi_scale: f32,
1948    ) -> Option<FontInstanceKey> {
1949        // Convert font size to StyleFontSize.
1950        //
1951        // `font_size_px as isize` saturates +inf / f32::MAX to isize::MAX (and
1952        // -inf / -f32::MAX to isize::MIN). `const_px` then multiplies by 1000
1953        // (FP_PRECISION_MULTIPLIER) inside `FloatValue::const_new`, which would
1954        // overflow. Clamp to the range that survives that multiply so an absurd
1955        // size misses cleanly instead of panicking.
1956        let font_size_isize = (font_size_px as isize).clamp(isize::MIN / 1000, isize::MAX / 1000);
1957        let font_size = StyleFontSize {
1958            inner: azul_css::props::basic::PixelValue::const_px(font_size_isize),
1959        };
1960
1961        // Convert to application units
1962        let font_size_au = font_size_to_au(font_size);
1963
1964        // Create DPI scale factor
1965        let dpi_scale_factor = DpiScaleFactor {
1966            inner: FloatValue::new(dpi_scale),
1967        };
1968
1969        // Get font family
1970        let font_family =
1971            css_property_cache.get_font_id_or_default(node_data, node_id, styled_node_state);
1972
1973        // Calculate hash and lookup font instance key
1974        let font_families_hash = StyleFontFamiliesHash::new(font_family.as_ref());
1975
1976        self.get_font_instance_key(&font_families_hash, font_size_au, dpi_scale_factor)
1977    }
1978
1979    #[must_use]
1980    pub fn get_font_instance_key(
1981        &self,
1982        font_families_hash: &StyleFontFamiliesHash,
1983        font_size_au: Au,
1984        dpi_scale: DpiScaleFactor,
1985    ) -> Option<FontInstanceKey> {
1986        let font_family_hash = self.get_font_family(font_families_hash)?;
1987        let font_key = self.get_font_key(font_family_hash)?;
1988        let (_, instances) = self.get_registered_font(font_key)?;
1989        instances.get(&(font_size_au, dpi_scale)).copied()
1990    }
1991
1992    // Delete all font family hashes that do not have a font key anymore
1993    //
1994    // AUDIT-TODO (font GC, resources.rs font leak — 2026-07-08):
1995    // Fonts and font instances are currently NEVER garbage-collected. This helper
1996    // only prunes `font_id_map` / `font_families_map` entries whose `FontKey` has
1997    // *already* vanished from `currently_registered_fonts` — but nothing ever
1998    // removes fonts from `currently_registered_fonts` in the first place, and this
1999    // helper itself has no callers. No `DeleteFont` / `DeleteFontInstance`
2000    // `ResourceUpdate` is ever emitted, so WebRender font memory grows unbounded
2001    // when an app cycles fonts (font pickers, editors, live CSS).
2002    //
2003    // To wire a real font GC mirroring the image GC (see `dll/.../wr_translate2.rs`
2004    // `garbage_collect_images` + `image_last_seen_epoch`), the following are needed
2005    // and MUST be done together (do not half-implement):
2006    //   1. Add `font_last_seen_epoch: OrderedMap<FontKey, u32>` (and, if instance-
2007    //      level GC is wanted, per-`FontInstanceKey` epochs) to `RendererResources`.
2008    //   2. In the display-list build (dll crate), after resolving each glyph run's
2009    //      `FontInstanceKey`, mark the owning `FontKey` (and instance) seen at the
2010    //      current epoch — exactly as images are marked in the image GC.
2011    //   3. Add a `garbage_collect_fonts(&mut self, now, keep_epochs, updates)` that,
2012    //      for every `FontKey` unseen for > keep_epochs frames, emits
2013    //      `DeleteFontInstance` for each of its instances then `DeleteFont`, and
2014    //      evicts the key from `currently_registered_fonts`, `font_hash_map`,
2015    //      `last_frame_registered_fonts`, and `font_id_map`/`font_families_map`
2016    //      (via this helper). Respect the "delete on current frame + 1" rule already
2017    //      documented on `last_frame_registered_fonts`.
2018    //   4. Call it once per frame from the same site as the image GC.
2019    // Left as a TODO because steps 2 and 4 are cross-crate (dll) and cannot be
2020    // implemented from `azul-core` alone; adding a GC method here without a caller
2021    // would just be more dead code.
2022    #[allow(dead_code)]
2023    fn remove_font_families_with_zero_references(&mut self) {
2024        let font_family_to_delete = self
2025            .font_id_map
2026            .iter()
2027            .filter_map(|(font_family, font_key)| {
2028                if self.currently_registered_fonts.contains_key(font_key) {
2029                    None
2030                } else {
2031                    Some(*font_family)
2032                }
2033            })
2034            .collect::<Vec<_>>();
2035
2036        for f in font_family_to_delete {
2037            self.font_id_map.remove(&f); // font key does not exist anymore
2038        }
2039
2040        let font_families_to_delete = self
2041            .font_families_map
2042            .iter()
2043            .filter_map(|(font_families, font_family)| {
2044                if self.font_id_map.contains_key(font_family) {
2045                    None
2046                } else {
2047                    Some(*font_families)
2048                }
2049            })
2050            .collect::<Vec<_>>();
2051
2052        for f in font_families_to_delete {
2053            self.font_families_map.remove(&f); // font family does not exist anymore
2054        }
2055    }
2056}
2057
2058// Result returned from rerender_image_callback() - should be used as:
2059//
2060// ```rust
2061// txn.update_image(
2062//     wr_translate_image_key(key),
2063//     wr_translate_image_descriptor(descriptor),
2064//     wr_translate_image_data(data),
2065//     &WrImageDirtyRect::All,
2066// );
2067// ```
2068#[derive(Debug, Clone)]
2069pub struct UpdateImageResult {
2070    pub key_to_update: ImageKey,
2071    pub new_descriptor: ImageDescriptor,
2072    pub new_image_data: ImageData,
2073}
2074
2075#[derive(Debug, Default)]
2076pub struct GlTextureCache {
2077    pub solved_textures:
2078        BTreeMap<DomId, BTreeMap<NodeId, (ImageKey, ImageDescriptor, ExternalImageId)>>,
2079    pub hashes: BTreeMap<(DomId, NodeId, ImageRefHash), ImageRefHash>,
2080}
2081
2082// necessary so the display list can be built in parallel
2083// SAFETY: only the raw pointers inside the contained `ImageRefHash`/key maps are
2084// non-`Send`-inferring; every stored value is a plain POD id/descriptor with no
2085// interior aliasing, so moving the cache to another thread is sound.
2086unsafe impl Send for GlTextureCache {}
2087
2088impl GlTextureCache {
2089    /// Initializes an empty cache
2090    #[must_use]
2091    pub const fn empty() -> Self {
2092        Self {
2093            solved_textures: BTreeMap::new(),
2094            hashes: BTreeMap::new(),
2095        }
2096    }
2097
2098    /// Updates a given texture
2099    ///
2100    /// This is called when a texture needs to be re-rendered (e.g., on resize or animation frame).
2101    /// It updates the texture in the `WebRender` external image cache and updates the internal
2102    /// descriptor to reflect the new size.
2103    ///
2104    /// # Arguments
2105    ///
2106    /// * `dom_id` - The DOM ID containing the texture
2107    /// * `node_id` - The node ID of the image element
2108    /// * `document_id` - The `WebRender` document ID
2109    /// * `epoch` - The current frame epoch
2110    /// * `new_texture` - The new texture to use
2111    /// * `insert_into_active_gl_textures_fn` - Function to insert the texture into the cache
2112    ///
2113    /// # Returns
2114    ///
2115    /// The `ExternalImageId` if successful, None if the texture wasn't found in the cache
2116    pub fn update_texture(
2117        &mut self,
2118        dom_id: DomId,
2119        node_id: NodeId,
2120        document_id: DocumentId,
2121        epoch: Epoch,
2122        new_texture: Texture,
2123        insert_into_active_gl_textures_fn: &GlStoreImageFn,
2124    ) -> Option<ExternalImageId> {
2125        let new_descriptor = new_texture.get_descriptor();
2126        let di_map = self.solved_textures.get_mut(&dom_id)?;
2127        let entry = di_map.get_mut(&node_id)?;
2128
2129        // Update the descriptor
2130        entry.1 = new_descriptor;
2131
2132        // The ExternalImageId is deterministic from (dom_id, node_id), so the cache
2133        // entry can keep referencing the same id across re-renders.
2134        let external_image_id = texture_external_image_id(dom_id, node_id);
2135        (insert_into_active_gl_textures_fn)(document_id, epoch, new_texture, external_image_id);
2136        entry.2 = external_image_id;
2137
2138        Some(external_image_id)
2139    }
2140}
2141
2142macro_rules! unique_id {
2143    ($struct_name:ident, $counter_name:ident) => {
2144        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
2145        #[repr(C)]
2146        pub struct $struct_name {
2147            pub id: usize,
2148        }
2149
2150        impl $struct_name {
2151            pub fn unique() -> Self {
2152                Self {
2153                    id: $counter_name.fetch_add(1, AtomicOrdering::SeqCst),
2154                }
2155            }
2156        }
2157    };
2158}
2159
2160// NOTE: the property key is unique across transform, color and opacity properties
2161static PROPERTY_KEY_COUNTER: AtomicUsize = AtomicUsize::new(0);
2162unique_id!(TransformKey, PROPERTY_KEY_COUNTER);
2163unique_id!(ColorKey, PROPERTY_KEY_COUNTER);
2164unique_id!(OpacityKey, PROPERTY_KEY_COUNTER);
2165
2166static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
2167unique_id!(ImageId, IMAGE_ID_COUNTER);
2168static FONT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
2169unique_id!(FontId, FONT_ID_COUNTER);
2170
2171#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
2172#[repr(C)]
2173pub struct ImageMask {
2174    pub image: ImageRef,
2175    pub rect: LogicalRect,
2176    pub repeat: bool,
2177}
2178
2179impl_option!(
2180    ImageMask,
2181    OptionImageMask,
2182    copy = false,
2183    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
2184);
2185
2186#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2187pub enum ImmediateFontId {
2188    Resolved((StyleFontFamilyHash, FontKey)),
2189    Unresolved(StyleFontFamilyVec),
2190}
2191
2192#[derive(Debug, Clone, PartialEq, PartialOrd)]
2193#[repr(C, u8)]
2194pub enum RawImageData {
2195    // 8-bit image data
2196    U8(U8Vec),
2197    // 16-bit image data
2198    U16(U16Vec),
2199    // HDR image data
2200    F32(F32Vec),
2201}
2202
2203impl RawImageData {
2204    #[must_use]
2205    pub const fn get_u8_vec_ref(&self) -> Option<&U8Vec> {
2206        match self {
2207            Self::U8(v) => Some(v),
2208            _ => None,
2209        }
2210    }
2211
2212    #[must_use]
2213    pub const fn get_u16_vec_ref(&self) -> Option<&U16Vec> {
2214        match self {
2215            Self::U16(v) => Some(v),
2216            _ => None,
2217        }
2218    }
2219
2220    #[must_use]
2221    pub const fn get_f32_vec_ref(&self) -> Option<&F32Vec> {
2222        match self {
2223            Self::F32(v) => Some(v),
2224            _ => None,
2225        }
2226    }
2227
2228    fn get_u8_vec(self) -> Option<U8Vec> {
2229        match self {
2230            Self::U8(v) => Some(v),
2231            _ => None,
2232        }
2233    }
2234
2235    fn get_u16_vec(self) -> Option<U16Vec> {
2236        match self {
2237            Self::U16(v) => Some(v),
2238            _ => None,
2239        }
2240    }
2241}
2242
2243#[derive(Debug, Clone, PartialEq, PartialOrd)]
2244#[repr(C)]
2245pub struct RawImage {
2246    pub pixels: RawImageData,
2247    pub width: usize,
2248    pub height: usize,
2249    pub premultiplied_alpha: bool,
2250    pub data_format: RawImageFormat,
2251    pub tag: U8Vec,
2252}
2253
2254/// A soft round brush for the painting API.
2255///
2256/// The same parameters drive the CPU
2257/// rasterizer ([`RawImage::paint_dot`]) and the GPU brush shader, so a stroke
2258/// looks identical whether it lands on a `RawImage` or a `Texture`.
2259#[repr(C)]
2260#[derive(Debug, Copy, Clone, PartialEq)]
2261pub struct Brush {
2262    /// Brush color (its alpha scales the dab opacity together with `flow`).
2263    pub color: ColorU,
2264    /// Brush radius in pixels.
2265    pub radius: f32,
2266    /// Edge hardness, `0.0` (fully feathered) .. `1.0` (hard edge). Opaque out
2267    /// to `hardness * radius`, then a smooth falloff to zero at the edge.
2268    pub hardness: f32,
2269    /// Per-dab opacity multiplier, `0.0`..`1.0`. Values < 1 let overlapping dabs
2270    /// build up smoothly (the "metaball"-like blend).
2271    pub flow: f32,
2272    /// Spacing between stamped dabs along a stroke, as a fraction of `radius`
2273    /// (e.g. `0.25` = a dab every quarter-radius). Smaller = smoother + slower.
2274    pub spacing: f32,
2275}
2276
2277impl Brush {
2278    /// A sensible default brush: medium-soft, full flow, dense spacing.
2279    #[must_use]
2280    pub const fn new(color: ColorU, radius: f32) -> Self {
2281        Self {
2282            color,
2283            radius,
2284            hardness: 0.5,
2285            flow: 1.0,
2286            spacing: 0.25,
2287        }
2288    }
2289}
2290
2291/// Brush dab coverage: `1.0` at the dab center, smoothly `0.0` at its edge.
2292///
2293/// `t` is `distance / radius` in `[0, 1]`; `hardness` in `[0, 1]`. Single source
2294/// of truth for the dab profile -- the GPU brush shader computes the identical
2295/// `1 - smoothstep(hardness, 1, t)` so CPU and GPU strokes match.
2296#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2297#[inline]
2298#[must_use]
2299pub fn brush_dab_coverage(t: f32, hardness: f32) -> f32 {
2300    let edge0 = hardness.clamp(0.0, 1.0);
2301    let denom = (1.0 - edge0).max(1.0e-4);
2302    let x = ((t - edge0) / denom).clamp(0.0, 1.0);
2303    1.0 - (x * x * (3.0 - 2.0 * x))
2304}
2305
2306impl RawImage {
2307    /// CPU painting: stamp one brush dab centered at (`cx`, `cy`) in pixel
2308    /// coordinates, alpha-over compositing a radial-falloff disc. Only 8-bit
2309    /// `RGBA8`/`BGRA8` images are painted (other formats are left untouched).
2310    /// This is the CPU mirror of the GPU brush shader.
2311    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2312    #[allow(
2313        clippy::cast_possible_truncation,
2314        clippy::cast_precision_loss,
2315        clippy::cast_sign_loss
2316    )] // image/graphics: bounded pixel/colour/dimension/unit casts
2317    #[allow(clippy::cast_possible_wrap)] // image/graphics: bounded pixel/colour casts
2318    pub fn paint_dot(&mut self, cx: f32, cy: f32, brush: Brush) {
2319        let r = brush.radius;
2320        // `!(r > 0.0)` intentionally also rejects NaN (`r <= 0.0` would not).
2321        #[allow(clippy::neg_cmp_op_on_partial_ord)]
2322        if !(r > 0.0) || self.width == 0 || self.height == 0 {
2323            return;
2324        }
2325        let bgr = match self.data_format {
2326            RawImageFormat::RGBA8 => false,
2327            RawImageFormat::BGRA8 => true,
2328            _ => return,
2329        };
2330        let (w, h) = (self.width as i32, self.height as i32);
2331        let buf: &mut [u8] = match self.pixels {
2332            RawImageData::U8(ref mut v) => v.as_mut(),
2333            _ => return,
2334        };
2335        let flow = brush.flow.clamp(0.0, 1.0) * (f32::from(brush.color.a) / 255.0);
2336        let (cr, cg, cb) = (
2337            f32::from(brush.color.r),
2338            f32::from(brush.color.g),
2339            f32::from(brush.color.b),
2340        );
2341        let x0 = (cx - r).floor().max(0.0) as i32;
2342        let y0 = (cy - r).floor().max(0.0) as i32;
2343        let x1 = ((cx + r).ceil() as i32).min(w);
2344        let y1 = ((cy + r).ceil() as i32).min(h);
2345        for y in y0..y1 {
2346            for x in x0..x1 {
2347                let dx = x as f32 + 0.5 - cx;
2348                let dy = y as f32 + 0.5 - cy;
2349                let dist = dx.hypot(dy);
2350                if dist > r {
2351                    continue;
2352                }
2353                let a = brush_dab_coverage(dist / r, brush.hardness) * flow;
2354                if a <= 0.0 {
2355                    continue;
2356                }
2357                let idx = ((y * w + x) as usize) * 4;
2358                // `width`/`height` are public and may exceed the actual buffer;
2359                // trust the buffer, not the claimed dimensions, so a mismatch
2360                // skips the pixel instead of indexing out of bounds.
2361                if idx + 4 > buf.len() {
2362                    continue;
2363                }
2364                let (ri, gi, bi, ai) = if bgr {
2365                    (idx + 2, idx + 1, idx, idx + 3)
2366                } else {
2367                    (idx, idx + 1, idx + 2, idx + 3)
2368                };
2369                let inv = 1.0 - a;
2370                buf[ri] = (cr * a + f32::from(buf[ri]) * inv)
2371                    .round()
2372                    .clamp(0.0, 255.0) as u8;
2373                buf[gi] = (cg * a + f32::from(buf[gi]) * inv)
2374                    .round()
2375                    .clamp(0.0, 255.0) as u8;
2376                buf[bi] = (cb * a + f32::from(buf[bi]) * inv)
2377                    .round()
2378                    .clamp(0.0, 255.0) as u8;
2379                buf[ai] = ((a + (f32::from(buf[ai]) / 255.0) * inv) * 255.0)
2380                    .round()
2381                    .clamp(0.0, 255.0) as u8;
2382            }
2383        }
2384    }
2385
2386    /// CPU painting: stamp a stroke by spacing dabs along the segment
2387    /// (`x0`,`y0`)->(`x1`,`y1`). Call once per pointer move with the previous and
2388    /// current positions for a continuous line.
2389    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2390    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
2391    pub fn paint_stroke(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, brush: Brush) {
2392        let dx = x1 - x0;
2393        let dy = y1 - y0;
2394        let len = dx.hypot(dy);
2395        // A non-finite length (infinite / NaN endpoint) would make `n` saturate
2396        // to i32::MAX and the `for i in 0..=n` loop run ~2.1 billion times. Bail
2397        // rather than spin: an infinite segment has no finite dabs to stamp.
2398        if !len.is_finite() {
2399            return;
2400        }
2401        let step = (brush.radius * brush.spacing.max(0.01)).max(0.5);
2402        let n = (len / step).floor() as i32;
2403        if n <= 0 {
2404            self.paint_dot(x1, y1, brush);
2405            return;
2406        }
2407        for i in 0..=n {
2408            let t = i as f32 / n as f32;
2409            self.paint_dot(x0 + dx * t, y0 + dy * t, brush);
2410        }
2411    }
2412}
2413
2414/// Multiplies the RGB channels of a single 4-byte BGRA/RGBA pixel by its alpha.
2415///
2416/// From webrender/wrench. These are slow. Gecko's gfx/2d/Swizzle.cpp has better
2417/// versions.
2418#[inline]
2419#[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2420fn premultiply_alpha(array: &mut [u8]) {
2421    if array.len() != 4 {
2422        return;
2423    }
2424    let a = u32::from(array[3]);
2425    array[0] = (((u32::from(array[0]) * a) + 128) / 255) as u8;
2426    array[1] = (((u32::from(array[1]) * a) + 128) / 255) as u8;
2427    array[2] = (((u32::from(array[2]) * a) + 128) / 255) as u8;
2428}
2429
2430#[inline]
2431#[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2432#[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2433fn normalize_u16(i: u16) -> u8 {
2434    ((f32::from(i) / f32::from(core::u16::MAX)) * f32::from(core::u8::MAX)) as u8
2435}
2436
2437const FOUR_BPP: usize = 4;
2438const TWO_CHANNELS: usize = 2;
2439const THREE_CHANNELS: usize = 3;
2440const FOUR_CHANNELS: usize = 4;
2441
2442impl RawImage {
2443    /// Returns a null / empty image
2444    #[must_use]
2445    pub fn null_image() -> Self {
2446        Self {
2447            pixels: RawImageData::U8(Vec::new().into()),
2448            width: 0,
2449            height: 0,
2450            premultiplied_alpha: true,
2451            data_format: RawImageFormat::BGRA8,
2452            tag: Vec::new().into(),
2453        }
2454    }
2455
2456    /// Allocates a width * height, single-channel mask, used for drawing CPU image masks
2457    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
2458    #[must_use]
2459    pub fn allocate_mask(size: LayoutSize) -> Self {
2460        Self {
2461            pixels: RawImageData::U8(
2462                vec![0; size.width.max(0) as usize * size.height.max(0) as usize].into(),
2463            ),
2464            width: size.width as usize,
2465            height: size.height as usize,
2466            premultiplied_alpha: true,
2467            data_format: RawImageFormat::R8,
2468            tag: Vec::new().into(),
2469        }
2470    }
2471
2472    /// Encodes a `RawImage` as BGRA8 bytes and premultiplies it if the alpha is not premultiplied
2473    ///
2474    /// Returns None if the width * height * BPP does not match
2475    ///
2476    /// TODO: autovectorization fails spectacularly, need to manually optimize!
2477    #[must_use]
2478    pub fn into_loaded_image_source(self) -> Option<(ImageData, ImageDescriptor)> {
2479        let Self {
2480            width,
2481            height,
2482            pixels,
2483            data_format,
2484            premultiplied_alpha,
2485            tag,
2486        } = self;
2487
2488        // Checked: a width*height that overflows usize is not a real image; return
2489        // None rather than panicking (debug) / wrapping to a bogus length (release).
2490        let expected_len = width.checked_mul(height)?;
2491
2492        // …and neither is one whose BYTE count overflows. Every `load_*` below
2493        // scales this pixel count by its channel count (2, 3 or 4) to validate the
2494        // input buffer, and allocates a 4-byte-per-pixel BGRA output buffer, so a
2495        // pixel count that cannot survive `* 4` cannot describe a real image
2496        // either. Without this guard those multiplies wrapped in release (an empty
2497        // buffer then *validated* as a 2^31 x 2^31 image) and panicked in debug —
2498        // and because the callers are `extern "C"` widget callbacks, that panic is
2499        // a non-unwinding ABORT that `catch_unwind` cannot contain. One check here
2500        // covers all 20 multiplication sites.
2501        expected_len.checked_mul(FOUR_BPP)?;
2502
2503        let (bytes, data_format, is_opaque): (U8Vec, RawImageFormat, bool) = match data_format {
2504            RawImageFormat::R8 => {
2505                let (bytes, is_opaque) = Self::load_r8(pixels, expected_len)?;
2506                (bytes, RawImageFormat::R8, is_opaque)
2507            }
2508            RawImageFormat::RG8 => {
2509                let (bytes, is_opaque) = Self::load_rg8(pixels, expected_len, premultiplied_alpha)?;
2510                (bytes, RawImageFormat::BGRA8, is_opaque)
2511            }
2512            RawImageFormat::RGB8 => {
2513                let (bytes, is_opaque) = Self::load_rgb8(pixels, expected_len)?;
2514                (bytes, RawImageFormat::BGRA8, is_opaque)
2515            }
2516            RawImageFormat::RGBA8 => {
2517                let (bytes, is_opaque) =
2518                    Self::load_rgba8(pixels, expected_len, premultiplied_alpha)?;
2519                (bytes, RawImageFormat::BGRA8, is_opaque)
2520            }
2521            RawImageFormat::R16 => {
2522                let (bytes, is_opaque) = Self::load_r16(pixels, expected_len)?;
2523                (bytes, RawImageFormat::BGRA8, is_opaque)
2524            }
2525            RawImageFormat::RG16 => {
2526                let (bytes, is_opaque) = Self::load_rg16(pixels, expected_len)?;
2527                (bytes, RawImageFormat::BGRA8, is_opaque)
2528            }
2529            RawImageFormat::RGB16 => {
2530                let (bytes, is_opaque) = Self::load_rgb16(pixels, expected_len)?;
2531                (bytes, RawImageFormat::BGRA8, is_opaque)
2532            }
2533            RawImageFormat::RGBA16 => {
2534                let (bytes, is_opaque) =
2535                    Self::load_rgba16(pixels, expected_len, premultiplied_alpha)?;
2536                (bytes, RawImageFormat::BGRA8, is_opaque)
2537            }
2538            RawImageFormat::BGR8 => {
2539                let (bytes, is_opaque) = Self::load_bgr8(pixels, expected_len)?;
2540                (bytes, RawImageFormat::BGRA8, is_opaque)
2541            }
2542            RawImageFormat::BGRA8 => {
2543                let (bytes, is_opaque) =
2544                    Self::load_bgra8(pixels, expected_len, premultiplied_alpha)?;
2545                (bytes, RawImageFormat::BGRA8, is_opaque)
2546            }
2547            RawImageFormat::RGBF32 => {
2548                let (bytes, is_opaque) = Self::load_rgbf32(pixels, expected_len)?;
2549                (bytes, RawImageFormat::BGRA8, is_opaque)
2550            }
2551            RawImageFormat::RGBAF32 => {
2552                let (bytes, is_opaque) =
2553                    Self::load_rgbaf32(pixels, expected_len, premultiplied_alpha)?;
2554                (bytes, RawImageFormat::BGRA8, is_opaque)
2555            }
2556        };
2557
2558        let image_data = ImageData::Raw(SharedRawImageData::new(bytes));
2559        let image_descriptor = ImageDescriptor {
2560            format: data_format,
2561            width,
2562            height,
2563            offset: 0,
2564            stride: None.into(),
2565            flags: ImageDescriptorFlags {
2566                is_opaque,
2567                allow_mipmaps: true,
2568            },
2569        };
2570
2571        Some((image_data, image_descriptor))
2572    }
2573
2574    /// Keep R8 data as-is — `WebRender` supports R8 natively. This is important for
2575    /// image mask clips which need the single-channel data (white=visible,
2576    /// black=clipped). Stays in `R8` format; never opaque.
2577    fn load_r8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2578        let pixels = pixels.get_u8_vec()?;
2579
2580        if pixels.len() != expected_len {
2581            return None;
2582        }
2583
2584        Some((pixels, false))
2585    }
2586
2587    fn load_rg8(
2588        pixels: RawImageData,
2589        expected_len: usize,
2590        premultiplied_alpha: bool,
2591    ) -> Option<(U8Vec, bool)> {
2592        let pixels = pixels.get_u8_vec()?;
2593
2594        if pixels.len() != expected_len * TWO_CHANNELS {
2595            return None;
2596        }
2597
2598        let mut is_opaque = true;
2599        let mut px = vec![0; expected_len * FOUR_BPP];
2600
2601        // TODO: check that this function is SIMD optimized
2602        for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
2603            let grey = greyalpha[0];
2604            let alpha = greyalpha[1];
2605
2606            if alpha != 255 {
2607                is_opaque = false;
2608            }
2609
2610            px[pixel_index * FOUR_BPP] = grey;
2611            px[(pixel_index * FOUR_BPP) + 1] = grey;
2612            px[(pixel_index * FOUR_BPP) + 2] = grey;
2613            px[(pixel_index * FOUR_BPP) + 3] = alpha;
2614
2615            if !premultiplied_alpha {
2616                premultiply_alpha(
2617                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2618                );
2619            }
2620        }
2621
2622        Some((px.into(), is_opaque))
2623    }
2624
2625    fn load_rgb8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2626        let pixels = pixels.get_u8_vec()?;
2627
2628        if pixels.len() != expected_len * THREE_CHANNELS {
2629            return None;
2630        }
2631
2632        let mut px = vec![0; expected_len * FOUR_BPP];
2633
2634        // TODO: check that this function is SIMD optimized
2635        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2636            let red = rgb[0];
2637            let green = rgb[1];
2638            let blue = rgb[2];
2639
2640            px[pixel_index * FOUR_BPP] = blue;
2641            px[(pixel_index * FOUR_BPP) + 1] = green;
2642            px[(pixel_index * FOUR_BPP) + 2] = red;
2643            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2644        }
2645
2646        Some((px.into(), true))
2647    }
2648
2649    fn load_rgba8(
2650        pixels: RawImageData,
2651        expected_len: usize,
2652        premultiplied_alpha: bool,
2653    ) -> Option<(U8Vec, bool)> {
2654        let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
2655
2656        if pixels.len() != expected_len * FOUR_CHANNELS {
2657            return None;
2658        }
2659
2660        let mut is_opaque = true;
2661
2662        // TODO: check that this function is SIMD optimized
2663        // no extra allocation necessary, but swizzling
2664        if premultiplied_alpha {
2665            for rgba in pixels.chunks_exact_mut(4) {
2666                let (r, gba) = rgba.split_first_mut()?;
2667                core::mem::swap(r, gba.get_mut(1)?);
2668                let a = rgba.get_mut(3)?;
2669                if *a != 255 {
2670                    is_opaque = false;
2671                }
2672            }
2673        } else {
2674            for rgba in pixels.chunks_exact_mut(4) {
2675                // RGBA => BGRA
2676                let (r, gba) = rgba.split_first_mut()?;
2677                core::mem::swap(r, gba.get_mut(1)?);
2678                let a = rgba.get_mut(3)?;
2679                if *a != 255 {
2680                    is_opaque = false;
2681                }
2682                premultiply_alpha(rgba); // <-
2683            }
2684        }
2685
2686        Some((pixels.into(), is_opaque))
2687    }
2688
2689    fn load_r16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2690        let pixels = pixels.get_u16_vec()?;
2691
2692        if pixels.len() != expected_len {
2693            return None;
2694        }
2695
2696        let mut px = vec![0; expected_len * FOUR_BPP];
2697
2698        // TODO: check that this function is SIMD optimized
2699        for (pixel_index, grey_u16) in pixels.as_ref().iter().enumerate() {
2700            let grey_u8 = normalize_u16(*grey_u16);
2701            px[pixel_index * FOUR_BPP] = grey_u8;
2702            px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
2703            px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
2704            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2705        }
2706
2707        Some((px.into(), true))
2708    }
2709
2710    fn load_rg16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2711        let pixels = pixels.get_u16_vec()?;
2712
2713        if pixels.len() != expected_len * TWO_CHANNELS {
2714            return None;
2715        }
2716
2717        let mut is_opaque = true;
2718        let mut px = vec![0; expected_len * FOUR_BPP];
2719
2720        // TODO: check that this function is SIMD optimized
2721        for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
2722            let grey_u8 = normalize_u16(greyalpha[0]);
2723            let alpha_u8 = normalize_u16(greyalpha[1]);
2724
2725            if alpha_u8 != 255 {
2726                is_opaque = false;
2727            }
2728
2729            px[pixel_index * FOUR_BPP] = grey_u8;
2730            px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
2731            px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
2732            px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2733        }
2734
2735        Some((px.into(), is_opaque))
2736    }
2737
2738    fn load_rgb16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2739        let pixels = pixels.get_u16_vec()?;
2740
2741        if pixels.len() != expected_len * THREE_CHANNELS {
2742            return None;
2743        }
2744
2745        let mut px = vec![0; expected_len * FOUR_BPP];
2746
2747        // TODO: check that this function is SIMD optimized
2748        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2749            let red_u8 = normalize_u16(rgb[0]);
2750            let green_u8 = normalize_u16(rgb[1]);
2751            let blue_u8 = normalize_u16(rgb[2]);
2752
2753            px[pixel_index * FOUR_BPP] = blue_u8;
2754            px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2755            px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2756            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2757        }
2758
2759        Some((px.into(), true))
2760    }
2761
2762    fn load_rgba16(
2763        pixels: RawImageData,
2764        expected_len: usize,
2765        premultiplied_alpha: bool,
2766    ) -> Option<(U8Vec, bool)> {
2767        let pixels = pixels.get_u16_vec()?;
2768
2769        if pixels.len() != expected_len * FOUR_CHANNELS {
2770            return None;
2771        }
2772
2773        let mut is_opaque = true;
2774        let mut px = vec![0; expected_len * FOUR_BPP];
2775
2776        // TODO: check that this function is SIMD optimized
2777        if premultiplied_alpha {
2778            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2779                let red_u8 = normalize_u16(rgba[0]);
2780                let green_u8 = normalize_u16(rgba[1]);
2781                let blue_u8 = normalize_u16(rgba[2]);
2782                let alpha_u8 = normalize_u16(rgba[3]);
2783
2784                if alpha_u8 != 255 {
2785                    is_opaque = false;
2786                }
2787
2788                px[pixel_index * FOUR_BPP] = blue_u8;
2789                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2790                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2791                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2792            }
2793        } else {
2794            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2795                let red_u8 = normalize_u16(rgba[0]);
2796                let green_u8 = normalize_u16(rgba[1]);
2797                let blue_u8 = normalize_u16(rgba[2]);
2798                let alpha_u8 = normalize_u16(rgba[3]);
2799
2800                if alpha_u8 != 255 {
2801                    is_opaque = false;
2802                }
2803
2804                px[pixel_index * FOUR_BPP] = blue_u8;
2805                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2806                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2807                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2808                premultiply_alpha(
2809                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2810                );
2811            }
2812        }
2813
2814        Some((px.into(), is_opaque))
2815    }
2816
2817    fn load_bgr8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2818        let pixels = pixels.get_u8_vec()?;
2819
2820        if pixels.len() != expected_len * THREE_CHANNELS {
2821            return None;
2822        }
2823
2824        let mut px = vec![0; expected_len * FOUR_BPP];
2825
2826        // TODO: check that this function is SIMD optimized
2827        for (pixel_index, bgr) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2828            let blue = bgr[0];
2829            let green = bgr[1];
2830            let red = bgr[2];
2831
2832            px[pixel_index * FOUR_BPP] = blue;
2833            px[(pixel_index * FOUR_BPP) + 1] = green;
2834            px[(pixel_index * FOUR_BPP) + 2] = red;
2835            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2836        }
2837
2838        Some((px.into(), true))
2839    }
2840
2841    fn load_bgra8(
2842        pixels: RawImageData,
2843        expected_len: usize,
2844        premultiplied_alpha: bool,
2845    ) -> Option<(U8Vec, bool)> {
2846        let mut is_opaque = true;
2847
2848        let bytes: U8Vec = if premultiplied_alpha {
2849            // DO NOT CLONE THE IMAGE HERE!
2850            let pixels = pixels.get_u8_vec()?;
2851
2852            if pixels.len() != expected_len * FOUR_BPP {
2853                return None;
2854            }
2855
2856            is_opaque = pixels
2857                .as_ref()
2858                .chunks_exact(FOUR_CHANNELS)
2859                .all(|bgra| bgra[3] == 255);
2860
2861            pixels
2862        } else {
2863            let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
2864
2865            if pixels.len() != expected_len * FOUR_BPP {
2866                return None;
2867            }
2868
2869            for bgra in pixels.chunks_exact_mut(FOUR_CHANNELS) {
2870                if bgra[3] != 255 {
2871                    is_opaque = false;
2872                }
2873                premultiply_alpha(bgra);
2874            }
2875            pixels.into()
2876        };
2877
2878        Some((bytes, is_opaque))
2879    }
2880
2881    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2882    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2883    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
2884    fn load_rgbf32(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
2885        let pixels = pixels.get_f32_vec_ref()?;
2886
2887        if pixels.len() != expected_len * THREE_CHANNELS {
2888            return None;
2889        }
2890
2891        let mut px = vec![0; expected_len * FOUR_BPP];
2892
2893        // TODO: check that this function is SIMD optimized
2894        for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
2895            let red_u8 = (rgb[0] * 255.0) as u8;
2896            let green_u8 = (rgb[1] * 255.0) as u8;
2897            let blue_u8 = (rgb[2] * 255.0) as u8;
2898
2899            px[pixel_index * FOUR_BPP] = blue_u8;
2900            px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2901            px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2902            px[(pixel_index * FOUR_BPP) + 3] = 0xff;
2903        }
2904
2905        Some((px.into(), true))
2906    }
2907
2908    #[allow(clippy::cast_possible_truncation)] // image/graphics: bounded pixel/colour/dimension/unit casts
2909    #[allow(clippy::cast_sign_loss)] // image/graphics: bounded pixel/colour casts
2910    #[allow(clippy::needless_pass_by_value)] // owned RawImageData taken by value (image decode entry point)
2911    fn load_rgbaf32(
2912        pixels: RawImageData,
2913        expected_len: usize,
2914        premultiplied_alpha: bool,
2915    ) -> Option<(U8Vec, bool)> {
2916        let pixels = pixels.get_f32_vec_ref()?;
2917
2918        if pixels.len() != expected_len * FOUR_CHANNELS {
2919            return None;
2920        }
2921
2922        let mut is_opaque = true;
2923        let mut px = vec![0; expected_len * FOUR_BPP];
2924
2925        // TODO: check that this function is SIMD optimized
2926        if premultiplied_alpha {
2927            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2928                let red_u8 = (rgba[0] * 255.0) as u8;
2929                let green_u8 = (rgba[1] * 255.0) as u8;
2930                let blue_u8 = (rgba[2] * 255.0) as u8;
2931                let alpha_u8 = (rgba[3] * 255.0) as u8;
2932
2933                if alpha_u8 != 255 {
2934                    is_opaque = false;
2935                }
2936
2937                px[pixel_index * FOUR_BPP] = blue_u8;
2938                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2939                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2940                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2941            }
2942        } else {
2943            for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
2944                let red_u8 = (rgba[0] * 255.0) as u8;
2945                let green_u8 = (rgba[1] * 255.0) as u8;
2946                let blue_u8 = (rgba[2] * 255.0) as u8;
2947                let alpha_u8 = (rgba[3] * 255.0) as u8;
2948
2949                if alpha_u8 != 255 {
2950                    is_opaque = false;
2951                }
2952
2953                px[pixel_index * FOUR_BPP] = blue_u8;
2954                px[(pixel_index * FOUR_BPP) + 1] = green_u8;
2955                px[(pixel_index * FOUR_BPP) + 2] = red_u8;
2956                px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
2957                premultiply_alpha(
2958                    &mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
2959                );
2960            }
2961        }
2962
2963        Some((px.into(), is_opaque))
2964    }
2965}
2966
2967impl_option!(
2968    RawImage,
2969    OptionRawImage,
2970    copy = false,
2971    [Debug, Clone, PartialEq, PartialOrd]
2972);
2973
2974#[must_use]
2975pub fn font_size_to_au(font_size: StyleFontSize) -> Au {
2976    Au::from_px(
2977        font_size
2978            .inner
2979            .to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE),
2980    )
2981}
2982
2983pub type FontInstanceFlags = u32;
2984
2985// Common flags
2986pub const FONT_INSTANCE_FLAG_SYNTHETIC_BOLD: u32 = 1 << 1;
2987pub const FONT_INSTANCE_FLAG_EMBEDDED_BITMAPS: u32 = 1 << 2;
2988pub const FONT_INSTANCE_FLAG_SUBPIXEL_BGR: u32 = 1 << 3;
2989pub const FONT_INSTANCE_FLAG_TRANSPOSE: u32 = 1 << 4;
2990pub const FONT_INSTANCE_FLAG_FLIP_X: u32 = 1 << 5;
2991pub const FONT_INSTANCE_FLAG_FLIP_Y: u32 = 1 << 6;
2992pub const FONT_INSTANCE_FLAG_SUBPIXEL_POSITION: u32 = 1 << 7;
2993
2994// Windows flags
2995pub const FONT_INSTANCE_FLAG_FORCE_GDI: u32 = 1 << 16;
2996
2997// Mac flags
2998pub const FONT_INSTANCE_FLAG_FONT_SMOOTHING: u32 = 1 << 16;
2999
3000// FreeType flags
3001pub const FONT_INSTANCE_FLAG_FORCE_AUTOHINT: u32 = 1 << 16;
3002pub const FONT_INSTANCE_FLAG_NO_AUTOHINT: u32 = 1 << 17;
3003pub const FONT_INSTANCE_FLAG_VERTICAL_LAYOUT: u32 = 1 << 18;
3004pub const FONT_INSTANCE_FLAG_LCD_VERTICAL: u32 = 1 << 19;
3005
3006#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3007pub struct GlyphOptions {
3008    pub render_mode: FontRenderMode,
3009    pub flags: FontInstanceFlags,
3010}
3011
3012#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3013pub enum FontRenderMode {
3014    Mono,
3015    Alpha,
3016    Subpixel,
3017}
3018
3019#[cfg(target_arch = "wasm32")]
3020#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3021pub struct FontInstancePlatformOptions {
3022    // empty for now
3023}
3024
3025#[cfg(target_os = "windows")]
3026#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3027pub struct FontInstancePlatformOptions {
3028    pub gamma: u16,
3029    pub contrast: u8,
3030    pub cleartype_level: u8,
3031}
3032
3033#[cfg(target_os = "macos")]
3034#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3035pub struct FontInstancePlatformOptions {
3036    pub unused: u32,
3037}
3038
3039#[cfg(target_os = "linux")]
3040#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3041pub struct FontInstancePlatformOptions {
3042    pub lcd_filter: FontLCDFilter,
3043    pub hinting: FontHinting,
3044}
3045
3046// Mobile targets — empty platform-options struct keeps the
3047// `FontInstanceOptions { platform_options: Option<...>, .. }` field
3048// well-typed without inheriting Linux's freetype-specific tunables.
3049#[cfg(any(target_os = "android", target_os = "ios"))]
3050#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3051pub struct FontInstancePlatformOptions {
3052    pub unused: u32,
3053}
3054
3055#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3056pub enum FontHinting {
3057    None,
3058    Mono,
3059    Light,
3060    Normal,
3061    LCD,
3062}
3063
3064#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
3065pub enum FontLCDFilter {
3066    None,
3067    #[default]
3068    Default,
3069    Light,
3070    Legacy,
3071}
3072
3073#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3074pub struct FontInstanceOptions {
3075    pub render_mode: FontRenderMode,
3076    pub flags: FontInstanceFlags,
3077    pub bg_color: ColorU,
3078    /// When `bg_color.a` is != 0 and `render_mode` is `FontRenderMode::Subpixel`,
3079    /// the text will be rendered with `bg_color.r/g/b` as an opaque estimated
3080    /// background color.
3081    pub synthetic_italics: SyntheticItalics,
3082}
3083
3084impl Default for FontInstanceOptions {
3085    fn default() -> Self {
3086        Self {
3087            render_mode: FontRenderMode::Subpixel,
3088            flags: 0,
3089            bg_color: ColorU::TRANSPARENT,
3090            synthetic_italics: SyntheticItalics::default(),
3091        }
3092    }
3093}
3094
3095#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
3096pub struct SyntheticItalics {
3097    pub angle: i16,
3098}
3099
3100/// Reference-counted wrapper around raw image bytes (`U8Vec`).
3101/// This allows sharing image data between azul-core and webrender without cloning.
3102///
3103/// Similar to `ImageRef` but specifically for raw byte data, avoiding the overhead
3104/// of the full `DecodedImage` enum when we just need the bytes.
3105#[derive(Debug)]
3106#[repr(C)]
3107pub struct SharedRawImageData {
3108    /// Shared pointer to the raw image bytes
3109    pub data: *const U8Vec,
3110    /// Reference counter - when it reaches 0, the data is deallocated
3111    pub copies: *const AtomicUsize,
3112    /// Whether to run the destructor (for FFI safety)
3113    pub run_destructor: bool,
3114}
3115
3116impl SharedRawImageData {
3117    /// Create a new `SharedRawImageData` from a `U8Vec`
3118    #[must_use]
3119    pub fn new(data: U8Vec) -> Self {
3120        Self {
3121            data: Box::into_raw(Box::new(data)),
3122            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
3123            run_destructor: true,
3124        }
3125    }
3126
3127    /// Get a reference to the underlying bytes
3128    #[must_use]
3129    pub fn as_ref(&self) -> &[u8] {
3130        // SAFETY: `data` is a non-null, live `Box<U8Vec>` owned by this handle (and its
3131        // clones) until the last copy drops; the borrow is tied to `&self`.
3132        unsafe { (*self.data).as_ref() }
3133    }
3134
3135    /// Alias for `as_ref()` - get the raw bytes as a slice
3136    #[must_use]
3137    pub fn get_bytes(&self) -> &[u8] {
3138        self.as_ref()
3139    }
3140
3141    /// Get a pointer to the raw bytes for hashing/identification
3142    #[must_use]
3143    pub fn as_ptr(&self) -> *const u8 {
3144        // SAFETY: `data` is a non-null, live `Box<U8Vec>` (see `as_ref`).
3145        unsafe { (*self.data).as_ref().as_ptr() }
3146    }
3147
3148    /// Get the length of the data
3149    #[must_use]
3150    pub const fn len(&self) -> usize {
3151        // SAFETY: `data` is a non-null, live `Box<U8Vec>` (see `as_ref`).
3152        unsafe { (*self.data).len() }
3153    }
3154
3155    /// Check if the data is empty
3156    #[must_use]
3157    pub const fn is_empty(&self) -> bool {
3158        self.len() == 0
3159    }
3160
3161    /// Try to extract the `U8Vec` if this is the only reference
3162    /// Returns None if there are other references
3163    #[must_use]
3164    pub fn into_inner(self) -> Option<U8Vec> {
3165        // SAFETY: `data`/`copies` are non-null heap allocations from `Box::into_raw` in
3166        // `new()`. When `copies == 1` we are the sole owner, so reclaiming both Boxes
3167        // and `forget`-ing `self` transfers ownership without a double free.
3168        unsafe {
3169            if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
3170                let data = Box::from_raw(self.data.cast_mut());
3171                drop(Box::from_raw(self.copies.cast_mut()));
3172                core::mem::forget(self); // don't run the destructor
3173                Some(*data)
3174            } else {
3175                None
3176            }
3177        }
3178    }
3179}
3180
3181// SAFETY: the raw pointers only address heap `Box`es of `Send`/`Sync` data, and all
3182// cross-thread refcount mutation goes through the `AtomicUsize` in `copies`.
3183unsafe impl Send for SharedRawImageData {}
3184unsafe impl Sync for SharedRawImageData {}
3185
3186impl Clone for SharedRawImageData {
3187    fn clone(&self) -> Self {
3188        // SAFETY: `copies` is a non-null, live `AtomicUsize` shared by all clones; the
3189        // atomic increment balances the `fetch_sub` in `Drop`.
3190        unsafe {
3191            self.copies
3192                .as_ref()
3193                .map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
3194        }
3195        Self {
3196            data: self.data,
3197            copies: self.copies,
3198            run_destructor: true,
3199        }
3200    }
3201}
3202
3203impl Drop for SharedRawImageData {
3204    fn drop(&mut self) {
3205        self.run_destructor = false;
3206        // SAFETY: `data`/`copies` are non-null, live `Box`es shared by all clones.
3207        // `fetch_sub` returns the pre-decrement count, so `== 1` means we are the last
3208        // owner; only then do we reclaim both Boxes exactly once.
3209        unsafe {
3210            let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
3211            if copies == 1 {
3212                drop(Box::from_raw(self.data.cast_mut()));
3213                drop(Box::from_raw(self.copies.cast_mut()));
3214            }
3215        }
3216    }
3217}
3218
3219impl PartialEq for SharedRawImageData {
3220    fn eq(&self, rhs: &Self) -> bool {
3221        core::ptr::eq(self.data, rhs.data)
3222    }
3223}
3224
3225impl Eq for SharedRawImageData {}
3226
3227impl PartialOrd for SharedRawImageData {
3228    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
3229        Some(self.cmp(other))
3230    }
3231}
3232
3233impl Ord for SharedRawImageData {
3234    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
3235        (self.data as usize).cmp(&(other.data as usize))
3236    }
3237}
3238
3239impl Hash for SharedRawImageData {
3240    fn hash<H>(&self, state: &mut H)
3241    where
3242        H: Hasher,
3243    {
3244        (self.data as usize).hash(state);
3245    }
3246}
3247
3248/// Represents the backing store of an arbitrary series of pixels for display by
3249/// `WebRender`. This storage can take several forms.
3250#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3251#[repr(C, u8)]
3252pub enum ImageData {
3253    /// A simple series of bytes, provided by the embedding and owned by `WebRender`.
3254    /// The format is stored out-of-band, currently in `ImageDescriptor`.
3255    Raw(SharedRawImageData),
3256    /// An image owned by the embedding, and referenced by `WebRender`. This may
3257    /// take the form of a texture or a heap-allocated buffer.
3258    External(ExternalImageData),
3259}
3260
3261/// Storage format identifier for externally-managed images.
3262#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
3263#[repr(C, u8)]
3264pub enum ExternalImageType {
3265    /// The image is texture-backed.
3266    TextureHandle(ImageBufferKind),
3267    /// The image is heap-allocated by the embedding.
3268    Buffer,
3269}
3270
3271/// An arbitrary identifier for an external image provided by the
3272/// application. It must be a unique identifier for each external
3273/// image.
3274#[repr(C)]
3275#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
3276pub struct ExternalImageId {
3277    pub inner: u64,
3278}
3279
3280static LAST_EXTERNAL_IMAGE_ID: AtomicUsize = AtomicUsize::new(0);
3281
3282impl Default for ExternalImageId {
3283    fn default() -> Self {
3284        Self::new()
3285    }
3286}
3287
3288impl ExternalImageId {
3289    /// Creates a new, unique `ExternalImageId`
3290    pub fn new() -> Self {
3291        Self {
3292            inner: LAST_EXTERNAL_IMAGE_ID.fetch_add(1, AtomicOrdering::SeqCst) as u64,
3293        }
3294    }
3295}
3296
3297#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
3298#[repr(C, u8)]
3299pub enum GlyphOutlineOperation {
3300    MoveTo(OutlineMoveTo),
3301    LineTo(OutlineLineTo),
3302    QuadraticCurveTo(OutlineQuadTo),
3303    CubicCurveTo(OutlineCubicTo),
3304    ClosePath,
3305}
3306
3307impl_option!(
3308    GlyphOutlineOperation,
3309    OptionGlyphOutlineOperation,
3310    copy = false,
3311    [Debug, Clone, PartialEq, Eq, PartialOrd]
3312);
3313
3314// MoveTo in em units
3315#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
3316#[repr(C)]
3317pub struct OutlineMoveTo {
3318    pub x: i16,
3319    pub y: i16,
3320}
3321
3322// LineTo in em units
3323#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
3324#[repr(C)]
3325pub struct OutlineLineTo {
3326    pub x: i16,
3327    pub y: i16,
3328}
3329
3330// QuadTo in em units
3331#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
3332#[repr(C)]
3333pub struct OutlineQuadTo {
3334    pub ctrl_1_x: i16,
3335    pub ctrl_1_y: i16,
3336    pub end_x: i16,
3337    pub end_y: i16,
3338}
3339
3340// CubicTo in em units
3341#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
3342#[repr(C)]
3343pub struct OutlineCubicTo {
3344    pub ctrl_1_x: i16,
3345    pub ctrl_1_y: i16,
3346    pub ctrl_2_x: i16,
3347    pub ctrl_2_y: i16,
3348    pub end_x: i16,
3349    pub end_y: i16,
3350}
3351
3352#[derive(Debug, Clone, PartialEq, PartialOrd)]
3353#[repr(C)]
3354pub struct GlyphOutline {
3355    pub operations: GlyphOutlineOperationVec,
3356}
3357
3358azul_css::impl_vec!(
3359    GlyphOutlineOperation,
3360    GlyphOutlineOperationVec,
3361    GlyphOutlineOperationVecDestructor,
3362    GlyphOutlineOperationVecDestructorType,
3363    GlyphOutlineOperationVecSlice,
3364    OptionGlyphOutlineOperation
3365);
3366azul_css::impl_vec_clone!(
3367    GlyphOutlineOperation,
3368    GlyphOutlineOperationVec,
3369    GlyphOutlineOperationVecDestructor
3370);
3371azul_css::impl_vec_debug!(GlyphOutlineOperation, GlyphOutlineOperationVec);
3372azul_css::impl_vec_partialord!(GlyphOutlineOperation, GlyphOutlineOperationVec);
3373azul_css::impl_vec_partialeq!(GlyphOutlineOperation, GlyphOutlineOperationVec);
3374
3375#[derive(Debug, Clone, Copy)]
3376#[repr(C)]
3377pub struct OwnedGlyphBoundingBox {
3378    pub max_x: i16,
3379    pub max_y: i16,
3380    pub min_x: i16,
3381    pub min_y: i16,
3382}
3383
3384/// Specifies the type of texture target in driver terms.
3385#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3386#[repr(C)]
3387pub enum ImageBufferKind {
3388    /// Standard texture. This maps to `GL_TEXTURE_2D` in OpenGL.
3389    Texture2D = 0,
3390    /// Rectangle texture. This maps to `GL_TEXTURE_RECTANGLE` in OpenGL. This
3391    /// is similar to a standard texture, with a few subtle differences
3392    /// (no mipmaps, non-power-of-two dimensions, different coordinate space)
3393    /// that make it useful for representing the kinds of textures we use
3394    /// in `WebRender`. See <https://www.khronos.org/opengl/wiki/Rectangle_Texture>
3395    /// for background on Rectangle textures.
3396    TextureRect = 1,
3397    /// External texture. This maps to `GL_TEXTURE_EXTERNAL_OES` in OpenGL, which
3398    /// is an extension. This is used for image formats that OpenGL doesn't
3399    /// understand, particularly YUV. See
3400    /// <https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt>
3401    TextureExternal = 2,
3402}
3403
3404/// Descriptor for external image resources. See `ImageData`.
3405#[repr(C)]
3406#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
3407pub struct ExternalImageData {
3408    /// The identifier of this external image, provided by the embedding.
3409    pub id: ExternalImageId,
3410    /// For multi-plane images (i.e. YUV), indicates the plane of the
3411    /// original image that this struct represents. 0 for single-plane images.
3412    pub channel_index: u8,
3413    /// Storage format identifier.
3414    pub image_type: ExternalImageType,
3415}
3416
3417pub type TileSize = u16;
3418
3419#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
3420pub enum ImageDirtyRect {
3421    All,
3422    Partial(LayoutRect),
3423}
3424
3425#[derive(Debug, Clone, PartialEq, PartialOrd)]
3426pub enum ResourceUpdate {
3427    AddFont(AddFont),
3428    DeleteFont(FontKey),
3429    AddFontInstance(AddFontInstance),
3430    DeleteFontInstance(FontInstanceKey),
3431    AddImage(AddImage),
3432    UpdateImage(UpdateImage),
3433    DeleteImage(ImageKey),
3434}
3435
3436#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3437pub struct AddImage {
3438    pub key: ImageKey,
3439    pub descriptor: ImageDescriptor,
3440    pub data: ImageData,
3441    pub tiling: Option<TileSize>,
3442}
3443
3444#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
3445pub struct UpdateImage {
3446    pub key: ImageKey,
3447    pub descriptor: ImageDescriptor,
3448    pub data: ImageData,
3449    pub dirty_rect: ImageDirtyRect,
3450}
3451
3452/// Message to add a font to `WebRender`.
3453/// Contains a reference to the parsed font data.
3454#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
3455pub struct AddFont {
3456    pub key: FontKey,
3457    pub font: FontRef,
3458}
3459
3460impl fmt::Debug for AddFont {
3461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3462        write!(
3463            f,
3464            "AddFont {{ key: {:?}, font: {:?} }}",
3465            self.key, self.font
3466        )
3467    }
3468}
3469
3470#[derive(Debug, Clone, PartialEq, PartialOrd)]
3471pub struct AddFontInstance {
3472    pub key: FontInstanceKey,
3473    pub font_key: FontKey,
3474    pub glyph_size: (Au, DpiScaleFactor),
3475    pub options: Option<FontInstanceOptions>,
3476    pub platform_options: Option<FontInstancePlatformOptions>,
3477    pub variations: Vec<FontVariation>,
3478}
3479
3480#[repr(C)]
3481#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
3482pub struct FontVariation {
3483    pub tag: u32,
3484    pub value: f32,
3485}
3486
3487#[repr(C)]
3488#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
3489pub struct Epoch {
3490    inner: u32,
3491}
3492
3493impl fmt::Display for Epoch {
3494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3495        write!(f, "{}", self.inner)
3496    }
3497}
3498
3499impl Default for Epoch {
3500    fn default() -> Self {
3501        Self::new()
3502    }
3503}
3504
3505impl Epoch {
3506    // prevent raw access to the .inner field so that
3507    // you can grep the codebase for .increment() to see
3508    // exactly where the epoch is being incremented
3509    #[must_use]
3510    pub const fn new() -> Self {
3511        Self { inner: 0 }
3512    }
3513    #[must_use]
3514    pub const fn from(i: u32) -> Self {
3515        Self { inner: i }
3516    }
3517    #[must_use]
3518    pub const fn into_u32(&self) -> u32 {
3519        self.inner
3520    }
3521
3522    // We don't want the epoch to increase to u32::MAX, since
3523    // u32::MAX represents an invalid epoch, which could confuse webrender
3524    pub const fn increment(&mut self) {
3525        use core::u32;
3526        const MAX_ID: u32 = u32::MAX - 1;
3527        *self = match self.inner {
3528            MAX_ID => Self { inner: 0 },
3529            other => Self {
3530                inner: other.saturating_add(1),
3531            },
3532        };
3533    }
3534}
3535
3536// App units that this font instance was registered for
3537#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)]
3538pub struct Au(pub i32);
3539
3540pub const AU_PER_PX: i32 = 60;
3541pub const MAX_AU: i32 = (1 << 30) - 1;
3542pub const MIN_AU: i32 = -(1 << 30) - 1;
3543
3544impl Au {
3545    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
3546    #[must_use]
3547    pub fn from_px(px: f32) -> Self {
3548        let target_app_units = (px * AU_PER_PX as f32) as i32;
3549        Self(target_app_units.clamp(MIN_AU, MAX_AU))
3550    }
3551    #[allow(clippy::cast_precision_loss)] // image/graphics: bounded pixel/colour/dimension/unit casts
3552    #[must_use]
3553    pub fn into_px(&self) -> f32 {
3554        self.0 as f32 / AU_PER_PX as f32
3555    }
3556}
3557
3558// Debug, PartialEq, Eq, PartialOrd, Ord
3559#[derive(Debug)]
3560pub enum AddFontMsg {
3561    // add font: font key, font bytes + font index
3562    Font(FontKey, StyleFontFamilyHash, FontRef),
3563    Instance(AddFontInstance, (Au, DpiScaleFactor)),
3564}
3565
3566impl AddFontMsg {
3567    #[must_use]
3568    pub fn into_resource_update(&self) -> ResourceUpdate {
3569        use self::AddFontMsg::{Font, Instance};
3570        match self {
3571            Font(font_key, _, font_ref) => ResourceUpdate::AddFont(AddFont {
3572                key: *font_key,
3573                font: font_ref.clone(),
3574            }),
3575            Instance(fi, _) => ResourceUpdate::AddFontInstance(fi.clone()),
3576        }
3577    }
3578}
3579
3580#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
3581pub enum DeleteFontMsg {
3582    Font(FontKey),
3583    Instance(FontInstanceKey, (Au, DpiScaleFactor)),
3584}
3585
3586impl DeleteFontMsg {
3587    #[must_use]
3588    pub const fn into_resource_update(&self) -> ResourceUpdate {
3589        use self::DeleteFontMsg::{Font, Instance};
3590        match self {
3591            Font(f) => ResourceUpdate::DeleteFont(*f),
3592            Instance(fi, _) => ResourceUpdate::DeleteFontInstance(*fi),
3593        }
3594    }
3595}
3596
3597#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
3598pub struct AddImageMsg(pub AddImage);
3599
3600impl AddImageMsg {
3601    #[must_use]
3602    pub fn into_resource_update(&self) -> ResourceUpdate {
3603        ResourceUpdate::AddImage(self.0.clone())
3604    }
3605}
3606
3607#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3608#[repr(C)]
3609pub struct LoadedFontSource {
3610    pub data: U8Vec,
3611    pub index: u32,
3612    pub load_outlines: bool,
3613}
3614
3615// function to load the font source from a file
3616pub type LoadFontFn = fn(&StyleFontFamily, &FcFontCache) -> Option<LoadedFontSource>;
3617
3618// function to parse the font given the loaded font source
3619pub type ParseFontFn = fn(LoadedFontSource) -> Option<FontRef>; // = Option<Box<azul_text_layout::Font>>
3620
3621pub type GlStoreImageFn = fn(DocumentId, Epoch, Texture, ExternalImageId);
3622
3623/// Compute the deterministic `ExternalImageId` that the OpenGL texture cache uses
3624/// for a texture bound to a specific DOM node.
3625///
3626/// The same `(DomId, NodeId)` always
3627/// maps to the same `ExternalImageId`, so cached display lists keep working across
3628/// frames.
3629#[must_use]
3630pub fn texture_external_image_id(dom_id: DomId, node_id: NodeId) -> ExternalImageId {
3631    let dom = dom_id.inner as u64;
3632    let node = node_id.index() as u64;
3633    debug_assert!(u32::try_from(dom).is_ok(), "DomId exceeds 32-bit range");
3634    debug_assert!(u32::try_from(node).is_ok(), "NodeId exceeds 32-bit range");
3635    ExternalImageId {
3636        inner: (dom << 32) | (node & 0xFFFF_FFFF),
3637    }
3638}
3639
3640/// Compute the `ExternalImageId` for a static GL texture identified by its
3641/// `ImageRefHash`. Mirrors `image_ref_hash_to_image_key` so a given image hash
3642/// produces the same identifiers everywhere.
3643#[must_use]
3644pub const fn image_ref_hash_to_external_image_id(hash: ImageRefHash) -> ExternalImageId {
3645    ExternalImageId { inner: hash.inner }
3646}
3647
3648/// Given the fonts of the current frame, returns `AddFont` and `AddFontInstance`s of
3649/// which fonts / instances are currently not in the `current_registered_fonts` and
3650/// need to be added.
3651///
3652/// Deleting fonts can only be done after the entire frame has finished drawing,
3653/// otherwise (if removing fonts would happen after every DOM) we'd constantly
3654/// add-and-remove fonts after every `VirtualViewCallback`, which would cause a lot of
3655/// I/O waiting.
3656#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3657pub fn build_add_font_resource_updates(
3658    renderer_resources: &mut RendererResources,
3659    dpi: DpiScaleFactor,
3660    fc_cache: &FcFontCache,
3661    id_namespace: IdNamespace,
3662    fonts_in_dom: &OrderedMap<ImmediateFontId, FastBTreeSet<Au>>,
3663    font_source_load_fn: LoadFontFn,
3664    parse_font_fn: ParseFontFn,
3665) -> Vec<(StyleFontFamilyHash, AddFontMsg)> {
3666    let mut resource_updates = Vec::new();
3667    let mut font_instances_added_this_frame = FastBTreeSet::new();
3668
3669    'outer: for (im_font_id, font_sizes) in fonts_in_dom {
3670        macro_rules! insert_font_instances {
3671            ($font_family_hash:expr, $font_key:expr, $font_size:expr) => {{
3672                let font_instance_key_exists = renderer_resources
3673                    .currently_registered_fonts
3674                    .get(&$font_key)
3675                    .and_then(|(_, font_instances)| font_instances.get(&($font_size, dpi)))
3676                    .is_some()
3677                    || font_instances_added_this_frame.contains(&($font_key, ($font_size, dpi)));
3678
3679                if !font_instance_key_exists {
3680                    let font_instance_key = FontInstanceKey::unique(id_namespace);
3681
3682                    // For some reason the gamma is way to low on Windows
3683                    #[cfg(target_os = "windows")]
3684                    let platform_options = FontInstancePlatformOptions {
3685                        gamma: 300,
3686                        contrast: 100,
3687                        cleartype_level: 100,
3688                    };
3689
3690                    #[cfg(target_os = "linux")]
3691                    let platform_options = FontInstancePlatformOptions {
3692                        lcd_filter: FontLCDFilter::Default,
3693                        hinting: FontHinting::Normal,
3694                    };
3695
3696                    #[cfg(target_os = "macos")]
3697                    let platform_options = FontInstancePlatformOptions::default();
3698
3699                    #[cfg(target_arch = "wasm32")]
3700                    let platform_options = FontInstancePlatformOptions::default();
3701
3702                    #[cfg(any(target_os = "android", target_os = "ios"))]
3703                    let platform_options = FontInstancePlatformOptions::default();
3704
3705                    let options = FontInstanceOptions {
3706                        render_mode: FontRenderMode::Subpixel,
3707                        flags: FONT_INSTANCE_FLAG_NO_AUTOHINT,
3708                        ..Default::default()
3709                    };
3710
3711                    font_instances_added_this_frame.insert(($font_key, ($font_size, dpi)));
3712                    resource_updates.push((
3713                        $font_family_hash,
3714                        AddFontMsg::Instance(
3715                            AddFontInstance {
3716                                key: font_instance_key,
3717                                font_key: $font_key,
3718                                glyph_size: ($font_size, dpi),
3719                                options: Some(options),
3720                                platform_options: Some(platform_options),
3721                                variations: alloc::vec::Vec::new(),
3722                            },
3723                            ($font_size, dpi),
3724                        ),
3725                    ));
3726                }
3727            }};
3728        }
3729
3730        match im_font_id {
3731            ImmediateFontId::Resolved((font_family_hash, font_id)) => {
3732                // nothing to do, font is already added,
3733                // just insert the missing font instances
3734                for font_size in font_sizes {
3735                    insert_font_instances!(*font_family_hash, *font_id, *font_size);
3736                }
3737            }
3738            ImmediateFontId::Unresolved(style_font_families) => {
3739                // If the font is already loaded during the current frame,
3740                // do not attempt to load it again
3741                //
3742                // This prevents duplicated loading for fonts in different orders, i.e.
3743                // - vec!["Times New Roman", "serif"] and
3744                // - vec!["sans", "Times New Roman"]
3745                // ... will resolve to the same font instead of creating two fonts
3746
3747                // If there is no font key, that means there's also no font instances
3748                let mut font_family_hash = None;
3749                let font_families_hash = StyleFontFamiliesHash::new(style_font_families.as_ref());
3750
3751                // Find the first font that can be loaded and parsed
3752                'inner: for family in style_font_families.as_ref() {
3753                    let current_family_hash = StyleFontFamilyHash::new(family);
3754
3755                    if let Some(font_id) = renderer_resources.font_id_map.get(&current_family_hash)
3756                    {
3757                        // font key already exists
3758                        for font_size in font_sizes {
3759                            insert_font_instances!(current_family_hash, *font_id, *font_size);
3760                        }
3761                        continue 'outer;
3762                    }
3763
3764                    let font_ref = match family {
3765                        StyleFontFamily::Ref(r) => r.clone(), // Clone the FontRef
3766                        other => {
3767                            // Load and parse the font
3768                            let Some(font_data) = (font_source_load_fn)(other, fc_cache) else {
3769                                continue 'inner;
3770                            };
3771
3772                            match (parse_font_fn)(font_data) {
3773                                Some(s) => s,
3774                                None => continue 'inner,
3775                            }
3776                        }
3777                    };
3778
3779                    // font loaded properly
3780                    font_family_hash = Some((current_family_hash, font_ref));
3781                    break 'inner;
3782                }
3783
3784                // No font could be loaded: try again next frame.
3785                let Some((font_family_hash, font_ref)) = font_family_hash else {
3786                    continue 'outer;
3787                };
3788
3789                // Generate a new font key, store the mapping between hash and font key
3790                let font_key = FontKey::unique(id_namespace);
3791                let add_font_msg = AddFontMsg::Font(font_key, font_family_hash, font_ref);
3792
3793                renderer_resources
3794                    .font_id_map
3795                    .insert(font_family_hash, font_key);
3796                renderer_resources
3797                    .font_families_map
3798                    .insert(font_families_hash, font_family_hash);
3799                resource_updates.push((font_family_hash, add_font_msg));
3800
3801                // Insert font sizes for the newly generated font key
3802                for font_size in font_sizes {
3803                    insert_font_instances!(font_family_hash, font_key, *font_size);
3804                }
3805            }
3806        }
3807    }
3808
3809    resource_updates
3810}
3811
3812/// Given the images of the current frame, returns `AddImage`s of
3813/// which image keys are currently not in the `current_registered_images` and
3814/// need to be added.
3815///
3816/// Returns Vec<(`ImageRefHash`, `AddImageMsg`)> where:
3817/// - `ImageRefHash`: Stable hash of the `ImageRef` pointer
3818/// - `AddImageMsg`: Message to add the image to `WebRender`
3819///
3820/// The `ImageKey` in `AddImageMsg` is generated directly from the `ImageRefHash` using
3821/// `image_ref_hash_to_image_key()`, so no separate mapping table is needed.
3822///
3823/// Deleting images can only be done after the entire frame has finished drawing,
3824/// otherwise (if removing images would happen after every DOM) we'd constantly
3825/// add-and-remove images after every `VirtualViewCallback`, which would cause a lot of
3826/// I/O waiting.
3827#[allow(unused_variables)]
3828pub fn build_add_image_resource_updates(
3829    renderer_resources: &RendererResources,
3830    id_namespace: IdNamespace,
3831    epoch: Epoch,
3832    document_id: &DocumentId,
3833    images_in_dom: &FastBTreeSet<ImageRef>,
3834    insert_into_active_gl_textures: GlStoreImageFn,
3835) -> Vec<(ImageRefHash, AddImageMsg)> {
3836    images_in_dom
3837        .iter()
3838        .filter_map(|image_ref| {
3839            let image_ref_hash = image_ref_get_hash(image_ref);
3840
3841            if renderer_resources
3842                .currently_registered_images
3843                .contains_key(&image_ref_hash)
3844            {
3845                return None;
3846            }
3847
3848            // NOTE: The image_ref.clone() is a shallow clone,
3849            // does not actually clone the data
3850            match image_ref.get_data() {
3851                DecodedImage::Gl(texture) => {
3852                    let descriptor = texture.get_descriptor();
3853                    let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
3854                    // The ExternalImageId is derived from the same stable hash that
3855                    // produces the ImageKey, so the GL texture cache and WebRender
3856                    // agree on a single identifier for this texture.
3857                    let external_image_id = image_ref_hash_to_external_image_id(image_ref_hash);
3858                    // NOTE: The texture is not really cloned here,
3859                    (insert_into_active_gl_textures)(
3860                        *document_id,
3861                        epoch,
3862                        texture.clone(),
3863                        external_image_id,
3864                    );
3865                    Some((
3866                        image_ref_hash,
3867                        AddImageMsg(AddImage {
3868                            key,
3869                            data: ImageData::External(ExternalImageData {
3870                                id: external_image_id,
3871                                channel_index: 0,
3872                                image_type: ExternalImageType::TextureHandle(
3873                                    ImageBufferKind::Texture2D,
3874                                ),
3875                            }),
3876                            descriptor,
3877                            tiling: None,
3878                        }),
3879                    ))
3880                }
3881                DecodedImage::Raw((descriptor, data)) => {
3882                    let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
3883                    Some((
3884                        image_ref_hash,
3885                        AddImageMsg(AddImage {
3886                            key,
3887                            data: data.clone(), // deep-copy except in the &'static case
3888                            descriptor: *descriptor, /* deep-copy, but struct is not very
3889                                                 * large */
3890                            tiling: None,
3891                        }),
3892                    ))
3893                }
3894                // NullImage has nothing to upload; texture callbacks are handled after
3895                // layout is done.
3896                DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => None,
3897            }
3898        })
3899        .collect()
3900}
3901
3902/// Submits the `AddFont`, `AddFontInstance` and `AddImage` resources to the `RenderApi`.
3903///
3904/// Extends `currently_registered_images` and `currently_registered_fonts` by the
3905/// `last_frame_image_keys` and `last_frame_font_keys`, so that we don't lose track of
3906/// what font and image keys are currently in the API.
3907#[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
3908pub fn add_resources(
3909    renderer_resources: &mut RendererResources,
3910    all_resource_updates: &mut Vec<ResourceUpdate>,
3911    add_font_resources: Vec<(StyleFontFamilyHash, AddFontMsg)>,
3912    add_image_resources: Vec<(ImageRefHash, AddImageMsg)>,
3913) {
3914    all_resource_updates.extend(
3915        add_font_resources
3916            .iter()
3917            .map(|(_, f)| f.into_resource_update()),
3918    );
3919    all_resource_updates.extend(
3920        add_image_resources
3921            .iter()
3922            .map(|(_, i)| i.into_resource_update()),
3923    );
3924
3925    for (image_ref_hash, add_image_msg) in &add_image_resources {
3926        renderer_resources.currently_registered_images.insert(
3927            *image_ref_hash,
3928            ResolvedImage {
3929                key: add_image_msg.0.key,
3930                descriptor: add_image_msg.0.descriptor,
3931            },
3932        );
3933        // Keep the reverse lookup (`ImageKey` -> `ImageRefHash`) in sync with the
3934        // forward map so display-list translation can resolve keys back to hashes.
3935        renderer_resources
3936            .image_key_map
3937            .insert(add_image_msg.0.key, *image_ref_hash);
3938    }
3939
3940    for (_, add_font_msg) in add_font_resources {
3941        use self::AddFontMsg::{Font, Instance};
3942        match add_font_msg {
3943            Font(fk, font_family_hash, font_ref) => {
3944                renderer_resources
3945                    .currently_registered_fonts
3946                    .entry(fk)
3947                    .or_insert_with(|| (font_ref.clone(), OrderedMap::default()));
3948
3949                // CRITICAL: Map font_hash to FontKey so we can look it up during rendering
3950                renderer_resources
3951                    .font_hash_map
3952                    .insert(font_ref.get_hash(), fk);
3953            }
3954            Instance(fi, size) => {
3955                if let Some((_, instances)) = renderer_resources
3956                    .currently_registered_fonts
3957                    .get_mut(&fi.font_key)
3958                {
3959                    instances.insert(size, fi.key);
3960                }
3961            }
3962        }
3963    }
3964}
3965
3966#[cfg(test)]
3967#[path = "resources_test.rs"]
3968mod resources_test;