#[cfg(not(feature = "std"))]
use alloc::string::ToString;
use alloc::{alloc::Layout, boxed::Box, collections::BTreeMap, sync::Arc, vec::Vec};
use core::{
ffi::c_void,
fmt,
sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
};
#[cfg(feature = "std")]
use std::hash::Hash;
use azul_css::{
css::{CssPath, CssPropertyValue},
props::{
basic::{
AnimationInterpolationFunction, FontRef, InterpolateResolver, LayoutRect, LayoutSize,
},
property::{CssProperty, CssPropertyType},
},
system::SystemStyle,
AzString,
};
use rust_fontconfig::{FcFontCache, OwnedFontSource};
use crate::{
dom::{Dom, DomId, DomNodeId, EventFilter, OptionDom},
geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition, PhysicalSize},
gl::OptionGlContextPtr,
hit_test::OverflowingScrollNode,
id::{NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeId},
prop_cache::CssPropertyCache,
refany::{OptionRefAny, RefAny},
resources::{
DpiScaleFactor, FontInstanceKey, IdNamespace, ImageCache, ImageMask, ImageRef,
RendererResources,
},
styled_dom::{
NodeHierarchyItemId, NodeHierarchyItemVec, StyledNode,
StyledNodeVec,
},
task::{
Duration as AzDuration, GetSystemTimeCallback, Instant as AzInstant, Instant,
TerminateTimer, ThreadId, ThreadReceiver, ThreadSendMsg, TimerId,
},
window::{
AzStringPair, KeyboardState, MouseState, OptionChar, RawWindowHandle, UpdateFocusWarning,
WindowFlags, WindowSize, WindowTheme,
},
FastBTreeSet, OrderedMap,
};
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Update {
DoNothing,
RefreshDom,
RefreshDomAllWindows,
}
impl Update {
pub fn max_self(&mut self, other: Self) {
if (*self == Self::DoNothing && other != Self::DoNothing)
|| (*self == Self::RefreshDom && other == Self::RefreshDomAllWindows)
{
*self = other;
}
}
}
pub type LayoutCallbackType = extern "C" fn(RefAny, LayoutCallbackInfo) -> Dom;
extern "C" fn default_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
Dom::create_body()
}
#[repr(C)]
pub struct LayoutCallback {
pub cb: LayoutCallbackType,
pub ctx: OptionRefAny,
}
impl_callback!(LayoutCallback, LayoutCallbackType);
impl LayoutCallback {
pub fn create<I: Into<Self>>(cb: I) -> Self {
cb.into()
}
}
crate::impl_managed_callback! {
wrapper: LayoutCallback,
info_ty: LayoutCallbackInfo,
return_ty: Dom,
default_ret: Dom::create_body(),
invoker_static: LAYOUT_CALLBACK_INVOKER,
invoker_ty: AzLayoutCallbackInvoker,
thunk_fn: az_layout_callback_thunk,
setter_fn: AzApp_setLayoutCallbackInvoker,
from_handle_fn: AzLayoutCallback_createFromHostHandle,
}
impl Default for LayoutCallback {
fn default() -> Self {
Self {
cb: default_layout_callback,
ctx: OptionRefAny::None,
}
}
}
pub type VirtualViewCallbackType = extern "C" fn(RefAny, VirtualViewCallbackInfo) -> VirtualViewReturn;
#[repr(C)]
pub struct VirtualViewCallback {
pub cb: VirtualViewCallbackType,
pub ctx: OptionRefAny,
}
impl_callback!(VirtualViewCallback, VirtualViewCallbackType);
crate::impl_managed_callback! {
wrapper: VirtualViewCallback,
info_ty: VirtualViewCallbackInfo,
return_ty: VirtualViewReturn,
default_ret: VirtualViewReturn::default(),
invoker_static: VIRTUAL_VIEW_CALLBACK_INVOKER,
invoker_ty: AzVirtualViewCallbackInvoker,
thunk_fn: az_virtual_view_callback_thunk,
setter_fn: AzApp_setVirtualViewCallbackInvoker,
from_handle_fn: AzVirtualViewCallback_createFromHostHandle,
}
impl VirtualViewCallback {
pub fn create(cb: VirtualViewCallbackType) -> Self {
Self {
cb,
ctx: OptionRefAny::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C, u8)]
pub enum VirtualViewCallbackReason {
InitialRender,
DomRecreated,
BoundsExpanded,
EdgeScrolled(EdgeType),
ScrollBeyondContent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum EdgeType {
Top,
Bottom,
Left,
Right,
}
#[derive(Debug)]
#[repr(C)]
pub struct VirtualViewCallbackInfo {
pub reason: VirtualViewCallbackReason,
pub system_fonts: *const FcFontCache,
pub image_cache: *const ImageCache,
pub window_theme: WindowTheme,
pub bounds: HidpiAdjustedBounds,
pub scroll_size: LogicalSize,
pub scroll_offset: LogicalPosition,
pub virtual_scroll_size: LogicalSize,
pub virtual_scroll_offset: LogicalPosition,
callable_ptr: *const OptionRefAny,
measure_dom_fn: *const c_void,
measure_dom_ctx: *mut c_void,
_abi_mut: *mut c_void,
}
pub type MeasureDomFn = extern "C" fn(*mut c_void, *mut Dom, LogicalSize) -> LogicalSize;
impl Clone for VirtualViewCallbackInfo {
#[allow(clippy::used_underscore_binding)] fn clone(&self) -> Self {
Self {
reason: self.reason,
system_fonts: self.system_fonts,
image_cache: self.image_cache,
window_theme: self.window_theme,
bounds: self.bounds,
scroll_size: self.scroll_size,
scroll_offset: self.scroll_offset,
virtual_scroll_size: self.virtual_scroll_size,
virtual_scroll_offset: self.virtual_scroll_offset,
callable_ptr: self.callable_ptr,
measure_dom_fn: self.measure_dom_fn,
measure_dom_ctx: self.measure_dom_ctx,
_abi_mut: self._abi_mut,
}
}
}
impl VirtualViewCallbackInfo {
#[must_use] pub const fn new<'a>(
reason: VirtualViewCallbackReason,
system_fonts: &'a FcFontCache,
image_cache: &'a ImageCache,
window_theme: WindowTheme,
bounds: HidpiAdjustedBounds,
scroll_size: LogicalSize,
scroll_offset: LogicalPosition,
virtual_scroll_size: LogicalSize,
virtual_scroll_offset: LogicalPosition,
) -> Self {
Self {
reason,
system_fonts: core::ptr::from_ref::<FcFontCache>(system_fonts),
image_cache: core::ptr::from_ref::<ImageCache>(image_cache),
window_theme,
bounds,
scroll_size,
scroll_offset,
virtual_scroll_size,
virtual_scroll_offset,
callable_ptr: core::ptr::null(),
measure_dom_fn: core::ptr::null(),
measure_dom_ctx: core::ptr::null_mut(),
_abi_mut: core::ptr::null_mut(),
}
}
pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
}
pub fn set_measure_dom_fn(&mut self, f: MeasureDomFn, ctx: *mut c_void) {
self.measure_dom_fn = f as *const c_void;
self.measure_dom_ctx = ctx;
}
#[must_use] pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
if self.measure_dom_fn.is_null() {
return LogicalSize::zero();
}
let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
let mut dom = core::mem::ManuallyDrop::new(dom);
f(self.measure_dom_ctx, core::ptr::from_mut::<Dom>(&mut dom), available)
}
#[must_use] pub fn get_ctx(&self) -> OptionRefAny {
if self.callable_ptr.is_null() {
OptionRefAny::None
} else {
unsafe { (*self.callable_ptr).clone() }
}
}
#[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
self.bounds
}
const fn internal_get_system_fonts(&self) -> &FcFontCache {
unsafe { &*self.system_fonts }
}
const fn internal_get_image_cache(&self) -> &ImageCache {
unsafe { &*self.image_cache }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct VirtualViewReturn {
pub dom: OptionDom,
pub scroll_size: LogicalSize,
pub scroll_offset: LogicalPosition,
pub virtual_scroll_size: LogicalSize,
pub virtual_scroll_offset: LogicalPosition,
}
impl Default for VirtualViewReturn {
fn default() -> Self {
Self {
dom: OptionDom::None,
scroll_size: LogicalSize::zero(),
scroll_offset: LogicalPosition::zero(),
virtual_scroll_size: LogicalSize::zero(),
virtual_scroll_offset: LogicalPosition::zero(),
}
}
}
impl VirtualViewReturn {
#[must_use] pub const fn with_dom(
dom: Dom,
scroll_size: LogicalSize,
scroll_offset: LogicalPosition,
virtual_scroll_size: LogicalSize,
virtual_scroll_offset: LogicalPosition,
) -> Self {
Self {
dom: OptionDom::Some(dom),
scroll_size,
scroll_offset,
virtual_scroll_size,
virtual_scroll_offset,
}
}
#[must_use] pub const fn keep_current(
scroll_size: LogicalSize,
scroll_offset: LogicalPosition,
virtual_scroll_size: LogicalSize,
virtual_scroll_offset: LogicalPosition,
) -> Self {
Self {
dom: OptionDom::None,
scroll_size,
scroll_offset,
virtual_scroll_size,
virtual_scroll_offset,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct TimerCallbackReturn {
pub should_update: Update,
pub should_terminate: TerminateTimer,
}
impl TimerCallbackReturn {
#[must_use] pub const fn create(should_update: Update, should_terminate: TerminateTimer) -> Self {
Self {
should_update,
should_terminate,
}
}
#[must_use] pub const fn continue_unchanged() -> Self {
Self {
should_update: Update::DoNothing,
should_terminate: TerminateTimer::Continue,
}
}
#[must_use] pub const fn continue_and_refresh_dom() -> Self {
Self {
should_update: Update::RefreshDom,
should_terminate: TerminateTimer::Continue,
}
}
#[must_use] pub const fn terminate_unchanged() -> Self {
Self {
should_update: Update::DoNothing,
should_terminate: TerminateTimer::Terminate,
}
}
#[must_use] pub const fn terminate_and_refresh_dom() -> Self {
Self {
should_update: Update::RefreshDom,
should_terminate: TerminateTimer::Terminate,
}
}
}
impl Default for TimerCallbackReturn {
fn default() -> Self {
Self::continue_unchanged()
}
}
#[derive(Debug)]
#[repr(C)]
pub struct LayoutCallbackInfoRefData<'a> {
pub image_cache: &'a ImageCache,
pub gl_context: &'a OptionGlContextPtr,
pub system_fonts: &'a FcFontCache,
pub system_style: Arc<SystemStyle>,
pub active_route: Option<&'a crate::resources::RouteMatch>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum RelayoutReason {
#[default]
Initial,
RefreshDom,
Resize,
ThemeChange,
RouteChange,
Other,
}
#[repr(C)]
pub struct LayoutCallbackInfo {
ref_data: *const LayoutCallbackInfoRefData<'static>,
pub window_size: WindowSize,
pub theme: WindowTheme,
pub relayout_reason: RelayoutReason,
callable_ptr: *const OptionRefAny,
_abi_mut: *mut c_void,
}
impl Clone for LayoutCallbackInfo {
#[allow(clippy::used_underscore_binding)] fn clone(&self) -> Self {
Self {
ref_data: self.ref_data,
window_size: self.window_size,
theme: self.theme,
relayout_reason: self.relayout_reason,
callable_ptr: self.callable_ptr,
_abi_mut: self._abi_mut,
}
}
}
impl core::fmt::Debug for LayoutCallbackInfo {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LayoutCallbackInfo")
.field("window_size", &self.window_size)
.field("theme", &self.theme)
.field("relayout_reason", &self.relayout_reason)
.finish_non_exhaustive()
}
}
impl LayoutCallbackInfo {
#[must_use] pub const fn new<'a>(
ref_data: &'a LayoutCallbackInfoRefData<'a>,
window_size: WindowSize,
theme: WindowTheme,
) -> Self {
Self::new_with_reason(ref_data, window_size, theme, RelayoutReason::Initial)
}
#[allow(clippy::unnecessary_cast)]
#[must_use] pub const fn new_with_reason<'a>(
ref_data: &'a LayoutCallbackInfoRefData<'a>,
window_size: WindowSize,
theme: WindowTheme,
relayout_reason: RelayoutReason,
) -> Self {
Self {
ref_data: core::ptr::from_ref::<LayoutCallbackInfoRefData<'a>>(ref_data)
as *const LayoutCallbackInfoRefData<'static>,
window_size,
theme,
relayout_reason,
callable_ptr: core::ptr::null(),
_abi_mut: core::ptr::null_mut(),
}
}
#[must_use] pub const fn relayout_reason(&self) -> RelayoutReason {
self.relayout_reason
}
pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
}
#[must_use] pub fn get_ctx(&self) -> OptionRefAny {
if self.callable_ptr.is_null() {
OptionRefAny::None
} else {
unsafe { (*self.callable_ptr).clone() }
}
}
#[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
unsafe { (*self.ref_data).system_style.clone() }
}
const fn internal_get_image_cache(&self) -> &ImageCache {
unsafe { (*self.ref_data).image_cache }
}
const fn internal_get_system_fonts(&self) -> &FcFontCache {
unsafe { (*self.ref_data).system_fonts }
}
const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
unsafe { (*self.ref_data).gl_context }
}
#[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
self.internal_get_gl_context().clone()
}
#[must_use] pub fn get_system_fonts(&self) -> Vec<AzStringPair> {
let fc_cache = self.internal_get_system_fonts();
fc_cache
.list()
.into_iter()
.filter_map(|(pattern, font_id)| {
let source = fc_cache.get_font_by_id(&font_id)?;
match source {
OwnedFontSource::Memory(_) => None,
OwnedFontSource::Disk(d) => Some((pattern.name.as_ref()?.clone(), d.path)),
}
})
.map(|(k, v)| AzStringPair {
key: k.into(),
value: v.into(),
})
.collect()
}
#[must_use] pub fn get_image(&self, image_id: &AzString) -> Option<ImageRef> {
self.internal_get_image_cache()
.get_css_image_id(image_id)
.cloned()
}
#[must_use] pub const fn get_active_route(&self) -> Option<&crate::resources::RouteMatch> {
unsafe { (*self.ref_data).active_route }
}
#[must_use] pub fn get_route_param(&self, key: &str) -> Option<&AzString> {
self.get_active_route()?.get_param(key)
}
#[must_use] pub fn window_width_less_than(&self, px: f32) -> bool {
self.window_size.dimensions.width < px
}
#[must_use] pub fn window_width_greater_than(&self, px: f32) -> bool {
self.window_size.dimensions.width > px
}
#[must_use] pub fn window_width_between(&self, min_px: f32, max_px: f32) -> bool {
let width = self.window_size.dimensions.width;
width >= min_px && width <= max_px
}
#[must_use] pub fn window_height_less_than(&self, px: f32) -> bool {
self.window_size.dimensions.height < px
}
#[must_use] pub fn window_height_greater_than(&self, px: f32) -> bool {
self.window_size.dimensions.height > px
}
#[must_use] pub fn window_height_between(&self, min_px: f32, max_px: f32) -> bool {
let height = self.window_size.dimensions.height;
height >= min_px && height <= max_px
}
#[must_use] pub const fn get_window_width(&self) -> f32 {
self.window_size.dimensions.width
}
#[must_use] pub const fn get_window_height(&self) -> f32 {
self.window_size.dimensions.height
}
#[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_dpi_factor(&self) -> f32 {
self.window_size.dpi as f32 / 96.0
}
}
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct HidpiAdjustedBounds {
pub logical_size: LogicalSize,
pub hidpi_factor: DpiScaleFactor,
}
impl HidpiAdjustedBounds {
#[inline]
#[allow(clippy::cast_precision_loss)] #[must_use] pub const fn from_bounds(bounds: LayoutSize, hidpi_factor: DpiScaleFactor) -> Self {
let logical_size = LogicalSize::new(bounds.width as f32, bounds.height as f32);
Self {
logical_size,
hidpi_factor,
}
}
#[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
self.get_logical_size()
.to_physical(self.get_hidpi_factor().inner.get())
}
#[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
self.logical_size
}
#[must_use] pub const fn get_hidpi_factor(&self) -> DpiScaleFactor {
self.hidpi_factor
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum FocusTarget {
Id(DomNodeId),
Path(FocusTargetPath),
Previous,
Next,
First,
Last,
NoFocus,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct FocusTargetPath {
pub dom: DomId,
pub css_path: CssPath,
}
pub type CoreCallbackType = usize;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreCallback {
pub cb: CoreCallbackType,
pub ctx: OptionRefAny,
}
impl From<CoreCallbackType> for CoreCallback {
fn from(cb: CoreCallbackType) -> Self {
Self {
cb,
ctx: OptionRefAny::None,
}
}
}
impl_option!(
CoreCallback,
OptionCoreCallback,
[Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash]
);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreCallbackData {
pub event: EventFilter,
pub callback: CoreCallback,
pub refany: RefAny,
}
impl_option!(
CoreCallbackData,
OptionCoreCallbackData,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(CoreCallbackData, CoreCallbackDataVec, CoreCallbackDataVecDestructor, CoreCallbackDataVecDestructorType, CoreCallbackDataVecSlice, OptionCoreCallbackData);
impl_vec_clone!(
CoreCallbackData,
CoreCallbackDataVec,
CoreCallbackDataVecDestructor
);
impl_vec_mut!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_debug!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_partialord!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_ord!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_partialeq!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_eq!(CoreCallbackData, CoreCallbackDataVec);
impl_vec_hash!(CoreCallbackData, CoreCallbackDataVec);
impl CoreCallbackDataVec {
#[inline]
#[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CoreCallbackData> {
NodeDataContainerRef {
internal: self.as_ref(),
}
}
#[inline]
pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, CoreCallbackData> {
NodeDataContainerRefMut {
internal: self.as_mut(),
}
}
}
pub type CoreRenderImageCallbackType = usize;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreRenderImageCallback {
pub cb: CoreRenderImageCallbackType,
pub ctx: OptionRefAny,
}
impl From<CoreRenderImageCallbackType> for CoreRenderImageCallback {
fn from(cb: CoreRenderImageCallbackType) -> Self {
Self {
cb,
ctx: OptionRefAny::None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CoreImageCallback {
pub refany: RefAny,
pub callback: CoreRenderImageCallback,
}
impl_option!(
CoreImageCallback,
OptionCoreImageCallback,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::too_many_lines,
clippy::cast_precision_loss,
clippy::unusual_byte_groupings
)]
mod autotest_generated {
use alloc::string::String;
use super::*;
use crate::{
events::HoverEventFilter,
resources::{RawImageFormat, RouteMatch},
window::StringPairVec,
};
fn s(v: &str) -> AzString {
AzString::from(String::from(v))
}
fn win(width: f32, height: f32, dpi: u32) -> WindowSize {
WindowSize {
dimensions: LogicalSize::new(width, height),
dpi,
min_dimensions: None.into(),
max_dimensions: None.into(),
}
}
struct Fixture {
fonts: FcFontCache,
images: ImageCache,
style: Arc<SystemStyle>,
gl: OptionGlContextPtr,
route: Option<RouteMatch>,
}
impl Fixture {
fn new() -> Self {
Self {
fonts: FcFontCache::default(),
images: ImageCache::default(),
style: Arc::new(SystemStyle::default()),
gl: OptionGlContextPtr::None,
route: None,
}
}
fn with_route(route: RouteMatch) -> Self {
let mut f = Self::new();
f.route = Some(route);
f
}
fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
LayoutCallbackInfoRefData {
image_cache: &self.images,
gl_context: &self.gl,
system_fonts: &self.fonts,
system_style: self.style.clone(),
active_route: self.route.as_ref(),
}
}
}
fn user_route() -> RouteMatch {
RouteMatch {
pattern: s("/user/:id"),
params: StringPairVec::from_vec(Vec::from([
AzStringPair {
key: s("id"),
value: s("42"),
},
AzStringPair {
key: s("\u{1F600}"),
value: s("emoji"),
},
])),
}
}
fn vv_info<'a>(
fonts: &'a FcFontCache,
images: &'a ImageCache,
bounds: HidpiAdjustedBounds,
) -> VirtualViewCallbackInfo {
VirtualViewCallbackInfo::new(
VirtualViewCallbackReason::InitialRender,
fonts,
images,
WindowTheme::LightMode,
bounds,
LogicalSize::new(100.0, 200.0),
LogicalPosition::new(1.0, 2.0),
LogicalSize::new(1000.0, 2000.0),
LogicalPosition::new(3.0, 4.0),
)
}
fn bounds_1x1() -> HidpiAdjustedBounds {
HidpiAdjustedBounds::from_bounds(LayoutSize::new(1, 1), DpiScaleFactor::new(1.0))
}
const ALL_UPDATES: [Update; 3] = [
Update::DoNothing,
Update::RefreshDom,
Update::RefreshDomAllWindows,
];
#[test]
fn update_max_self_is_exhaustively_ord_max() {
for a in ALL_UPDATES {
for b in ALL_UPDATES {
let mut got = a;
got.max_self(b);
assert_eq!(
got,
core::cmp::max(a, b),
"max_self({a:?}, {b:?}) disagrees with Ord::max"
);
}
}
}
#[test]
fn update_max_self_is_idempotent_and_monotone() {
for a in ALL_UPDATES {
let mut got = a;
got.max_self(a);
assert_eq!(got, a);
let mut top = Update::RefreshDomAllWindows;
top.max_self(a);
assert_eq!(top, Update::RefreshDomAllWindows);
let mut m = a;
m.max_self(Update::DoNothing);
assert!(m >= a);
}
}
#[test]
fn update_max_self_fold_is_order_independent() {
for a in ALL_UPDATES {
for b in ALL_UPDATES {
for c in ALL_UPDATES {
let mut fwd = a;
fwd.max_self(b);
fwd.max_self(c);
let mut rev = c;
rev.max_self(b);
rev.max_self(a);
assert_eq!(fwd, rev, "fold of {a:?},{b:?},{c:?} is order-dependent");
}
}
}
}
static ALT_LAYOUT_CALLS: AtomicUsize = AtomicUsize::new(0);
extern "C" fn alt_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
ALT_LAYOUT_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
Dom::create_body()
}
#[test]
fn default_layout_callback_returns_body_and_does_not_panic() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, win(0.0, 0.0, 0), WindowTheme::DarkMode);
let dom = default_layout_callback(RefAny::new(0u32), info);
assert_eq!(dom, Dom::create_body());
}
#[test]
fn layout_callback_create_stores_the_given_fn_and_null_ctx() {
let from_default = LayoutCallback::create(default_layout_callback as LayoutCallbackType);
assert!(
from_default.ctx.is_none(),
"native-Rust create() must leave the FFI ctx empty"
);
assert_eq!(from_default, LayoutCallback::default());
let from_alt = LayoutCallback::create(alt_layout_callback as LayoutCallbackType);
assert!(from_alt.ctx.is_none());
assert_ne!(
from_alt, from_default,
"create() ignored its argument (or the two fns were ICF-folded)"
);
let fx = Fixture::new();
let rd = fx.ref_data();
let before = ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst);
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
let _ = (from_alt.cb)(RefAny::new(()), info);
assert_eq!(ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst), before + 1);
}
extern "C" fn vv_keep_current_cb(_: RefAny, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
VirtualViewReturn::keep_current(
info.scroll_size,
info.scroll_offset,
info.virtual_scroll_size,
info.virtual_scroll_offset,
)
}
#[test]
fn virtual_view_callback_create_round_trips_through_the_fn_ptr() {
let cb = VirtualViewCallback::create(vv_keep_current_cb as VirtualViewCallbackType);
assert!(cb.ctx.is_none());
let fonts = FcFontCache::default();
let images = ImageCache::default();
let info = vv_info(&fonts, &images, bounds_1x1());
let ret = (cb.cb)(RefAny::new(0u8), info);
assert!(ret.dom.is_none());
assert_eq!(ret.scroll_size, LogicalSize::new(100.0, 200.0));
assert_eq!(ret.scroll_offset, LogicalPosition::new(1.0, 2.0));
assert_eq!(ret.virtual_scroll_size, LogicalSize::new(1000.0, 2000.0));
assert_eq!(ret.virtual_scroll_offset, LogicalPosition::new(3.0, 4.0));
}
#[test]
fn virtual_view_callback_info_new_holds_its_fields() {
let fonts = FcFontCache::default();
let images = ImageCache::default();
let bounds = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(800, 600),
DpiScaleFactor::new(2.0),
);
let info = vv_info(&fonts, &images, bounds);
assert_eq!(info.reason, VirtualViewCallbackReason::InitialRender);
assert_eq!(info.window_theme, WindowTheme::LightMode);
assert_eq!(info.get_bounds().get_logical_size(), LogicalSize::new(800.0, 600.0));
assert_eq!(info.get_bounds().get_hidpi_factor(), DpiScaleFactor::new(2.0));
assert_eq!(info.scroll_size, LogicalSize::new(100.0, 200.0));
assert!(core::ptr::eq(info.internal_get_system_fonts(), &fonts));
assert!(core::ptr::eq(info.internal_get_image_cache(), &images));
assert!(info.get_ctx().is_none());
assert_eq!(
info.measure_dom(Dom::create_body(), LogicalSize::new(10.0, 10.0)),
LogicalSize::zero()
);
let cloned = info.clone();
assert_eq!(cloned.reason, info.reason);
assert!(core::ptr::eq(cloned.internal_get_system_fonts(), &fonts));
assert!(cloned.get_ctx().is_none());
}
#[test]
fn virtual_view_callback_info_new_survives_nan_and_infinite_geometry() {
let fonts = FcFontCache::default();
let images = ImageCache::default();
let info = VirtualViewCallbackInfo::new(
VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
&fonts,
&images,
WindowTheme::DarkMode,
HidpiAdjustedBounds::from_bounds(
LayoutSize::new(isize::MAX, isize::MIN),
DpiScaleFactor::new(f32::NAN),
),
LogicalSize::new(f32::NAN, f32::INFINITY),
LogicalPosition::new(f32::NEG_INFINITY, f32::MAX),
LogicalSize::new(f32::MIN, 0.0),
LogicalPosition::new(-0.0, f32::EPSILON),
);
assert!(info.scroll_size.width.is_nan());
assert!(info.scroll_size.height.is_infinite());
assert!(info.scroll_offset.x.is_infinite() && info.scroll_offset.x.is_sign_negative());
assert_eq!(info.virtual_scroll_size.width, f32::MIN);
assert_eq!(info.reason, VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
assert!(info.get_ctx().is_none());
assert!(info.get_bounds().get_logical_size().width > 0.0);
}
#[test]
fn virtual_view_callback_info_get_ctx_clones_without_double_free() {
let fonts = FcFontCache::default();
let images = ImageCache::default();
let mut info = vv_info(&fonts, &images, bounds_1x1());
assert!(info.get_ctx().is_none());
let callable = OptionRefAny::Some(RefAny::new(0xDEAD_BEEF_u32));
info.set_callable_ptr(&callable);
for _ in 0..64 {
let got = info.get_ctx();
assert!(got.is_some());
drop(got);
}
let mut got = info.get_ctx();
match got {
OptionRefAny::Some(ref mut r) => {
let inner = r.downcast_ref::<u32>().expect("ctx should hold a u32");
assert_eq!(*inner, 0xDEAD_BEEF_u32);
}
OptionRefAny::None => panic!("callable_ptr was set, get_ctx() returned None"),
}
drop(got);
let mut orig = callable;
match orig {
OptionRefAny::Some(ref mut r) => {
assert_eq!(*r.downcast_ref::<u32>().unwrap(), 0xDEAD_BEEF_u32);
}
OptionRefAny::None => panic!("original callable was consumed"),
}
}
static MEASURE_CALLS: AtomicUsize = AtomicUsize::new(0);
extern "C" fn test_measure_dom_fn(
ctx: *mut c_void,
dom: *mut Dom,
available: LogicalSize,
) -> LogicalSize {
MEASURE_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
let dom = unsafe { core::ptr::read(dom) };
drop(dom);
if !ctx.is_null() {
unsafe {
*ctx.cast::<u32>() = 0xABCD;
}
}
LogicalSize::new(available.width * 2.0, available.height / 2.0)
}
#[test]
fn measure_dom_without_hook_returns_zero_for_every_input() {
let fonts = FcFontCache::default();
let images = ImageCache::default();
let info = vv_info(&fonts, &images, bounds_1x1());
for available in [
LogicalSize::zero(),
LogicalSize::new(-1.0, -1.0),
LogicalSize::new(f32::NAN, f32::NAN),
LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
LogicalSize::new(f32::MAX, f32::MIN),
LogicalSize::new(1.0, 1_000_000.0),
] {
assert_eq!(
info.measure_dom(Dom::create_body(), available),
LogicalSize::zero()
);
}
}
#[test]
fn measure_dom_with_hook_forwards_ctx_and_available_and_consumes_the_dom() {
let fonts = FcFontCache::default();
let images = ImageCache::default();
let mut info = vv_info(&fonts, &images, bounds_1x1());
let mut ctx_val: u32 = 0;
info.set_measure_dom_fn(
test_measure_dom_fn,
core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
);
let before = MEASURE_CALLS.load(AtomicOrdering::SeqCst);
let out = info.measure_dom(Dom::create_body(), LogicalSize::new(100.0, 40.0));
assert!(MEASURE_CALLS.load(AtomicOrdering::SeqCst) > before);
assert_eq!(out, LogicalSize::new(200.0, 20.0));
assert_eq!(ctx_val, 0xABCD, "measure ctx pointer was not forwarded");
let natural = info.measure_dom(Dom::create_body(), LogicalSize::new(320.0, 1_000_000.0));
assert_eq!(natural, LogicalSize::new(640.0, 500_000.0));
let nan = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::NAN, 4.0));
assert!(nan.width.is_nan());
assert_eq!(nan.height, 2.0);
let inf = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::INFINITY, 4.0));
assert!(inf.width.is_infinite());
}
#[test]
fn measure_dom_hook_can_be_replaced_and_last_writer_wins() {
let fonts = FcFontCache::default();
let images = ImageCache::default();
let mut info = vv_info(&fonts, &images, bounds_1x1());
info.set_measure_dom_fn(test_measure_dom_fn, core::ptr::null_mut());
let first = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
assert_eq!(first, LogicalSize::new(4.0, 4.0));
let mut ctx_val: u32 = 0;
info.set_measure_dom_fn(
test_measure_dom_fn,
core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
);
let second = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
assert_eq!(second, first);
assert_eq!(ctx_val, 0xABCD);
}
#[test]
fn virtual_view_return_with_dom_and_keep_current_hold_their_fields() {
let ss = LogicalSize::new(600.0, 30.0);
let so = LogicalPosition::new(0.0, 300.0);
let vss = LogicalSize::new(600.0, 30_000.0);
let vso = LogicalPosition::zero();
let with = VirtualViewReturn::with_dom(Dom::create_body(), ss, so, vss, vso);
assert!(with.dom.is_some(), "with_dom must produce OptionDom::Some");
assert_eq!(with.scroll_size, ss);
assert_eq!(with.scroll_offset, so);
assert_eq!(with.virtual_scroll_size, vss);
assert_eq!(with.virtual_scroll_offset, vso);
assert_eq!(with.dom, OptionDom::Some(Dom::create_body()));
let keep = VirtualViewReturn::keep_current(ss, so, vss, vso);
assert!(keep.dom.is_none(), "keep_current must produce OptionDom::None");
assert_eq!(keep.scroll_size, ss);
assert_eq!(keep.scroll_offset, so);
assert_eq!(keep.virtual_scroll_size, vss);
assert_eq!(keep.virtual_scroll_offset, vso);
assert_ne!(with, keep);
let d = VirtualViewReturn::default();
assert_eq!(
d,
VirtualViewReturn::keep_current(
LogicalSize::zero(),
LogicalPosition::zero(),
LogicalSize::zero(),
LogicalPosition::zero()
)
);
}
#[test]
fn virtual_view_return_keep_current_passes_extreme_values_through_unclamped() {
let z = VirtualViewReturn::keep_current(
LogicalSize::zero(),
LogicalPosition::zero(),
LogicalSize::zero(),
LogicalPosition::zero(),
);
assert_eq!(z.scroll_size, LogicalSize::zero());
assert_eq!(z.virtual_scroll_size, LogicalSize::zero());
let n = VirtualViewReturn::keep_current(
LogicalSize::new(-1.0, -0.0),
LogicalPosition::new(f32::MIN, f32::MAX),
LogicalSize::new(f32::MAX, f32::MIN_POSITIVE),
LogicalPosition::new(-f32::EPSILON, 0.0),
);
assert_eq!(n.scroll_size.width, -1.0);
assert_eq!(n.scroll_offset.x, f32::MIN);
assert_eq!(n.scroll_offset.y, f32::MAX);
assert_eq!(n.virtual_scroll_size.width, f32::MAX);
assert_eq!(n.virtual_scroll_size.height, f32::MIN_POSITIVE);
let x = VirtualViewReturn::keep_current(
LogicalSize::new(f32::NAN, f32::INFINITY),
LogicalPosition::new(f32::NEG_INFINITY, f32::NAN),
LogicalSize::new(f32::INFINITY, f32::NAN),
LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
);
assert!(x.scroll_size.width.is_nan());
assert!(x.scroll_size.height.is_infinite() && x.scroll_size.height.is_sign_positive());
assert!(x.scroll_offset.x.is_infinite() && x.scroll_offset.x.is_sign_negative());
assert!(x.scroll_offset.y.is_nan());
assert!(x.virtual_scroll_offset.y.is_infinite());
assert!(x.dom.is_none());
}
#[test]
fn timer_callback_return_constructors_match_their_documented_flags() {
let c = TimerCallbackReturn::continue_unchanged();
assert_eq!(c.should_update, Update::DoNothing);
assert_eq!(c.should_terminate, TerminateTimer::Continue);
let cr = TimerCallbackReturn::continue_and_refresh_dom();
assert_eq!(cr.should_update, Update::RefreshDom);
assert_eq!(cr.should_terminate, TerminateTimer::Continue);
let t = TimerCallbackReturn::terminate_unchanged();
assert_eq!(t.should_update, Update::DoNothing);
assert_eq!(t.should_terminate, TerminateTimer::Terminate);
let tr = TimerCallbackReturn::terminate_and_refresh_dom();
assert_eq!(tr.should_update, Update::RefreshDom);
assert_eq!(tr.should_terminate, TerminateTimer::Terminate);
let all = [c, cr, t, tr];
for (i, a) in all.iter().enumerate() {
for (j, b) in all.iter().enumerate() {
assert_eq!(i == j, a == b, "constructors {i} and {j} collide");
}
}
assert_eq!(TimerCallbackReturn::default(), c);
}
#[test]
fn timer_callback_return_create_round_trips_every_flag_combination() {
for u in ALL_UPDATES {
for t in [TerminateTimer::Continue, TerminateTimer::Terminate] {
let r = TimerCallbackReturn::create(u, t);
assert_eq!(r.should_update, u);
assert_eq!(r.should_terminate, t);
}
}
assert_eq!(
TimerCallbackReturn::create(Update::DoNothing, TerminateTimer::Continue),
TimerCallbackReturn::continue_unchanged()
);
assert_eq!(
TimerCallbackReturn::create(Update::RefreshDom, TerminateTimer::Terminate),
TimerCallbackReturn::terminate_and_refresh_dom()
);
let all_windows =
TimerCallbackReturn::create(Update::RefreshDomAllWindows, TerminateTimer::Terminate);
assert_eq!(all_windows.should_update, Update::RefreshDomAllWindows);
}
#[test]
fn layout_callback_info_new_defaults_to_initial_reason_and_holds_fields() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, win(1280.0, 720.0, 192), WindowTheme::DarkMode);
assert_eq!(info.relayout_reason(), RelayoutReason::Initial);
assert_eq!(info.theme, WindowTheme::DarkMode);
assert_eq!(info.get_window_width(), 1280.0);
assert_eq!(info.get_window_height(), 720.0);
assert_eq!(info.get_dpi_factor(), 2.0);
assert!(info.get_ctx().is_none());
assert!(core::ptr::eq(info.internal_get_image_cache(), &fx.images));
assert!(core::ptr::eq(info.internal_get_system_fonts(), &fx.fonts));
assert!(core::ptr::eq(info.internal_get_gl_context(), &fx.gl));
assert!(info.get_gl_context().is_none());
}
#[test]
fn layout_callback_info_new_with_reason_round_trips_every_reason() {
let fx = Fixture::new();
let rd = fx.ref_data();
for reason in [
RelayoutReason::Initial,
RelayoutReason::RefreshDom,
RelayoutReason::Resize,
RelayoutReason::ThemeChange,
RelayoutReason::RouteChange,
RelayoutReason::Other,
] {
let info = LayoutCallbackInfo::new_with_reason(
&rd,
WindowSize::default(),
WindowTheme::LightMode,
reason,
);
assert_eq!(info.relayout_reason(), reason);
assert_eq!(info.clone().relayout_reason(), reason);
}
assert_eq!(RelayoutReason::default(), RelayoutReason::Initial);
}
#[test]
fn layout_callback_info_get_system_style_shares_the_arc() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
let a = info.get_system_style();
let b = info.get_system_style();
assert!(Arc::ptr_eq(&a, &b));
assert!(Arc::ptr_eq(&a, &fx.style));
let before = Arc::strong_count(&fx.style);
for _ in 0..128 {
drop(info.get_system_style());
}
assert_eq!(Arc::strong_count(&fx.style), before);
}
#[test]
fn layout_callback_info_get_ctx_is_none_until_set_then_clones_safely() {
let fx = Fixture::new();
let rd = fx.ref_data();
let mut info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
assert!(info.get_ctx().is_none(), "native path must have a null ctx");
let callable = OptionRefAny::Some(RefAny::new(7u64));
info.set_callable_ptr(&callable);
for _ in 0..64 {
assert!(info.get_ctx().is_some());
}
let mut got = info.get_ctx();
match got {
OptionRefAny::Some(ref mut r) => assert_eq!(*r.downcast_ref::<u64>().unwrap(), 7),
OptionRefAny::None => panic!("get_ctx() lost the callable"),
}
drop(got);
let cloned = info.clone();
assert!(cloned.get_ctx().is_some());
}
#[test]
fn layout_callback_info_get_system_fonts_is_empty_for_an_empty_cache() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
let fonts: Vec<AzStringPair> = info.get_system_fonts();
assert!(fonts.is_empty());
assert_eq!(info.get_system_fonts().len(), fonts.len());
}
#[test]
fn get_image_returns_none_for_missing_empty_and_hostile_ids() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
assert!(info.get_image(&s("")).is_none());
assert!(info.get_image(&s(" ")).is_none());
assert!(info.get_image(&s("nope")).is_none());
assert!(info.get_image(&s("\u{1F600}\u{0301}")).is_none());
assert!(info.get_image(&s("\0")).is_none());
assert!(info.get_image(&s(&"x".repeat(100_000))).is_none());
}
#[test]
fn get_image_finds_an_inserted_id_and_is_exact_match() {
let mut fx = Fixture::new();
fx.images.add_css_image_id(
s("logo"),
ImageRef::null_image(2, 2, RawImageFormat::RGBA8, Vec::new()),
);
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
assert!(info.get_image(&s("logo")).is_some(), "positive control");
assert!(info.get_image(&s("Logo")).is_none());
assert!(info.get_image(&s(" logo")).is_none());
assert!(info.get_image(&s("logo ")).is_none());
assert!(info.get_image(&s("log")).is_none());
assert!(info.get_image(&s("logos")).is_none());
}
#[test]
fn get_route_param_returns_none_when_no_route_is_active() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
assert!(info.get_active_route().is_none());
for key in ["", " ", "\t\n", "id", "\u{1F600}", "\0", "../../etc/passwd"] {
assert!(info.get_route_param(key).is_none(), "key {key:?}");
}
}
#[test]
fn get_route_param_valid_minimal_and_unicode_positive_controls() {
let fx = Fixture::with_route(user_route());
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
let route = info.get_active_route().expect("route was configured");
assert_eq!(route.pattern.as_str(), "/user/:id");
assert_eq!(info.get_route_param("id").map(AzString::as_str), Some("42"));
assert_eq!(
info.get_route_param("\u{1F600}").map(AzString::as_str),
Some("emoji")
);
}
#[test]
fn get_route_param_rejects_malformed_keys_without_trimming_or_folding() {
let fx = Fixture::with_route(user_route());
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
assert!(info.get_route_param("").is_none());
assert!(info.get_route_param(" ").is_none());
assert!(info.get_route_param("\t\n").is_none());
assert!(info.get_route_param(" id").is_none());
assert!(info.get_route_param("id ").is_none());
assert!(info.get_route_param(" id ").is_none());
assert!(info.get_route_param("id;garbage").is_none());
assert!(info.get_route_param("ID").is_none());
assert!(info.get_route_param("Id").is_none());
assert!(info.get_route_param("i").is_none());
assert!(info.get_route_param("idd").is_none());
assert!(info.get_route_param("\0").is_none());
assert!(info.get_route_param("id\0").is_none());
assert!(info.get_route_param("\u{7F}\u{1}\u{2}").is_none());
for key in [
"0",
"-0",
"9223372036854775807",
"-9223372036854775808",
"18446744073709551616",
"NaN",
"inf",
"-inf",
"1e400",
"0.0000000000000000001",
] {
assert!(info.get_route_param(key).is_none(), "key {key:?}");
}
assert!(info.get_route_param("i\u{0301}d").is_none());
assert!(info.get_route_param("\u{1F600}\u{1F600}").is_none());
}
#[test]
fn get_route_param_handles_pathological_key_sizes_and_nesting() {
let fx = Fixture::with_route(user_route());
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
let huge = "x".repeat(1_000_000);
assert!(info.get_route_param(&huge).is_none());
let long_id = alloc::format!("id{}", "0".repeat(1_000_000));
assert!(info.get_route_param(&long_id).is_none());
let nested = "[".repeat(10_000) + &"]".repeat(10_000);
assert!(info.get_route_param(&nested).is_none());
}
#[test]
fn get_route_param_preserves_huge_and_unicode_values() {
let big = "v".repeat(200_000);
let route = RouteMatch {
pattern: s("/blob/:data"),
params: StringPairVec::from_vec(Vec::from([AzStringPair {
key: s("data"),
value: s(&big),
}])),
};
let fx = Fixture::with_route(route);
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
let got = info.get_route_param("data").expect("param exists");
assert_eq!(got.as_str().len(), 200_000);
}
#[test]
fn window_predicates_obey_trichotomy_and_the_between_identity() {
let fx = Fixture::new();
let rd = fx.ref_data();
let probes = [
0.0f32,
-0.0,
1.0,
-1.0,
640.0,
f32::MIN,
f32::MAX,
f32::MIN_POSITIVE,
f32::INFINITY,
f32::NEG_INFINITY,
];
for &dim in &probes {
let info = LayoutCallbackInfo::new(&rd, win(dim, dim, 96), WindowTheme::LightMode);
for &px in &probes {
let lt = info.window_width_less_than(px);
let gt = info.window_width_greater_than(px);
let eq = info.get_window_width() == px;
assert_eq!(
u8::from(lt) + u8::from(gt) + u8::from(eq),
1,
"trichotomy broken for width {dim} vs {px}"
);
assert_eq!(info.window_height_less_than(px), lt);
assert_eq!(info.window_height_greater_than(px), gt);
for &px2 in &probes {
assert_eq!(
info.window_width_between(px, px2),
!info.window_width_less_than(px) && !info.window_width_greater_than(px2),
"between identity broken for width {dim} in [{px}, {px2}]"
);
assert_eq!(
info.window_height_between(px, px2),
info.window_width_between(px, px2)
);
}
}
}
}
#[test]
fn window_predicates_with_inverted_and_degenerate_ranges() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
assert!(!info.window_width_between(1000.0, 100.0));
assert!(!info.window_height_between(1000.0, 100.0));
assert!(info.window_width_between(640.0, 640.0));
assert!(info.window_height_between(480.0, 480.0));
assert!(!info.window_width_between(639.9, 639.95));
assert!(info.window_width_between(640.0, 1000.0));
assert!(info.window_width_between(0.0, 640.0));
assert!(!info.window_width_less_than(640.0));
assert!(!info.window_width_greater_than(640.0));
assert!(info.window_width_less_than(640.001));
assert!(info.window_width_greater_than(639.999));
assert!(info.window_width_between(f32::NEG_INFINITY, f32::INFINITY));
}
#[test]
fn window_predicates_are_all_false_for_nan_probes() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
assert!(!info.window_width_less_than(f32::NAN));
assert!(!info.window_width_greater_than(f32::NAN));
assert!(!info.window_width_between(f32::NAN, f32::NAN));
assert!(!info.window_width_between(f32::NAN, 10_000.0));
assert!(!info.window_width_between(0.0, f32::NAN));
assert!(!info.window_height_less_than(f32::NAN));
assert!(!info.window_height_greater_than(f32::NAN));
assert!(!info.window_height_between(f32::NAN, f32::NAN));
assert!(!info.window_height_between(f32::NAN, 10_000.0));
assert!(!info.window_height_between(0.0, f32::NAN));
}
#[test]
fn window_predicates_are_all_false_for_a_nan_sized_window() {
let fx = Fixture::new();
let rd = fx.ref_data();
let info = LayoutCallbackInfo::new(
&rd,
win(f32::NAN, f32::NAN, 96),
WindowTheme::LightMode,
);
assert!(info.get_window_width().is_nan());
assert!(info.get_window_height().is_nan());
for px in [0.0f32, 640.0, f32::MAX, f32::INFINITY, f32::NEG_INFINITY] {
assert!(!info.window_width_less_than(px));
assert!(!info.window_width_greater_than(px));
assert!(!info.window_height_less_than(px));
assert!(!info.window_height_greater_than(px));
assert!(!info.window_width_between(f32::NEG_INFINITY, px));
assert!(!info.window_height_between(px, f32::INFINITY));
}
}
#[test]
fn get_dpi_factor_at_zero_and_u32_limits() {
let fx = Fixture::new();
let rd = fx.ref_data();
let base = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 96), WindowTheme::LightMode);
assert_eq!(base.get_dpi_factor(), 1.0);
let hidpi = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 192), WindowTheme::LightMode);
assert_eq!(hidpi.get_dpi_factor(), 2.0);
let zero = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 0), WindowTheme::LightMode);
assert_eq!(zero.get_dpi_factor(), 0.0);
let max = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, u32::MAX), WindowTheme::LightMode);
let f = max.get_dpi_factor();
assert!(f.is_finite() && f > 0.0, "dpi factor {f} is not finite");
assert_eq!(f, (u32::MAX as f32) / 96.0);
let one = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 1), WindowTheme::LightMode);
assert!(one.get_dpi_factor() > 0.0);
}
#[test]
fn hidpi_adjusted_bounds_from_bounds_holds_its_fields() {
let b = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(800, 600),
DpiScaleFactor::new(1.5),
);
assert_eq!(b.get_logical_size(), LogicalSize::new(800.0, 600.0));
assert_eq!(b.get_hidpi_factor(), DpiScaleFactor::new(1.5));
assert_eq!(b.logical_size, b.get_logical_size());
assert_eq!(b.hidpi_factor, b.get_hidpi_factor());
let p = b.get_physical_size();
assert_eq!(p.width, 1200);
assert_eq!(p.height, 900);
}
#[test]
fn hidpi_adjusted_bounds_at_zero() {
let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(0, 0), DpiScaleFactor::new(1.0));
assert_eq!(b.get_logical_size(), LogicalSize::zero());
let p = b.get_physical_size();
assert_eq!(p.width, 0);
assert_eq!(p.height, 0);
let z = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(1920, 1080),
DpiScaleFactor::new(0.0),
);
let zp = z.get_physical_size();
assert_eq!(zp.width, 0);
assert_eq!(zp.height, 0);
}
#[test]
fn hidpi_adjusted_bounds_physical_size_saturates_on_negative_input() {
let b = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(-100, -50),
DpiScaleFactor::new(1.0),
);
assert_eq!(b.get_logical_size(), LogicalSize::new(-100.0, -50.0));
let p = b.get_physical_size();
assert_eq!(p.width, 0, "negative logical width must clamp to 0, not wrap");
assert_eq!(p.height, 0, "negative logical height must clamp to 0, not wrap");
let neg_scale = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(100, 100),
DpiScaleFactor::new(-2.0),
);
let np = neg_scale.get_physical_size();
assert_eq!(np.width, 0);
assert_eq!(np.height, 0);
}
#[test]
fn hidpi_adjusted_bounds_physical_size_saturates_at_the_upper_limit() {
let b = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(isize::MAX, isize::MAX),
DpiScaleFactor::new(1.0),
);
let p = b.get_physical_size();
assert_eq!(p.width, u32::MAX);
assert_eq!(p.height, u32::MAX);
let min = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(isize::MIN, isize::MIN),
DpiScaleFactor::new(1.0),
);
let mp = min.get_physical_size();
assert_eq!(mp.width, 0);
assert_eq!(mp.height, 0);
let huge_scale = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(1000, 1000),
DpiScaleFactor::new(f32::MAX),
);
let hp = huge_scale.get_physical_size();
assert_eq!(hp.width, u32::MAX);
assert_eq!(hp.height, u32::MAX);
}
#[test]
fn hidpi_adjusted_bounds_physical_size_with_nan_and_infinite_scale() {
let nan = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(100, 100),
DpiScaleFactor::new(f32::NAN),
);
assert_eq!(nan.get_hidpi_factor().inner.get(), 0.0);
let np = nan.get_physical_size();
assert_eq!(np.width, 0);
assert_eq!(np.height, 0);
let inf = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(100, 100),
DpiScaleFactor::new(f32::INFINITY),
);
assert!(inf.get_hidpi_factor().inner.get().is_finite());
let ip = inf.get_physical_size();
assert_eq!(ip.width, u32::MAX);
assert_eq!(ip.height, u32::MAX);
let neg_inf = HidpiAdjustedBounds::from_bounds(
LayoutSize::new(100, 100),
DpiScaleFactor::new(f32::NEG_INFINITY),
);
let nip = neg_inf.get_physical_size();
assert_eq!(nip.width, 0);
assert_eq!(nip.height, 0);
}
#[test]
fn hidpi_adjusted_bounds_physical_size_rounds_to_nearest() {
let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(3, 3), DpiScaleFactor::new(1.5));
let p = b.get_physical_size();
assert_eq!(p.width, 5, "3 * 1.5 = 4.5 must round to 5");
assert_eq!(p.height, 5);
let p2 = b.get_physical_size();
assert_eq!(p.width, p2.width);
assert_eq!(p.height, p2.height);
}
fn cb_data(cb: usize) -> CoreCallbackData {
CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::MouseOver),
callback: CoreCallback::from(cb),
refany: RefAny::new(cb),
}
}
#[test]
fn core_callback_data_vec_as_container_on_empty_vecs_does_not_panic() {
let empty = CoreCallbackDataVec::new();
assert_eq!(empty.as_container().len(), 0);
assert!(empty.as_container().internal.is_empty());
let from_empty_vec = CoreCallbackDataVec::from_vec(Vec::new());
assert_eq!(from_empty_vec.as_container().len(), 0);
let mut mut_empty = CoreCallbackDataVec::from_vec(Vec::new());
assert!(mut_empty.as_container_mut().internal.is_empty());
}
#[test]
fn core_callback_data_vec_as_container_matches_the_backing_vec() {
let v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2), cb_data(3)]));
let c = v.as_container();
assert_eq!(c.len(), 3);
assert_eq!(c.len(), v.len());
assert_eq!(c.internal[0].callback.cb, 1);
assert_eq!(c.internal[2].callback.cb, 3);
assert!(core::ptr::eq(c.internal.as_ptr(), v.as_slice().as_ptr()));
}
#[test]
fn core_callback_data_vec_as_container_mut_writes_through() {
let mut v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2)]));
{
let mut c = v.as_container_mut();
assert_eq!(c.internal.len(), 2);
c.internal[0].callback.cb = 99;
c.internal[1].event = EventFilter::Hover(HoverEventFilter::MouseDown);
}
let c = v.as_container();
assert_eq!(c.internal[0].callback.cb, 99);
assert_eq!(
c.internal[1].event,
EventFilter::Hover(HoverEventFilter::MouseDown)
);
assert_eq!(c.len(), 2);
}
}