#[cfg(not(feature = "std"))]
use alloc::string::ToString;
use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
use core::{
fmt,
hash::{Hash, Hasher},
sync::atomic::{AtomicU64, AtomicUsize, Ordering as AtomicOrdering},
};
use azul_css::{
codegen::format::GetHash,
props::basic::{
pixel::DEFAULT_FONT_SIZE, ColorU, FloatValue, FontRef, LayoutRect, LayoutSize,
StyleFontFamily, StyleFontFamilyVec, StyleFontSize,
},
system::SystemStyle,
AzString, F32Vec, LayoutDebugMessage, OptionI32, StringVec, U16Vec, U32Vec, U8Vec,
};
use rust_fontconfig::FcFontCache;
pub use crate::callbacks::{
CoreImageCallback, CoreRenderImageCallback, CoreRenderImageCallbackType,
};
use crate::{
callbacks::{LayoutCallback, VirtualViewCallback},
dom::{DomId, NodeData, NodeType},
geom::{LogicalPosition, LogicalRect, LogicalSize},
gl::{OptionGlContextPtr, Texture},
hit_test::DocumentId,
id::NodeId,
prop_cache::CssPropertyCache,
refany::RefAny,
styled_dom::{
NodeHierarchyItemId, StyleFontFamiliesHash, StyleFontFamilyHash, StyledDom, StyledNodeState,
},
ui_solver::GlyphInstance,
window::{AzStringPair, OptionChar, StringPairVec},
xml::{
ComponentDef, ComponentDefVec, ComponentId, ComponentLibrary, ComponentLibraryVec,
ComponentSource, RegisterComponentFn, RegisterComponentLibraryFn,
},
FastBTreeSet, OrderedMap,
};
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C)]
pub enum UpdateImageType {
Background,
Content,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct DpiScaleFactor {
pub inner: FloatValue,
}
impl DpiScaleFactor {
#[must_use] pub fn new(f: f32) -> Self {
Self {
inner: FloatValue::new(f),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum AppTerminationBehavior {
ReturnToMain,
RunForever,
#[default]
EndProcess,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct NamedFont {
pub name: AzString,
pub bytes: U8Vec,
}
impl_option!(
NamedFont,
OptionNamedFont,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl NamedFont {
#[must_use] pub const fn new(name: AzString, bytes: U8Vec) -> Self {
Self { name, bytes }
}
}
impl_vec!(NamedFont, NamedFontVec, NamedFontVecDestructor, NamedFontVecDestructorType, NamedFontVecSlice, OptionNamedFont);
impl_vec_mut!(NamedFont, NamedFontVec);
impl_vec_debug!(NamedFont, NamedFontVec);
impl_vec_partialeq!(NamedFont, NamedFontVec);
impl_vec_eq!(NamedFont, NamedFontVec);
impl_vec_partialord!(NamedFont, NamedFontVec);
impl_vec_ord!(NamedFont, NamedFontVec);
impl_vec_hash!(NamedFont, NamedFontVec);
impl_vec_clone!(NamedFont, NamedFontVec, NamedFontVecDestructor);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct LoadedFont {
pub font_hash: u64,
pub family_name: AzString,
pub num_glyphs: u32,
pub has_bytes: bool,
}
impl_option!(
LoadedFont,
OptionLoadedFont,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl LoadedFont {
#[must_use] pub const fn new(font_hash: u64, family_name: AzString, num_glyphs: u32, has_bytes: bool) -> Self {
Self {
font_hash,
family_name,
num_glyphs,
has_bytes,
}
}
}
impl_vec!(LoadedFont, LoadedFontVec, LoadedFontVecDestructor, LoadedFontVecDestructorType, LoadedFontVecSlice, OptionLoadedFont);
impl_vec_mut!(LoadedFont, LoadedFontVec);
impl_vec_debug!(LoadedFont, LoadedFontVec);
impl_vec_partialeq!(LoadedFont, LoadedFontVec);
impl_vec_eq!(LoadedFont, LoadedFontVec);
impl_vec_partialord!(LoadedFont, LoadedFontVec);
impl_vec_ord!(LoadedFont, LoadedFontVec);
impl_vec_hash!(LoadedFont, LoadedFontVec);
impl_vec_clone!(LoadedFont, LoadedFontVec, LoadedFontVecDestructor);
#[allow(variant_size_differences)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
#[derive(Default)]
pub enum FontLoadingConfig {
#[default]
LoadAllSystemFonts,
LoadOnlyFamilies(StringVec),
BundledFontsOnly,
}
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct CssMockEnvironment {
pub theme: azul_css::dynamic_selector::OptionThemeCondition,
pub language: azul_css::OptionString,
pub os_version: azul_css::dynamic_selector::OptionOsVersion,
pub os: azul_css::dynamic_selector::OptionOsCondition,
pub desktop_env: azul_css::dynamic_selector::OptionLinuxDesktopEnv,
pub viewport_width: azul_css::OptionF32,
pub viewport_height: azul_css::OptionF32,
pub prefers_reduced_motion: azul_css::OptionBool,
pub prefers_high_contrast: azul_css::OptionBool,
}
impl CssMockEnvironment {
#[must_use] pub fn linux() -> Self {
Self {
os: azul_css::dynamic_selector::OptionOsCondition::Some(azul_css::dynamic_selector::OsCondition::Linux),
..Default::default()
}
}
#[must_use] pub fn windows() -> Self {
Self {
os: azul_css::dynamic_selector::OptionOsCondition::Some(azul_css::dynamic_selector::OsCondition::Windows),
..Default::default()
}
}
#[must_use] pub fn macos() -> Self {
Self {
os: azul_css::dynamic_selector::OptionOsCondition::Some(azul_css::dynamic_selector::OsCondition::MacOS),
..Default::default()
}
}
#[must_use] pub fn dark_theme() -> Self {
Self {
theme: azul_css::dynamic_selector::OptionThemeCondition::Some(azul_css::dynamic_selector::ThemeCondition::Dark),
..Default::default()
}
}
#[must_use] pub fn light_theme() -> Self {
Self {
theme: azul_css::dynamic_selector::OptionThemeCondition::Some(azul_css::dynamic_selector::ThemeCondition::Light),
..Default::default()
}
}
pub fn apply_to(&self, ctx: &mut azul_css::dynamic_selector::DynamicSelectorContext) {
if let azul_css::dynamic_selector::OptionOsCondition::Some(os) = self.os {
ctx.os = os;
}
if let azul_css::dynamic_selector::OptionOsVersion::Some(os_version) = self.os_version {
ctx.os_version = os_version;
}
if let azul_css::dynamic_selector::OptionLinuxDesktopEnv::Some(de) = self.desktop_env {
ctx.desktop_env = azul_css::dynamic_selector::OptionLinuxDesktopEnv::Some(de);
}
if let azul_css::dynamic_selector::OptionThemeCondition::Some(ref theme) = self.theme {
ctx.theme = theme.clone();
}
if let azul_css::OptionString::Some(ref lang) = self.language {
ctx.language = lang.clone();
}
if let azul_css::OptionBool::Some(reduced) = self.prefers_reduced_motion {
ctx.prefers_reduced_motion = if reduced {
azul_css::dynamic_selector::BoolCondition::True
} else {
azul_css::dynamic_selector::BoolCondition::False
};
}
if let azul_css::OptionBool::Some(high_contrast) = self.prefers_high_contrast {
ctx.prefers_high_contrast = if high_contrast {
azul_css::dynamic_selector::BoolCondition::True
} else {
azul_css::dynamic_selector::BoolCondition::False
};
}
if let azul_css::OptionF32::Some(w) = self.viewport_width {
ctx.viewport_width = w;
}
if let azul_css::OptionF32::Some(h) = self.viewport_height {
ctx.viewport_height = h;
}
}
}
impl_option!(
CssMockEnvironment,
OptionCssMockEnvironment,
copy = false,
[Debug, Clone]
);
#[repr(C)]
pub struct Route {
pub pattern: AzString,
pub layout_callback: LayoutCallback,
}
impl Clone for Route {
fn clone(&self) -> Self {
Self { pattern: self.pattern.clone(), layout_callback: self.layout_callback.clone() }
}
}
impl fmt::Debug for Route {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Route")
.field("pattern", &self.pattern)
.field("layout_callback", &self.layout_callback)
.finish()
}
}
impl PartialEq for Route { fn eq(&self, o: &Self) -> bool { self.pattern == o.pattern && self.layout_callback == o.layout_callback } }
impl Eq for Route {}
impl PartialOrd for Route { fn partial_cmp(&self, o: &Self) -> Option<core::cmp::Ordering> { Some(self.cmp(o)) } }
impl Ord for Route { fn cmp(&self, o: &Self) -> core::cmp::Ordering { self.pattern.cmp(&o.pattern).then_with(|| self.layout_callback.cmp(&o.layout_callback)) } }
impl Hash for Route { fn hash<H: Hasher>(&self, state: &mut H) { self.pattern.hash(state); self.layout_callback.hash(state); } }
impl_option!(Route, OptionRoute, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
impl_vec!(Route, RouteVec, RouteVecDestructor, RouteVecDestructorType, RouteVecSlice, OptionRoute);
impl_vec_mut!(Route, RouteVec);
impl_vec_debug!(Route, RouteVec);
impl_vec_clone!(Route, RouteVec, RouteVecDestructor);
impl_vec_partialeq!(Route, RouteVec);
impl_vec_eq!(Route, RouteVec);
impl_vec_partialord!(Route, RouteVec);
impl_vec_ord!(Route, RouteVec);
impl_vec_hash!(Route, RouteVec);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct RouteMatch {
pub pattern: AzString,
pub params: StringPairVec,
}
impl RouteMatch {
#[must_use] pub fn get_param(&self, key: &str) -> Option<&AzString> {
self.params.get_key(key)
}
}
impl_option!(RouteMatch, OptionRouteMatch, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
#[allow(clippy::similar_names)] #[must_use] pub fn match_route(pattern: &str, path: &str) -> Option<RouteMatch> {
let pat_segs: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if pat_segs.len() != path_segs.len() {
return None;
}
let mut params = Vec::new();
for (pat, val) in pat_segs.iter().zip(path_segs.iter()) {
if let Some(param_name) = pat.strip_prefix(':') {
params.push(AzStringPair {
key: AzString::from(param_name.to_string()),
value: AzString::from((*val).to_string()),
});
} else if pat != val {
return None;
}
}
Some(RouteMatch {
pattern: AzString::from(pattern.to_string()),
params: StringPairVec::from_vec(params),
})
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct AppConfig {
pub log_level: AppLogLevel,
pub enable_visual_panic_hook: bool,
pub enable_logging_on_panic: bool,
pub termination_behavior: AppTerminationBehavior,
pub icon_provider: crate::icon::IconProviderHandle,
pub bundled_fonts: NamedFontVec,
pub font_loading: FontLoadingConfig,
pub mock_css_environment: OptionCssMockEnvironment,
pub system_style: SystemStyle,
pub component_libraries: ComponentLibraryVec,
pub routes: RouteVec,
}
impl AppConfig {
#[must_use] pub fn create() -> Self {
let log_level = AppLogLevel::Error;
let icon_provider = crate::icon::IconProviderHandle::new();
let bundled_fonts = NamedFontVec::from_const_slice(&[]);
let font_loading = FontLoadingConfig::default();
let system_style = SystemStyle::detect();
let mut s = Self {
log_level,
enable_visual_panic_hook: false,
enable_logging_on_panic: true,
termination_behavior: AppTerminationBehavior::default(),
icon_provider,
bundled_fonts,
font_loading,
mock_css_environment: OptionCssMockEnvironment::None,
system_style,
component_libraries: ComponentLibraryVec::from_const_slice(&[]),
routes: RouteVec::from_const_slice(&[]),
};
let register_builtin: crate::xml::RegisterComponentLibraryFnType =
crate::xml::register_builtin_components;
s.add_component_library(
AzString::from_const_str("builtin"),
register_builtin,
);
s
}
#[must_use] pub fn with_mock_environment(mut self, env: CssMockEnvironment) -> Self {
self.mock_css_environment = OptionCssMockEnvironment::Some(env);
self
}
pub fn add_component<R: Into<RegisterComponentFn>>(&mut self, library: AzString, register_fn: R) {
let register_fn = register_fn.into();
let component = (register_fn.cb)();
let empty_libs = ComponentLibraryVec::from_const_slice(&[]);
let mut libs = core::mem::replace(&mut self.component_libraries, empty_libs).into_library_owned_vec();
if let Some(existing_lib) = libs.iter_mut().find(|l| l.name.as_str() == library.as_str()) {
let empty_comps = ComponentDefVec::from_const_slice(&[]);
let mut comps = core::mem::replace(&mut existing_lib.components, empty_comps).into_library_owned_vec();
if let Some(ec) = comps.iter_mut().find(|c| c.id.name.as_str() == component.id.name.as_str()) {
*ec = component;
} else {
comps.push(component);
}
existing_lib.components = ComponentDefVec::from_vec(comps);
} else {
libs.push(ComponentLibrary {
name: library,
version: AzString::from_const_str("1.0.0"),
description: AzString::from_const_str(""),
components: ComponentDefVec::from_vec(alloc::vec![component]),
exportable: true,
modifiable: true,
data_models: crate::xml::ComponentDataModelVec::from_const_slice(&[]),
enum_models: crate::xml::ComponentEnumModelVec::from_const_slice(&[]),
});
}
self.component_libraries = ComponentLibraryVec::from_vec(libs);
}
pub fn add_component_library<R: Into<RegisterComponentLibraryFn>>(&mut self, name: AzString, register_fn: R) {
let register_fn = register_fn.into();
let mut library = (register_fn.cb)();
library.name = name;
let empty_libs = ComponentLibraryVec::from_const_slice(&[]);
let mut libs = core::mem::replace(&mut self.component_libraries, empty_libs).into_library_owned_vec();
if let Some(existing) = libs.iter_mut().find(|l| l.name.as_str() == library.name.as_str()) {
*existing = library;
} else {
libs.push(library);
}
self.component_libraries = ComponentLibraryVec::from_vec(libs);
}
pub fn add_route<P: Into<AzString>, L: Into<LayoutCallback>>(&mut self, pattern: P, layout_fn: L) {
let route = Route {
pattern: pattern.into(),
layout_callback: layout_fn.into(),
};
let empty = RouteVec::from_const_slice(&[]);
let mut routes = core::mem::replace(&mut self.routes, empty).into_library_owned_vec();
if let Some(existing) = routes.iter_mut().find(|r| r.pattern.as_str() == route.pattern.as_str()) {
*existing = route;
} else {
routes.push(route);
}
self.routes = RouteVec::from_vec(routes);
}
#[must_use] pub fn match_route_for_path(&self, path: &str) -> Option<(&Route, RouteMatch)> {
for route in self.routes.as_ref() {
if let Some(m) = match_route(route.pattern.as_str(), path) {
return Some((route, m));
}
}
None
}
}
impl Default for AppConfig {
fn default() -> Self {
Self::create()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum AppLogLevel {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ImageDescriptor {
pub format: RawImageFormat,
pub width: usize,
pub height: usize,
pub stride: OptionI32,
pub offset: i32,
pub flags: ImageDescriptorFlags,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ImageDescriptorFlags {
pub is_opaque: bool,
pub allow_mipmaps: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IdNamespace(pub u32);
impl ::core::fmt::Display for IdNamespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IdNamespace({})", self.0)
}
}
impl ::core::fmt::Debug for IdNamespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum RawImageFormat {
R8,
RG8,
RGB8,
RGBA8,
R16,
RG16,
RGB16,
RGBA16,
BGR8,
BGRA8,
RGBF32,
RGBAF32,
}
static IMAGE_KEY: AtomicU64 = AtomicU64::new(1);
static FONT_KEY: AtomicU64 = AtomicU64::new(0);
static FONT_INSTANCE_KEY: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ImageKey {
pub namespace: IdNamespace,
pub key: u64,
}
impl ImageKey {
pub const DUMMY: Self = Self {
namespace: IdNamespace(0),
key: 0,
};
pub fn unique(render_api_namespace: IdNamespace) -> Self {
Self {
namespace: render_api_namespace,
key: IMAGE_KEY.fetch_add(1, AtomicOrdering::SeqCst),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontKey {
pub namespace: IdNamespace,
pub key: u64,
}
impl FontKey {
pub fn unique(render_api_namespace: IdNamespace) -> Self {
Self {
namespace: render_api_namespace,
key: FONT_KEY.fetch_add(1, AtomicOrdering::SeqCst),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontInstanceKey {
pub namespace: IdNamespace,
pub key: u64,
}
impl FontInstanceKey {
pub fn unique(render_api_namespace: IdNamespace) -> Self {
Self {
namespace: render_api_namespace,
key: FONT_INSTANCE_KEY.fetch_add(1, AtomicOrdering::SeqCst),
}
}
}
#[derive(Debug)]
pub enum DecodedImage {
NullImage {
width: usize,
height: usize,
format: RawImageFormat,
tag: Vec<u8>,
},
Gl(Texture),
Raw((ImageDescriptor, ImageData)),
Callback(CoreImageCallback),
}
#[derive(Debug)]
#[repr(C)]
pub struct ImageRef {
pub data: *const DecodedImage,
pub copies: *const AtomicUsize,
pub id: u64,
pub run_destructor: bool,
}
static IMAGE_REF_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
#[must_use]
fn next_image_ref_id() -> u64 {
IMAGE_REF_ID_COUNTER.fetch_add(1, AtomicOrdering::SeqCst)
}
impl ImageRef {
#[must_use] pub const fn get_hash(&self) -> ImageRefHash {
image_ref_get_hash(self)
}
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Ord, Eq)]
#[repr(C)]
pub struct ImageRefHash {
pub inner: u64,
}
impl_option!(
ImageRef,
OptionImageRef,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl ImageRef {
#[must_use] pub fn into_inner(self) -> Option<DecodedImage> {
unsafe {
if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
let data = Box::from_raw(self.data.cast_mut());
drop(Box::from_raw(self.copies.cast_mut()));
core::mem::forget(self); Some(*data)
} else {
None
}
}
}
#[must_use] pub const fn get_data(&self) -> &DecodedImage {
unsafe { &*self.data }
}
#[must_use] pub fn get_image_callback(&self) -> Option<&CoreImageCallback> {
if unsafe { self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) != Some(1) } {
return None; }
match unsafe { &*self.data } {
DecodedImage::Callback(gl_texture_callback) => Some(gl_texture_callback),
_ => None,
}
}
pub fn get_image_callback_mut(&mut self) -> Option<&mut CoreImageCallback> {
if unsafe { self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) != Some(1) } {
return None; }
match unsafe { &mut *self.data.cast_mut() } {
DecodedImage::Callback(gl_texture_callback) => Some(gl_texture_callback),
_ => None,
}
}
#[must_use] pub fn deep_copy(&self) -> Self {
let new_data = match self.get_data() {
DecodedImage::NullImage {
width,
height,
format,
tag,
} => DecodedImage::NullImage {
width: *width,
height: *height,
format: *format,
tag: tag.clone(),
},
DecodedImage::Gl(tex) => DecodedImage::NullImage {
width: tex.size.width as usize,
height: tex.size.height as usize,
format: tex.format,
tag: Vec::new(),
},
DecodedImage::Raw((descriptor, data)) => {
DecodedImage::Raw((*descriptor, data.clone()))
}
DecodedImage::Callback(cb) => DecodedImage::Callback(cb.clone()),
};
Self::new(new_data)
}
#[must_use] pub const fn is_null_image(&self) -> bool {
matches!(self.get_data(), DecodedImage::NullImage { .. })
}
#[must_use] pub const fn is_gl_texture(&self) -> bool {
matches!(self.get_data(), DecodedImage::Gl(_))
}
#[must_use] pub const fn is_raw_image(&self) -> bool {
matches!(self.get_data(), DecodedImage::Raw((_, _)))
}
#[must_use] pub const fn is_callback(&self) -> bool {
matches!(self.get_data(), DecodedImage::Callback(_))
}
#[must_use] pub fn get_rawimage(&self) -> Option<RawImage> {
match self.get_data() {
DecodedImage::Raw((image_descriptor, image_data)) => Some(RawImage {
pixels: match image_data {
ImageData::Raw(shared_data) => {
let data_clone = shared_data.clone();
data_clone.into_inner().map_or_else(|| RawImageData::U8(shared_data.as_ref().to_vec().into()), RawImageData::U8)
}
ImageData::External(_) => return None,
},
width: image_descriptor.width,
height: image_descriptor.height,
premultiplied_alpha: true,
data_format: image_descriptor.format,
tag: Vec::new().into(),
}),
_ => None,
}
}
#[must_use] pub fn get_bytes(&self) -> Option<&[u8]> {
match self.get_data() {
DecodedImage::Raw((_, image_data)) => match image_data {
ImageData::Raw(shared_data) => Some(shared_data.as_ref()),
ImageData::External(_) => None,
},
_ => None,
}
}
#[must_use] pub fn get_bytes_ptr(&self) -> *const u8 {
match self.get_data() {
DecodedImage::Raw((_, image_data)) => match image_data {
ImageData::Raw(shared_data) => shared_data.as_ptr(),
ImageData::External(_) => core::ptr::null(),
},
_ => core::ptr::null(),
}
}
#[allow(clippy::cast_precision_loss)] #[must_use] pub const fn get_size(&self) -> LogicalSize {
match self.get_data() {
DecodedImage::NullImage { width, height, .. } => {
LogicalSize::new(*width as f32, *height as f32)
}
DecodedImage::Gl(tex) => {
LogicalSize::new(tex.size.width as f32, tex.size.height as f32)
}
DecodedImage::Raw((image_descriptor, _)) => LogicalSize::new(
image_descriptor.width as f32,
image_descriptor.height as f32,
),
DecodedImage::Callback(_) => LogicalSize::new(0.0, 0.0),
}
}
#[must_use] pub fn null_image(width: usize, height: usize, format: RawImageFormat, tag: Vec<u8>) -> Self {
Self::new(DecodedImage::NullImage {
width,
height,
format,
tag,
})
}
pub fn callback<C: Into<CoreRenderImageCallback>>(callback: C, data: RefAny) -> Self {
Self::new(DecodedImage::Callback(CoreImageCallback {
callback: callback.into(),
refany: data,
}))
}
#[must_use] pub fn new_rawimage(image_data: RawImage) -> Option<Self> {
let (image_data, image_descriptor) = image_data.into_loaded_image_source()?;
Some(Self::new(DecodedImage::Raw((image_descriptor, image_data))))
}
#[must_use] pub fn new_gltexture(texture: Texture) -> Self {
Self::new(DecodedImage::Gl(texture))
}
fn new(data: DecodedImage) -> Self {
Self {
data: Box::into_raw(Box::new(data)),
copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
id: next_image_ref_id(),
run_destructor: true,
}
}
}
unsafe impl Send for ImageRef {}
unsafe impl Sync for ImageRef {}
impl PartialEq for ImageRef {
fn eq(&self, rhs: &Self) -> bool {
self.id == rhs.id
}
}
impl PartialOrd for ImageRef {
fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
Some(self.id.cmp(&other.id))
}
}
impl Ord for ImageRef {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl Eq for ImageRef {}
impl Hash for ImageRef {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.id.hash(state);
}
}
impl Clone for ImageRef {
fn clone(&self) -> Self {
unsafe {
self.copies
.as_ref()
.map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
}
Self {
data: self.data, copies: self.copies, id: self.id, run_destructor: true,
}
}
}
impl Drop for ImageRef {
fn drop(&mut self) {
self.run_destructor = false;
unsafe {
let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
if copies == 1 {
drop(Box::from_raw(self.data.cast_mut()));
drop(Box::from_raw(self.copies.cast_mut()));
}
}
}
}
#[must_use] pub const fn image_ref_get_hash(ir: &ImageRef) -> ImageRefHash {
ImageRefHash {
inner: ir.id,
}
}
#[must_use] pub const fn image_ref_hash_to_image_key(hash: ImageRefHash, namespace: IdNamespace) -> ImageKey {
ImageKey {
namespace,
key: hash.inner,
}
}
#[must_use] pub fn font_ref_get_hash(fr: &FontRef) -> u64 {
fr.get_hash()
}
#[derive(Debug)]
#[derive(Default)]
pub struct ImageCache {
pub image_id_map: OrderedMap<AzString, ImageRef>,
}
impl ImageCache {
#[must_use] pub fn new() -> Self {
Self::default()
}
pub fn add_css_image_id(&mut self, css_id: AzString, image: ImageRef) {
self.image_id_map.insert(css_id, image);
}
#[must_use] pub fn get_css_image_id(&self, css_id: &AzString) -> Option<&ImageRef> {
self.image_id_map.get(css_id)
}
pub fn delete_css_image_id(&mut self, css_id: &AzString) {
self.image_id_map.remove(css_id);
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ResolvedImage {
pub key: ImageKey,
pub descriptor: ImageDescriptor,
}
pub trait RendererResourcesTrait: fmt::Debug {
fn get_font_family(
&self,
style_font_families_hash: &StyleFontFamiliesHash,
) -> Option<&StyleFontFamilyHash>;
fn get_font_key(&self, style_font_family_hash: &StyleFontFamilyHash) -> Option<&FontKey>;
fn get_registered_font(
&self,
font_key: &FontKey,
) -> Option<&(FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)>;
fn get_image(&self, hash: &ImageRefHash) -> Option<&ResolvedImage>;
fn update_image(
&mut self,
image_ref_hash: &ImageRefHash,
descriptor: ImageDescriptor,
);
}
impl RendererResourcesTrait for RendererResources {
fn get_font_family(
&self,
style_font_families_hash: &StyleFontFamiliesHash,
) -> Option<&StyleFontFamilyHash> {
self.font_families_map.get(style_font_families_hash)
}
fn get_font_key(&self, style_font_family_hash: &StyleFontFamilyHash) -> Option<&FontKey> {
self.font_id_map.get(style_font_family_hash)
}
fn get_registered_font(
&self,
font_key: &FontKey,
) -> Option<&(FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)> {
self.currently_registered_fonts.get(font_key)
}
fn get_image(&self, hash: &ImageRefHash) -> Option<&ResolvedImage> {
self.currently_registered_images.get(hash)
}
fn update_image(
&mut self,
image_ref_hash: &ImageRefHash,
descriptor: ImageDescriptor,
) {
if let Some(s) = self.currently_registered_images.get_mut(image_ref_hash) {
s.descriptor = descriptor;
}
}
}
#[derive(Default)]
pub struct RendererResources {
pub currently_registered_images: OrderedMap<ImageRefHash, ResolvedImage>,
pub image_key_map: OrderedMap<ImageKey, ImageRefHash>,
pub image_last_seen_epoch: OrderedMap<ImageRefHash, u32>,
pub currently_registered_fonts:
OrderedMap<FontKey, (FontRef, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>)>,
pub last_frame_registered_fonts:
OrderedMap<FontKey, OrderedMap<(Au, DpiScaleFactor), FontInstanceKey>>,
pub font_families_map: OrderedMap<StyleFontFamiliesHash, StyleFontFamilyHash>,
pub font_id_map: OrderedMap<StyleFontFamilyHash, FontKey>,
pub font_hash_map: OrderedMap<u64, FontKey>,
}
impl fmt::Debug for RendererResources {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RendererResources {{
currently_registered_images: {:#?},
currently_registered_fonts: {:#?},
font_families_map: {:#?},
font_id_map: {:#?},
}}",
self.currently_registered_images.keys().collect::<Vec<_>>(),
self.currently_registered_fonts.keys().collect::<Vec<_>>(),
self.font_families_map.keys().collect::<Vec<_>>(),
self.font_id_map.keys().collect::<Vec<_>>(),
)
}
}
impl RendererResources {
#[must_use] pub fn get_renderable_font_data(
&self,
font_instance_key: &FontInstanceKey,
) -> Option<(&FontRef, Au, DpiScaleFactor)> {
self.currently_registered_fonts
.iter()
.find_map(|(font_key, (font_ref, instances))| {
instances.iter().find_map(|((au, dpi), instance_key)| {
if *instance_key == *font_instance_key {
Some((font_ref, *au, *dpi))
} else {
None
}
})
})
}
#[allow(clippy::cast_possible_truncation)] pub fn get_font_instance_key_for_text(
&self,
font_size_px: f32,
css_property_cache: &CssPropertyCache,
node_data: &NodeData,
node_id: &NodeId,
styled_node_state: &StyledNodeState,
dpi_scale: f32,
) -> Option<FontInstanceKey> {
let font_size = StyleFontSize {
inner: azul_css::props::basic::PixelValue::const_px(font_size_px as isize),
};
let font_size_au = font_size_to_au(font_size);
let dpi_scale_factor = DpiScaleFactor {
inner: FloatValue::new(dpi_scale),
};
let font_family =
css_property_cache.get_font_id_or_default(node_data, node_id, styled_node_state);
let font_families_hash = StyleFontFamiliesHash::new(font_family.as_ref());
self.get_font_instance_key(&font_families_hash, font_size_au, dpi_scale_factor)
}
#[must_use] pub fn get_font_instance_key(
&self,
font_families_hash: &StyleFontFamiliesHash,
font_size_au: Au,
dpi_scale: DpiScaleFactor,
) -> Option<FontInstanceKey> {
let font_family_hash = self.get_font_family(font_families_hash)?;
let font_key = self.get_font_key(font_family_hash)?;
let (_, instances) = self.get_registered_font(font_key)?;
instances.get(&(font_size_au, dpi_scale)).copied()
}
#[allow(dead_code)]
fn remove_font_families_with_zero_references(&mut self) {
let font_family_to_delete = self
.font_id_map
.iter()
.filter_map(|(font_family, font_key)| {
if self.currently_registered_fonts.contains_key(font_key) {
None
} else {
Some(*font_family)
}
})
.collect::<Vec<_>>();
for f in font_family_to_delete {
self.font_id_map.remove(&f); }
let font_families_to_delete = self
.font_families_map
.iter()
.filter_map(|(font_families, font_family)| {
if self.font_id_map.contains_key(font_family) {
None
} else {
Some(*font_families)
}
})
.collect::<Vec<_>>();
for f in font_families_to_delete {
self.font_families_map.remove(&f); }
}
}
#[derive(Debug, Clone)]
pub struct UpdateImageResult {
pub key_to_update: ImageKey,
pub new_descriptor: ImageDescriptor,
pub new_image_data: ImageData,
}
#[derive(Debug, Default)]
pub struct GlTextureCache {
pub solved_textures:
BTreeMap<DomId, BTreeMap<NodeId, (ImageKey, ImageDescriptor, ExternalImageId)>>,
pub hashes: BTreeMap<(DomId, NodeId, ImageRefHash), ImageRefHash>,
}
unsafe impl Send for GlTextureCache {}
impl GlTextureCache {
#[must_use] pub const fn empty() -> Self {
Self {
solved_textures: BTreeMap::new(),
hashes: BTreeMap::new(),
}
}
pub fn update_texture(
&mut self,
dom_id: DomId,
node_id: NodeId,
document_id: DocumentId,
epoch: Epoch,
new_texture: Texture,
insert_into_active_gl_textures_fn: &GlStoreImageFn,
) -> Option<ExternalImageId> {
let new_descriptor = new_texture.get_descriptor();
let di_map = self.solved_textures.get_mut(&dom_id)?;
let entry = di_map.get_mut(&node_id)?;
entry.1 = new_descriptor;
let external_image_id = texture_external_image_id(dom_id, node_id);
(insert_into_active_gl_textures_fn)(document_id, epoch, new_texture, external_image_id);
entry.2 = external_image_id;
Some(external_image_id)
}
}
macro_rules! unique_id {
($struct_name:ident, $counter_name:ident) => {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[repr(C)]
pub struct $struct_name {
pub id: usize,
}
impl $struct_name {
pub fn unique() -> Self {
Self {
id: $counter_name.fetch_add(1, AtomicOrdering::SeqCst),
}
}
}
};
}
static PROPERTY_KEY_COUNTER: AtomicUsize = AtomicUsize::new(0);
unique_id!(TransformKey, PROPERTY_KEY_COUNTER);
unique_id!(ColorKey, PROPERTY_KEY_COUNTER);
unique_id!(OpacityKey, PROPERTY_KEY_COUNTER);
static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
unique_id!(ImageId, IMAGE_ID_COUNTER);
static FONT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
unique_id!(FontId, FONT_ID_COUNTER);
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C)]
pub struct ImageMask {
pub image: ImageRef,
pub rect: LogicalRect,
pub repeat: bool,
}
impl_option!(
ImageMask,
OptionImageMask,
copy = false,
[Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ImmediateFontId {
Resolved((StyleFontFamilyHash, FontKey)),
Unresolved(StyleFontFamilyVec),
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[repr(C, u8)]
pub enum RawImageData {
U8(U8Vec),
U16(U16Vec),
F32(F32Vec),
}
impl RawImageData {
#[must_use] pub const fn get_u8_vec_ref(&self) -> Option<&U8Vec> {
match self {
Self::U8(v) => Some(v),
_ => None,
}
}
#[must_use] pub const fn get_u16_vec_ref(&self) -> Option<&U16Vec> {
match self {
Self::U16(v) => Some(v),
_ => None,
}
}
#[must_use] pub const fn get_f32_vec_ref(&self) -> Option<&F32Vec> {
match self {
Self::F32(v) => Some(v),
_ => None,
}
}
fn get_u8_vec(self) -> Option<U8Vec> {
match self {
Self::U8(v) => Some(v),
_ => None,
}
}
fn get_u16_vec(self) -> Option<U16Vec> {
match self {
Self::U16(v) => Some(v),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct RawImage {
pub pixels: RawImageData,
pub width: usize,
pub height: usize,
pub premultiplied_alpha: bool,
pub data_format: RawImageFormat,
pub tag: U8Vec,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Brush {
pub color: ColorU,
pub radius: f32,
pub hardness: f32,
pub flow: f32,
pub spacing: f32,
}
impl Brush {
#[must_use] pub const fn new(color: ColorU, radius: f32) -> Self {
Self {
color,
radius,
hardness: 0.5,
flow: 1.0,
spacing: 0.25,
}
}
}
#[allow(clippy::suboptimal_flops)] #[inline]
#[must_use] pub fn brush_dab_coverage(t: f32, hardness: f32) -> f32 {
let edge0 = hardness.clamp(0.0, 1.0);
let denom = (1.0 - edge0).max(1.0e-4);
let x = ((t - edge0) / denom).clamp(0.0, 1.0);
1.0 - (x * x * (3.0 - 2.0 * x))
}
impl RawImage {
#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] #[allow(clippy::cast_possible_wrap)] pub fn paint_dot(&mut self, cx: f32, cy: f32, brush: Brush) {
let r = brush.radius;
#[allow(clippy::neg_cmp_op_on_partial_ord)]
if !(r > 0.0) || self.width == 0 || self.height == 0 {
return;
}
let bgr = match self.data_format {
RawImageFormat::RGBA8 => false,
RawImageFormat::BGRA8 => true,
_ => return,
};
let (w, h) = (self.width as i32, self.height as i32);
let buf: &mut [u8] = match self.pixels {
RawImageData::U8(ref mut v) => v.as_mut(),
_ => return,
};
let flow = brush.flow.clamp(0.0, 1.0) * (f32::from(brush.color.a) / 255.0);
let (cr, cg, cb) = (
f32::from(brush.color.r),
f32::from(brush.color.g),
f32::from(brush.color.b),
);
let x0 = (cx - r).floor().max(0.0) as i32;
let y0 = (cy - r).floor().max(0.0) as i32;
let x1 = ((cx + r).ceil() as i32).min(w);
let y1 = ((cy + r).ceil() as i32).min(h);
for y in y0..y1 {
for x in x0..x1 {
let dx = x as f32 + 0.5 - cx;
let dy = y as f32 + 0.5 - cy;
let dist = dx.hypot(dy);
if dist > r {
continue;
}
let a = brush_dab_coverage(dist / r, brush.hardness) * flow;
if a <= 0.0 {
continue;
}
let idx = ((y * w + x) as usize) * 4;
let (ri, gi, bi, ai) = if bgr {
(idx + 2, idx + 1, idx, idx + 3)
} else {
(idx, idx + 1, idx + 2, idx + 3)
};
let inv = 1.0 - a;
buf[ri] = (cr * a + f32::from(buf[ri]) * inv).round().clamp(0.0, 255.0) as u8;
buf[gi] = (cg * a + f32::from(buf[gi]) * inv).round().clamp(0.0, 255.0) as u8;
buf[bi] = (cb * a + f32::from(buf[bi]) * inv).round().clamp(0.0, 255.0) as u8;
buf[ai] =
((a + (f32::from(buf[ai]) / 255.0) * inv) * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn paint_stroke(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, brush: Brush) {
let dx = x1 - x0;
let dy = y1 - y0;
let len = dx.hypot(dy);
let step = (brush.radius * brush.spacing.max(0.01)).max(0.5);
let n = (len / step).floor() as i32;
if n <= 0 {
self.paint_dot(x1, y1, brush);
return;
}
for i in 0..=n {
let t = i as f32 / n as f32;
self.paint_dot(x0 + dx * t, y0 + dy * t, brush);
}
}
}
#[inline]
#[allow(clippy::cast_possible_truncation)] fn premultiply_alpha(array: &mut [u8]) {
if array.len() != 4 {
return;
}
let a = u32::from(array[3]);
array[0] = (((u32::from(array[0]) * a) + 128) / 255) as u8;
array[1] = (((u32::from(array[1]) * a) + 128) / 255) as u8;
array[2] = (((u32::from(array[2]) * a) + 128) / 255) as u8;
}
#[inline]
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] fn normalize_u16(i: u16) -> u8 {
((f32::from(i) / f32::from(core::u16::MAX)) * f32::from(core::u8::MAX)) as u8
}
const FOUR_BPP: usize = 4;
const TWO_CHANNELS: usize = 2;
const THREE_CHANNELS: usize = 3;
const FOUR_CHANNELS: usize = 4;
impl RawImage {
#[must_use] pub fn null_image() -> Self {
Self {
pixels: RawImageData::U8(Vec::new().into()),
width: 0,
height: 0,
premultiplied_alpha: true,
data_format: RawImageFormat::BGRA8,
tag: Vec::new().into(),
}
}
#[allow(clippy::cast_sign_loss)] #[must_use] pub fn allocate_mask(size: LayoutSize) -> Self {
Self {
pixels: RawImageData::U8(
vec![0; size.width.max(0) as usize * size.height.max(0) as usize].into(),
),
width: size.width as usize,
height: size.height as usize,
premultiplied_alpha: true,
data_format: RawImageFormat::R8,
tag: Vec::new().into(),
}
}
#[must_use] pub fn into_loaded_image_source(self) -> Option<(ImageData, ImageDescriptor)> {
let Self {
width,
height,
pixels,
data_format,
premultiplied_alpha,
tag,
} = self;
let expected_len = width * height;
let (bytes, data_format, is_opaque): (U8Vec, RawImageFormat, bool) = match data_format {
RawImageFormat::R8 => {
let (bytes, is_opaque) = Self::load_r8(pixels, expected_len)?;
(bytes, RawImageFormat::R8, is_opaque)
}
RawImageFormat::RG8 => {
let (bytes, is_opaque) = Self::load_rg8(pixels, expected_len, premultiplied_alpha)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RGB8 => {
let (bytes, is_opaque) = Self::load_rgb8(pixels, expected_len)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RGBA8 => {
let (bytes, is_opaque) = Self::load_rgba8(pixels, expected_len, premultiplied_alpha)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::R16 => {
let (bytes, is_opaque) = Self::load_r16(pixels, expected_len)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RG16 => {
let (bytes, is_opaque) = Self::load_rg16(pixels, expected_len)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RGB16 => {
let (bytes, is_opaque) = Self::load_rgb16(pixels, expected_len)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RGBA16 => {
let (bytes, is_opaque) =
Self::load_rgba16(pixels, expected_len, premultiplied_alpha)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::BGR8 => {
let (bytes, is_opaque) = Self::load_bgr8(pixels, expected_len)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::BGRA8 => {
let (bytes, is_opaque) = Self::load_bgra8(pixels, expected_len, premultiplied_alpha)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RGBF32 => {
let (bytes, is_opaque) = Self::load_rgbf32(pixels, expected_len)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
RawImageFormat::RGBAF32 => {
let (bytes, is_opaque) =
Self::load_rgbaf32(pixels, expected_len, premultiplied_alpha)?;
(bytes, RawImageFormat::BGRA8, is_opaque)
}
};
let image_data = ImageData::Raw(SharedRawImageData::new(bytes));
let image_descriptor = ImageDescriptor {
format: data_format,
width,
height,
offset: 0,
stride: None.into(),
flags: ImageDescriptorFlags {
is_opaque,
allow_mipmaps: true,
},
};
Some((image_data, image_descriptor))
}
fn load_r8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u8_vec()?;
if pixels.len() != expected_len {
return None;
}
Some((pixels, false))
}
fn load_rg8(
pixels: RawImageData,
expected_len: usize,
premultiplied_alpha: bool,
) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u8_vec()?;
if pixels.len() != expected_len * TWO_CHANNELS {
return None;
}
let mut is_opaque = true;
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
let grey = greyalpha[0];
let alpha = greyalpha[1];
if alpha != 255 {
is_opaque = false;
}
px[pixel_index * FOUR_BPP] = grey;
px[(pixel_index * FOUR_BPP) + 1] = grey;
px[(pixel_index * FOUR_BPP) + 2] = grey;
px[(pixel_index * FOUR_BPP) + 3] = alpha;
if !premultiplied_alpha {
premultiply_alpha(
&mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
);
}
}
Some((px.into(), is_opaque))
}
fn load_rgb8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u8_vec()?;
if pixels.len() != expected_len * THREE_CHANNELS {
return None;
}
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
let red = rgb[0];
let green = rgb[1];
let blue = rgb[2];
px[pixel_index * FOUR_BPP] = blue;
px[(pixel_index * FOUR_BPP) + 1] = green;
px[(pixel_index * FOUR_BPP) + 2] = red;
px[(pixel_index * FOUR_BPP) + 3] = 0xff;
}
Some((px.into(), true))
}
fn load_rgba8(
pixels: RawImageData,
expected_len: usize,
premultiplied_alpha: bool,
) -> Option<(U8Vec, bool)> {
let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
if pixels.len() != expected_len * FOUR_CHANNELS {
return None;
}
let mut is_opaque = true;
if premultiplied_alpha {
for rgba in pixels.chunks_exact_mut(4) {
let (r, gba) = rgba.split_first_mut()?;
core::mem::swap(r, gba.get_mut(1)?);
let a = rgba.get_mut(3)?;
if *a != 255 {
is_opaque = false;
}
}
} else {
for rgba in pixels.chunks_exact_mut(4) {
let (r, gba) = rgba.split_first_mut()?;
core::mem::swap(r, gba.get_mut(1)?);
let a = rgba.get_mut(3)?;
if *a != 255 {
is_opaque = false;
}
premultiply_alpha(rgba); }
}
Some((pixels.into(), is_opaque))
}
fn load_r16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u16_vec()?;
if pixels.len() != expected_len {
return None;
}
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, grey_u16) in pixels.as_ref().iter().enumerate() {
let grey_u8 = normalize_u16(*grey_u16);
px[pixel_index * FOUR_BPP] = grey_u8;
px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
px[(pixel_index * FOUR_BPP) + 3] = 0xff;
}
Some((px.into(), true))
}
fn load_rg16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u16_vec()?;
if pixels.len() != expected_len * TWO_CHANNELS {
return None;
}
let mut is_opaque = true;
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, greyalpha) in pixels.as_ref().chunks_exact(TWO_CHANNELS).enumerate() {
let grey_u8 = normalize_u16(greyalpha[0]);
let alpha_u8 = normalize_u16(greyalpha[1]);
if alpha_u8 != 255 {
is_opaque = false;
}
px[pixel_index * FOUR_BPP] = grey_u8;
px[(pixel_index * FOUR_BPP) + 1] = grey_u8;
px[(pixel_index * FOUR_BPP) + 2] = grey_u8;
px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
}
Some((px.into(), is_opaque))
}
fn load_rgb16(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u16_vec()?;
if pixels.len() != expected_len * THREE_CHANNELS {
return None;
}
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
let red_u8 = normalize_u16(rgb[0]);
let green_u8 = normalize_u16(rgb[1]);
let blue_u8 = normalize_u16(rgb[2]);
px[pixel_index * FOUR_BPP] = blue_u8;
px[(pixel_index * FOUR_BPP) + 1] = green_u8;
px[(pixel_index * FOUR_BPP) + 2] = red_u8;
px[(pixel_index * FOUR_BPP) + 3] = 0xff;
}
Some((px.into(), true))
}
fn load_rgba16(
pixels: RawImageData,
expected_len: usize,
premultiplied_alpha: bool,
) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u16_vec()?;
if pixels.len() != expected_len * FOUR_CHANNELS {
return None;
}
let mut is_opaque = true;
let mut px = vec![0; expected_len * FOUR_BPP];
if premultiplied_alpha {
for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
let red_u8 = normalize_u16(rgba[0]);
let green_u8 = normalize_u16(rgba[1]);
let blue_u8 = normalize_u16(rgba[2]);
let alpha_u8 = normalize_u16(rgba[3]);
if alpha_u8 != 255 {
is_opaque = false;
}
px[pixel_index * FOUR_BPP] = blue_u8;
px[(pixel_index * FOUR_BPP) + 1] = green_u8;
px[(pixel_index * FOUR_BPP) + 2] = red_u8;
px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
}
} else {
for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
let red_u8 = normalize_u16(rgba[0]);
let green_u8 = normalize_u16(rgba[1]);
let blue_u8 = normalize_u16(rgba[2]);
let alpha_u8 = normalize_u16(rgba[3]);
if alpha_u8 != 255 {
is_opaque = false;
}
px[pixel_index * FOUR_BPP] = blue_u8;
px[(pixel_index * FOUR_BPP) + 1] = green_u8;
px[(pixel_index * FOUR_BPP) + 2] = red_u8;
px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
premultiply_alpha(
&mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
);
}
}
Some((px.into(), is_opaque))
}
fn load_bgr8(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_u8_vec()?;
if pixels.len() != expected_len * THREE_CHANNELS {
return None;
}
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, bgr) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
let blue = bgr[0];
let green = bgr[1];
let red = bgr[2];
px[pixel_index * FOUR_BPP] = blue;
px[(pixel_index * FOUR_BPP) + 1] = green;
px[(pixel_index * FOUR_BPP) + 2] = red;
px[(pixel_index * FOUR_BPP) + 3] = 0xff;
}
Some((px.into(), true))
}
fn load_bgra8(
pixels: RawImageData,
expected_len: usize,
premultiplied_alpha: bool,
) -> Option<(U8Vec, bool)> {
let mut is_opaque = true;
let bytes: U8Vec = if premultiplied_alpha {
let pixels = pixels.get_u8_vec()?;
if pixels.len() != expected_len * FOUR_BPP {
return None;
}
is_opaque = pixels
.as_ref()
.chunks_exact(FOUR_CHANNELS)
.all(|bgra| bgra[3] == 255);
pixels
} else {
let mut pixels: Vec<u8> = pixels.get_u8_vec()?.into_library_owned_vec();
if pixels.len() != expected_len * FOUR_BPP {
return None;
}
for bgra in pixels.chunks_exact_mut(FOUR_CHANNELS) {
if bgra[3] != 255 {
is_opaque = false;
}
premultiply_alpha(bgra);
}
pixels.into()
};
Some((bytes, is_opaque))
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] #[allow(clippy::needless_pass_by_value)] fn load_rgbf32(pixels: RawImageData, expected_len: usize) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_f32_vec_ref()?;
if pixels.len() != expected_len * THREE_CHANNELS {
return None;
}
let mut px = vec![0; expected_len * FOUR_BPP];
for (pixel_index, rgb) in pixels.as_ref().chunks_exact(THREE_CHANNELS).enumerate() {
let red_u8 = (rgb[0] * 255.0) as u8;
let green_u8 = (rgb[1] * 255.0) as u8;
let blue_u8 = (rgb[2] * 255.0) as u8;
px[pixel_index * FOUR_BPP] = blue_u8;
px[(pixel_index * FOUR_BPP) + 1] = green_u8;
px[(pixel_index * FOUR_BPP) + 2] = red_u8;
px[(pixel_index * FOUR_BPP) + 3] = 0xff;
}
Some((px.into(), true))
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] #[allow(clippy::needless_pass_by_value)] fn load_rgbaf32(
pixels: RawImageData,
expected_len: usize,
premultiplied_alpha: bool,
) -> Option<(U8Vec, bool)> {
let pixels = pixels.get_f32_vec_ref()?;
if pixels.len() != expected_len * FOUR_CHANNELS {
return None;
}
let mut is_opaque = true;
let mut px = vec![0; expected_len * FOUR_BPP];
if premultiplied_alpha {
for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
let red_u8 = (rgba[0] * 255.0) as u8;
let green_u8 = (rgba[1] * 255.0) as u8;
let blue_u8 = (rgba[2] * 255.0) as u8;
let alpha_u8 = (rgba[3] * 255.0) as u8;
if alpha_u8 != 255 {
is_opaque = false;
}
px[pixel_index * FOUR_BPP] = blue_u8;
px[(pixel_index * FOUR_BPP) + 1] = green_u8;
px[(pixel_index * FOUR_BPP) + 2] = red_u8;
px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
}
} else {
for (pixel_index, rgba) in pixels.as_ref().chunks_exact(FOUR_CHANNELS).enumerate() {
let red_u8 = (rgba[0] * 255.0) as u8;
let green_u8 = (rgba[1] * 255.0) as u8;
let blue_u8 = (rgba[2] * 255.0) as u8;
let alpha_u8 = (rgba[3] * 255.0) as u8;
if alpha_u8 != 255 {
is_opaque = false;
}
px[pixel_index * FOUR_BPP] = blue_u8;
px[(pixel_index * FOUR_BPP) + 1] = green_u8;
px[(pixel_index * FOUR_BPP) + 2] = red_u8;
px[(pixel_index * FOUR_BPP) + 3] = alpha_u8;
premultiply_alpha(
&mut px[(pixel_index * FOUR_BPP)..((pixel_index * FOUR_BPP) + FOUR_BPP)],
);
}
}
Some((px.into(), is_opaque))
}
}
impl_option!(
RawImage,
OptionRawImage,
copy = false,
[Debug, Clone, PartialEq, PartialOrd]
);
#[must_use] pub fn font_size_to_au(font_size: StyleFontSize) -> Au {
Au::from_px(font_size.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE))
}
pub type FontInstanceFlags = u32;
pub const FONT_INSTANCE_FLAG_SYNTHETIC_BOLD: u32 = 1 << 1;
pub const FONT_INSTANCE_FLAG_EMBEDDED_BITMAPS: u32 = 1 << 2;
pub const FONT_INSTANCE_FLAG_SUBPIXEL_BGR: u32 = 1 << 3;
pub const FONT_INSTANCE_FLAG_TRANSPOSE: u32 = 1 << 4;
pub const FONT_INSTANCE_FLAG_FLIP_X: u32 = 1 << 5;
pub const FONT_INSTANCE_FLAG_FLIP_Y: u32 = 1 << 6;
pub const FONT_INSTANCE_FLAG_SUBPIXEL_POSITION: u32 = 1 << 7;
pub const FONT_INSTANCE_FLAG_FORCE_GDI: u32 = 1 << 16;
pub const FONT_INSTANCE_FLAG_FONT_SMOOTHING: u32 = 1 << 16;
pub const FONT_INSTANCE_FLAG_FORCE_AUTOHINT: u32 = 1 << 16;
pub const FONT_INSTANCE_FLAG_NO_AUTOHINT: u32 = 1 << 17;
pub const FONT_INSTANCE_FLAG_VERTICAL_LAYOUT: u32 = 1 << 18;
pub const FONT_INSTANCE_FLAG_LCD_VERTICAL: u32 = 1 << 19;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct GlyphOptions {
pub render_mode: FontRenderMode,
pub flags: FontInstanceFlags,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum FontRenderMode {
Mono,
Alpha,
Subpixel,
}
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FontInstancePlatformOptions {
}
#[cfg(target_os = "windows")]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FontInstancePlatformOptions {
pub gamma: u16,
pub contrast: u8,
pub cleartype_level: u8,
}
#[cfg(target_os = "macos")]
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FontInstancePlatformOptions {
pub unused: u32,
}
#[cfg(target_os = "linux")]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FontInstancePlatformOptions {
pub lcd_filter: FontLCDFilter,
pub hinting: FontHinting,
}
#[cfg(any(target_os = "android", target_os = "ios"))]
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FontInstancePlatformOptions {
pub unused: u32,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum FontHinting {
None,
Mono,
Light,
Normal,
LCD,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[derive(Default)]
pub enum FontLCDFilter {
None,
#[default]
Default,
Light,
Legacy,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct FontInstanceOptions {
pub render_mode: FontRenderMode,
pub flags: FontInstanceFlags,
pub bg_color: ColorU,
pub synthetic_italics: SyntheticItalics,
}
impl Default for FontInstanceOptions {
fn default() -> Self {
Self {
render_mode: FontRenderMode::Subpixel,
flags: 0,
bg_color: ColorU::TRANSPARENT,
synthetic_italics: SyntheticItalics::default(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[derive(Default)]
pub struct SyntheticItalics {
pub angle: i16,
}
#[derive(Debug)]
#[repr(C)]
pub struct SharedRawImageData {
pub data: *const U8Vec,
pub copies: *const AtomicUsize,
pub run_destructor: bool,
}
impl SharedRawImageData {
#[must_use] pub fn new(data: U8Vec) -> Self {
Self {
data: Box::into_raw(Box::new(data)),
copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
run_destructor: true,
}
}
#[must_use] pub fn as_ref(&self) -> &[u8] {
unsafe { (*self.data).as_ref() }
}
#[must_use] pub fn get_bytes(&self) -> &[u8] {
self.as_ref()
}
#[must_use] pub fn as_ptr(&self) -> *const u8 {
unsafe { (*self.data).as_ref().as_ptr() }
}
#[must_use] pub const fn len(&self) -> usize {
unsafe { (*self.data).len() }
}
#[must_use] pub const fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use] pub fn into_inner(self) -> Option<U8Vec> {
unsafe {
if self.copies.as_ref().map(|m| m.load(AtomicOrdering::SeqCst)) == Some(1) {
let data = Box::from_raw(self.data.cast_mut());
drop(Box::from_raw(self.copies.cast_mut()));
core::mem::forget(self); Some(*data)
} else {
None
}
}
}
}
unsafe impl Send for SharedRawImageData {}
unsafe impl Sync for SharedRawImageData {}
impl Clone for SharedRawImageData {
fn clone(&self) -> Self {
unsafe {
self.copies
.as_ref()
.map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
}
Self {
data: self.data,
copies: self.copies,
run_destructor: true,
}
}
}
impl Drop for SharedRawImageData {
fn drop(&mut self) {
self.run_destructor = false;
unsafe {
let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
if copies == 1 {
drop(Box::from_raw(self.data.cast_mut()));
drop(Box::from_raw(self.copies.cast_mut()));
}
}
}
}
impl PartialEq for SharedRawImageData {
fn eq(&self, rhs: &Self) -> bool {
core::ptr::eq(self.data, rhs.data)
}
}
impl Eq for SharedRawImageData {}
impl PartialOrd for SharedRawImageData {
fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SharedRawImageData {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
(self.data as usize).cmp(&(other.data as usize))
}
}
impl Hash for SharedRawImageData {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
(self.data as usize).hash(state);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[repr(C, u8)]
pub enum ImageData {
Raw(SharedRawImageData),
External(ExternalImageData),
}
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[repr(C, u8)]
pub enum ExternalImageType {
TextureHandle(ImageBufferKind),
Buffer,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ExternalImageId {
pub inner: u64,
}
static LAST_EXTERNAL_IMAGE_ID: AtomicUsize = AtomicUsize::new(0);
impl Default for ExternalImageId {
fn default() -> Self {
Self::new()
}
}
impl ExternalImageId {
pub fn new() -> Self {
Self {
inner: LAST_EXTERNAL_IMAGE_ID.fetch_add(1, AtomicOrdering::SeqCst) as u64,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C, u8)]
pub enum GlyphOutlineOperation {
MoveTo(OutlineMoveTo),
LineTo(OutlineLineTo),
QuadraticCurveTo(OutlineQuadTo),
CubicCurveTo(OutlineCubicTo),
ClosePath,
}
impl_option!(
GlyphOutlineOperation,
OptionGlyphOutlineOperation,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd]
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct OutlineMoveTo {
pub x: i16,
pub y: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct OutlineLineTo {
pub x: i16,
pub y: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct OutlineQuadTo {
pub ctrl_1_x: i16,
pub ctrl_1_y: i16,
pub end_x: i16,
pub end_y: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
#[repr(C)]
pub struct OutlineCubicTo {
pub ctrl_1_x: i16,
pub ctrl_1_y: i16,
pub ctrl_2_x: i16,
pub ctrl_2_y: i16,
pub end_x: i16,
pub end_y: i16,
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[repr(C)]
pub struct GlyphOutline {
pub operations: GlyphOutlineOperationVec,
}
azul_css::impl_vec!(GlyphOutlineOperation, GlyphOutlineOperationVec, GlyphOutlineOperationVecDestructor, GlyphOutlineOperationVecDestructorType, GlyphOutlineOperationVecSlice, OptionGlyphOutlineOperation);
azul_css::impl_vec_clone!(
GlyphOutlineOperation,
GlyphOutlineOperationVec,
GlyphOutlineOperationVecDestructor
);
azul_css::impl_vec_debug!(GlyphOutlineOperation, GlyphOutlineOperationVec);
azul_css::impl_vec_partialord!(GlyphOutlineOperation, GlyphOutlineOperationVec);
azul_css::impl_vec_partialeq!(GlyphOutlineOperation, GlyphOutlineOperationVec);
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct OwnedGlyphBoundingBox {
pub max_x: i16,
pub max_y: i16,
pub min_x: i16,
pub min_y: i16,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[repr(C)]
pub enum ImageBufferKind {
Texture2D = 0,
TextureRect = 1,
TextureExternal = 2,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct ExternalImageData {
pub id: ExternalImageId,
pub channel_index: u8,
pub image_type: ExternalImageType,
}
pub type TileSize = u16;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
pub enum ImageDirtyRect {
All,
Partial(LayoutRect),
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum ResourceUpdate {
AddFont(AddFont),
DeleteFont(FontKey),
AddFontInstance(AddFontInstance),
DeleteFontInstance(FontInstanceKey),
AddImage(AddImage),
UpdateImage(UpdateImage),
DeleteImage(ImageKey),
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct AddImage {
pub key: ImageKey,
pub descriptor: ImageDescriptor,
pub data: ImageData,
pub tiling: Option<TileSize>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
pub struct UpdateImage {
pub key: ImageKey,
pub descriptor: ImageDescriptor,
pub data: ImageData,
pub dirty_rect: ImageDirtyRect,
}
#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct AddFont {
pub key: FontKey,
pub font: FontRef,
}
impl fmt::Debug for AddFont {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AddFont {{ key: {:?}, font: {:?} }}",
self.key, self.font
)
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct AddFontInstance {
pub key: FontInstanceKey,
pub font_key: FontKey,
pub glyph_size: (Au, DpiScaleFactor),
pub options: Option<FontInstanceOptions>,
pub platform_options: Option<FontInstancePlatformOptions>,
pub variations: Vec<FontVariation>,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
pub struct FontVariation {
pub tag: u32,
pub value: f32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Epoch {
inner: u32,
}
impl fmt::Display for Epoch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl Default for Epoch {
fn default() -> Self {
Self::new()
}
}
impl Epoch {
#[must_use] pub const fn new() -> Self {
Self { inner: 0 }
}
#[must_use] pub const fn from(i: u32) -> Self {
Self { inner: i }
}
#[must_use] pub const fn into_u32(&self) -> u32 {
self.inner
}
pub const fn increment(&mut self) {
use core::u32;
const MAX_ID: u32 = u32::MAX - 1;
*self = match self.inner {
MAX_ID => Self { inner: 0 },
other => Self {
inner: other.saturating_add(1),
},
};
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)]
pub struct Au(pub i32);
pub const AU_PER_PX: i32 = 60;
pub const MAX_AU: i32 = (1 << 30) - 1;
pub const MIN_AU: i32 = -(1 << 30) - 1;
impl Au {
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] #[must_use] pub fn from_px(px: f32) -> Self {
let target_app_units = (px * AU_PER_PX as f32) as i32;
Self(target_app_units.clamp(MIN_AU, MAX_AU))
}
#[allow(clippy::cast_precision_loss)] #[must_use] pub fn into_px(&self) -> f32 {
self.0 as f32 / AU_PER_PX as f32
}
}
#[derive(Debug)]
pub enum AddFontMsg {
Font(FontKey, StyleFontFamilyHash, FontRef),
Instance(AddFontInstance, (Au, DpiScaleFactor)),
}
impl AddFontMsg {
#[must_use] pub fn into_resource_update(&self) -> ResourceUpdate {
use self::AddFontMsg::{Font, Instance};
match self {
Font(font_key, _, font_ref) => ResourceUpdate::AddFont(AddFont {
key: *font_key,
font: font_ref.clone(),
}),
Instance(fi, _) => ResourceUpdate::AddFontInstance(fi.clone()),
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum DeleteFontMsg {
Font(FontKey),
Instance(FontInstanceKey, (Au, DpiScaleFactor)),
}
impl DeleteFontMsg {
#[must_use] pub const fn into_resource_update(&self) -> ResourceUpdate {
use self::DeleteFontMsg::{Font, Instance};
match self {
Font(f) => ResourceUpdate::DeleteFont(*f),
Instance(fi, _) => ResourceUpdate::DeleteFontInstance(*fi),
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct AddImageMsg(pub AddImage);
impl AddImageMsg {
#[must_use] pub fn into_resource_update(&self) -> ResourceUpdate {
ResourceUpdate::AddImage(self.0.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct LoadedFontSource {
pub data: U8Vec,
pub index: u32,
pub load_outlines: bool,
}
pub type LoadFontFn = fn(&StyleFontFamily, &FcFontCache) -> Option<LoadedFontSource>;
pub type ParseFontFn = fn(LoadedFontSource) -> Option<FontRef>;
pub type GlStoreImageFn = fn(DocumentId, Epoch, Texture, ExternalImageId);
#[must_use] pub fn texture_external_image_id(dom_id: DomId, node_id: NodeId) -> ExternalImageId {
let dom = dom_id.inner as u64;
let node = node_id.index() as u64;
debug_assert!(u32::try_from(dom).is_ok(), "DomId exceeds 32-bit range");
debug_assert!(u32::try_from(node).is_ok(), "NodeId exceeds 32-bit range");
ExternalImageId {
inner: (dom << 32) | (node & 0xFFFF_FFFF),
}
}
#[must_use] pub const fn image_ref_hash_to_external_image_id(hash: ImageRefHash) -> ExternalImageId {
ExternalImageId {
inner: hash.inner,
}
}
#[allow(clippy::too_many_lines)] pub fn build_add_font_resource_updates(
renderer_resources: &mut RendererResources,
dpi: DpiScaleFactor,
fc_cache: &FcFontCache,
id_namespace: IdNamespace,
fonts_in_dom: &OrderedMap<ImmediateFontId, FastBTreeSet<Au>>,
font_source_load_fn: LoadFontFn,
parse_font_fn: ParseFontFn,
) -> Vec<(StyleFontFamilyHash, AddFontMsg)> {
let mut resource_updates = Vec::new();
let mut font_instances_added_this_frame = FastBTreeSet::new();
'outer: for (im_font_id, font_sizes) in fonts_in_dom {
macro_rules! insert_font_instances {
($font_family_hash:expr, $font_key:expr, $font_size:expr) => {{
let font_instance_key_exists = renderer_resources
.currently_registered_fonts
.get(&$font_key)
.and_then(|(_, font_instances)| font_instances.get(&($font_size, dpi)))
.is_some()
|| font_instances_added_this_frame.contains(&($font_key, ($font_size, dpi)));
if !font_instance_key_exists {
let font_instance_key = FontInstanceKey::unique(id_namespace);
#[cfg(target_os = "windows")]
let platform_options = FontInstancePlatformOptions {
gamma: 300,
contrast: 100,
cleartype_level: 100,
};
#[cfg(target_os = "linux")]
let platform_options = FontInstancePlatformOptions {
lcd_filter: FontLCDFilter::Default,
hinting: FontHinting::Normal,
};
#[cfg(target_os = "macos")]
let platform_options = FontInstancePlatformOptions::default();
#[cfg(target_arch = "wasm32")]
let platform_options = FontInstancePlatformOptions::default();
#[cfg(any(target_os = "android", target_os = "ios"))]
let platform_options = FontInstancePlatformOptions::default();
let options = FontInstanceOptions {
render_mode: FontRenderMode::Subpixel,
flags: FONT_INSTANCE_FLAG_NO_AUTOHINT,
..Default::default()
};
font_instances_added_this_frame.insert(($font_key, ($font_size, dpi)));
resource_updates.push((
$font_family_hash,
AddFontMsg::Instance(
AddFontInstance {
key: font_instance_key,
font_key: $font_key,
glyph_size: ($font_size, dpi),
options: Some(options),
platform_options: Some(platform_options),
variations: alloc::vec::Vec::new(),
},
($font_size, dpi),
),
));
}
}};
}
match im_font_id {
ImmediateFontId::Resolved((font_family_hash, font_id)) => {
for font_size in font_sizes {
insert_font_instances!(*font_family_hash, *font_id, *font_size);
}
}
ImmediateFontId::Unresolved(style_font_families) => {
let mut font_family_hash = None;
let font_families_hash = StyleFontFamiliesHash::new(style_font_families.as_ref());
'inner: for family in style_font_families.as_ref() {
let current_family_hash = StyleFontFamilyHash::new(family);
if let Some(font_id) = renderer_resources.font_id_map.get(¤t_family_hash)
{
for font_size in font_sizes {
insert_font_instances!(current_family_hash, *font_id, *font_size);
}
continue 'outer;
}
let font_ref = match family {
StyleFontFamily::Ref(r) => r.clone(), other => {
let Some(font_data) = (font_source_load_fn)(other, fc_cache) else {
continue 'inner;
};
match (parse_font_fn)(font_data) {
Some(s) => s,
None => continue 'inner,
}
}
};
font_family_hash = Some((current_family_hash, font_ref));
break 'inner;
}
let (font_family_hash, font_ref) = match font_family_hash {
None => continue 'outer, Some(s) => s,
};
let font_key = FontKey::unique(id_namespace);
let add_font_msg = AddFontMsg::Font(font_key, font_family_hash, font_ref);
renderer_resources
.font_id_map
.insert(font_family_hash, font_key);
renderer_resources
.font_families_map
.insert(font_families_hash, font_family_hash);
resource_updates.push((font_family_hash, add_font_msg));
for font_size in font_sizes {
insert_font_instances!(font_family_hash, font_key, *font_size);
}
}
}
}
resource_updates
}
#[allow(unused_variables)]
pub fn build_add_image_resource_updates(
renderer_resources: &RendererResources,
id_namespace: IdNamespace,
epoch: Epoch,
document_id: &DocumentId,
images_in_dom: &FastBTreeSet<ImageRef>,
insert_into_active_gl_textures: GlStoreImageFn,
) -> Vec<(ImageRefHash, AddImageMsg)> {
images_in_dom
.iter()
.filter_map(|image_ref| {
let image_ref_hash = image_ref_get_hash(image_ref);
if renderer_resources
.currently_registered_images
.contains_key(&image_ref_hash)
{
return None;
}
match image_ref.get_data() {
DecodedImage::Gl(texture) => {
let descriptor = texture.get_descriptor();
let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
let external_image_id = image_ref_hash_to_external_image_id(image_ref_hash);
(insert_into_active_gl_textures)(
*document_id,
epoch,
texture.clone(),
external_image_id,
);
Some((
image_ref_hash,
AddImageMsg(AddImage {
key,
data: ImageData::External(ExternalImageData {
id: external_image_id,
channel_index: 0,
image_type: ExternalImageType::TextureHandle(
ImageBufferKind::Texture2D,
),
}),
descriptor,
tiling: None,
}),
))
}
DecodedImage::Raw((descriptor, data)) => {
let key = image_ref_hash_to_image_key(image_ref_hash, id_namespace);
Some((
image_ref_hash,
AddImageMsg(AddImage {
key,
data: data.clone(), descriptor: *descriptor,
tiling: None,
}),
))
}
DecodedImage::NullImage { .. } | DecodedImage::Callback(_) => None,
}
})
.collect()
}
#[allow(clippy::needless_pass_by_value)] pub fn add_resources(
renderer_resources: &mut RendererResources,
all_resource_updates: &mut Vec<ResourceUpdate>,
add_font_resources: Vec<(StyleFontFamilyHash, AddFontMsg)>,
add_image_resources: Vec<(ImageRefHash, AddImageMsg)>,
) {
all_resource_updates.extend(
add_font_resources
.iter()
.map(|(_, f)| f.into_resource_update()),
);
all_resource_updates.extend(
add_image_resources
.iter()
.map(|(_, i)| i.into_resource_update()),
);
for (image_ref_hash, add_image_msg) in &add_image_resources {
renderer_resources.currently_registered_images.insert(
*image_ref_hash,
ResolvedImage {
key: add_image_msg.0.key,
descriptor: add_image_msg.0.descriptor,
},
);
renderer_resources
.image_key_map
.insert(add_image_msg.0.key, *image_ref_hash);
}
for (_, add_font_msg) in add_font_resources {
use self::AddFontMsg::{Font, Instance};
match add_font_msg {
Font(fk, font_family_hash, font_ref) => {
renderer_resources
.currently_registered_fonts
.entry(fk)
.or_insert_with(|| (font_ref.clone(), OrderedMap::default()));
renderer_resources
.font_hash_map
.insert(font_ref.get_hash(), fk);
}
Instance(fi, size) => {
if let Some((_, instances)) = renderer_resources
.currently_registered_fonts
.get_mut(&fi.font_key)
{
instances.insert(size, fi.key);
}
}
}
}
}
#[cfg(test)]
#[allow(clippy::items_after_statements, clippy::redundant_clone, clippy::cast_possible_truncation, clippy::cast_sign_loss, trivial_casts, clippy::borrow_as_ptr, clippy::cast_ptr_alignment, clippy::unused_self, unused_qualifications, unreachable_pub, private_interfaces)] mod tests {
use super::*;
#[test]
fn normalize_u16_maps_full_range() {
assert_eq!(normalize_u16(0), 0);
assert_eq!(normalize_u16(u16::MAX), 255);
let mid = normalize_u16(u16::MAX / 2);
assert!((126..=128).contains(&mid), "midpoint normalized to {mid}");
assert_eq!(normalize_u16(256), 0);
assert_eq!(normalize_u16(257), 1);
}
#[test]
fn load_bgra8_rejects_wrong_length() {
let short = RawImageData::U8(vec![0u8; 4 * 3].into()); assert!(RawImage::load_bgra8(short, 4, true).is_none());
let ok = RawImageData::U8(vec![255u8; 4 * 4].into()); assert!(RawImage::load_bgra8(ok, 4, true).is_some());
let short2 = RawImageData::U8(vec![0u8; 4 * 2].into());
assert!(RawImage::load_bgra8(short2, 4, false).is_none());
}
#[test]
fn imageref_get_data_reads_backing_box() {
let img = ImageRef::null_image(2, 3, RawImageFormat::RGBA8, vec![7, 8]);
match img.get_data() {
DecodedImage::NullImage { width, height, tag, .. } => {
assert_eq!((*width, *height), (2, 3));
assert_eq!(tag.as_slice(), &[7, 8]);
}
_ => panic!("expected NullImage"),
}
}
#[test]
fn imageref_clone_shares_refcount_and_identity() {
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let c = img.clone();
assert_eq!(img, c); assert!(c.into_inner().is_none());
assert!(img.into_inner().is_some());
}
#[test]
fn imageref_deep_copy_has_distinct_identity() {
let img = ImageRef::null_image(4, 4, RawImageFormat::RGBA8, vec![1]);
let d = img.deep_copy();
assert_ne!(img, d);
drop(img);
assert_eq!(d.get_size().width as usize, 4);
}
#[test]
fn imageref_last_drop_frees_once() {
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let c = img.clone();
drop(img);
drop(c);
}
#[test]
fn imageref_get_callback_none_for_non_callback_and_when_shared() {
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
assert!(img.get_image_callback().is_none());
let c = img.clone();
assert!(img.get_image_callback().is_none()); drop(c);
}
#[test]
fn shared_raw_image_data_read_paths() {
let s = SharedRawImageData::new(vec![10u8, 20, 30].into());
assert_eq!(s.as_ref(), &[10, 20, 30]);
assert_eq!(s.len(), 3);
assert!(!s.is_empty());
assert_eq!(unsafe { *s.as_ptr() }, 10);
assert!(SharedRawImageData::new(Vec::<u8>::new().into()).is_empty());
}
#[test]
fn shared_raw_image_data_clone_shares_alloc() {
let s = SharedRawImageData::new(vec![1u8, 2, 3, 4].into());
let c = s.clone();
assert_eq!(s, c); assert_eq!(s.as_ptr(), c.as_ptr());
assert!(c.into_inner().is_none()); let inner = s.into_inner().expect("sole owner extraction");
assert_eq!(inner.as_ref(), &[1, 2, 3, 4]);
}
#[test]
fn shared_raw_image_data_last_drop_frees_once() {
let s = SharedRawImageData::new(vec![0u8; 8].into());
let c = s.clone();
drop(s);
drop(c);
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::items_after_statements,
clippy::redundant_clone,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_lossless,
clippy::unreadable_literal,
clippy::too_many_lines,
clippy::many_single_char_names,
clippy::similar_names,
unused_qualifications,
unreachable_pub,
private_interfaces
)] mod autotest_generated {
use alloc::string::String;
use super::*;
fn dummy_font_ref() -> FontRef {
static DUMMY_FONT_DATA: u8 = 0;
extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
FontRef::new(
core::ptr::addr_of!(DUMMY_FONT_DATA).cast::<core::ffi::c_void>(),
dummy_destructor,
)
}
fn load_font_none(_: &StyleFontFamily, _: &FcFontCache) -> Option<LoadedFontSource> {
None
}
fn parse_font_none(_: LoadedFontSource) -> Option<FontRef> {
None
}
fn store_gl_texture_noop(_: DocumentId, _: Epoch, _: Texture, _: ExternalImageId) {}
fn test_document_id() -> DocumentId {
DocumentId {
namespace_id: IdNamespace(7),
id: 0,
}
}
fn rgba8_image(w: usize, h: usize) -> RawImage {
RawImage {
pixels: RawImageData::U8(vec![0u8; w * h * 4].into()),
width: w,
height: h,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
}
}
fn opaque_red() -> ColorU {
ColorU {
r: 255,
g: 0,
b: 0,
a: 255,
}
}
#[test]
fn match_route_valid_minimal_positive_control() {
let m = match_route("/user/:id", "/user/42").expect("documented example must match");
assert_eq!(m.pattern.as_str(), "/user/:id");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
let root = match_route("/", "/").expect("root must match root");
assert!(root.params.as_ref().is_empty());
assert!(match_route("/about", "/settings").is_none());
}
#[test]
fn match_route_empty_input_does_not_panic() {
let m = match_route("", "").expect("empty vs empty is a zero-segment match");
assert!(m.params.as_ref().is_empty());
assert!(match_route("", "/").is_some()); assert!(match_route("/", "").is_some());
assert!(match_route("", "/a").is_none()); assert!(match_route("/a", "").is_none());
}
#[test]
fn match_route_whitespace_only_is_not_trimmed() {
assert!(match_route(" ", " ").is_some());
assert!(match_route(" ", "\t\n").is_none());
assert!(match_route("/ ", "/").is_none()); assert!(match_route("/\t\n", "/\t\n").is_some());
}
#[test]
fn match_route_garbage_never_panics() {
for pat in [
"\0\0\0",
"///////",
"::::",
":",
"%%%$#@!",
"\u{feff}",
"/a/../../etc/passwd",
] {
for path in ["", "/", "\0", "/a/b/c", "%%%$#@!", "\u{feff}"] {
let _ = match_route(pat, path);
}
}
assert!(match_route("///////", "/").is_some());
let m = match_route("/:", "/hello").expect("empty param name still matches");
assert_eq!(m.get_param("").map(AzString::as_str), Some("hello"));
}
#[test]
fn match_route_leading_trailing_junk_is_rejected_or_ignored() {
assert!(match_route("/user/:id/", "/user/42").is_some());
assert!(match_route("/user/:id", "/user/42/").is_some());
assert!(match_route(" /about ", "/about").is_none());
assert!(match_route("/about", "/about;garbage").is_none());
}
#[test]
fn match_route_boundary_number_strings_are_opaque_segments() {
for v in [
"0",
"-0",
"9223372036854775807",
"-9223372036854775808",
"1e400",
"NaN",
"inf",
"-inf",
"0.0000000000000000001",
] {
let path = String::from("/user/") + v;
let m = match_route("/user/:id", &path).expect("any segment matches a :param");
assert_eq!(m.get_param("id").map(AzString::as_str), Some(v));
}
}
#[test]
fn match_route_unicode_multibyte_does_not_panic() {
let m = match_route("/user/:id", "/user/\u{1F600}").expect("emoji segment matches");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
let m = match_route("/:é\u{0301}", "/e\u{0301}\u{202E}x").expect("unicode param name");
assert_eq!(
m.get_param("é\u{0301}").map(AzString::as_str),
Some("e\u{0301}\u{202E}x")
);
assert!(match_route("/é", "/e\u{0301}").is_none());
}
#[test]
fn match_route_extremely_long_input_does_not_hang() {
let huge = String::from("/") + &"a".repeat(1_000_000);
assert!(match_route("/x", &huge).is_none());
let m = match_route("/:id", &huge).expect("one long segment is still one segment");
assert_eq!(m.get_param("id").map(|s| s.as_str().len()), Some(1_000_000));
}
#[test]
fn match_route_deeply_nested_input_does_not_stack_overflow() {
let deep = "/a".repeat(10_000);
let m = match_route(&deep, &deep).expect("identical deep paths match");
assert!(m.params.as_ref().is_empty());
let all_params = "/:p".repeat(10_000);
let m = match_route(&all_params, &deep).expect("10k params extract");
assert_eq!(m.params.as_ref().len(), 10_000);
assert_eq!(m.get_param("p").map(AzString::as_str), Some("a"));
let brackets = String::from("/") + &"[".repeat(10_000);
assert!(match_route("/:x", &brackets).is_some());
}
#[test]
fn match_route_segment_count_mismatch_is_none() {
assert!(match_route("/a/:b", "/a").is_none());
assert!(match_route("/a", "/a/b").is_none());
assert!(match_route("/:a/:b/:c", "/1/2").is_none());
}
#[test]
fn route_match_get_param_missing_keys_return_none() {
let empty = RouteMatch {
pattern: AzString::from_const_str("/"),
params: StringPairVec::from_vec(Vec::new()),
};
assert!(empty.get_param("").is_none());
assert!(empty.get_param(" ").is_none());
assert!(empty.get_param("\t\n").is_none());
assert!(empty.get_param("\u{1F600}").is_none());
assert!(empty.get_param("\0").is_none());
assert!(empty.get_param(&"k".repeat(100_000)).is_none());
let m = match_route("/u/:id", "/u/7").expect("valid");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("7"));
assert!(m.get_param("ID").is_none()); assert!(m.get_param("i").is_none()); assert!(m.get_param(":id").is_none()); }
#[test]
fn app_config_match_route_for_path_adversarial_inputs() {
let mut config = AppConfig::create();
let cb: crate::callbacks::LayoutCallbackType = autotest_layout;
extern "C" fn autotest_layout(
_: RefAny,
_: crate::callbacks::LayoutCallbackInfo,
) -> crate::dom::Dom {
crate::dom::Dom::create_body()
}
config.add_route(AzString::from_const_str("/"), cb);
config.add_route(AzString::from_const_str("/user/:id"), cb);
let (route, m) = config
.match_route_for_path("/user/42")
.expect("registered route must match");
assert_eq!(route.pattern.as_str(), "/user/:id");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
assert!(config.match_route_for_path("").is_some());
assert!(config.match_route_for_path("/").is_some());
assert!(config.match_route_for_path("/nope/nope/nope").is_none());
assert!(config.match_route_for_path("\0\0").is_none());
assert!(config.match_route_for_path(" ").is_none());
let long = String::from("/user/") + &"9".repeat(500_000);
assert!(config.match_route_for_path(&long).is_some());
let m = config
.match_route_for_path("/user/\u{1F600}")
.expect("unicode param");
assert_eq!(m.1.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
}
#[test]
fn app_config_add_route_replaces_same_pattern_and_orders_by_insertion() {
extern "C" fn layout_a(_: RefAny, _: crate::callbacks::LayoutCallbackInfo) -> crate::dom::Dom {
crate::dom::Dom::create_body()
}
let cb: crate::callbacks::LayoutCallbackType = layout_a;
let mut config = AppConfig::create();
assert!(config.routes.as_ref().is_empty());
config.add_route(AzString::from_const_str("/dup"), cb);
config.add_route(AzString::from_const_str("/dup"), cb);
assert_eq!(config.routes.as_ref().len(), 1, "same pattern must replace");
let mut config = AppConfig::create();
config.add_route(AzString::from_const_str("/:anything"), cb);
config.add_route(AzString::from_const_str("/about"), cb);
let (route, _) = config.match_route_for_path("/about").expect("matches");
assert_eq!(route.pattern.as_str(), "/:anything");
}
#[test]
fn dpi_scale_factor_new_handles_nan_and_infinities() {
assert_eq!(DpiScaleFactor::new(0.0).inner.get(), 0.0);
assert_eq!(DpiScaleFactor::new(1.0).inner.get(), 1.0);
assert_eq!(DpiScaleFactor::new(f32::NAN).inner.get(), 0.0);
assert!(DpiScaleFactor::new(f32::INFINITY).inner.get().is_finite());
assert!(DpiScaleFactor::new(f32::NEG_INFINITY).inner.get().is_finite());
assert!(DpiScaleFactor::new(f32::MAX).inner.get().is_finite());
assert!(DpiScaleFactor::new(f32::MIN).inner.get().is_finite());
assert_eq!(DpiScaleFactor::new(f32::MIN_POSITIVE).inner.get(), 0.0);
assert_eq!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(1.5));
assert_ne!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(2.0));
}
#[test]
fn named_font_new_keeps_fields_verbatim() {
let f = NamedFont::new(
AzString::from_const_str(""),
U8Vec::from_vec(Vec::new()),
);
assert_eq!(f.name.as_str(), "");
assert!(f.bytes.as_ref().is_empty());
let bytes = vec![0u8, 255, 128];
let f = NamedFont::new(AzString::from(String::from("\u{1F600}")), bytes.clone().into());
assert_eq!(f.name.as_str(), "\u{1F600}");
assert_eq!(f.bytes.as_ref(), bytes.as_slice());
}
#[test]
fn loaded_font_new_keeps_fields_verbatim_at_limits() {
let f = LoadedFont::new(0, AzString::from_const_str(""), 0, false);
assert_eq!(f.font_hash, 0);
assert_eq!(f.num_glyphs, 0);
assert!(!f.has_bytes);
let f = LoadedFont::new(u64::MAX, AzString::from_const_str("x"), u32::MAX, true);
assert_eq!(f.font_hash, u64::MAX);
assert_eq!(f.num_glyphs, u32::MAX);
assert!(f.has_bytes);
}
#[test]
fn brush_new_defaults_and_extreme_radius() {
let b = Brush::new(opaque_red(), 4.0);
assert_eq!(b.radius, 4.0);
assert_eq!(b.hardness, 0.5);
assert_eq!(b.flow, 1.0);
assert_eq!(b.spacing, 0.25);
assert_eq!(b.color, opaque_red());
assert!(Brush::new(opaque_red(), f32::NAN).radius.is_nan());
assert_eq!(Brush::new(opaque_red(), -0.0).radius, -0.0);
assert_eq!(Brush::new(opaque_red(), f32::INFINITY).radius, f32::INFINITY);
}
#[test]
fn image_cache_new_is_empty_and_default_is_neutral() {
let cache = ImageCache::new();
assert!(cache.image_id_map.is_empty());
assert!(ImageCache::default().image_id_map.is_empty());
}
#[test]
fn gl_texture_cache_empty_is_neutral() {
let cache = GlTextureCache::empty();
assert!(cache.solved_textures.is_empty());
assert!(cache.hashes.is_empty());
let d = GlTextureCache::default();
assert!(d.solved_textures.is_empty());
assert!(d.hashes.is_empty());
}
#[test]
fn external_image_id_new_is_monotonic() {
let a = ExternalImageId::new();
let b = ExternalImageId::new();
assert!(b.inner > a.inner, "the counter must strictly increase");
assert!(ExternalImageId::default().inner > b.inner);
}
#[test]
fn shared_raw_image_data_new_invariants() {
let empty = SharedRawImageData::new(U8Vec::from_vec(Vec::new()));
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
assert!(empty.as_ref().is_empty());
assert!(empty.get_bytes().is_empty());
assert!(!empty.as_ptr().is_null());
let big = SharedRawImageData::new(vec![7u8; 100_000].into());
assert_eq!(big.len(), 100_000);
assert!(!big.is_empty());
assert_eq!(big.as_ref().len(), big.len());
assert_eq!(big.get_bytes(), big.as_ref());
assert_eq!(big.into_inner().expect("sole owner").as_ref().len(), 100_000);
}
#[test]
fn app_config_create_registers_builtins_and_defaults() {
let config = AppConfig::create();
assert_eq!(config.log_level, AppLogLevel::Error);
assert!(!config.enable_visual_panic_hook);
assert!(config.enable_logging_on_panic);
assert_eq!(config.termination_behavior, AppTerminationBehavior::EndProcess);
assert!(config.routes.as_ref().is_empty());
assert!(matches!(
config.mock_css_environment,
OptionCssMockEnvironment::None
));
let libs = config.component_libraries.as_ref();
assert_eq!(libs.len(), 1);
assert_eq!(libs[0].name.as_str(), "builtin");
assert!(!libs[0].components.as_ref().is_empty());
}
#[test]
fn app_config_add_component_library_replaces_same_name() {
let register: crate::xml::RegisterComponentLibraryFnType =
crate::xml::register_builtin_components;
let mut config = AppConfig::create();
let n_builtin = config.component_libraries.as_ref()[0].components.as_ref().len();
config.add_component_library(AzString::from_const_str("builtin"), register);
assert_eq!(config.component_libraries.as_ref().len(), 1);
assert_eq!(
config.component_libraries.as_ref()[0].components.as_ref().len(),
n_builtin
);
config.add_component_library(AzString::from_const_str(""), register);
config.add_component_library(AzString::from_const_str("\u{1F600}"), register);
assert_eq!(config.component_libraries.as_ref().len(), 3);
assert_eq!(config.component_libraries.as_ref()[2].name.as_str(), "\u{1F600}");
}
#[test]
fn app_config_with_mock_environment_sets_the_option() {
let config = AppConfig::create().with_mock_environment(CssMockEnvironment::dark_theme());
match config.mock_css_environment {
OptionCssMockEnvironment::Some(env) => {
assert!(matches!(
env.theme,
azul_css::dynamic_selector::OptionThemeCondition::Some(
azul_css::dynamic_selector::ThemeCondition::Dark
)
));
}
OptionCssMockEnvironment::None => panic!("mock env must be Some"),
}
let config = AppConfig::create()
.with_mock_environment(CssMockEnvironment::linux())
.with_mock_environment(CssMockEnvironment::windows());
match config.mock_css_environment {
OptionCssMockEnvironment::Some(env) => assert!(matches!(
env.os,
azul_css::dynamic_selector::OptionOsCondition::Some(
azul_css::dynamic_selector::OsCondition::Windows
)
)),
OptionCssMockEnvironment::None => panic!("mock env must be Some"),
}
}
#[test]
fn css_mock_environment_presets_only_set_their_own_field() {
use azul_css::dynamic_selector::{
OptionOsCondition, OptionThemeCondition, OsCondition, ThemeCondition,
};
for (mock, os) in [
(CssMockEnvironment::linux(), OsCondition::Linux),
(CssMockEnvironment::windows(), OsCondition::Windows),
(CssMockEnvironment::macos(), OsCondition::MacOS),
] {
assert!(matches!(mock.os, OptionOsCondition::Some(o) if o == os));
assert!(matches!(mock.theme, OptionThemeCondition::None));
assert!(matches!(mock.viewport_width, azul_css::OptionF32::None));
}
assert!(matches!(
CssMockEnvironment::dark_theme().theme,
OptionThemeCondition::Some(ThemeCondition::Dark)
));
assert!(matches!(
CssMockEnvironment::light_theme().theme,
OptionThemeCondition::Some(ThemeCondition::Light)
));
assert!(matches!(
CssMockEnvironment::dark_theme().os,
OptionOsCondition::None
));
}
#[test]
fn css_mock_environment_apply_to_overrides_only_set_fields() {
use azul_css::dynamic_selector::{
BoolCondition, DynamicSelectorContext, OptionOsCondition, OptionThemeCondition,
OsCondition, ThemeCondition,
};
let mut ctx = DynamicSelectorContext::default();
let before_os = ctx.os;
let before_lang = ctx.language.clone();
let before_w = ctx.viewport_width;
CssMockEnvironment::default().apply_to(&mut ctx);
assert_eq!(ctx.os, before_os);
assert_eq!(ctx.language.as_str(), before_lang.as_str());
assert_eq!(ctx.viewport_width, before_w);
let mock = CssMockEnvironment {
os: OptionOsCondition::Some(OsCondition::Windows),
theme: OptionThemeCondition::Some(ThemeCondition::Dark),
language: azul_css::OptionString::Some(AzString::from_const_str("de-DE")),
viewport_width: azul_css::OptionF32::Some(f32::NAN),
viewport_height: azul_css::OptionF32::Some(f32::INFINITY),
prefers_reduced_motion: azul_css::OptionBool::Some(true),
prefers_high_contrast: azul_css::OptionBool::Some(false),
..Default::default()
};
let mut ctx = DynamicSelectorContext::default();
mock.apply_to(&mut ctx);
assert_eq!(ctx.os, OsCondition::Windows);
assert_eq!(ctx.theme, ThemeCondition::Dark);
assert_eq!(ctx.language.as_str(), "de-DE");
assert!(ctx.viewport_width.is_nan());
assert_eq!(ctx.viewport_height, f32::INFINITY);
assert_eq!(ctx.prefers_reduced_motion, BoolCondition::True);
assert_eq!(ctx.prefers_high_contrast, BoolCondition::False);
let mut ctx2 = ctx.clone();
mock.apply_to(&mut ctx2);
assert_eq!(ctx2.os, ctx.os);
assert_eq!(ctx2.theme, ctx.theme);
}
#[test]
fn brush_dab_coverage_boundaries_and_monotonicity() {
assert_eq!(brush_dab_coverage(0.0, 0.5), 1.0);
assert_eq!(brush_dab_coverage(1.0, 0.5), 0.0);
assert_eq!(brush_dab_coverage(-5.0, 0.5), 1.0);
assert_eq!(brush_dab_coverage(2.0, 0.5), 0.0);
assert_eq!(brush_dab_coverage(f32::INFINITY, 0.5), 0.0);
assert_eq!(brush_dab_coverage(f32::NEG_INFINITY, 0.5), 1.0);
let mut prev = f32::INFINITY;
for i in 0..=100 {
let t = i as f32 / 100.0;
let c = brush_dab_coverage(t, 0.5);
assert!((0.0..=1.0).contains(&c), "coverage {c} out of range at t={t}");
assert!(c <= prev + 1.0e-6, "not monotonic at t={t}");
prev = c;
}
}
#[test]
fn brush_dab_coverage_hardness_limits_never_divide_by_zero() {
assert_eq!(brush_dab_coverage(0.5, 1.0), 1.0);
assert!(brush_dab_coverage(1.0, 1.0).is_finite());
assert_eq!(brush_dab_coverage(1.0, 1.0), 1.0); assert_eq!(brush_dab_coverage(2.0, 1.0), 0.0);
assert_eq!(brush_dab_coverage(0.5, -10.0), brush_dab_coverage(0.5, 0.0));
assert_eq!(brush_dab_coverage(0.5, 10.0), brush_dab_coverage(0.5, 1.0));
assert_eq!(
brush_dab_coverage(0.5, f32::NEG_INFINITY),
brush_dab_coverage(0.5, 0.0)
);
assert!(brush_dab_coverage(0.5, f32::INFINITY).is_finite());
}
#[test]
fn brush_dab_coverage_nan_propagates_without_panicking() {
assert!(brush_dab_coverage(f32::NAN, 0.5).is_nan());
assert!(brush_dab_coverage(0.5, f32::NAN).is_nan());
assert!(brush_dab_coverage(f32::NAN, f32::NAN).is_nan());
}
#[test]
fn normalize_u16_is_monotonic_and_saturating() {
assert_eq!(normalize_u16(u16::MIN), 0);
assert_eq!(normalize_u16(u16::MAX), u8::MAX);
let mut prev = 0u8;
for i in (0..=u16::MAX).step_by(97) {
let v = normalize_u16(i);
assert!(v >= prev, "normalize_u16 must be monotonic ({i} -> {v})");
prev = v;
}
assert!(normalize_u16(u16::MAX - 1) <= u8::MAX);
}
#[test]
fn premultiply_alpha_ignores_non_4_byte_slices() {
for len in [0usize, 1, 2, 3, 5, 8] {
let mut buf = vec![200u8; len];
let before = buf.clone();
premultiply_alpha(&mut buf);
assert_eq!(buf, before, "len {len} must be left untouched");
}
}
#[test]
fn premultiply_alpha_boundary_values_never_overflow() {
let mut opaque = [255u8, 128, 0, 255];
premultiply_alpha(&mut opaque);
assert_eq!(opaque, [255, 128, 0, 255]);
let mut transparent = [255u8, 255, 255, 0];
premultiply_alpha(&mut transparent);
assert_eq!(transparent, [0, 0, 0, 0]);
let mut half = [255u8, 128, 0, 128];
premultiply_alpha(&mut half);
assert_eq!(half, [128, 64, 0, 128]);
let mut max = [255u8, 255, 255, 255];
premultiply_alpha(&mut max);
assert_eq!(max, [255, 255, 255, 255]);
}
#[test]
fn au_from_px_saturates_at_limits_and_nan() {
assert_eq!(Au::from_px(0.0).0, 0);
assert_eq!(Au::from_px(-0.0).0, 0);
assert_eq!(Au::from_px(1.0).0, AU_PER_PX);
assert_eq!(Au::from_px(-1.0).0, -AU_PER_PX);
assert_eq!(Au::from_px(f32::NAN).0, 0);
assert_eq!(Au::from_px(f32::INFINITY).0, MAX_AU);
assert_eq!(Au::from_px(f32::NEG_INFINITY).0, MIN_AU);
assert_eq!(Au::from_px(f32::MAX).0, MAX_AU);
assert_eq!(Au::from_px(f32::MIN).0, MIN_AU);
for px in [-1.0e9_f32, -1.0, 0.5, 16.0, 1.0e9] {
let au = Au::from_px(px).0;
assert!((MIN_AU..=MAX_AU).contains(&au), "{px} -> {au} escaped the clamp");
}
}
#[test]
fn au_px_round_trip_is_stable() {
for px in [0.0_f32, 0.5, 1.0, 12.0, 16.0, 72.5, -3.25, 1000.0] {
let back = Au::from_px(px).into_px();
assert!(
(back - px).abs() <= 1.0 / AU_PER_PX as f32,
"{px} round-tripped to {back}"
);
}
assert_eq!(Au::from_px(16.0).into_px(), 16.0);
assert!(Au(MAX_AU).into_px().is_finite());
assert!(Au(MIN_AU).into_px().is_finite());
assert!(Au(i32::MIN).into_px().is_finite());
assert!(Au(i32::MAX).into_px().is_finite());
}
#[test]
fn font_size_to_au_zero_negative_and_typical() {
use azul_css::props::basic::PixelValue;
let au = |px: isize| {
font_size_to_au(StyleFontSize {
inner: PixelValue::const_px(px),
})
.0
};
assert_eq!(au(0), 0);
assert_eq!(au(16), 16 * AU_PER_PX);
assert_eq!(au(-10), -10 * AU_PER_PX);
assert!((MIN_AU..=MAX_AU).contains(&au(1_000_000)));
assert!((MIN_AU..=MAX_AU).contains(&au(-1_000_000)));
}
#[test]
fn epoch_new_from_and_into_u32() {
assert_eq!(Epoch::new().into_u32(), 0);
assert_eq!(Epoch::default().into_u32(), 0);
assert_eq!(Epoch::from(0).into_u32(), 0);
assert_eq!(Epoch::from(1).into_u32(), 1);
assert_eq!(Epoch::from(u32::MAX).into_u32(), u32::MAX);
assert_eq!(Epoch::from(u32::MAX - 1).into_u32(), u32::MAX - 1);
}
#[test]
fn epoch_increment_wraps_at_max_minus_one_and_never_reaches_max() {
let mut e = Epoch::new();
e.increment();
assert_eq!(e.into_u32(), 1);
let mut e = Epoch::from(u32::MAX - 1);
e.increment();
assert_eq!(e.into_u32(), 0, "MAX-1 must wrap to 0, never to u32::MAX");
let mut e = Epoch::from(u32::MAX);
e.increment();
assert_eq!(e.into_u32(), u32::MAX);
let mut e = Epoch::from(u32::MAX - 3);
for _ in 0..8 {
e.increment();
assert_ne!(e.into_u32(), u32::MAX);
}
}
#[test]
fn epoch_display_is_non_empty_for_edge_values() {
assert_eq!(alloc::format!("{}", Epoch::new()), "0");
assert_eq!(alloc::format!("{}", Epoch::from(42)), "42");
assert_eq!(
alloc::format!("{}", Epoch::from(u32::MAX)),
alloc::format!("{}", u32::MAX)
);
assert!(!alloc::format!("{:?}", Epoch::default()).is_empty());
}
#[test]
fn id_namespace_display_and_debug_are_well_formed() {
assert_eq!(alloc::format!("{}", IdNamespace(0)), "IdNamespace(0)");
assert_eq!(
alloc::format!("{}", IdNamespace(u32::MAX)),
alloc::format!("IdNamespace({})", u32::MAX)
);
assert_eq!(
alloc::format!("{:?}", IdNamespace(7)),
alloc::format!("{}", IdNamespace(7))
);
}
#[test]
fn unique_keys_are_strictly_increasing_and_keep_their_namespace() {
let ns = IdNamespace(u32::MAX);
let a = ImageKey::unique(ns);
let b = ImageKey::unique(ns);
assert_eq!(a.namespace, ns);
assert!(b.key > a.key, "ImageKey counter must strictly increase");
assert_eq!(ImageKey::DUMMY.key, 0);
assert_ne!(a, ImageKey::DUMMY);
let a = FontKey::unique(ns);
let b = FontKey::unique(ns);
assert_eq!(a.namespace, ns);
assert!(b.key > a.key);
let a = FontInstanceKey::unique(IdNamespace(0));
let b = FontInstanceKey::unique(IdNamespace(0));
assert_eq!(a.namespace, IdNamespace(0));
assert!(b.key > a.key);
}
#[test]
fn image_ref_id_counter_is_monotonic_and_never_zero() {
let a = next_image_ref_id();
let b = next_image_ref_id();
assert!(a > 0 && b > a);
}
#[test]
fn image_ref_hash_conversions_are_lossless_and_agree() {
let img = ImageRef::null_image(1, 1, RawImageFormat::RGBA8, Vec::new());
let hash = img.get_hash();
assert_eq!(hash, image_ref_get_hash(&img));
let key = image_ref_hash_to_image_key(hash, IdNamespace(9));
assert_eq!(key.namespace, IdNamespace(9));
assert_eq!(key.key, hash.inner, "the u64 id must survive verbatim");
let ext = image_ref_hash_to_external_image_id(hash);
assert_eq!(ext.inner, hash.inner);
for inner in [0u64, 1, u64::MAX, u64::MAX - 1] {
let h = ImageRefHash { inner };
assert_eq!(image_ref_hash_to_image_key(h, IdNamespace(0)).key, inner);
assert_eq!(image_ref_hash_to_external_image_id(h).inner, inner);
}
}
#[test]
fn texture_external_image_id_is_deterministic_and_collision_free() {
let id = |d: usize, n: usize| texture_external_image_id(DomId { inner: d }, NodeId::new(n));
assert_eq!(id(3, 7), id(3, 7));
assert_eq!(id(0, 0).inner, 0);
assert_eq!(id(1, 2).inner, (1u64 << 32) | 2);
assert_ne!(id(0, 1), id(1, 0));
assert_eq!(id(0, u32::MAX as usize).inner, u64::from(u32::MAX));
assert_eq!(
id(u32::MAX as usize, 0).inner,
u64::from(u32::MAX) << 32
);
}
#[test]
fn raw_image_data_typed_getters_only_match_their_own_variant() {
let u8v = RawImageData::U8(vec![1u8, 2].into());
let u16v = RawImageData::U16(vec![1u16, 2].into());
let f32v = RawImageData::F32(vec![1.0f32, 2.0].into());
assert_eq!(u8v.get_u8_vec_ref().map(|v| v.len()), Some(2));
assert!(u8v.get_u16_vec_ref().is_none());
assert!(u8v.get_f32_vec_ref().is_none());
assert!(u16v.get_u8_vec_ref().is_none());
assert_eq!(u16v.get_u16_vec_ref().map(|v| v.len()), Some(2));
assert!(u16v.get_f32_vec_ref().is_none());
assert!(f32v.get_u8_vec_ref().is_none());
assert!(f32v.get_u16_vec_ref().is_none());
assert_eq!(f32v.get_f32_vec_ref().map(|v| v.len()), Some(2));
let empty = RawImageData::U8(U8Vec::from_vec(Vec::new()));
assert_eq!(empty.get_u8_vec_ref().map(|v| v.len()), Some(0));
assert!(RawImageData::U8(vec![9u8].into()).get_u8_vec().is_some());
assert!(RawImageData::U16(vec![9u16].into()).get_u8_vec().is_none());
assert!(RawImageData::U16(vec![9u16].into()).get_u16_vec().is_some());
assert!(RawImageData::F32(vec![9.0f32].into()).get_u16_vec().is_none());
}
#[test]
fn load_fns_reject_wrong_payload_type() {
let u16_1px = || RawImageData::U16(vec![0u16; 4].into());
let f32_1px = || RawImageData::F32(vec![0.0f32; 4].into());
let u8_1px = || RawImageData::U8(vec![0u8; 4].into());
assert!(RawImage::load_r8(u16_1px(), 4).is_none());
assert!(RawImage::load_rg8(f32_1px(), 2, true).is_none());
assert!(RawImage::load_rgb8(u16_1px(), 1).is_none());
assert!(RawImage::load_rgba8(f32_1px(), 1, true).is_none());
assert!(RawImage::load_r16(u8_1px(), 4).is_none());
assert!(RawImage::load_rg16(f32_1px(), 2).is_none());
assert!(RawImage::load_rgb16(u8_1px(), 1).is_none());
assert!(RawImage::load_rgba16(u8_1px(), 1, true).is_none());
assert!(RawImage::load_bgr8(u16_1px(), 1).is_none());
assert!(RawImage::load_bgra8(u16_1px(), 1, true).is_none());
assert!(RawImage::load_rgbf32(u8_1px(), 1).is_none());
assert!(RawImage::load_rgbaf32(u16_1px(), 1, true).is_none());
}
#[test]
fn load_fns_reject_every_wrong_length() {
assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 3].into()), 4).is_none());
assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 5].into()), 4).is_none());
assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 3].into()), 2, true).is_none());
assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 5].into()), 2, true).is_none());
assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 7].into()), 2).is_none());
assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 7].into()), 2, true).is_none());
assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 9].into()), 2, false).is_none());
assert!(RawImage::load_r16(RawImageData::U16(vec![0u16; 3].into()), 4).is_none());
assert!(RawImage::load_rg16(RawImageData::U16(vec![0u16; 3].into()), 2).is_none());
assert!(RawImage::load_rgb16(RawImageData::U16(vec![0u16; 5].into()), 2).is_none());
assert!(RawImage::load_rgba16(RawImageData::U16(vec![0u16; 7].into()), 2, true).is_none());
assert!(RawImage::load_bgr8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
assert!(RawImage::load_bgra8(RawImageData::U8(vec![0u8; 7].into()), 2, false).is_none());
assert!(RawImage::load_rgbf32(RawImageData::F32(vec![0.0f32; 5].into()), 2).is_none());
assert!(
RawImage::load_rgbaf32(RawImageData::F32(vec![0.0f32; 7].into()), 2, true).is_none()
);
}
#[test]
fn load_fns_accept_zero_pixels() {
let empty_u8 = || RawImageData::U8(U8Vec::from_vec(Vec::new()));
let empty_u16 = || RawImageData::U16(U16Vec::from_vec(Vec::new()));
let empty_f32 = || RawImageData::F32(F32Vec::from_vec(Vec::new()));
assert_eq!(RawImage::load_r8(empty_u8(), 0).map(|(b, o)| (b.len(), o)), Some((0, false)));
assert_eq!(RawImage::load_rg8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rgb8(empty_u8(), 0).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rgba8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_r16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rg16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rgb16(empty_u16(), 0).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rgba16(empty_u16(), 0, true).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_bgr8(empty_u8(), 0).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_bgra8(empty_u8(), 0, true).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rgbf32(empty_f32(), 0).map(|(b, _)| b.len()), Some(0));
assert_eq!(RawImage::load_rgbaf32(empty_f32(), 0, true).map(|(b, _)| b.len()), Some(0));
}
#[test]
fn load_r8_passes_data_through_and_is_never_opaque() {
let (bytes, is_opaque) =
RawImage::load_r8(RawImageData::U8(vec![0u8, 128, 255, 1].into()), 4)
.expect("exact length");
assert_eq!(bytes.as_ref(), &[0, 128, 255, 1]);
assert!(!is_opaque, "R8 is documented as never opaque");
}
#[test]
fn load_rgb8_and_bgr8_swizzle_to_bgra_opaque() {
let (bytes, is_opaque) =
RawImage::load_rgb8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[3, 2, 1, 255]);
assert!(is_opaque);
let (bytes, is_opaque) =
RawImage::load_bgr8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[1, 2, 3, 255]);
assert!(is_opaque);
}
#[test]
fn load_rgba8_swizzles_and_detects_transparency() {
let (bytes, is_opaque) =
RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 255].into()), 1, true)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[30, 20, 10, 255]);
assert!(is_opaque);
let (_, is_opaque) =
RawImage::load_rgba8(RawImageData::U8(vec![0u8, 0, 0, 254].into()), 1, true)
.expect("1 px");
assert!(!is_opaque);
let (bytes, is_opaque) =
RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 128].into()), 1, false)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[15, 10, 5, 128]);
assert!(!is_opaque);
let (bytes, _) =
RawImage::load_rgba8(RawImageData::U8(vec![255u8, 255, 255, 0].into()), 1, false)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
}
#[test]
fn load_rg8_expands_grey_to_bgra() {
let (bytes, is_opaque) =
RawImage::load_rg8(RawImageData::U8(vec![100u8, 255].into()), 1, true).expect("1 px");
assert_eq!(bytes.as_ref(), &[100, 100, 100, 255]);
assert!(is_opaque);
let (bytes, is_opaque) =
RawImage::load_rg8(RawImageData::U8(vec![100u8, 128].into()), 1, false).expect("1 px");
assert_eq!(bytes.as_ref(), &[50, 50, 50, 128]);
assert!(!is_opaque);
}
#[test]
fn load_16_bit_formats_normalize_to_8_bit() {
let (bytes, is_opaque) =
RawImage::load_r16(RawImageData::U16(vec![u16::MAX].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[255, 255, 255, 255]);
assert!(is_opaque);
let (bytes, is_opaque) =
RawImage::load_rg16(RawImageData::U16(vec![0u16, u16::MAX].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 0, 255]);
assert!(is_opaque);
let (bytes, _) = RawImage::load_rgb16(
RawImageData::U16(vec![u16::MAX, 0, 0].into()),
1,
)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 255, 255]);
let (bytes, is_opaque) = RawImage::load_rgba16(
RawImageData::U16(vec![u16::MAX, u16::MAX, u16::MAX, 0].into()),
1,
false,
)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
assert!(!is_opaque);
}
#[test]
fn load_f32_formats_saturate_on_out_of_range_nan_and_inf() {
let (bytes, is_opaque) = RawImage::load_rgbf32(
RawImageData::F32(vec![2.0f32, -1.0, f32::NAN].into()),
1,
)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 255, 255], "b=NaN->0, g=-1->0, r=2.0->255");
assert!(is_opaque);
let (bytes, is_opaque) = RawImage::load_rgbaf32(
RawImageData::F32(vec![f32::INFINITY, f32::NEG_INFINITY, 0.5, 1.0].into()),
1,
true,
)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[127, 0, 255, 255]);
assert!(is_opaque);
let (_, is_opaque) = RawImage::load_rgbaf32(
RawImageData::F32(vec![1.0f32, 1.0, 1.0, f32::NAN].into()),
1,
true,
)
.expect("1 px");
assert!(!is_opaque);
}
#[test]
fn raw_image_null_image_encodes_to_an_empty_bgra8_descriptor() {
let null = RawImage::null_image();
assert_eq!(null.width, 0);
assert_eq!(null.height, 0);
assert_eq!(null.data_format, RawImageFormat::BGRA8);
assert!(null.premultiplied_alpha);
let (data, descriptor) = null
.into_loaded_image_source()
.expect("a 0x0 image is still a valid (empty) source");
assert_eq!(descriptor.width, 0);
assert_eq!(descriptor.height, 0);
assert_eq!(descriptor.format, RawImageFormat::BGRA8);
assert_eq!(descriptor.offset, 0);
match data {
ImageData::Raw(bytes) => assert!(bytes.is_empty()),
ImageData::External(_) => panic!("a RawImage must never encode to External"),
}
}
#[test]
fn raw_image_allocate_mask_zero_and_negative_sizes() {
let mask = RawImage::allocate_mask(LayoutSize::zero());
assert_eq!(mask.data_format, RawImageFormat::R8);
assert_eq!(mask.width, 0);
assert_eq!(mask.height, 0);
assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
let mask = RawImage::allocate_mask(LayoutSize::new(4, 4));
assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(16));
assert!(mask
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
let mask = RawImage::allocate_mask(LayoutSize::new(-4, 4));
assert_eq!(
mask.pixels.get_u8_vec_ref().map(|v| v.len()),
Some(0),
"a negative extent must never allocate"
);
assert!(mask.width > 1_000_000, "negative width wraps via `as usize`");
}
#[test]
fn raw_image_mask_round_trips_as_r8() {
let mask = RawImage::allocate_mask(LayoutSize::new(2, 2));
let (data, descriptor) = mask.into_loaded_image_source().expect("consistent mask");
assert_eq!(descriptor.format, RawImageFormat::R8);
assert_eq!((descriptor.width, descriptor.height), (2, 2));
assert!(!descriptor.flags.is_opaque, "R8 is never opaque");
match data {
ImageData::Raw(bytes) => assert_eq!(bytes.len(), 4),
ImageData::External(_) => panic!("expected raw bytes"),
}
}
#[test]
fn raw_image_rgba8_encode_decode_round_trip() {
let raw = RawImage {
pixels: RawImageData::U8(vec![10u8, 20, 30, 255].into()),
width: 1,
height: 1,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
let img = ImageRef::new_rawimage(raw).expect("1x1 RGBA8 with 4 bytes is valid");
assert!(img.is_raw_image());
assert!(!img.is_null_image());
assert!(!img.is_gl_texture());
assert!(!img.is_callback());
assert_eq!(img.get_size(), LogicalSize::new(1.0, 1.0));
assert_eq!(img.get_bytes(), Some(&[30u8, 20, 10, 255][..]));
assert!(!img.get_bytes_ptr().is_null());
let decoded = img.get_rawimage().expect("raw image round-trips");
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.data_format, RawImageFormat::BGRA8);
assert!(decoded.premultiplied_alpha);
assert_eq!(
decoded.pixels.get_u8_vec_ref().map(|v| v.as_ref().to_vec()),
Some(vec![30, 20, 10, 255])
);
}
#[test]
fn image_ref_new_rawimage_rejects_dimension_mismatch() {
let too_small = RawImage {
pixels: RawImageData::U8(vec![0u8; 4].into()),
width: 2,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(ImageRef::new_rawimage(too_small).is_none());
let too_big = RawImage {
pixels: RawImageData::U8(vec![0u8; 64].into()),
width: 2,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(ImageRef::new_rawimage(too_big).is_none());
let wrong_type = RawImage {
pixels: RawImageData::U16(vec![0u16; 16].into()),
width: 2,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(ImageRef::new_rawimage(wrong_type).is_none());
}
#[test]
fn image_ref_null_image_predicates_and_accessors() {
let img = ImageRef::null_image(0, 0, RawImageFormat::BGRA8, Vec::new());
assert!(img.is_null_image());
assert!(!img.is_raw_image());
assert!(!img.is_gl_texture());
assert!(!img.is_callback());
assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
assert!(img.get_bytes().is_none());
assert!(img.get_rawimage().is_none());
assert!(img.get_bytes_ptr().is_null());
assert!(img.get_image_callback().is_none());
assert!(matches!(img.get_data(), DecodedImage::NullImage { .. }));
}
#[test]
fn image_ref_null_image_at_usize_max_reports_a_finite_size() {
let img = ImageRef::null_image(usize::MAX, usize::MAX, RawImageFormat::R8, Vec::new());
let size = img.get_size();
assert!(size.width.is_finite() && size.height.is_finite());
assert!(size.width > 0.0 && size.height > 0.0);
assert!(img.is_null_image());
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, vec![9u8; 10_000]);
match img.get_data() {
DecodedImage::NullImage { tag, .. } => assert_eq!(tag.len(), 10_000),
_ => panic!("expected NullImage"),
}
}
#[test]
fn image_ref_hash_identity_rules() {
let a = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let b = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
assert_ne!(a.get_hash(), b.get_hash());
assert_ne!(a, b);
let a2 = a.clone();
assert_eq!(a.get_hash(), a2.get_hash());
assert_eq!(a, a2);
let deep = a.deep_copy();
assert_ne!(a.get_hash(), deep.get_hash());
assert!(deep.is_null_image());
assert_eq!(deep.get_size(), a.get_size());
}
#[test]
fn image_ref_callback_accessors() {
let mut img = ImageRef::callback(0usize, RefAny::new(123u32));
assert!(img.is_callback());
assert!(!img.is_null_image());
assert!(!img.is_raw_image());
assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
assert!(img.get_bytes().is_none());
assert!(img.get_bytes_ptr().is_null());
assert!(img.get_rawimage().is_none());
assert!(img.get_image_callback().is_some());
assert!(img.get_image_callback_mut().is_some());
let clone = img.clone();
assert!(img.get_image_callback().is_none());
assert!(img.get_image_callback_mut().is_none());
drop(clone);
assert!(img.get_image_callback().is_some());
}
#[test]
fn image_ref_deep_copy_of_a_callback_keeps_it_a_callback() {
let img = ImageRef::callback(0usize, RefAny::new(1u8));
let deep = img.deep_copy();
assert!(deep.is_callback());
assert_ne!(img.get_hash(), deep.get_hash());
}
#[test]
fn image_ref_into_inner_only_when_sole_owner() {
let img = ImageRef::null_image(2, 2, RawImageFormat::RGBA8, vec![1, 2, 3]);
let clone = img.clone();
assert!(clone.into_inner().is_none(), "shared -> must refuse");
let inner = img.into_inner().expect("sole owner -> takes ownership");
match inner {
DecodedImage::NullImage {
width,
height,
format,
tag,
} => {
assert_eq!((width, height), (2, 2));
assert_eq!(format, RawImageFormat::RGBA8);
assert_eq!(tag, vec![1, 2, 3]);
}
_ => panic!("expected NullImage"),
}
}
#[test]
fn image_cache_add_get_delete_round_trip() {
let mut cache = ImageCache::new();
let key = AzString::from_const_str("my_image");
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let hash = img.get_hash();
assert!(cache.get_css_image_id(&key).is_none());
cache.add_css_image_id(key.clone(), img);
assert_eq!(cache.get_css_image_id(&key).map(ImageRef::get_hash), Some(hash));
let img2 = ImageRef::null_image(2, 2, RawImageFormat::R8, Vec::new());
let hash2 = img2.get_hash();
cache.add_css_image_id(key.clone(), img2);
assert_eq!(cache.image_id_map.len(), 1);
assert_eq!(cache.get_css_image_id(&key).map(ImageRef::get_hash), Some(hash2));
cache.delete_css_image_id(&key);
assert!(cache.get_css_image_id(&key).is_none());
assert!(cache.image_id_map.is_empty());
cache.delete_css_image_id(&key);
cache.delete_css_image_id(&AzString::from_const_str("never-existed"));
}
#[test]
fn image_cache_handles_empty_and_unicode_keys() {
let mut cache = ImageCache::new();
let empty = AzString::from_const_str("");
let unicode = AzString::from(String::from("\u{1F600}\u{0301}"));
let long = AzString::from("k".repeat(100_000));
cache.add_css_image_id(
empty.clone(),
ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
);
cache.add_css_image_id(
unicode.clone(),
ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
);
cache.add_css_image_id(
long.clone(),
ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
);
assert_eq!(cache.image_id_map.len(), 3);
assert!(cache.get_css_image_id(&empty).is_some());
assert!(cache.get_css_image_id(&unicode).is_some());
assert!(cache.get_css_image_id(&long).is_some());
assert!(cache
.get_css_image_id(&AzString::from_const_str("\u{1F600}"))
.is_none());
}
#[test]
fn renderer_resources_lookups_on_an_empty_registry_are_none() {
let rr = RendererResources::default();
let ns = IdNamespace(1);
assert!(rr
.get_renderable_font_data(&FontInstanceKey::unique(ns))
.is_none());
let families = StyleFontFamiliesHash::new(&[]);
assert!(rr
.get_font_instance_key(&families, Au(0), DpiScaleFactor::new(1.0))
.is_none());
assert!(rr
.get_font_instance_key(&families, Au(MAX_AU), DpiScaleFactor::new(f32::NAN))
.is_none());
assert!(rr.get_image(&ImageRefHash { inner: 0 }).is_none());
assert!(rr.get_font_key(&StyleFontFamilyHash::new(&StyleFontFamily::System(
AzString::from_const_str("Arial")
))).is_none());
}
#[test]
fn renderer_resources_gc_helper_is_a_noop_on_empty_maps() {
let mut rr = RendererResources::default();
rr.remove_font_families_with_zero_references();
assert!(rr.font_id_map.is_empty());
assert!(rr.font_families_map.is_empty());
let family = StyleFontFamily::System(AzString::from_const_str("Arial"));
let family_hash = StyleFontFamilyHash::new(&family);
let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
rr.font_id_map.insert(family_hash, FontKey::unique(IdNamespace(1)));
rr.font_families_map.insert(families_hash, family_hash);
rr.remove_font_families_with_zero_references();
assert!(rr.font_id_map.is_empty(), "dangling font key must be pruned");
assert!(rr.font_families_map.is_empty());
}
#[test]
fn get_font_instance_key_for_text_is_none_on_empty_resources_for_all_sane_sizes() {
let rr = RendererResources::default();
let cache = CssPropertyCache::default();
let node = NodeData::default();
let node_id = NodeId::new(0);
let state = StyledNodeState::default();
for size in [0.0_f32, -0.0, 1.0, -12.0, f32::NAN, 1.0e6, -1.0e6] {
for dpi in [1.0_f32, 0.0, -1.0, f32::NAN, f32::INFINITY] {
assert!(
rr.get_font_instance_key_for_text(size, &cache, &node, &node_id, &state, dpi)
.is_none(),
"size={size} dpi={dpi} must miss cleanly"
);
}
}
}
#[test]
#[ignore = "BUG: font_size_px = inf/f32::MAX overflows `isize * 1000` inside \
PixelValue::const_px -> panics instead of saturating"]
fn bug_get_font_instance_key_for_text_overflow_panics_on_infinite_font_size() {
let rr = RendererResources::default();
let cache = CssPropertyCache::default();
let node = NodeData::default();
let node_id = NodeId::new(0);
let state = StyledNodeState::default();
assert!(rr
.get_font_instance_key_for_text(
f32::INFINITY,
&cache,
&node,
&node_id,
&state,
1.0
)
.is_none());
}
#[test]
fn font_ref_get_hash_is_stable_per_font_and_distinct_across_fonts() {
let a = dummy_font_ref();
let b = dummy_font_ref();
assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a));
assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a.clone()));
assert_ne!(
font_ref_get_hash(&a),
font_ref_get_hash(&b),
"two distinct FontRefs must not share a hash"
);
}
#[test]
fn build_add_font_resource_updates_on_empty_input_is_empty() {
let mut rr = RendererResources::default();
let fonts = OrderedMap::new();
let updates = build_add_font_resource_updates(
&mut rr,
DpiScaleFactor::new(1.0),
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert!(updates.is_empty());
assert!(rr.font_id_map.is_empty());
}
#[test]
fn build_add_font_resource_updates_skips_unloadable_fonts() {
let mut rr = RendererResources::default();
let mut fonts = OrderedMap::new();
let mut sizes = FastBTreeSet::new();
sizes.insert(Au::from_px(16.0));
fonts.insert(
ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![
StyleFontFamily::System(AzString::from_const_str("DoesNotExist")),
])),
sizes,
);
let updates = build_add_font_resource_updates(
&mut rr,
DpiScaleFactor::new(1.0),
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert!(updates.is_empty(), "an unloadable font must add no resources");
assert!(rr.font_id_map.is_empty());
assert!(rr.font_families_map.is_empty());
}
#[test]
fn build_add_font_resource_updates_registers_a_font_and_deduplicates_sizes() {
let mut rr = RendererResources::default();
let font = dummy_font_ref();
let family = StyleFontFamily::Ref(font.clone());
let dpi = DpiScaleFactor::new(1.0);
let mut sizes = FastBTreeSet::new();
sizes.insert(Au::from_px(16.0));
sizes.insert(Au::from_px(24.0));
sizes.insert(Au::from_px(16.0)); assert_eq!(sizes.len(), 2);
let mut fonts = OrderedMap::new();
fonts.insert(
ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![family.clone()])),
sizes,
);
let updates = build_add_font_resource_updates(
&mut rr,
dpi,
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert_eq!(updates.len(), 3);
assert_eq!(
updates
.iter()
.filter(|(_, m)| matches!(m, AddFontMsg::Font(..)))
.count(),
1
);
assert_eq!(
updates
.iter()
.filter(|(_, m)| matches!(m, AddFontMsg::Instance(..)))
.count(),
2
);
assert_eq!(rr.font_id_map.len(), 1);
assert_eq!(rr.font_families_map.len(), 1);
let mut all_updates = Vec::new();
add_resources(&mut rr, &mut all_updates, updates, Vec::new());
assert_eq!(all_updates.len(), 3);
let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
.is_some());
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(24.0), dpi)
.is_some());
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(99.0), dpi)
.is_none());
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(16.0), DpiScaleFactor::new(2.0))
.is_none());
let key = rr
.get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
.expect("registered");
let (font_ref, au, got_dpi) = rr
.get_renderable_font_data(&key)
.expect("registered instance must be renderable");
assert_eq!(font_ref.get_hash(), font.get_hash());
assert_eq!(au, Au::from_px(16.0));
assert_eq!(got_dpi, dpi);
let again = build_add_font_resource_updates(
&mut rr,
dpi,
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert!(again.is_empty(), "already-registered fonts must not be re-added");
}
#[test]
fn add_font_msg_into_resource_update_preserves_keys() {
let font = dummy_font_ref();
let key = FontKey::unique(IdNamespace(3));
let family_hash = StyleFontFamilyHash::new(&StyleFontFamily::Ref(font.clone()));
let msg = AddFontMsg::Font(key, family_hash, font.clone());
match msg.into_resource_update() {
ResourceUpdate::AddFont(add) => {
assert_eq!(add.key, key);
assert_eq!(add.font.get_hash(), font.get_hash());
}
other => panic!("expected AddFont, got {other:?}"),
}
}
#[test]
fn delete_font_msg_into_resource_update_preserves_keys() {
let fk = FontKey::unique(IdNamespace(1));
match DeleteFontMsg::Font(fk).into_resource_update() {
ResourceUpdate::DeleteFont(k) => assert_eq!(k, fk),
other => panic!("expected DeleteFont, got {other:?}"),
}
let fik = FontInstanceKey::unique(IdNamespace(1));
let size = (Au::from_px(16.0), DpiScaleFactor::new(1.0));
match DeleteFontMsg::Instance(fik, size).into_resource_update() {
ResourceUpdate::DeleteFontInstance(k) => assert_eq!(k, fik),
other => panic!("expected DeleteFontInstance, got {other:?}"),
}
}
#[test]
fn add_image_msg_into_resource_update_preserves_the_key_and_descriptor() {
let key = ImageKey::unique(IdNamespace(2));
let descriptor = ImageDescriptor {
format: RawImageFormat::BGRA8,
width: 3,
height: 5,
stride: None.into(),
offset: 0,
flags: ImageDescriptorFlags {
is_opaque: false,
allow_mipmaps: true,
},
};
let msg = AddImageMsg(AddImage {
key,
descriptor,
data: ImageData::Raw(SharedRawImageData::new(vec![0u8; 60].into())),
tiling: None,
});
match msg.into_resource_update() {
ResourceUpdate::AddImage(add) => {
assert_eq!(add.key, key);
assert_eq!(add.descriptor, descriptor);
assert!(add.tiling.is_none());
}
other => panic!("expected AddImage, got {other:?}"),
}
}
#[test]
fn build_add_image_resource_updates_skips_null_and_callback_images() {
let rr = RendererResources::default();
let mut images = FastBTreeSet::new();
images.insert(ImageRef::null_image(4, 4, RawImageFormat::RGBA8, Vec::new()));
images.insert(ImageRef::callback(0usize, RefAny::new(0u8)));
let updates = build_add_image_resource_updates(
&rr,
IdNamespace(1),
Epoch::new(),
&test_document_id(),
&images,
store_gl_texture_noop,
);
assert!(updates.is_empty());
let empty = FastBTreeSet::new();
assert!(build_add_image_resource_updates(
&rr,
IdNamespace(1),
Epoch::new(),
&test_document_id(),
&empty,
store_gl_texture_noop,
)
.is_empty());
}
#[test]
fn build_add_image_resource_updates_then_add_resources_round_trip() {
let mut rr = RendererResources::default();
let img = ImageRef::new_rawimage(rgba8_image(2, 2)).expect("valid 2x2");
let hash = img.get_hash();
let ns = IdNamespace(11);
let mut images = FastBTreeSet::new();
images.insert(img.clone());
let updates = build_add_image_resource_updates(
&rr,
ns,
Epoch::new(),
&test_document_id(),
&images,
store_gl_texture_noop,
);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].0, hash);
assert_eq!(updates[0].1 .0.key, image_ref_hash_to_image_key(hash, ns));
assert_eq!(updates[0].1 .0.descriptor.width, 2);
assert_eq!(updates[0].1 .0.descriptor.height, 2);
let key = updates[0].1 .0.key;
let mut all_updates = Vec::new();
add_resources(&mut rr, &mut all_updates, Vec::new(), updates);
assert_eq!(all_updates.len(), 1);
assert!(matches!(all_updates[0], ResourceUpdate::AddImage(_)));
assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
assert_eq!(rr.image_key_map.get(&key), Some(&hash));
let again = build_add_image_resource_updates(
&rr,
ns,
Epoch::new(),
&test_document_id(),
&images,
store_gl_texture_noop,
);
assert!(again.is_empty());
let new_descriptor = ImageDescriptor {
format: RawImageFormat::BGRA8,
width: 8,
height: 8,
stride: None.into(),
offset: 0,
flags: ImageDescriptorFlags {
is_opaque: true,
allow_mipmaps: true,
},
};
rr.update_image(&hash, new_descriptor);
assert_eq!(rr.get_image(&hash).map(|r| r.descriptor.width), Some(8));
assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
rr.update_image(&ImageRefHash { inner: u64::MAX }, new_descriptor);
}
#[test]
fn add_resources_with_empty_input_changes_nothing() {
let mut rr = RendererResources::default();
let mut updates = Vec::new();
add_resources(&mut rr, &mut updates, Vec::new(), Vec::new());
assert!(updates.is_empty());
assert!(rr.currently_registered_images.is_empty());
assert!(rr.currently_registered_fonts.is_empty());
assert!(rr.image_key_map.is_empty());
}
#[test]
fn paint_dot_composites_at_the_center_and_leaves_far_pixels_alone() {
let mut img = rgba8_image(4, 4);
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (1 * 4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255], "center pixel must be opaque red");
assert_eq!(&px[0..4], &[0, 0, 0, 0], "pixels beyond the radius stay untouched");
}
#[test]
fn paint_dot_honours_bgra_channel_order() {
let mut img = rgba8_image(4, 4);
img.data_format = RawImageFormat::BGRA8;
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (1 * 4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[0, 0, 255, 255]);
}
#[test]
fn paint_dot_rejects_degenerate_radii_and_sizes() {
let untouched = |img: &RawImage| {
img.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0)
};
for r in [0.0_f32, -1.0, -0.0, f32::NAN, f32::NEG_INFINITY] {
let mut img = rgba8_image(4, 4);
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), r));
assert!(untouched(&img), "radius {r} must not paint");
}
let mut img = rgba8_image(0, 0);
img.paint_dot(0.0, 0.0, Brush::new(opaque_red(), 4.0));
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
for format in [
RawImageFormat::R8,
RawImageFormat::RGB8,
RawImageFormat::RGBA16,
RawImageFormat::RGBAF32,
] {
let mut img = rgba8_image(4, 4);
img.data_format = format;
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
assert!(untouched(&img), "format {format:?} must not be painted");
}
}
#[test]
fn paint_dot_with_nan_and_infinite_coordinates_is_a_safe_noop() {
for (cx, cy) in [
(f32::NAN, 2.0_f32),
(2.0, f32::NAN),
(f32::NAN, f32::NAN),
(f32::INFINITY, 2.0),
(f32::NEG_INFINITY, 2.0),
(2.0, f32::INFINITY),
(1.0e30, 1.0e30),
(-1.0e30, -1.0e30),
] {
let mut img = rgba8_image(4, 4);
img.paint_dot(cx, cy, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
assert!(
px.iter().all(|b| *b == 0),
"({cx}, {cy}) must not paint anything"
);
}
}
#[test]
fn paint_dot_alpha_saturates_and_never_overflows() {
let mut img = rgba8_image(4, 4);
let brush = Brush::new(opaque_red(), 2.0);
for _ in 0..50 {
img.paint_dot(2.0, 2.0, brush);
}
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (1 * 4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
let mut img = rgba8_image(4, 4);
let mut nan_brush = Brush::new(opaque_red(), 2.0);
nan_brush.hardness = f32::NAN;
img.paint_dot(2.0, 2.0, nan_brush);
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
}
#[test]
fn paint_dot_zero_flow_and_transparent_color_do_not_paint() {
let mut img = rgba8_image(4, 4);
let mut brush = Brush::new(opaque_red(), 2.0);
brush.flow = 0.0;
img.paint_dot(2.0, 2.0, brush);
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
let mut img = rgba8_image(4, 4);
let transparent = ColorU {
r: 255,
g: 0,
b: 0,
a: 0,
};
img.paint_dot(2.0, 2.0, Brush::new(transparent, 2.0));
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
let mut img = rgba8_image(4, 4);
let mut brush = Brush::new(opaque_red(), 2.0);
brush.flow = -5.0;
img.paint_dot(2.0, 2.0, brush);
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
}
#[test]
fn paint_stroke_paints_both_endpoints() {
let mut img = rgba8_image(8, 8);
img.paint_stroke(1.5, 1.5, 6.5, 6.5, Brush::new(opaque_red(), 1.5));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let alpha_at = |x: usize, y: usize| px[(y * 8 + x) * 4 + 3];
assert!(alpha_at(1, 1) > 0, "start of the stroke must be painted");
assert!(alpha_at(6, 6) > 0, "end of the stroke must be painted");
assert_eq!(alpha_at(7, 0), 0, "off-line pixels stay untouched");
}
#[test]
fn paint_stroke_zero_length_stamps_a_single_dab() {
let mut img = rgba8_image(4, 4);
img.paint_stroke(2.0, 2.0, 2.0, 2.0, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (1 * 4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
}
#[test]
fn paint_stroke_degenerate_brush_params_do_not_divide_by_zero_or_hang() {
for spacing in [0.0_f32, -1.0, f32::NAN] {
let mut img = rgba8_image(8, 8);
let mut brush = Brush::new(opaque_red(), 2.0);
brush.spacing = spacing;
img.paint_stroke(0.0, 0.0, 7.0, 7.0, brush);
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(8 * 8 * 4));
}
let mut img = rgba8_image(4, 4);
img.paint_stroke(f32::NAN, 0.0, 1.0, 1.0, Brush::new(opaque_red(), 1.0));
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
let mut img = rgba8_image(4, 4);
img.paint_stroke(0.0, 0.0, 3.0, 3.0, Brush::new(opaque_red(), 0.0));
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
}
#[test]
#[ignore = "BUG: an infinite stroke length makes n = i32::MAX -> ~2.1 billion \
paint_dot calls (effectively an infinite loop). DO NOT un-ignore \
without a length/step guard in paint_stroke."]
fn bug_paint_stroke_with_infinite_endpoint_loops_2_billion_times() {
let mut img = rgba8_image(4, 4);
img.paint_stroke(0.0, 0.0, f32::INFINITY, 0.0, Brush::new(opaque_red(), 2.0));
}
#[test]
#[ignore = "BUG: paint_dot trusts self.width/height over the pixel buffer -> \
index-out-of-bounds panic when they disagree"]
fn bug_paint_dot_indexes_out_of_bounds_when_dims_exceed_the_buffer() {
let mut img = RawImage {
pixels: RawImageData::U8(vec![0u8; 4].into()),
width: 100,
height: 100,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
img.paint_dot(50.0, 50.0, Brush::new(opaque_red(), 4.0));
}
#[test]
#[ignore = "BUG: into_loaded_image_source computes `width * height` (and then \
`expected_len * channels`) without checked arithmetic -> overflow \
panic instead of the documented None"]
fn bug_into_loaded_image_source_overflows_on_huge_dimensions() {
let img = RawImage {
pixels: RawImageData::U8(vec![0u8; 4].into()),
width: usize::MAX,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(img.into_loaded_image_source().is_none());
}
}