1#[cfg(not(feature = "std"))]
24use alloc::string::ToString;
25use alloc::{alloc::Layout, boxed::Box, collections::BTreeMap, sync::Arc, vec::Vec};
26use core::{
27 ffi::c_void,
28 fmt,
29 sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
30};
31#[cfg(feature = "std")]
32use std::hash::Hash;
33
34use azul_css::{
35 css::{CssPath, CssPropertyValue},
36 props::{
37 basic::{
38 AnimationInterpolationFunction, FontRef, InterpolateResolver, LayoutRect, LayoutSize,
39 },
40 property::{CssProperty, CssPropertyType},
41 },
42 system::SystemStyle,
43 AzString,
44};
45use rust_fontconfig::{FcFontCache, OwnedFontSource};
46
47use crate::{
48 dom::{Dom, DomId, DomNodeId, EventFilter, OptionDom},
49 geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition, PhysicalSize},
50 gl::OptionGlContextPtr,
51 hit_test::OverflowingScrollNode,
52 id::{NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeId},
53 prop_cache::CssPropertyCache,
54 refany::{OptionRefAny, RefAny},
55 resources::{
56 DpiScaleFactor, FontInstanceKey, IdNamespace, ImageCache, ImageMask, ImageRef,
57 RendererResources,
58 },
59 styled_dom::{
60 NodeHierarchyItemId, NodeHierarchyItemVec, StyledNode,
61 StyledNodeVec,
62 },
63 task::{
64 Duration as AzDuration, GetSystemTimeCallback, Instant as AzInstant, Instant,
65 TerminateTimer, ThreadId, ThreadReceiver, ThreadSendMsg, TimerId,
66 },
67 window::{
68 AzStringPair, KeyboardState, MouseState, OptionChar, RawWindowHandle, UpdateFocusWarning,
69 WindowFlags, WindowSize, WindowTheme,
70 },
71 FastBTreeSet, OrderedMap,
72};
73
74#[repr(C)]
76#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub enum Update {
78 DoNothing,
80 RefreshDom,
83 RefreshDomAllWindows,
85}
86
87impl Update {
88 pub fn max_self(&mut self, other: Self) {
89 if (*self == Self::DoNothing && other != Self::DoNothing)
90 || (*self == Self::RefreshDom && other == Self::RefreshDomAllWindows)
91 {
92 *self = other;
93 }
94 }
95}
96
97pub type LayoutCallbackType = extern "C" fn(RefAny, LayoutCallbackInfo) -> Dom;
114
115extern "C" fn default_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
116 Dom::create_body()
117}
118
119#[repr(C)]
128pub struct LayoutCallback {
129 pub cb: LayoutCallbackType,
130 pub ctx: OptionRefAny,
133}
134
135impl_callback!(LayoutCallback, LayoutCallbackType);
136
137impl LayoutCallback {
138 pub fn create<I: Into<Self>>(cb: I) -> Self {
139 cb.into()
140 }
141}
142
143crate::impl_managed_callback! {
150 wrapper: LayoutCallback,
151 info_ty: LayoutCallbackInfo,
152 return_ty: Dom,
153 default_ret: Dom::create_body(),
154 invoker_static: LAYOUT_CALLBACK_INVOKER,
155 invoker_ty: AzLayoutCallbackInvoker,
156 thunk_fn: az_layout_callback_thunk,
157 setter_fn: AzApp_setLayoutCallbackInvoker,
158 from_handle_fn: AzLayoutCallback_createFromHostHandle,
159}
160
161impl Default for LayoutCallback {
162 fn default() -> Self {
163 Self {
164 cb: default_layout_callback,
165 ctx: OptionRefAny::None,
166 }
167 }
168}
169
170pub type VirtualViewCallbackType = extern "C" fn(RefAny, VirtualViewCallbackInfo) -> VirtualViewReturn;
173
174#[repr(C)]
177pub struct VirtualViewCallback {
178 pub cb: VirtualViewCallbackType,
179 pub ctx: OptionRefAny,
182}
183impl_callback!(VirtualViewCallback, VirtualViewCallbackType);
184
185crate::impl_managed_callback! {
187 wrapper: VirtualViewCallback,
188 info_ty: VirtualViewCallbackInfo,
189 return_ty: VirtualViewReturn,
190 default_ret: VirtualViewReturn::default(),
191 invoker_static: VIRTUAL_VIEW_CALLBACK_INVOKER,
192 invoker_ty: AzVirtualViewCallbackInvoker,
193 thunk_fn: az_virtual_view_callback_thunk,
194 setter_fn: AzApp_setVirtualViewCallbackInvoker,
195 from_handle_fn: AzVirtualViewCallback_createFromHostHandle,
196}
197
198impl VirtualViewCallback {
199 pub fn create(cb: VirtualViewCallbackType) -> Self {
200 Self {
201 cb,
202 ctx: OptionRefAny::None,
203 }
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211#[repr(C, u8)]
212pub enum VirtualViewCallbackReason {
213 InitialRender,
215 DomRecreated,
217 BoundsExpanded,
219 EdgeScrolled(EdgeType),
221 ScrollBeyondContent,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227#[repr(C)]
228pub enum EdgeType {
229 Top,
230 Bottom,
231 Left,
232 Right,
233}
234
235#[derive(Debug)]
236#[repr(C)]
237pub struct VirtualViewCallbackInfo {
238 pub reason: VirtualViewCallbackReason,
239 pub system_fonts: *const FcFontCache,
240 pub image_cache: *const ImageCache,
241 pub window_theme: WindowTheme,
242 pub bounds: HidpiAdjustedBounds,
243 pub scroll_size: LogicalSize,
244 pub scroll_offset: LogicalPosition,
245 pub virtual_scroll_size: LogicalSize,
246 pub virtual_scroll_offset: LogicalPosition,
247 callable_ptr: *const OptionRefAny,
250 measure_dom_fn: *const c_void,
256 measure_dom_ctx: *mut c_void,
257 _abi_mut: *mut c_void,
259}
260
261pub type MeasureDomFn = extern "C" fn(*mut c_void, *mut Dom, LogicalSize) -> LogicalSize;
265
266impl Clone for VirtualViewCallbackInfo {
267 #[allow(clippy::used_underscore_binding)] fn clone(&self) -> Self {
269 Self {
270 reason: self.reason,
271 system_fonts: self.system_fonts,
272 image_cache: self.image_cache,
273 window_theme: self.window_theme,
274 bounds: self.bounds,
275 scroll_size: self.scroll_size,
276 scroll_offset: self.scroll_offset,
277 virtual_scroll_size: self.virtual_scroll_size,
278 virtual_scroll_offset: self.virtual_scroll_offset,
279 callable_ptr: self.callable_ptr,
280 measure_dom_fn: self.measure_dom_fn,
281 measure_dom_ctx: self.measure_dom_ctx,
282 _abi_mut: self._abi_mut,
283 }
284 }
285}
286
287impl VirtualViewCallbackInfo {
288 #[must_use] pub const fn new<'a>(
289 reason: VirtualViewCallbackReason,
290 system_fonts: &'a FcFontCache,
291 image_cache: &'a ImageCache,
292 window_theme: WindowTheme,
293 bounds: HidpiAdjustedBounds,
294 scroll_size: LogicalSize,
295 scroll_offset: LogicalPosition,
296 virtual_scroll_size: LogicalSize,
297 virtual_scroll_offset: LogicalPosition,
298 ) -> Self {
299 Self {
300 reason,
301 system_fonts: core::ptr::from_ref::<FcFontCache>(system_fonts),
302 image_cache: core::ptr::from_ref::<ImageCache>(image_cache),
303 window_theme,
304 bounds,
305 scroll_size,
306 scroll_offset,
307 virtual_scroll_size,
308 virtual_scroll_offset,
309 callable_ptr: core::ptr::null(),
310 measure_dom_fn: core::ptr::null(),
311 measure_dom_ctx: core::ptr::null_mut(),
312 _abi_mut: core::ptr::null_mut(),
313 }
314 }
315
316 pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
318 self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
319 }
320
321 pub fn set_measure_dom_fn(&mut self, f: MeasureDomFn, ctx: *mut c_void) {
324 self.measure_dom_fn = f as *const c_void;
325 self.measure_dom_ctx = ctx;
326 }
327
328 #[must_use] pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
341 if self.measure_dom_fn.is_null() {
342 return LogicalSize::zero();
343 }
344 let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
347 let mut dom = core::mem::ManuallyDrop::new(dom);
348 f(self.measure_dom_ctx, core::ptr::from_mut::<Dom>(&mut dom), available)
349 }
350
351 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
353 if self.callable_ptr.is_null() {
354 OptionRefAny::None
355 } else {
356 unsafe { (*self.callable_ptr).clone() }
357 }
358 }
359
360 #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
361 self.bounds
362 }
363
364 const fn internal_get_system_fonts(&self) -> &FcFontCache {
365 unsafe { &*self.system_fonts }
366 }
367 const fn internal_get_image_cache(&self) -> &ImageCache {
368 unsafe { &*self.image_cache }
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
384#[repr(C)]
385pub struct VirtualViewReturn {
386 pub dom: OptionDom,
394
395 pub scroll_size: LogicalSize,
403
404 pub scroll_offset: LogicalPosition,
413
414 pub virtual_scroll_size: LogicalSize,
422
423 pub virtual_scroll_offset: LogicalPosition,
428}
429
430impl Default for VirtualViewReturn {
431 fn default() -> Self {
432 Self {
433 dom: OptionDom::None,
434 scroll_size: LogicalSize::zero(),
435 scroll_offset: LogicalPosition::zero(),
436 virtual_scroll_size: LogicalSize::zero(),
437 virtual_scroll_offset: LogicalPosition::zero(),
438 }
439 }
440}
441
442impl VirtualViewReturn {
443 #[must_use] pub const fn with_dom(
454 dom: Dom,
455 scroll_size: LogicalSize,
456 scroll_offset: LogicalPosition,
457 virtual_scroll_size: LogicalSize,
458 virtual_scroll_offset: LogicalPosition,
459 ) -> Self {
460 Self {
461 dom: OptionDom::Some(dom),
462 scroll_size,
463 scroll_offset,
464 virtual_scroll_size,
465 virtual_scroll_offset,
466 }
467 }
468
469 #[must_use] pub const fn keep_current(
481 scroll_size: LogicalSize,
482 scroll_offset: LogicalPosition,
483 virtual_scroll_size: LogicalSize,
484 virtual_scroll_offset: LogicalPosition,
485 ) -> Self {
486 Self {
487 dom: OptionDom::None,
488 scroll_size,
489 scroll_offset,
490 virtual_scroll_size,
491 virtual_scroll_offset,
492 }
493 }
494
495}
496
497#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
502#[repr(C)]
503pub struct TimerCallbackReturn {
504 pub should_update: Update,
505 pub should_terminate: TerminateTimer,
506}
507
508impl TimerCallbackReturn {
509 #[must_use] pub const fn create(should_update: Update, should_terminate: TerminateTimer) -> Self {
511 Self {
512 should_update,
513 should_terminate,
514 }
515 }
516
517 #[must_use] pub const fn continue_unchanged() -> Self {
519 Self {
520 should_update: Update::DoNothing,
521 should_terminate: TerminateTimer::Continue,
522 }
523 }
524
525 #[must_use] pub const fn continue_and_refresh_dom() -> Self {
527 Self {
528 should_update: Update::RefreshDom,
529 should_terminate: TerminateTimer::Continue,
530 }
531 }
532
533 #[must_use] pub const fn terminate_unchanged() -> Self {
535 Self {
536 should_update: Update::DoNothing,
537 should_terminate: TerminateTimer::Terminate,
538 }
539 }
540
541 #[must_use] pub const fn terminate_and_refresh_dom() -> Self {
543 Self {
544 should_update: Update::RefreshDom,
545 should_terminate: TerminateTimer::Terminate,
546 }
547 }
548}
549
550impl Default for TimerCallbackReturn {
551 fn default() -> Self {
552 Self::continue_unchanged()
553 }
554}
555
556#[derive(Debug)]
560#[repr(C)]
561pub struct LayoutCallbackInfoRefData<'a> {
570 pub image_cache: &'a ImageCache,
572 pub gl_context: &'a OptionGlContextPtr,
574 pub system_fonts: &'a FcFontCache,
576 pub system_style: Arc<SystemStyle>,
579 pub active_route: Option<&'a crate::resources::RouteMatch>,
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
593#[repr(C)]
594#[derive(Default)]
595pub enum RelayoutReason {
596 #[default]
598 Initial,
599 RefreshDom,
601 Resize,
605 ThemeChange,
607 RouteChange,
611 Other,
613}
614
615
616#[repr(C)]
617pub struct LayoutCallbackInfo {
618 ref_data: *const LayoutCallbackInfoRefData<'static>,
621 pub window_size: WindowSize,
625 pub theme: WindowTheme,
627 pub relayout_reason: RelayoutReason,
629 callable_ptr: *const OptionRefAny,
632 _abi_mut: *mut c_void,
634}
635
636impl Clone for LayoutCallbackInfo {
637 #[allow(clippy::used_underscore_binding)] fn clone(&self) -> Self {
639 Self {
640 ref_data: self.ref_data,
641 window_size: self.window_size,
642 theme: self.theme,
643 relayout_reason: self.relayout_reason,
644 callable_ptr: self.callable_ptr,
645 _abi_mut: self._abi_mut,
646 }
647 }
648}
649
650impl core::fmt::Debug for LayoutCallbackInfo {
651 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
652 f.debug_struct("LayoutCallbackInfo")
653 .field("window_size", &self.window_size)
654 .field("theme", &self.theme)
655 .field("relayout_reason", &self.relayout_reason)
656 .finish_non_exhaustive()
657 }
658}
659
660impl LayoutCallbackInfo {
661 #[must_use] pub const fn new<'a>(
662 ref_data: &'a LayoutCallbackInfoRefData<'a>,
663 window_size: WindowSize,
664 theme: WindowTheme,
665 ) -> Self {
666 Self::new_with_reason(ref_data, window_size, theme, RelayoutReason::Initial)
667 }
668
669 #[allow(clippy::unnecessary_cast)]
672 #[must_use] pub const fn new_with_reason<'a>(
673 ref_data: &'a LayoutCallbackInfoRefData<'a>,
674 window_size: WindowSize,
675 theme: WindowTheme,
676 relayout_reason: RelayoutReason,
677 ) -> Self {
678 Self {
679 ref_data: core::ptr::from_ref::<LayoutCallbackInfoRefData<'a>>(ref_data)
682 as *const LayoutCallbackInfoRefData<'static>,
683 window_size,
684 theme,
685 relayout_reason,
686 callable_ptr: core::ptr::null(),
687 _abi_mut: core::ptr::null_mut(),
688 }
689 }
690
691 #[must_use] pub const fn relayout_reason(&self) -> RelayoutReason {
693 self.relayout_reason
694 }
695
696 pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
698 self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
699 }
700
701 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
703 if self.callable_ptr.is_null() {
704 OptionRefAny::None
705 } else {
706 unsafe { (*self.callable_ptr).clone() }
707 }
708 }
709
710 #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
712 unsafe { (*self.ref_data).system_style.clone() }
713 }
714
715 const fn internal_get_image_cache(&self) -> &ImageCache {
716 unsafe { (*self.ref_data).image_cache }
717 }
718 const fn internal_get_system_fonts(&self) -> &FcFontCache {
719 unsafe { (*self.ref_data).system_fonts }
720 }
721 const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
722 unsafe { (*self.ref_data).gl_context }
723 }
724
725 #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
726 self.internal_get_gl_context().clone()
727 }
728
729 #[must_use] pub fn get_system_fonts(&self) -> Vec<AzStringPair> {
730 let fc_cache = self.internal_get_system_fonts();
731
732 fc_cache
733 .list()
734 .into_iter()
735 .filter_map(|(pattern, font_id)| {
736 let source = fc_cache.get_font_by_id(&font_id)?;
737 match source {
738 OwnedFontSource::Memory(_) => None,
739 OwnedFontSource::Disk(d) => Some((pattern.name.as_ref()?.clone(), d.path)),
740 }
741 })
742 .map(|(k, v)| AzStringPair {
743 key: k.into(),
744 value: v.into(),
745 })
746 .collect()
747 }
748
749 #[must_use] pub fn get_image(&self, image_id: &AzString) -> Option<ImageRef> {
750 self.internal_get_image_cache()
751 .get_css_image_id(image_id)
752 .cloned()
753 }
754
755 #[must_use] pub const fn get_active_route(&self) -> Option<&crate::resources::RouteMatch> {
759 unsafe { (*self.ref_data).active_route }
760 }
761
762 #[must_use] pub fn get_route_param(&self, key: &str) -> Option<&AzString> {
766 self.get_active_route()?.get_param(key)
767 }
768
769 #[must_use] pub fn window_width_less_than(&self, px: f32) -> bool {
772 self.window_size.dimensions.width < px
773 }
774
775 #[must_use] pub fn window_width_greater_than(&self, px: f32) -> bool {
777 self.window_size.dimensions.width > px
778 }
779
780 #[must_use] pub fn window_width_between(&self, min_px: f32, max_px: f32) -> bool {
782 let width = self.window_size.dimensions.width;
783 width >= min_px && width <= max_px
784 }
785
786 #[must_use] pub fn window_height_less_than(&self, px: f32) -> bool {
788 self.window_size.dimensions.height < px
789 }
790
791 #[must_use] pub fn window_height_greater_than(&self, px: f32) -> bool {
793 self.window_size.dimensions.height > px
794 }
795
796 #[must_use] pub fn window_height_between(&self, min_px: f32, max_px: f32) -> bool {
798 let height = self.window_size.dimensions.height;
799 height >= min_px && height <= max_px
800 }
801
802 #[must_use] pub const fn get_window_width(&self) -> f32 {
804 self.window_size.dimensions.width
805 }
806
807 #[must_use] pub const fn get_window_height(&self) -> f32 {
809 self.window_size.dimensions.height
810 }
811
812 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_dpi_factor(&self) -> f32 {
815 self.window_size.dpi as f32 / 96.0
816 }
817}
818
819#[derive(Debug, Copy, Clone)]
824#[repr(C)]
825pub struct HidpiAdjustedBounds {
826 pub logical_size: LogicalSize,
827 pub hidpi_factor: DpiScaleFactor,
828}
829
830impl HidpiAdjustedBounds {
831 #[inline]
832 #[allow(clippy::cast_precision_loss)] #[must_use] pub const fn from_bounds(bounds: LayoutSize, hidpi_factor: DpiScaleFactor) -> Self {
834 let logical_size = LogicalSize::new(bounds.width as f32, bounds.height as f32);
835 Self {
836 logical_size,
837 hidpi_factor,
838 }
839 }
840
841 #[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
842 self.get_logical_size()
843 .to_physical(self.get_hidpi_factor().inner.get())
844 }
845
846 #[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
847 self.logical_size
848 }
849
850 #[must_use] pub const fn get_hidpi_factor(&self) -> DpiScaleFactor {
851 self.hidpi_factor
852 }
853}
854
855#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
857#[repr(C, u8)]
858pub enum FocusTarget {
859 Id(DomNodeId),
860 Path(FocusTargetPath),
861 Previous,
862 Next,
863 First,
864 Last,
865 NoFocus,
866}
867
868#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
869#[repr(C)]
870pub struct FocusTargetPath {
871 pub dom: DomId,
872 pub css_path: CssPath,
873}
874
875pub type CoreCallbackType = usize;
899
900#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
909#[repr(C)]
910pub struct CoreCallback {
911 pub cb: CoreCallbackType,
912 pub ctx: OptionRefAny,
915}
916
917impl From<CoreCallbackType> for CoreCallback {
920 fn from(cb: CoreCallbackType) -> Self {
921 Self {
922 cb,
923 ctx: OptionRefAny::None,
924 }
925 }
926}
927
928impl_option!(
929 CoreCallback,
930 OptionCoreCallback,
931 [Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash]
932);
933
934#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
936#[repr(C)]
937pub struct CoreCallbackData {
938 pub event: EventFilter,
939 pub callback: CoreCallback,
940 pub refany: RefAny,
941}
942
943impl_option!(
944 CoreCallbackData,
945 OptionCoreCallbackData,
946 copy = false,
947 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
948);
949
950impl_vec!(CoreCallbackData, CoreCallbackDataVec, CoreCallbackDataVecDestructor, CoreCallbackDataVecDestructorType, CoreCallbackDataVecSlice, OptionCoreCallbackData);
951impl_vec_clone!(
952 CoreCallbackData,
953 CoreCallbackDataVec,
954 CoreCallbackDataVecDestructor
955);
956impl_vec_mut!(CoreCallbackData, CoreCallbackDataVec);
957impl_vec_debug!(CoreCallbackData, CoreCallbackDataVec);
958impl_vec_partialord!(CoreCallbackData, CoreCallbackDataVec);
959impl_vec_ord!(CoreCallbackData, CoreCallbackDataVec);
960impl_vec_partialeq!(CoreCallbackData, CoreCallbackDataVec);
961impl_vec_eq!(CoreCallbackData, CoreCallbackDataVec);
962impl_vec_hash!(CoreCallbackData, CoreCallbackDataVec);
963
964impl CoreCallbackDataVec {
965 #[inline]
966 #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CoreCallbackData> {
967 NodeDataContainerRef {
968 internal: self.as_ref(),
969 }
970 }
971 #[inline]
972 pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, CoreCallbackData> {
973 NodeDataContainerRefMut {
974 internal: self.as_mut(),
975 }
976 }
977}
978
979pub type CoreRenderImageCallbackType = usize;
983
984#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
986#[repr(C)]
987pub struct CoreRenderImageCallback {
988 pub cb: CoreRenderImageCallbackType,
989 pub ctx: OptionRefAny,
992}
993
994impl From<CoreRenderImageCallbackType> for CoreRenderImageCallback {
997 fn from(cb: CoreRenderImageCallbackType) -> Self {
998 Self {
999 cb,
1000 ctx: OptionRefAny::None,
1001 }
1002 }
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1007#[repr(C)]
1008pub struct CoreImageCallback {
1009 pub refany: RefAny,
1010 pub callback: CoreRenderImageCallback,
1011}
1012
1013impl_option!(
1014 CoreImageCallback,
1015 OptionCoreImageCallback,
1016 copy = false,
1017 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1018);
1019
1020#[cfg(test)]
1021#[allow(
1022 clippy::float_cmp,
1023 clippy::too_many_lines,
1024 clippy::cast_precision_loss,
1025 clippy::unusual_byte_groupings
1026)]
1027mod autotest_generated {
1028 use alloc::string::String;
1029
1030 use super::*;
1031 use crate::{
1032 events::HoverEventFilter,
1033 resources::{RawImageFormat, RouteMatch},
1034 window::StringPairVec,
1035 };
1036
1037 fn s(v: &str) -> AzString {
1040 AzString::from(String::from(v))
1041 }
1042
1043 fn win(width: f32, height: f32, dpi: u32) -> WindowSize {
1044 WindowSize {
1045 dimensions: LogicalSize::new(width, height),
1046 dpi,
1047 min_dimensions: None.into(),
1048 max_dimensions: None.into(),
1049 }
1050 }
1051
1052 struct Fixture {
1056 fonts: FcFontCache,
1057 images: ImageCache,
1058 style: Arc<SystemStyle>,
1059 gl: OptionGlContextPtr,
1060 route: Option<RouteMatch>,
1061 }
1062
1063 impl Fixture {
1064 fn new() -> Self {
1065 Self {
1066 fonts: FcFontCache::default(),
1067 images: ImageCache::default(),
1068 style: Arc::new(SystemStyle::default()),
1069 gl: OptionGlContextPtr::None,
1070 route: None,
1071 }
1072 }
1073
1074 fn with_route(route: RouteMatch) -> Self {
1075 let mut f = Self::new();
1076 f.route = Some(route);
1077 f
1078 }
1079
1080 fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
1081 LayoutCallbackInfoRefData {
1082 image_cache: &self.images,
1083 gl_context: &self.gl,
1084 system_fonts: &self.fonts,
1085 system_style: self.style.clone(),
1086 active_route: self.route.as_ref(),
1087 }
1088 }
1089 }
1090
1091 fn user_route() -> RouteMatch {
1093 RouteMatch {
1094 pattern: s("/user/:id"),
1095 params: StringPairVec::from_vec(Vec::from([
1096 AzStringPair {
1097 key: s("id"),
1098 value: s("42"),
1099 },
1100 AzStringPair {
1101 key: s("\u{1F600}"),
1102 value: s("emoji"),
1103 },
1104 ])),
1105 }
1106 }
1107
1108 fn vv_info<'a>(
1109 fonts: &'a FcFontCache,
1110 images: &'a ImageCache,
1111 bounds: HidpiAdjustedBounds,
1112 ) -> VirtualViewCallbackInfo {
1113 VirtualViewCallbackInfo::new(
1114 VirtualViewCallbackReason::InitialRender,
1115 fonts,
1116 images,
1117 WindowTheme::LightMode,
1118 bounds,
1119 LogicalSize::new(100.0, 200.0),
1120 LogicalPosition::new(1.0, 2.0),
1121 LogicalSize::new(1000.0, 2000.0),
1122 LogicalPosition::new(3.0, 4.0),
1123 )
1124 }
1125
1126 fn bounds_1x1() -> HidpiAdjustedBounds {
1127 HidpiAdjustedBounds::from_bounds(LayoutSize::new(1, 1), DpiScaleFactor::new(1.0))
1128 }
1129
1130 const ALL_UPDATES: [Update; 3] = [
1133 Update::DoNothing,
1134 Update::RefreshDom,
1135 Update::RefreshDomAllWindows,
1136 ];
1137
1138 #[test]
1142 fn update_max_self_is_exhaustively_ord_max() {
1143 for a in ALL_UPDATES {
1144 for b in ALL_UPDATES {
1145 let mut got = a;
1146 got.max_self(b);
1147 assert_eq!(
1148 got,
1149 core::cmp::max(a, b),
1150 "max_self({a:?}, {b:?}) disagrees with Ord::max"
1151 );
1152 }
1153 }
1154 }
1155
1156 #[test]
1157 fn update_max_self_is_idempotent_and_monotone() {
1158 for a in ALL_UPDATES {
1159 let mut got = a;
1161 got.max_self(a);
1162 assert_eq!(got, a);
1163
1164 let mut top = Update::RefreshDomAllWindows;
1166 top.max_self(a);
1167 assert_eq!(top, Update::RefreshDomAllWindows);
1168
1169 let mut m = a;
1171 m.max_self(Update::DoNothing);
1172 assert!(m >= a);
1173 }
1174 }
1175
1176 #[test]
1180 fn update_max_self_fold_is_order_independent() {
1181 for a in ALL_UPDATES {
1182 for b in ALL_UPDATES {
1183 for c in ALL_UPDATES {
1184 let mut fwd = a;
1185 fwd.max_self(b);
1186 fwd.max_self(c);
1187
1188 let mut rev = c;
1189 rev.max_self(b);
1190 rev.max_self(a);
1191
1192 assert_eq!(fwd, rev, "fold of {a:?},{b:?},{c:?} is order-dependent");
1193 }
1194 }
1195 }
1196 }
1197
1198 static ALT_LAYOUT_CALLS: AtomicUsize = AtomicUsize::new(0);
1201
1202 extern "C" fn alt_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
1206 ALT_LAYOUT_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1207 Dom::create_body()
1208 }
1209
1210 #[test]
1211 fn default_layout_callback_returns_body_and_does_not_panic() {
1212 let fx = Fixture::new();
1213 let rd = fx.ref_data();
1214 let info = LayoutCallbackInfo::new(&rd, win(0.0, 0.0, 0), WindowTheme::DarkMode);
1215
1216 let dom = default_layout_callback(RefAny::new(0u32), info);
1218 assert_eq!(dom, Dom::create_body());
1219 }
1220
1221 #[test]
1222 fn layout_callback_create_stores_the_given_fn_and_null_ctx() {
1223 let from_default = LayoutCallback::create(default_layout_callback as LayoutCallbackType);
1224 assert!(
1225 from_default.ctx.is_none(),
1226 "native-Rust create() must leave the FFI ctx empty"
1227 );
1228 assert_eq!(from_default, LayoutCallback::default());
1229
1230 let from_alt = LayoutCallback::create(alt_layout_callback as LayoutCallbackType);
1233 assert!(from_alt.ctx.is_none());
1234 assert_ne!(
1235 from_alt, from_default,
1236 "create() ignored its argument (or the two fns were ICF-folded)"
1237 );
1238
1239 let fx = Fixture::new();
1241 let rd = fx.ref_data();
1242 let before = ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst);
1243 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1244 let _ = (from_alt.cb)(RefAny::new(()), info);
1245 assert_eq!(ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst), before + 1);
1246 }
1247
1248 extern "C" fn vv_keep_current_cb(_: RefAny, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
1251 VirtualViewReturn::keep_current(
1252 info.scroll_size,
1253 info.scroll_offset,
1254 info.virtual_scroll_size,
1255 info.virtual_scroll_offset,
1256 )
1257 }
1258
1259 #[test]
1260 fn virtual_view_callback_create_round_trips_through_the_fn_ptr() {
1261 let cb = VirtualViewCallback::create(vv_keep_current_cb as VirtualViewCallbackType);
1262 assert!(cb.ctx.is_none());
1263
1264 let fonts = FcFontCache::default();
1265 let images = ImageCache::default();
1266 let info = vv_info(&fonts, &images, bounds_1x1());
1267
1268 let ret = (cb.cb)(RefAny::new(0u8), info);
1269 assert!(ret.dom.is_none());
1270 assert_eq!(ret.scroll_size, LogicalSize::new(100.0, 200.0));
1271 assert_eq!(ret.scroll_offset, LogicalPosition::new(1.0, 2.0));
1272 assert_eq!(ret.virtual_scroll_size, LogicalSize::new(1000.0, 2000.0));
1273 assert_eq!(ret.virtual_scroll_offset, LogicalPosition::new(3.0, 4.0));
1274 }
1275
1276 #[test]
1279 fn virtual_view_callback_info_new_holds_its_fields() {
1280 let fonts = FcFontCache::default();
1281 let images = ImageCache::default();
1282 let bounds = HidpiAdjustedBounds::from_bounds(
1283 LayoutSize::new(800, 600),
1284 DpiScaleFactor::new(2.0),
1285 );
1286 let info = vv_info(&fonts, &images, bounds);
1287
1288 assert_eq!(info.reason, VirtualViewCallbackReason::InitialRender);
1289 assert_eq!(info.window_theme, WindowTheme::LightMode);
1290 assert_eq!(info.get_bounds().get_logical_size(), LogicalSize::new(800.0, 600.0));
1291 assert_eq!(info.get_bounds().get_hidpi_factor(), DpiScaleFactor::new(2.0));
1292 assert_eq!(info.scroll_size, LogicalSize::new(100.0, 200.0));
1293
1294 assert!(core::ptr::eq(info.internal_get_system_fonts(), &fonts));
1296 assert!(core::ptr::eq(info.internal_get_image_cache(), &images));
1297
1298 assert!(info.get_ctx().is_none());
1300 assert_eq!(
1301 info.measure_dom(Dom::create_body(), LogicalSize::new(10.0, 10.0)),
1302 LogicalSize::zero()
1303 );
1304
1305 let cloned = info.clone();
1307 assert_eq!(cloned.reason, info.reason);
1308 assert!(core::ptr::eq(cloned.internal_get_system_fonts(), &fonts));
1309 assert!(cloned.get_ctx().is_none());
1310 }
1311
1312 #[test]
1313 fn virtual_view_callback_info_new_survives_nan_and_infinite_geometry() {
1314 let fonts = FcFontCache::default();
1315 let images = ImageCache::default();
1316 let info = VirtualViewCallbackInfo::new(
1317 VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
1318 &fonts,
1319 &images,
1320 WindowTheme::DarkMode,
1321 HidpiAdjustedBounds::from_bounds(
1322 LayoutSize::new(isize::MAX, isize::MIN),
1323 DpiScaleFactor::new(f32::NAN),
1324 ),
1325 LogicalSize::new(f32::NAN, f32::INFINITY),
1326 LogicalPosition::new(f32::NEG_INFINITY, f32::MAX),
1327 LogicalSize::new(f32::MIN, 0.0),
1328 LogicalPosition::new(-0.0, f32::EPSILON),
1329 );
1330
1331 assert!(info.scroll_size.width.is_nan());
1333 assert!(info.scroll_size.height.is_infinite());
1334 assert!(info.scroll_offset.x.is_infinite() && info.scroll_offset.x.is_sign_negative());
1335 assert_eq!(info.virtual_scroll_size.width, f32::MIN);
1336 assert_eq!(info.reason, VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
1337
1338 assert!(info.get_ctx().is_none());
1340 assert!(info.get_bounds().get_logical_size().width > 0.0);
1341 }
1342
1343 #[test]
1344 fn virtual_view_callback_info_get_ctx_clones_without_double_free() {
1345 let fonts = FcFontCache::default();
1346 let images = ImageCache::default();
1347 let mut info = vv_info(&fonts, &images, bounds_1x1());
1348
1349 assert!(info.get_ctx().is_none());
1351
1352 let callable = OptionRefAny::Some(RefAny::new(0xDEAD_BEEF_u32));
1353 info.set_callable_ptr(&callable);
1354
1355 for _ in 0..64 {
1358 let got = info.get_ctx();
1359 assert!(got.is_some());
1360 drop(got);
1361 }
1362
1363 let mut got = info.get_ctx();
1364 match got {
1365 OptionRefAny::Some(ref mut r) => {
1366 let inner = r.downcast_ref::<u32>().expect("ctx should hold a u32");
1367 assert_eq!(*inner, 0xDEAD_BEEF_u32);
1368 }
1369 OptionRefAny::None => panic!("callable_ptr was set, get_ctx() returned None"),
1370 }
1371 drop(got);
1372
1373 let mut orig = callable;
1375 match orig {
1376 OptionRefAny::Some(ref mut r) => {
1377 assert_eq!(*r.downcast_ref::<u32>().unwrap(), 0xDEAD_BEEF_u32);
1378 }
1379 OptionRefAny::None => panic!("original callable was consumed"),
1380 }
1381 }
1382
1383 static MEASURE_CALLS: AtomicUsize = AtomicUsize::new(0);
1386
1387 extern "C" fn test_measure_dom_fn(
1390 ctx: *mut c_void,
1391 dom: *mut Dom,
1392 available: LogicalSize,
1393 ) -> LogicalSize {
1394 MEASURE_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1395 let dom = unsafe { core::ptr::read(dom) };
1398 drop(dom);
1399 if !ctx.is_null() {
1400 unsafe {
1402 *ctx.cast::<u32>() = 0xABCD;
1403 }
1404 }
1405 LogicalSize::new(available.width * 2.0, available.height / 2.0)
1406 }
1407
1408 #[test]
1409 fn measure_dom_without_hook_returns_zero_for_every_input() {
1410 let fonts = FcFontCache::default();
1411 let images = ImageCache::default();
1412 let info = vv_info(&fonts, &images, bounds_1x1());
1413
1414 for available in [
1417 LogicalSize::zero(),
1418 LogicalSize::new(-1.0, -1.0),
1419 LogicalSize::new(f32::NAN, f32::NAN),
1420 LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
1421 LogicalSize::new(f32::MAX, f32::MIN),
1422 LogicalSize::new(1.0, 1_000_000.0),
1423 ] {
1424 assert_eq!(
1425 info.measure_dom(Dom::create_body(), available),
1426 LogicalSize::zero()
1427 );
1428 }
1429 }
1430
1431 #[test]
1432 fn measure_dom_with_hook_forwards_ctx_and_available_and_consumes_the_dom() {
1433 let fonts = FcFontCache::default();
1434 let images = ImageCache::default();
1435 let mut info = vv_info(&fonts, &images, bounds_1x1());
1436
1437 let mut ctx_val: u32 = 0;
1438 info.set_measure_dom_fn(
1439 test_measure_dom_fn,
1440 core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
1441 );
1442
1443 let before = MEASURE_CALLS.load(AtomicOrdering::SeqCst);
1446 let out = info.measure_dom(Dom::create_body(), LogicalSize::new(100.0, 40.0));
1447
1448 assert!(MEASURE_CALLS.load(AtomicOrdering::SeqCst) > before);
1449 assert_eq!(out, LogicalSize::new(200.0, 20.0));
1450 assert_eq!(ctx_val, 0xABCD, "measure ctx pointer was not forwarded");
1451
1452 let natural = info.measure_dom(Dom::create_body(), LogicalSize::new(320.0, 1_000_000.0));
1454 assert_eq!(natural, LogicalSize::new(640.0, 500_000.0));
1455
1456 let nan = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::NAN, 4.0));
1459 assert!(nan.width.is_nan());
1460 assert_eq!(nan.height, 2.0);
1461
1462 let inf = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::INFINITY, 4.0));
1463 assert!(inf.width.is_infinite());
1464 }
1465
1466 #[test]
1467 fn measure_dom_hook_can_be_replaced_and_last_writer_wins() {
1468 let fonts = FcFontCache::default();
1469 let images = ImageCache::default();
1470 let mut info = vv_info(&fonts, &images, bounds_1x1());
1471
1472 info.set_measure_dom_fn(test_measure_dom_fn, core::ptr::null_mut());
1473 let first = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
1475 assert_eq!(first, LogicalSize::new(4.0, 4.0));
1476
1477 let mut ctx_val: u32 = 0;
1478 info.set_measure_dom_fn(
1479 test_measure_dom_fn,
1480 core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
1481 );
1482 let second = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
1483 assert_eq!(second, first);
1484 assert_eq!(ctx_val, 0xABCD);
1485 }
1486
1487 #[test]
1490 fn virtual_view_return_with_dom_and_keep_current_hold_their_fields() {
1491 let ss = LogicalSize::new(600.0, 30.0);
1492 let so = LogicalPosition::new(0.0, 300.0);
1493 let vss = LogicalSize::new(600.0, 30_000.0);
1494 let vso = LogicalPosition::zero();
1495
1496 let with = VirtualViewReturn::with_dom(Dom::create_body(), ss, so, vss, vso);
1497 assert!(with.dom.is_some(), "with_dom must produce OptionDom::Some");
1498 assert_eq!(with.scroll_size, ss);
1499 assert_eq!(with.scroll_offset, so);
1500 assert_eq!(with.virtual_scroll_size, vss);
1501 assert_eq!(with.virtual_scroll_offset, vso);
1502 assert_eq!(with.dom, OptionDom::Some(Dom::create_body()));
1503
1504 let keep = VirtualViewReturn::keep_current(ss, so, vss, vso);
1505 assert!(keep.dom.is_none(), "keep_current must produce OptionDom::None");
1506 assert_eq!(keep.scroll_size, ss);
1507 assert_eq!(keep.scroll_offset, so);
1508 assert_eq!(keep.virtual_scroll_size, vss);
1509 assert_eq!(keep.virtual_scroll_offset, vso);
1510
1511 assert_ne!(with, keep);
1513
1514 let d = VirtualViewReturn::default();
1516 assert_eq!(
1517 d,
1518 VirtualViewReturn::keep_current(
1519 LogicalSize::zero(),
1520 LogicalPosition::zero(),
1521 LogicalSize::zero(),
1522 LogicalPosition::zero()
1523 )
1524 );
1525 }
1526
1527 #[test]
1528 fn virtual_view_return_keep_current_passes_extreme_values_through_unclamped() {
1529 let z = VirtualViewReturn::keep_current(
1531 LogicalSize::zero(),
1532 LogicalPosition::zero(),
1533 LogicalSize::zero(),
1534 LogicalPosition::zero(),
1535 );
1536 assert_eq!(z.scroll_size, LogicalSize::zero());
1537 assert_eq!(z.virtual_scroll_size, LogicalSize::zero());
1538
1539 let n = VirtualViewReturn::keep_current(
1541 LogicalSize::new(-1.0, -0.0),
1542 LogicalPosition::new(f32::MIN, f32::MAX),
1543 LogicalSize::new(f32::MAX, f32::MIN_POSITIVE),
1544 LogicalPosition::new(-f32::EPSILON, 0.0),
1545 );
1546 assert_eq!(n.scroll_size.width, -1.0);
1547 assert_eq!(n.scroll_offset.x, f32::MIN);
1548 assert_eq!(n.scroll_offset.y, f32::MAX);
1549 assert_eq!(n.virtual_scroll_size.width, f32::MAX);
1550 assert_eq!(n.virtual_scroll_size.height, f32::MIN_POSITIVE);
1551
1552 let x = VirtualViewReturn::keep_current(
1555 LogicalSize::new(f32::NAN, f32::INFINITY),
1556 LogicalPosition::new(f32::NEG_INFINITY, f32::NAN),
1557 LogicalSize::new(f32::INFINITY, f32::NAN),
1558 LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
1559 );
1560 assert!(x.scroll_size.width.is_nan());
1561 assert!(x.scroll_size.height.is_infinite() && x.scroll_size.height.is_sign_positive());
1562 assert!(x.scroll_offset.x.is_infinite() && x.scroll_offset.x.is_sign_negative());
1563 assert!(x.scroll_offset.y.is_nan());
1564 assert!(x.virtual_scroll_offset.y.is_infinite());
1565 assert!(x.dom.is_none());
1566 }
1567
1568 #[test]
1571 fn timer_callback_return_constructors_match_their_documented_flags() {
1572 let c = TimerCallbackReturn::continue_unchanged();
1573 assert_eq!(c.should_update, Update::DoNothing);
1574 assert_eq!(c.should_terminate, TerminateTimer::Continue);
1575
1576 let cr = TimerCallbackReturn::continue_and_refresh_dom();
1577 assert_eq!(cr.should_update, Update::RefreshDom);
1578 assert_eq!(cr.should_terminate, TerminateTimer::Continue);
1579
1580 let t = TimerCallbackReturn::terminate_unchanged();
1581 assert_eq!(t.should_update, Update::DoNothing);
1582 assert_eq!(t.should_terminate, TerminateTimer::Terminate);
1583
1584 let tr = TimerCallbackReturn::terminate_and_refresh_dom();
1585 assert_eq!(tr.should_update, Update::RefreshDom);
1586 assert_eq!(tr.should_terminate, TerminateTimer::Terminate);
1587
1588 let all = [c, cr, t, tr];
1590 for (i, a) in all.iter().enumerate() {
1591 for (j, b) in all.iter().enumerate() {
1592 assert_eq!(i == j, a == b, "constructors {i} and {j} collide");
1593 }
1594 }
1595
1596 assert_eq!(TimerCallbackReturn::default(), c);
1598 }
1599
1600 #[test]
1601 fn timer_callback_return_create_round_trips_every_flag_combination() {
1602 for u in ALL_UPDATES {
1603 for t in [TerminateTimer::Continue, TerminateTimer::Terminate] {
1604 let r = TimerCallbackReturn::create(u, t);
1605 assert_eq!(r.should_update, u);
1606 assert_eq!(r.should_terminate, t);
1607 }
1608 }
1609
1610 assert_eq!(
1612 TimerCallbackReturn::create(Update::DoNothing, TerminateTimer::Continue),
1613 TimerCallbackReturn::continue_unchanged()
1614 );
1615 assert_eq!(
1616 TimerCallbackReturn::create(Update::RefreshDom, TerminateTimer::Terminate),
1617 TimerCallbackReturn::terminate_and_refresh_dom()
1618 );
1619
1620 let all_windows =
1623 TimerCallbackReturn::create(Update::RefreshDomAllWindows, TerminateTimer::Terminate);
1624 assert_eq!(all_windows.should_update, Update::RefreshDomAllWindows);
1625 }
1626
1627 #[test]
1630 fn layout_callback_info_new_defaults_to_initial_reason_and_holds_fields() {
1631 let fx = Fixture::new();
1632 let rd = fx.ref_data();
1633 let info = LayoutCallbackInfo::new(&rd, win(1280.0, 720.0, 192), WindowTheme::DarkMode);
1634
1635 assert_eq!(info.relayout_reason(), RelayoutReason::Initial);
1636 assert_eq!(info.theme, WindowTheme::DarkMode);
1637 assert_eq!(info.get_window_width(), 1280.0);
1638 assert_eq!(info.get_window_height(), 720.0);
1639 assert_eq!(info.get_dpi_factor(), 2.0);
1640 assert!(info.get_ctx().is_none());
1641
1642 assert!(core::ptr::eq(info.internal_get_image_cache(), &fx.images));
1644 assert!(core::ptr::eq(info.internal_get_system_fonts(), &fx.fonts));
1645 assert!(core::ptr::eq(info.internal_get_gl_context(), &fx.gl));
1646 assert!(info.get_gl_context().is_none());
1647 }
1648
1649 #[test]
1650 fn layout_callback_info_new_with_reason_round_trips_every_reason() {
1651 let fx = Fixture::new();
1652 let rd = fx.ref_data();
1653
1654 for reason in [
1655 RelayoutReason::Initial,
1656 RelayoutReason::RefreshDom,
1657 RelayoutReason::Resize,
1658 RelayoutReason::ThemeChange,
1659 RelayoutReason::RouteChange,
1660 RelayoutReason::Other,
1661 ] {
1662 let info = LayoutCallbackInfo::new_with_reason(
1663 &rd,
1664 WindowSize::default(),
1665 WindowTheme::LightMode,
1666 reason,
1667 );
1668 assert_eq!(info.relayout_reason(), reason);
1669 assert_eq!(info.clone().relayout_reason(), reason);
1671 }
1672
1673 assert_eq!(RelayoutReason::default(), RelayoutReason::Initial);
1674 }
1675
1676 #[test]
1677 fn layout_callback_info_get_system_style_shares_the_arc() {
1678 let fx = Fixture::new();
1679 let rd = fx.ref_data();
1680 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1681
1682 let a = info.get_system_style();
1683 let b = info.get_system_style();
1684 assert!(Arc::ptr_eq(&a, &b));
1686 assert!(Arc::ptr_eq(&a, &fx.style));
1687
1688 let before = Arc::strong_count(&fx.style);
1690 for _ in 0..128 {
1691 drop(info.get_system_style());
1692 }
1693 assert_eq!(Arc::strong_count(&fx.style), before);
1694 }
1695
1696 #[test]
1697 fn layout_callback_info_get_ctx_is_none_until_set_then_clones_safely() {
1698 let fx = Fixture::new();
1699 let rd = fx.ref_data();
1700 let mut info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1701
1702 assert!(info.get_ctx().is_none(), "native path must have a null ctx");
1703
1704 let callable = OptionRefAny::Some(RefAny::new(7u64));
1705 info.set_callable_ptr(&callable);
1706
1707 for _ in 0..64 {
1708 assert!(info.get_ctx().is_some());
1709 }
1710
1711 let mut got = info.get_ctx();
1712 match got {
1713 OptionRefAny::Some(ref mut r) => assert_eq!(*r.downcast_ref::<u64>().unwrap(), 7),
1714 OptionRefAny::None => panic!("get_ctx() lost the callable"),
1715 }
1716 drop(got);
1717
1718 let cloned = info.clone();
1720 assert!(cloned.get_ctx().is_some());
1721 }
1722
1723 #[test]
1724 fn layout_callback_info_get_system_fonts_is_empty_for_an_empty_cache() {
1725 let fx = Fixture::new();
1726 let rd = fx.ref_data();
1727 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1728
1729 let fonts: Vec<AzStringPair> = info.get_system_fonts();
1731 assert!(fonts.is_empty());
1732 assert_eq!(info.get_system_fonts().len(), fonts.len());
1734 }
1735
1736 #[test]
1739 fn get_image_returns_none_for_missing_empty_and_hostile_ids() {
1740 let fx = Fixture::new();
1741 let rd = fx.ref_data();
1742 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1743
1744 assert!(info.get_image(&s("")).is_none());
1745 assert!(info.get_image(&s(" ")).is_none());
1746 assert!(info.get_image(&s("nope")).is_none());
1747 assert!(info.get_image(&s("\u{1F600}\u{0301}")).is_none());
1748 assert!(info.get_image(&s("\0")).is_none());
1749 assert!(info.get_image(&s(&"x".repeat(100_000))).is_none());
1750 }
1751
1752 #[test]
1753 fn get_image_finds_an_inserted_id_and_is_exact_match() {
1754 let mut fx = Fixture::new();
1755 fx.images.add_css_image_id(
1756 s("logo"),
1757 ImageRef::null_image(2, 2, RawImageFormat::RGBA8, Vec::new()),
1758 );
1759 let rd = fx.ref_data();
1760 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1761
1762 assert!(info.get_image(&s("logo")).is_some(), "positive control");
1763
1764 assert!(info.get_image(&s("Logo")).is_none());
1766 assert!(info.get_image(&s(" logo")).is_none());
1767 assert!(info.get_image(&s("logo ")).is_none());
1768 assert!(info.get_image(&s("log")).is_none());
1769 assert!(info.get_image(&s("logos")).is_none());
1770 }
1771
1772 #[test]
1775 fn get_route_param_returns_none_when_no_route_is_active() {
1776 let fx = Fixture::new();
1777 let rd = fx.ref_data();
1778 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1779
1780 assert!(info.get_active_route().is_none());
1781
1782 for key in ["", " ", "\t\n", "id", "\u{1F600}", "\0", "../../etc/passwd"] {
1784 assert!(info.get_route_param(key).is_none(), "key {key:?}");
1785 }
1786 }
1787
1788 #[test]
1789 fn get_route_param_valid_minimal_and_unicode_positive_controls() {
1790 let fx = Fixture::with_route(user_route());
1791 let rd = fx.ref_data();
1792 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1793
1794 let route = info.get_active_route().expect("route was configured");
1795 assert_eq!(route.pattern.as_str(), "/user/:id");
1796
1797 assert_eq!(info.get_route_param("id").map(AzString::as_str), Some("42"));
1799 assert_eq!(
1801 info.get_route_param("\u{1F600}").map(AzString::as_str),
1802 Some("emoji")
1803 );
1804 }
1805
1806 #[test]
1807 fn get_route_param_rejects_malformed_keys_without_trimming_or_folding() {
1808 let fx = Fixture::with_route(user_route());
1809 let rd = fx.ref_data();
1810 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1811
1812 assert!(info.get_route_param("").is_none());
1814 assert!(info.get_route_param(" ").is_none());
1815 assert!(info.get_route_param("\t\n").is_none());
1816
1817 assert!(info.get_route_param(" id").is_none());
1819 assert!(info.get_route_param("id ").is_none());
1820 assert!(info.get_route_param(" id ").is_none());
1821 assert!(info.get_route_param("id;garbage").is_none());
1822 assert!(info.get_route_param("ID").is_none());
1823 assert!(info.get_route_param("Id").is_none());
1824
1825 assert!(info.get_route_param("i").is_none());
1827 assert!(info.get_route_param("idd").is_none());
1828
1829 assert!(info.get_route_param("\0").is_none());
1831 assert!(info.get_route_param("id\0").is_none());
1832 assert!(info.get_route_param("\u{7F}\u{1}\u{2}").is_none());
1833
1834 for key in [
1836 "0",
1837 "-0",
1838 "9223372036854775807",
1839 "-9223372036854775808",
1840 "18446744073709551616",
1841 "NaN",
1842 "inf",
1843 "-inf",
1844 "1e400",
1845 "0.0000000000000000001",
1846 ] {
1847 assert!(info.get_route_param(key).is_none(), "key {key:?}");
1848 }
1849
1850 assert!(info.get_route_param("i\u{0301}d").is_none());
1852 assert!(info.get_route_param("\u{1F600}\u{1F600}").is_none());
1853 }
1854
1855 #[test]
1856 fn get_route_param_handles_pathological_key_sizes_and_nesting() {
1857 let fx = Fixture::with_route(user_route());
1858 let rd = fx.ref_data();
1859 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1860
1861 let huge = "x".repeat(1_000_000);
1863 assert!(info.get_route_param(&huge).is_none());
1864
1865 let long_id = alloc::format!("id{}", "0".repeat(1_000_000));
1867 assert!(info.get_route_param(&long_id).is_none());
1868
1869 let nested = "[".repeat(10_000) + &"]".repeat(10_000);
1872 assert!(info.get_route_param(&nested).is_none());
1873 }
1874
1875 #[test]
1876 fn get_route_param_preserves_huge_and_unicode_values() {
1877 let big = "v".repeat(200_000);
1878 let route = RouteMatch {
1879 pattern: s("/blob/:data"),
1880 params: StringPairVec::from_vec(Vec::from([AzStringPair {
1881 key: s("data"),
1882 value: s(&big),
1883 }])),
1884 };
1885 let fx = Fixture::with_route(route);
1886 let rd = fx.ref_data();
1887 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1888
1889 let got = info.get_route_param("data").expect("param exists");
1890 assert_eq!(got.as_str().len(), 200_000);
1891 }
1892
1893 #[test]
1896 fn window_predicates_obey_trichotomy_and_the_between_identity() {
1897 let fx = Fixture::new();
1898 let rd = fx.ref_data();
1899
1900 let probes = [
1901 0.0f32,
1902 -0.0,
1903 1.0,
1904 -1.0,
1905 640.0,
1906 f32::MIN,
1907 f32::MAX,
1908 f32::MIN_POSITIVE,
1909 f32::INFINITY,
1910 f32::NEG_INFINITY,
1911 ];
1912
1913 for &dim in &probes {
1914 let info = LayoutCallbackInfo::new(&rd, win(dim, dim, 96), WindowTheme::LightMode);
1915
1916 for &px in &probes {
1917 let lt = info.window_width_less_than(px);
1918 let gt = info.window_width_greater_than(px);
1919 let eq = info.get_window_width() == px;
1920
1921 assert_eq!(
1923 u8::from(lt) + u8::from(gt) + u8::from(eq),
1924 1,
1925 "trichotomy broken for width {dim} vs {px}"
1926 );
1927
1928 assert_eq!(info.window_height_less_than(px), lt);
1930 assert_eq!(info.window_height_greater_than(px), gt);
1931
1932 for &px2 in &probes {
1933 assert_eq!(
1935 info.window_width_between(px, px2),
1936 !info.window_width_less_than(px) && !info.window_width_greater_than(px2),
1937 "between identity broken for width {dim} in [{px}, {px2}]"
1938 );
1939 assert_eq!(
1940 info.window_height_between(px, px2),
1941 info.window_width_between(px, px2)
1942 );
1943 }
1944 }
1945 }
1946 }
1947
1948 #[test]
1949 fn window_predicates_with_inverted_and_degenerate_ranges() {
1950 let fx = Fixture::new();
1951 let rd = fx.ref_data();
1952 let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
1953
1954 assert!(!info.window_width_between(1000.0, 100.0));
1956 assert!(!info.window_height_between(1000.0, 100.0));
1957
1958 assert!(info.window_width_between(640.0, 640.0));
1960 assert!(info.window_height_between(480.0, 480.0));
1961 assert!(!info.window_width_between(639.9, 639.95));
1962
1963 assert!(info.window_width_between(640.0, 1000.0));
1965 assert!(info.window_width_between(0.0, 640.0));
1966
1967 assert!(!info.window_width_less_than(640.0));
1969 assert!(!info.window_width_greater_than(640.0));
1970 assert!(info.window_width_less_than(640.001));
1971 assert!(info.window_width_greater_than(639.999));
1972
1973 assert!(info.window_width_between(f32::NEG_INFINITY, f32::INFINITY));
1975 }
1976
1977 #[test]
1978 fn window_predicates_are_all_false_for_nan_probes() {
1979 let fx = Fixture::new();
1980 let rd = fx.ref_data();
1981 let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
1982
1983 assert!(!info.window_width_less_than(f32::NAN));
1985 assert!(!info.window_width_greater_than(f32::NAN));
1986 assert!(!info.window_width_between(f32::NAN, f32::NAN));
1987 assert!(!info.window_width_between(f32::NAN, 10_000.0));
1988 assert!(!info.window_width_between(0.0, f32::NAN));
1989
1990 assert!(!info.window_height_less_than(f32::NAN));
1991 assert!(!info.window_height_greater_than(f32::NAN));
1992 assert!(!info.window_height_between(f32::NAN, f32::NAN));
1993 assert!(!info.window_height_between(f32::NAN, 10_000.0));
1994 assert!(!info.window_height_between(0.0, f32::NAN));
1995 }
1996
1997 #[test]
1998 fn window_predicates_are_all_false_for_a_nan_sized_window() {
1999 let fx = Fixture::new();
2000 let rd = fx.ref_data();
2001 let info = LayoutCallbackInfo::new(
2002 &rd,
2003 win(f32::NAN, f32::NAN, 96),
2004 WindowTheme::LightMode,
2005 );
2006
2007 assert!(info.get_window_width().is_nan());
2008 assert!(info.get_window_height().is_nan());
2009
2010 for px in [0.0f32, 640.0, f32::MAX, f32::INFINITY, f32::NEG_INFINITY] {
2012 assert!(!info.window_width_less_than(px));
2013 assert!(!info.window_width_greater_than(px));
2014 assert!(!info.window_height_less_than(px));
2015 assert!(!info.window_height_greater_than(px));
2016 assert!(!info.window_width_between(f32::NEG_INFINITY, px));
2017 assert!(!info.window_height_between(px, f32::INFINITY));
2018 }
2019 }
2020
2021 #[test]
2022 fn get_dpi_factor_at_zero_and_u32_limits() {
2023 let fx = Fixture::new();
2024 let rd = fx.ref_data();
2025
2026 let base = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 96), WindowTheme::LightMode);
2028 assert_eq!(base.get_dpi_factor(), 1.0);
2029
2030 let hidpi = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 192), WindowTheme::LightMode);
2031 assert_eq!(hidpi.get_dpi_factor(), 2.0);
2032
2033 let zero = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 0), WindowTheme::LightMode);
2035 assert_eq!(zero.get_dpi_factor(), 0.0);
2036
2037 let max = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, u32::MAX), WindowTheme::LightMode);
2039 let f = max.get_dpi_factor();
2040 assert!(f.is_finite() && f > 0.0, "dpi factor {f} is not finite");
2041 assert_eq!(f, (u32::MAX as f32) / 96.0);
2042
2043 let one = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 1), WindowTheme::LightMode);
2045 assert!(one.get_dpi_factor() > 0.0);
2046 }
2047
2048 #[test]
2051 fn hidpi_adjusted_bounds_from_bounds_holds_its_fields() {
2052 let b = HidpiAdjustedBounds::from_bounds(
2053 LayoutSize::new(800, 600),
2054 DpiScaleFactor::new(1.5),
2055 );
2056 assert_eq!(b.get_logical_size(), LogicalSize::new(800.0, 600.0));
2057 assert_eq!(b.get_hidpi_factor(), DpiScaleFactor::new(1.5));
2058 assert_eq!(b.logical_size, b.get_logical_size());
2059 assert_eq!(b.hidpi_factor, b.get_hidpi_factor());
2060
2061 let p = b.get_physical_size();
2062 assert_eq!(p.width, 1200);
2063 assert_eq!(p.height, 900);
2064 }
2065
2066 #[test]
2067 fn hidpi_adjusted_bounds_at_zero() {
2068 let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(0, 0), DpiScaleFactor::new(1.0));
2069 assert_eq!(b.get_logical_size(), LogicalSize::zero());
2070 let p = b.get_physical_size();
2071 assert_eq!(p.width, 0);
2072 assert_eq!(p.height, 0);
2073
2074 let z = HidpiAdjustedBounds::from_bounds(
2076 LayoutSize::new(1920, 1080),
2077 DpiScaleFactor::new(0.0),
2078 );
2079 let zp = z.get_physical_size();
2080 assert_eq!(zp.width, 0);
2081 assert_eq!(zp.height, 0);
2082 }
2083
2084 #[test]
2089 fn hidpi_adjusted_bounds_physical_size_saturates_on_negative_input() {
2090 let b = HidpiAdjustedBounds::from_bounds(
2091 LayoutSize::new(-100, -50),
2092 DpiScaleFactor::new(1.0),
2093 );
2094 assert_eq!(b.get_logical_size(), LogicalSize::new(-100.0, -50.0));
2095
2096 let p = b.get_physical_size();
2097 assert_eq!(p.width, 0, "negative logical width must clamp to 0, not wrap");
2098 assert_eq!(p.height, 0, "negative logical height must clamp to 0, not wrap");
2099
2100 let neg_scale = HidpiAdjustedBounds::from_bounds(
2102 LayoutSize::new(100, 100),
2103 DpiScaleFactor::new(-2.0),
2104 );
2105 let np = neg_scale.get_physical_size();
2106 assert_eq!(np.width, 0);
2107 assert_eq!(np.height, 0);
2108 }
2109
2110 #[test]
2111 fn hidpi_adjusted_bounds_physical_size_saturates_at_the_upper_limit() {
2112 let b = HidpiAdjustedBounds::from_bounds(
2114 LayoutSize::new(isize::MAX, isize::MAX),
2115 DpiScaleFactor::new(1.0),
2116 );
2117 let p = b.get_physical_size();
2118 assert_eq!(p.width, u32::MAX);
2119 assert_eq!(p.height, u32::MAX);
2120
2121 let min = HidpiAdjustedBounds::from_bounds(
2123 LayoutSize::new(isize::MIN, isize::MIN),
2124 DpiScaleFactor::new(1.0),
2125 );
2126 let mp = min.get_physical_size();
2127 assert_eq!(mp.width, 0);
2128 assert_eq!(mp.height, 0);
2129
2130 let huge_scale = HidpiAdjustedBounds::from_bounds(
2132 LayoutSize::new(1000, 1000),
2133 DpiScaleFactor::new(f32::MAX),
2134 );
2135 let hp = huge_scale.get_physical_size();
2136 assert_eq!(hp.width, u32::MAX);
2137 assert_eq!(hp.height, u32::MAX);
2138 }
2139
2140 #[test]
2144 fn hidpi_adjusted_bounds_physical_size_with_nan_and_infinite_scale() {
2145 let nan = HidpiAdjustedBounds::from_bounds(
2146 LayoutSize::new(100, 100),
2147 DpiScaleFactor::new(f32::NAN),
2148 );
2149 assert_eq!(nan.get_hidpi_factor().inner.get(), 0.0);
2151 let np = nan.get_physical_size();
2152 assert_eq!(np.width, 0);
2153 assert_eq!(np.height, 0);
2154
2155 let inf = HidpiAdjustedBounds::from_bounds(
2156 LayoutSize::new(100, 100),
2157 DpiScaleFactor::new(f32::INFINITY),
2158 );
2159 assert!(inf.get_hidpi_factor().inner.get().is_finite());
2161 let ip = inf.get_physical_size();
2162 assert_eq!(ip.width, u32::MAX);
2163 assert_eq!(ip.height, u32::MAX);
2164
2165 let neg_inf = HidpiAdjustedBounds::from_bounds(
2166 LayoutSize::new(100, 100),
2167 DpiScaleFactor::new(f32::NEG_INFINITY),
2168 );
2169 let nip = neg_inf.get_physical_size();
2170 assert_eq!(nip.width, 0);
2171 assert_eq!(nip.height, 0);
2172 }
2173
2174 #[test]
2175 fn hidpi_adjusted_bounds_physical_size_rounds_to_nearest() {
2176 let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(3, 3), DpiScaleFactor::new(1.5));
2178 let p = b.get_physical_size();
2179 assert_eq!(p.width, 5, "3 * 1.5 = 4.5 must round to 5");
2180 assert_eq!(p.height, 5);
2181
2182 let p2 = b.get_physical_size();
2184 assert_eq!(p.width, p2.width);
2185 assert_eq!(p.height, p2.height);
2186 }
2187
2188 fn cb_data(cb: usize) -> CoreCallbackData {
2191 CoreCallbackData {
2192 event: EventFilter::Hover(HoverEventFilter::MouseOver),
2193 callback: CoreCallback::from(cb),
2194 refany: RefAny::new(cb),
2195 }
2196 }
2197
2198 #[test]
2199 fn core_callback_data_vec_as_container_on_empty_vecs_does_not_panic() {
2200 let empty = CoreCallbackDataVec::new();
2203 assert_eq!(empty.as_container().len(), 0);
2204 assert!(empty.as_container().internal.is_empty());
2205
2206 let from_empty_vec = CoreCallbackDataVec::from_vec(Vec::new());
2207 assert_eq!(from_empty_vec.as_container().len(), 0);
2208
2209 let mut mut_empty = CoreCallbackDataVec::from_vec(Vec::new());
2210 assert!(mut_empty.as_container_mut().internal.is_empty());
2211 }
2212
2213 #[test]
2214 fn core_callback_data_vec_as_container_matches_the_backing_vec() {
2215 let v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2), cb_data(3)]));
2216
2217 let c = v.as_container();
2218 assert_eq!(c.len(), 3);
2219 assert_eq!(c.len(), v.len());
2220 assert_eq!(c.internal[0].callback.cb, 1);
2221 assert_eq!(c.internal[2].callback.cb, 3);
2222
2223 assert!(core::ptr::eq(c.internal.as_ptr(), v.as_slice().as_ptr()));
2225 }
2226
2227 #[test]
2228 fn core_callback_data_vec_as_container_mut_writes_through() {
2229 let mut v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2)]));
2230
2231 {
2232 let mut c = v.as_container_mut();
2233 assert_eq!(c.internal.len(), 2);
2234 c.internal[0].callback.cb = 99;
2235 c.internal[1].event = EventFilter::Hover(HoverEventFilter::MouseDown);
2236 }
2237
2238 let c = v.as_container();
2240 assert_eq!(c.internal[0].callback.cb, 99);
2241 assert_eq!(
2242 c.internal[1].event,
2243 EventFilter::Hover(HoverEventFilter::MouseDown)
2244 );
2245 assert_eq!(c.len(), 2);
2246 }
2247}