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::{
50 LogicalPosition, LogicalRect, LogicalRectVec, LogicalSize, OptionLogicalPosition,
51 PhysicalSize,
52 },
53 gl::OptionGlContextPtr,
54 hit_test::OverflowingScrollNode,
55 id::{NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeId},
56 prop_cache::CssPropertyCache,
57 refany::{OptionRefAny, RefAny},
58 resources::{
59 DpiScaleFactor, FontInstanceKey, IdNamespace, ImageCache, ImageMask, ImageRef,
60 RendererResources,
61 },
62 styled_dom::{
63 NodeHierarchyItemId, NodeHierarchyItemVec, StyledNode,
64 StyledNodeVec,
65 },
66 task::{
67 Duration as AzDuration, GetSystemTimeCallback, Instant as AzInstant, Instant,
68 TerminateTimer, ThreadId, ThreadReceiver, ThreadSendMsg, TimerId,
69 },
70 window::{
71 AzStringPair, KeyboardState, MouseState, OptionChar, RawWindowHandle, UpdateFocusWarning,
72 WindowFlags, WindowSize, WindowTheme,
73 },
74 FastBTreeSet, OrderedMap,
75};
76
77#[repr(C)]
79#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
80pub enum Update {
81 DoNothing,
83 RefreshDom,
86 RefreshDomAllWindows,
88}
89
90impl Update {
91 pub fn max_self(&mut self, other: Self) {
92 if (*self == Self::DoNothing && other != Self::DoNothing)
93 || (*self == Self::RefreshDom && other == Self::RefreshDomAllWindows)
94 {
95 *self = other;
96 }
97 }
98}
99
100pub type LayoutCallbackType = extern "C" fn(RefAny, LayoutCallbackInfo) -> Dom;
117
118extern "C" fn default_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
119 Dom::create_body()
120}
121
122#[repr(C)]
131pub struct LayoutCallback {
132 pub cb: LayoutCallbackType,
133 pub ctx: OptionRefAny,
136}
137
138impl_callback!(LayoutCallback, LayoutCallbackType);
139
140impl LayoutCallback {
141 pub fn create<I: Into<Self>>(cb: I) -> Self {
142 cb.into()
143 }
144}
145
146crate::impl_managed_callback! {
153 wrapper: LayoutCallback,
154 info_ty: LayoutCallbackInfo,
155 return_ty: Dom,
156 default_ret: Dom::create_body(),
157 invoker_static: LAYOUT_CALLBACK_INVOKER,
158 invoker_ty: AzLayoutCallbackInvoker,
159 thunk_fn: az_layout_callback_thunk,
160 setter_fn: AzApp_setLayoutCallbackInvoker,
161 from_handle_fn: AzLayoutCallback_createFromHostHandle,
162}
163
164impl Default for LayoutCallback {
165 fn default() -> Self {
166 Self {
167 cb: default_layout_callback,
168 ctx: OptionRefAny::None,
169 }
170 }
171}
172
173pub type VirtualViewCallbackType = extern "C" fn(RefAny, VirtualViewCallbackInfo) -> VirtualViewReturn;
176
177#[repr(C)]
180pub struct VirtualViewCallback {
181 pub cb: VirtualViewCallbackType,
182 pub ctx: OptionRefAny,
185}
186impl_callback!(VirtualViewCallback, VirtualViewCallbackType);
187
188crate::impl_managed_callback! {
190 wrapper: VirtualViewCallback,
191 info_ty: VirtualViewCallbackInfo,
192 return_ty: VirtualViewReturn,
193 default_ret: VirtualViewReturn::default(),
194 invoker_static: VIRTUAL_VIEW_CALLBACK_INVOKER,
195 invoker_ty: AzVirtualViewCallbackInvoker,
196 thunk_fn: az_virtual_view_callback_thunk,
197 setter_fn: AzApp_setVirtualViewCallbackInvoker,
198 from_handle_fn: AzVirtualViewCallback_createFromHostHandle,
199}
200
201impl VirtualViewCallback {
202 pub fn create(cb: VirtualViewCallbackType) -> Self {
203 Self {
204 cb,
205 ctx: OptionRefAny::None,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
222#[repr(C)]
223pub struct CaretTweenInfo {
224 pub past: LogicalRect,
227 pub current: LogicalRect,
229 pub t: f32,
232}
233
234pub type CaretTweenCallbackType = extern "C" fn(RefAny, CaretTweenInfo) -> LogicalRect;
236
237#[repr(C)]
239pub struct CaretTweenCallback {
240 pub cb: CaretTweenCallbackType,
241 pub ctx: OptionRefAny,
244}
245impl_callback!(CaretTweenCallback, CaretTweenCallbackType);
246
247impl CaretTweenCallback {
248 pub fn create(cb: CaretTweenCallbackType) -> Self {
249 Self {
250 cb,
251 ctx: OptionRefAny::None,
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, PartialOrd)]
262#[repr(C)]
263pub struct SelectionTweenInfo {
264 pub past: LogicalRectVec,
266 pub current: LogicalRectVec,
268 pub t: f32,
270}
271
272pub type SelectionTweenCallbackType =
276 extern "C" fn(RefAny, SelectionTweenInfo) -> LogicalRectVec;
277
278#[repr(C)]
280pub struct SelectionTweenCallback {
281 pub cb: SelectionTweenCallbackType,
282 pub ctx: OptionRefAny,
285}
286impl_callback!(SelectionTweenCallback, SelectionTweenCallbackType);
287
288impl SelectionTweenCallback {
289 pub fn create(cb: SelectionTweenCallbackType) -> Self {
290 Self {
291 cb,
292 ctx: OptionRefAny::None,
293 }
294 }
295}
296
297#[inline]
306fn trapezoid_ease(t: f32) -> f32 {
307 const RAMP: f32 = 0.25;
308 const V: f32 = 1.0 / (1.0 - RAMP);
310 let t = t.clamp(0.0, 1.0);
311 if t < RAMP {
312 V * t * t / (2.0 * RAMP)
313 } else if t <= 1.0 - RAMP {
314 V * (RAMP / 2.0 + (t - RAMP))
315 } else {
316 let inv = 1.0 - t;
317 1.0 - V * inv * inv / (2.0 * RAMP)
318 }
319}
320
321#[inline]
322#[allow(clippy::suboptimal_flops)]
326fn lerp_rect(from: LogicalRect, to: LogicalRect, e: f32) -> LogicalRect {
327 LogicalRect {
328 origin: LogicalPosition {
329 x: from.origin.x + (to.origin.x - from.origin.x) * e,
330 y: from.origin.y + (to.origin.y - from.origin.y) * e,
331 },
332 size: LogicalSize {
333 width: from.size.width + (to.size.width - from.size.width) * e,
334 height: from.size.height + (to.size.height - from.size.height) * e,
335 },
336 }
337}
338
339#[must_use]
342pub extern "C" fn default_caret_tween(_data: RefAny, info: CaretTweenInfo) -> LogicalRect {
343 lerp_rect(info.past, info.current, trapezoid_ease(info.t))
344}
345
346#[must_use]
350pub extern "C" fn default_selection_tween(
351 _data: RefAny,
352 info: SelectionTweenInfo,
353) -> LogicalRectVec {
354 let e = trapezoid_ease(info.t);
355 let past = info.past.as_ref();
356 let out: Vec<LogicalRect> = info
357 .current
358 .as_ref()
359 .iter()
360 .enumerate()
361 .map(|(i, cur)| past.get(i).map_or(*cur, |p| lerp_rect(*p, *cur, e)))
362 .collect();
363 out.into()
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370#[repr(C, u8)]
371pub enum VirtualViewCallbackReason {
372 InitialRender,
374 DomRecreated,
376 BoundsExpanded,
378 EdgeScrolled(EdgeType),
380 ScrollBeyondContent,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386#[repr(C)]
387pub enum EdgeType {
388 Top,
389 Bottom,
390 Left,
391 Right,
392}
393
394#[derive(Debug)]
395#[repr(C)]
396pub struct VirtualViewCallbackInfo {
397 pub reason: VirtualViewCallbackReason,
398 pub system_fonts: *const FcFontCache,
399 pub image_cache: *const ImageCache,
400 pub window_theme: WindowTheme,
401 pub bounds: HidpiAdjustedBounds,
402 pub scroll_size: LogicalSize,
403 pub scroll_offset: LogicalPosition,
404 pub virtual_scroll_size: LogicalSize,
405 pub virtual_scroll_offset: LogicalPosition,
406 callable_ptr: *const OptionRefAny,
409 measure_dom_fn: *const c_void,
415 measure_dom_ctx: *mut c_void,
416 _abi_mut: *mut c_void,
418}
419
420pub type MeasureDomFn = extern "C" fn(*mut c_void, *mut Dom, LogicalSize) -> LogicalSize;
424
425impl Clone for VirtualViewCallbackInfo {
426 #[allow(clippy::used_underscore_binding)] fn clone(&self) -> Self {
428 Self {
429 reason: self.reason,
430 system_fonts: self.system_fonts,
431 image_cache: self.image_cache,
432 window_theme: self.window_theme,
433 bounds: self.bounds,
434 scroll_size: self.scroll_size,
435 scroll_offset: self.scroll_offset,
436 virtual_scroll_size: self.virtual_scroll_size,
437 virtual_scroll_offset: self.virtual_scroll_offset,
438 callable_ptr: self.callable_ptr,
439 measure_dom_fn: self.measure_dom_fn,
440 measure_dom_ctx: self.measure_dom_ctx,
441 _abi_mut: self._abi_mut,
442 }
443 }
444}
445
446impl VirtualViewCallbackInfo {
447 #[must_use] pub const fn new<'a>(
448 reason: VirtualViewCallbackReason,
449 system_fonts: &'a FcFontCache,
450 image_cache: &'a ImageCache,
451 window_theme: WindowTheme,
452 bounds: HidpiAdjustedBounds,
453 scroll_size: LogicalSize,
454 scroll_offset: LogicalPosition,
455 virtual_scroll_size: LogicalSize,
456 virtual_scroll_offset: LogicalPosition,
457 ) -> Self {
458 Self {
459 reason,
460 system_fonts: core::ptr::from_ref::<FcFontCache>(system_fonts),
461 image_cache: core::ptr::from_ref::<ImageCache>(image_cache),
462 window_theme,
463 bounds,
464 scroll_size,
465 scroll_offset,
466 virtual_scroll_size,
467 virtual_scroll_offset,
468 callable_ptr: core::ptr::null(),
469 measure_dom_fn: core::ptr::null(),
470 measure_dom_ctx: core::ptr::null_mut(),
471 _abi_mut: core::ptr::null_mut(),
472 }
473 }
474
475 pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
477 self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
478 }
479
480 pub fn set_measure_dom_fn(&mut self, f: MeasureDomFn, ctx: *mut c_void) {
483 self.measure_dom_fn = f as *const c_void;
484 self.measure_dom_ctx = ctx;
485 }
486
487 #[must_use] pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
500 if self.measure_dom_fn.is_null() {
501 return LogicalSize::zero();
502 }
503 let f: MeasureDomFn = unsafe { core::mem::transmute(self.measure_dom_fn) };
506 let mut dom = core::mem::ManuallyDrop::new(dom);
507 f(self.measure_dom_ctx, core::ptr::from_mut::<Dom>(&mut dom), available)
508 }
509
510 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
512 if self.callable_ptr.is_null() {
513 OptionRefAny::None
514 } else {
515 unsafe { (*self.callable_ptr).clone() }
516 }
517 }
518
519 #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
520 self.bounds
521 }
522
523 const fn internal_get_system_fonts(&self) -> &FcFontCache {
524 unsafe { &*self.system_fonts }
525 }
526 const fn internal_get_image_cache(&self) -> &ImageCache {
527 unsafe { &*self.image_cache }
528 }
529}
530
531#[derive(Debug, Clone, PartialEq, Eq)]
543#[repr(C)]
544pub struct VirtualViewReturn {
545 pub dom: OptionDom,
553
554 pub scroll_size: LogicalSize,
562
563 pub scroll_offset: LogicalPosition,
572
573 pub virtual_scroll_size: LogicalSize,
581
582 pub virtual_scroll_offset: LogicalPosition,
587}
588
589impl Default for VirtualViewReturn {
590 fn default() -> Self {
591 Self {
592 dom: OptionDom::None,
593 scroll_size: LogicalSize::zero(),
594 scroll_offset: LogicalPosition::zero(),
595 virtual_scroll_size: LogicalSize::zero(),
596 virtual_scroll_offset: LogicalPosition::zero(),
597 }
598 }
599}
600
601impl VirtualViewReturn {
602 #[must_use] pub const fn with_dom(
613 dom: Dom,
614 scroll_size: LogicalSize,
615 scroll_offset: LogicalPosition,
616 virtual_scroll_size: LogicalSize,
617 virtual_scroll_offset: LogicalPosition,
618 ) -> Self {
619 Self {
620 dom: OptionDom::Some(dom),
621 scroll_size,
622 scroll_offset,
623 virtual_scroll_size,
624 virtual_scroll_offset,
625 }
626 }
627
628 #[must_use] pub const fn keep_current(
640 scroll_size: LogicalSize,
641 scroll_offset: LogicalPosition,
642 virtual_scroll_size: LogicalSize,
643 virtual_scroll_offset: LogicalPosition,
644 ) -> Self {
645 Self {
646 dom: OptionDom::None,
647 scroll_size,
648 scroll_offset,
649 virtual_scroll_size,
650 virtual_scroll_offset,
651 }
652 }
653
654}
655
656#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
661#[repr(C)]
662pub struct TimerCallbackReturn {
663 pub should_update: Update,
664 pub should_terminate: TerminateTimer,
665}
666
667impl TimerCallbackReturn {
668 #[must_use] pub const fn create(should_update: Update, should_terminate: TerminateTimer) -> Self {
670 Self {
671 should_update,
672 should_terminate,
673 }
674 }
675
676 #[must_use] pub const fn continue_unchanged() -> Self {
678 Self {
679 should_update: Update::DoNothing,
680 should_terminate: TerminateTimer::Continue,
681 }
682 }
683
684 #[must_use] pub const fn continue_and_refresh_dom() -> Self {
686 Self {
687 should_update: Update::RefreshDom,
688 should_terminate: TerminateTimer::Continue,
689 }
690 }
691
692 #[must_use] pub const fn terminate_unchanged() -> Self {
694 Self {
695 should_update: Update::DoNothing,
696 should_terminate: TerminateTimer::Terminate,
697 }
698 }
699
700 #[must_use] pub const fn terminate_and_refresh_dom() -> Self {
702 Self {
703 should_update: Update::RefreshDom,
704 should_terminate: TerminateTimer::Terminate,
705 }
706 }
707}
708
709impl Default for TimerCallbackReturn {
710 fn default() -> Self {
711 Self::continue_unchanged()
712 }
713}
714
715#[derive(Debug)]
719#[repr(C)]
720pub struct LayoutCallbackInfoRefData<'a> {
729 pub image_cache: &'a ImageCache,
731 pub gl_context: &'a OptionGlContextPtr,
733 pub system_fonts: &'a FcFontCache,
735 pub system_style: Arc<SystemStyle>,
738 pub active_route: Option<&'a crate::resources::RouteMatch>,
741 pub monitors: crate::window::MonitorVec,
749}
750
751#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
760#[repr(C)]
761#[derive(Default)]
762pub enum RelayoutReason {
763 #[default]
765 Initial,
766 RefreshDom,
768 Resize,
772 ThemeChange,
774 RouteChange,
778 Other,
780}
781
782
783#[repr(C)]
784pub struct LayoutCallbackInfo {
785 ref_data: *const LayoutCallbackInfoRefData<'static>,
788 pub window_size: WindowSize,
792 pub theme: WindowTheme,
794 pub relayout_reason: RelayoutReason,
796 callable_ptr: *const OptionRefAny,
799 _abi_mut: *mut c_void,
801}
802
803#[repr(C)]
811#[derive(Debug, Clone, Copy, PartialEq)]
812pub struct SizeQuery {
813 pub axis: SizeQueryAxis,
814 pub op: SizeQueryOp,
815 pub threshold_px: f32,
816 pub answer: bool,
819}
820
821#[repr(C)]
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
824pub enum SizeQueryAxis {
825 Width,
826 Height,
827}
828
829#[repr(C)]
838#[derive(Debug, Clone, Copy, PartialEq, Eq)]
839pub enum SizeQueryOp {
840 LessThan,
842 GreaterThan,
844 GreaterOrEqual,
846 LessOrEqual,
848}
849
850impl SizeQuery {
851 #[must_use] pub fn answer_at(&self, size: LogicalSize) -> bool {
856 let dim = match self.axis {
857 SizeQueryAxis::Width => size.width,
858 SizeQueryAxis::Height => size.height,
859 };
860 match self.op {
861 SizeQueryOp::LessThan => dim < self.threshold_px,
862 SizeQueryOp::GreaterThan => dim > self.threshold_px,
863 SizeQueryOp::GreaterOrEqual => dim >= self.threshold_px,
864 SizeQueryOp::LessOrEqual => dim <= self.threshold_px,
865 }
866 }
867
868 #[must_use] pub fn flips_at(&self, size: LogicalSize) -> bool {
870 self.answer_at(size) != self.answer
871 }
872}
873
874#[cfg(feature = "std")]
889mod size_query_recorder {
890 use super::SizeQuery;
891
892 pub(super) const SIZE_QUERY_CAP: usize = 256;
895
896 std::thread_local! {
897 static RECORDED: core::cell::RefCell<(Vec<SizeQuery>, bool)> =
898 const { core::cell::RefCell::new((Vec::new(), false)) };
899 }
900
901 pub(super) fn record(q: SizeQuery) {
902 RECORDED.with(|r| {
903 let mut r = r.borrow_mut();
904 if r.0.len() >= SIZE_QUERY_CAP {
905 r.1 = true; } else {
907 r.0.push(q);
908 }
909 });
910 }
911
912 pub(super) fn take() -> (Vec<SizeQuery>, bool) {
916 RECORDED.with(|r| {
917 let mut r = r.borrow_mut();
918 let overflowed = r.1;
919 r.1 = false;
920 (core::mem::take(&mut r.0), overflowed)
921 })
922 }
923}
924
925#[cfg(feature = "std")]
926fn record_size_query(q: SizeQuery) {
927 size_query_recorder::record(q);
928}
929
930#[cfg(not(feature = "std"))]
935fn record_size_query(_q: SizeQuery) {}
936
937#[cfg(feature = "std")]
943#[must_use] pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
944 size_query_recorder::take()
945}
946
947#[cfg(not(feature = "std"))]
948#[must_use] pub fn take_recorded_size_queries() -> (alloc::vec::Vec<SizeQuery>, bool) {
949 (alloc::vec::Vec::new(), false)
950}
951
952impl Clone for LayoutCallbackInfo {
953 #[allow(clippy::used_underscore_binding)] fn clone(&self) -> Self {
955 Self {
956 ref_data: self.ref_data,
957 window_size: self.window_size,
958 theme: self.theme,
959 relayout_reason: self.relayout_reason,
960 callable_ptr: self.callable_ptr,
961 _abi_mut: self._abi_mut,
962 }
963 }
964}
965
966impl core::fmt::Debug for LayoutCallbackInfo {
967 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
968 f.debug_struct("LayoutCallbackInfo")
969 .field("window_size", &self.window_size)
970 .field("theme", &self.theme)
971 .field("relayout_reason", &self.relayout_reason)
972 .finish_non_exhaustive()
973 }
974}
975
976impl LayoutCallbackInfo {
977 #[must_use] pub const fn new<'a>(
978 ref_data: &'a LayoutCallbackInfoRefData<'a>,
979 window_size: WindowSize,
980 theme: WindowTheme,
981 ) -> Self {
982 Self::new_with_reason(ref_data, window_size, theme, RelayoutReason::Initial)
983 }
984
985 #[allow(clippy::unnecessary_cast)]
988 #[must_use] pub const fn new_with_reason<'a>(
989 ref_data: &'a LayoutCallbackInfoRefData<'a>,
990 window_size: WindowSize,
991 theme: WindowTheme,
992 relayout_reason: RelayoutReason,
993 ) -> Self {
994 Self {
995 ref_data: core::ptr::from_ref::<LayoutCallbackInfoRefData<'a>>(ref_data)
998 as *const LayoutCallbackInfoRefData<'static>,
999 window_size,
1000 theme,
1001 relayout_reason,
1002 callable_ptr: core::ptr::null(),
1003 _abi_mut: core::ptr::null_mut(),
1004 }
1005 }
1006
1007 #[must_use] pub const fn relayout_reason(&self) -> RelayoutReason {
1009 self.relayout_reason
1010 }
1011
1012
1013 #[must_use] pub fn viewport_bigger_than(&self, width_px: f32) -> bool {
1029 self.window_size.dimensions.width > width_px
1030 }
1031
1032 pub const fn set_callable_ptr(&mut self, callable: &OptionRefAny) {
1034 self.callable_ptr = core::ptr::from_ref::<OptionRefAny>(callable);
1035 }
1036
1037 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
1039 if self.callable_ptr.is_null() {
1040 OptionRefAny::None
1041 } else {
1042 unsafe { (*self.callable_ptr).clone() }
1043 }
1044 }
1045
1046 #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
1048 unsafe { (*self.ref_data).system_style.clone() }
1049 }
1050
1051 #[must_use] pub fn get_monitors(&self) -> crate::window::MonitorVec {
1055 unsafe { (*self.ref_data).monitors.clone() }
1056 }
1057
1058 #[must_use] pub fn get_max_monitor_size(&self) -> azul_css::props::basic::OptionLayoutSize {
1066 let monitors = unsafe { &(*self.ref_data).monitors };
1067 let mut best: Option<LayoutSize> = None;
1068 for m in monitors.as_ref() {
1069 let s = m.size;
1070 let better = best.is_none_or(|b| (s.width * s.height) > (b.width * b.height));
1071 if better {
1072 best = Some(s);
1073 }
1074 }
1075 best.into()
1076 }
1077
1078 const fn internal_get_image_cache(&self) -> &ImageCache {
1079 unsafe { (*self.ref_data).image_cache }
1080 }
1081 const fn internal_get_system_fonts(&self) -> &FcFontCache {
1082 unsafe { (*self.ref_data).system_fonts }
1083 }
1084 const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
1085 unsafe { (*self.ref_data).gl_context }
1086 }
1087
1088 #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
1089 self.internal_get_gl_context().clone()
1090 }
1091
1092 #[must_use] pub fn get_system_fonts(&self) -> Vec<AzStringPair> {
1093 let fc_cache = self.internal_get_system_fonts();
1094
1095 fc_cache
1096 .list()
1097 .into_iter()
1098 .filter_map(|(pattern, font_id)| {
1099 let source = fc_cache.get_font_by_id(&font_id)?;
1100 match source {
1101 OwnedFontSource::Memory(_) => None,
1102 OwnedFontSource::Disk(d) => Some((pattern.name.as_ref()?.clone(), d.path)),
1103 }
1104 })
1105 .map(|(k, v)| AzStringPair {
1106 key: k.into(),
1107 value: v.into(),
1108 })
1109 .collect()
1110 }
1111
1112 #[must_use] pub fn get_font_cache(&self) -> FcFontCache {
1126 self.internal_get_system_fonts().clone()
1127 }
1128
1129 #[must_use] pub fn get_image(&self, image_id: &AzString) -> Option<ImageRef> {
1130 self.internal_get_image_cache()
1131 .get_css_image_id(image_id)
1132 .cloned()
1133 }
1134
1135 #[must_use] pub const fn get_active_route(&self) -> Option<&crate::resources::RouteMatch> {
1139 unsafe { (*self.ref_data).active_route }
1140 }
1141
1142 #[must_use] pub fn get_route_param(&self, key: &str) -> Option<&AzString> {
1146 self.get_active_route()?.get_param(key)
1147 }
1148
1149 #[allow(clippy::unused_self)] fn record_width_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
1164 record_size_query(SizeQuery {
1165 axis: SizeQueryAxis::Width,
1166 op,
1167 threshold_px,
1168 answer,
1169 });
1170 answer
1171 }
1172
1173 #[allow(clippy::unused_self)] fn record_height_query(&self, op: SizeQueryOp, threshold_px: f32, answer: bool) -> bool {
1175 record_size_query(SizeQuery {
1176 axis: SizeQueryAxis::Height,
1177 op,
1178 threshold_px,
1179 answer,
1180 });
1181 answer
1182 }
1183
1184 #[must_use] pub fn window_width_less_than(&self, px: f32) -> bool {
1187 let answer = self.window_size.dimensions.width < px;
1188 self.record_width_query(SizeQueryOp::LessThan, px, answer)
1189 }
1190
1191 #[must_use] pub fn window_width_greater_than(&self, px: f32) -> bool {
1194 let answer = self.window_size.dimensions.width > px;
1195 self.record_width_query(SizeQueryOp::GreaterThan, px, answer)
1196 }
1197
1198 #[must_use] pub fn window_width_between(&self, min_px: f32, max_px: f32) -> bool {
1201 let width = self.window_size.dimensions.width;
1202 self.record_width_query(SizeQueryOp::GreaterOrEqual, min_px, width >= min_px)
1203 & self.record_width_query(SizeQueryOp::LessOrEqual, max_px, width <= max_px)
1204 }
1205
1206 #[must_use] pub fn window_height_less_than(&self, px: f32) -> bool {
1209 let answer = self.window_size.dimensions.height < px;
1210 self.record_height_query(SizeQueryOp::LessThan, px, answer)
1211 }
1212
1213 #[must_use] pub fn window_height_greater_than(&self, px: f32) -> bool {
1216 let answer = self.window_size.dimensions.height > px;
1217 self.record_height_query(SizeQueryOp::GreaterThan, px, answer)
1218 }
1219
1220 #[must_use] pub fn window_height_between(&self, min_px: f32, max_px: f32) -> bool {
1223 let height = self.window_size.dimensions.height;
1224 self.record_height_query(SizeQueryOp::GreaterOrEqual, min_px, height >= min_px)
1225 & self.record_height_query(SizeQueryOp::LessOrEqual, max_px, height <= max_px)
1226 }
1227
1228 #[must_use] pub const fn get_window_width(&self) -> f32 {
1230 self.window_size.dimensions.width
1231 }
1232
1233 #[must_use] pub const fn get_window_height(&self) -> f32 {
1235 self.window_size.dimensions.height
1236 }
1237
1238 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_dpi_factor(&self) -> f32 {
1241 self.window_size.dpi as f32 / 96.0
1242 }
1243}
1244
1245#[derive(Debug, Copy, Clone)]
1250#[repr(C)]
1251pub struct HidpiAdjustedBounds {
1252 pub logical_size: LogicalSize,
1253 pub hidpi_factor: DpiScaleFactor,
1254}
1255
1256impl HidpiAdjustedBounds {
1257 #[inline]
1258 #[allow(clippy::cast_precision_loss)] #[must_use] pub const fn from_bounds(bounds: LayoutSize, hidpi_factor: DpiScaleFactor) -> Self {
1260 let logical_size = LogicalSize::new(bounds.width as f32, bounds.height as f32);
1261 Self {
1262 logical_size,
1263 hidpi_factor,
1264 }
1265 }
1266
1267 #[must_use] pub fn get_physical_size(&self) -> PhysicalSize<u32> {
1268 self.get_logical_size()
1269 .to_physical(self.get_hidpi_factor().inner.get())
1270 }
1271
1272 #[must_use] pub const fn get_logical_size(&self) -> LogicalSize {
1273 self.logical_size
1274 }
1275
1276 #[must_use] pub const fn get_hidpi_factor(&self) -> DpiScaleFactor {
1277 self.hidpi_factor
1278 }
1279}
1280
1281#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1283#[repr(C, u8)]
1284pub enum FocusTarget {
1285 Id(DomNodeId),
1286 Path(FocusTargetPath),
1287 Previous,
1288 Next,
1289 First,
1290 Last,
1291 NoFocus,
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1295#[repr(C)]
1296pub struct FocusTargetPath {
1297 pub dom: DomId,
1298 pub css_path: CssPath,
1299}
1300
1301pub type CoreCallbackType = usize;
1325
1326#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1335#[repr(C)]
1336pub struct CoreCallback {
1337 pub cb: CoreCallbackType,
1338 pub ctx: OptionRefAny,
1341}
1342
1343impl From<CoreCallbackType> for CoreCallback {
1346 fn from(cb: CoreCallbackType) -> Self {
1347 Self {
1348 cb,
1349 ctx: OptionRefAny::None,
1350 }
1351 }
1352}
1353
1354impl_option!(
1355 CoreCallback,
1356 OptionCoreCallback,
1357 [Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash]
1358);
1359
1360#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1362#[repr(C)]
1363pub struct CoreCallbackData {
1364 pub event: EventFilter,
1365 pub callback: CoreCallback,
1366 pub refany: RefAny,
1367}
1368
1369impl_option!(
1370 CoreCallbackData,
1371 OptionCoreCallbackData,
1372 copy = false,
1373 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1374);
1375
1376impl_vec!(CoreCallbackData, CoreCallbackDataVec, CoreCallbackDataVecDestructor, CoreCallbackDataVecDestructorType, CoreCallbackDataVecSlice, OptionCoreCallbackData);
1377impl_vec_clone!(
1378 CoreCallbackData,
1379 CoreCallbackDataVec,
1380 CoreCallbackDataVecDestructor
1381);
1382impl_vec_mut!(CoreCallbackData, CoreCallbackDataVec);
1383impl_vec_debug!(CoreCallbackData, CoreCallbackDataVec);
1384impl_vec_partialord!(CoreCallbackData, CoreCallbackDataVec);
1385impl_vec_ord!(CoreCallbackData, CoreCallbackDataVec);
1386impl_vec_partialeq!(CoreCallbackData, CoreCallbackDataVec);
1387impl_vec_eq!(CoreCallbackData, CoreCallbackDataVec);
1388impl_vec_hash!(CoreCallbackData, CoreCallbackDataVec);
1389
1390impl CoreCallbackDataVec {
1391 #[inline]
1392 #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CoreCallbackData> {
1393 NodeDataContainerRef {
1394 internal: self.as_ref(),
1395 }
1396 }
1397 #[inline]
1398 pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, CoreCallbackData> {
1399 NodeDataContainerRefMut {
1400 internal: self.as_mut(),
1401 }
1402 }
1403}
1404
1405pub type CoreRenderImageCallbackType = usize;
1409
1410#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1412#[repr(C)]
1413pub struct CoreRenderImageCallback {
1414 pub cb: CoreRenderImageCallbackType,
1415 pub ctx: OptionRefAny,
1418}
1419
1420impl From<CoreRenderImageCallbackType> for CoreRenderImageCallback {
1423 fn from(cb: CoreRenderImageCallbackType) -> Self {
1424 Self {
1425 cb,
1426 ctx: OptionRefAny::None,
1427 }
1428 }
1429}
1430
1431#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1433#[repr(C)]
1434pub struct CoreImageCallback {
1435 pub refany: RefAny,
1436 pub callback: CoreRenderImageCallback,
1437}
1438
1439impl_option!(
1440 CoreImageCallback,
1441 OptionCoreImageCallback,
1442 copy = false,
1443 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1444);
1445
1446#[cfg(test)]
1447#[allow(
1448 clippy::float_cmp,
1449 clippy::too_many_lines,
1450 clippy::cast_precision_loss,
1451 clippy::unusual_byte_groupings
1452)]
1453mod autotest_generated {
1454 use alloc::string::String;
1455
1456 use super::*;
1457 use crate::{
1458 events::HoverEventFilter,
1459 resources::{RawImageFormat, RouteMatch},
1460 window::StringPairVec,
1461 };
1462
1463 fn s(v: &str) -> AzString {
1466 AzString::from(String::from(v))
1467 }
1468
1469 fn win(width: f32, height: f32, dpi: u32) -> WindowSize {
1470 WindowSize {
1471 dimensions: LogicalSize::new(width, height),
1472 dpi,
1473 min_dimensions: None.into(),
1474 max_dimensions: None.into(),
1475 }
1476 }
1477
1478 struct Fixture {
1482 fonts: FcFontCache,
1483 images: ImageCache,
1484 style: Arc<SystemStyle>,
1485 gl: OptionGlContextPtr,
1486 route: Option<RouteMatch>,
1487 }
1488
1489 impl Fixture {
1490 fn new() -> Self {
1491 Self {
1492 fonts: FcFontCache::default(),
1493 images: ImageCache::default(),
1494 style: Arc::new(SystemStyle::default()),
1495 gl: OptionGlContextPtr::None,
1496 route: None,
1497 }
1498 }
1499
1500 fn with_route(route: RouteMatch) -> Self {
1501 let mut f = Self::new();
1502 f.route = Some(route);
1503 f
1504 }
1505
1506 fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
1507 LayoutCallbackInfoRefData {
1508 image_cache: &self.images,
1509 gl_context: &self.gl,
1510 system_fonts: &self.fonts,
1511 system_style: self.style.clone(),
1512 active_route: self.route.as_ref(),
1513 monitors: crate::window::MonitorVec::from_const_slice(&[]),
1514 }
1515 }
1516 }
1517
1518 #[test]
1522 fn max_monitor_size_is_largest_by_area_or_none() {
1523 use azul_css::props::basic::LayoutSize;
1524
1525 use crate::window::{Monitor, MonitorVec};
1526
1527 let fixture = Fixture::new();
1528 let mut rd = fixture.ref_data();
1529 rd.monitors = MonitorVec::from_vec(Vec::from([
1530 Monitor {
1531 size: LayoutSize::new(1920, 1080),
1532 ..Monitor::default()
1533 },
1534 Monitor {
1535 size: LayoutSize::new(2560, 1440),
1536 ..Monitor::default()
1537 },
1538 Monitor {
1539 size: LayoutSize::new(800, 600),
1540 ..Monitor::default()
1541 },
1542 ]));
1543 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1544 let max: Option<LayoutSize> = info.get_max_monitor_size().into();
1545 assert_eq!(max, Some(LayoutSize::new(2560, 1440)));
1546 assert_eq!(info.get_monitors().len(), 3);
1547
1548 let rd2 = fixture.ref_data(); let info2 = LayoutCallbackInfo::new(&rd2, WindowSize::default(), WindowTheme::LightMode);
1550 let none: Option<LayoutSize> = info2.get_max_monitor_size().into();
1551 assert_eq!(none, None);
1552 }
1553
1554 fn user_route() -> RouteMatch {
1556 RouteMatch {
1557 pattern: s("/user/:id"),
1558 params: StringPairVec::from_vec(Vec::from([
1559 AzStringPair {
1560 key: s("id"),
1561 value: s("42"),
1562 },
1563 AzStringPair {
1564 key: s("\u{1F600}"),
1565 value: s("emoji"),
1566 },
1567 ])),
1568 }
1569 }
1570
1571 fn vv_info<'a>(
1572 fonts: &'a FcFontCache,
1573 images: &'a ImageCache,
1574 bounds: HidpiAdjustedBounds,
1575 ) -> VirtualViewCallbackInfo {
1576 VirtualViewCallbackInfo::new(
1577 VirtualViewCallbackReason::InitialRender,
1578 fonts,
1579 images,
1580 WindowTheme::LightMode,
1581 bounds,
1582 LogicalSize::new(100.0, 200.0),
1583 LogicalPosition::new(1.0, 2.0),
1584 LogicalSize::new(1000.0, 2000.0),
1585 LogicalPosition::new(3.0, 4.0),
1586 )
1587 }
1588
1589 fn bounds_1x1() -> HidpiAdjustedBounds {
1590 HidpiAdjustedBounds::from_bounds(LayoutSize::new(1, 1), DpiScaleFactor::new(1.0))
1591 }
1592
1593 const ALL_UPDATES: [Update; 3] = [
1596 Update::DoNothing,
1597 Update::RefreshDom,
1598 Update::RefreshDomAllWindows,
1599 ];
1600
1601 #[test]
1605 fn update_max_self_is_exhaustively_ord_max() {
1606 for a in ALL_UPDATES {
1607 for b in ALL_UPDATES {
1608 let mut got = a;
1609 got.max_self(b);
1610 assert_eq!(
1611 got,
1612 core::cmp::max(a, b),
1613 "max_self({a:?}, {b:?}) disagrees with Ord::max"
1614 );
1615 }
1616 }
1617 }
1618
1619 #[test]
1620 fn update_max_self_is_idempotent_and_monotone() {
1621 for a in ALL_UPDATES {
1622 let mut got = a;
1624 got.max_self(a);
1625 assert_eq!(got, a);
1626
1627 let mut top = Update::RefreshDomAllWindows;
1629 top.max_self(a);
1630 assert_eq!(top, Update::RefreshDomAllWindows);
1631
1632 let mut m = a;
1634 m.max_self(Update::DoNothing);
1635 assert!(m >= a);
1636 }
1637 }
1638
1639 #[test]
1643 fn update_max_self_fold_is_order_independent() {
1644 for a in ALL_UPDATES {
1645 for b in ALL_UPDATES {
1646 for c in ALL_UPDATES {
1647 let mut fwd = a;
1648 fwd.max_self(b);
1649 fwd.max_self(c);
1650
1651 let mut rev = c;
1652 rev.max_self(b);
1653 rev.max_self(a);
1654
1655 assert_eq!(fwd, rev, "fold of {a:?},{b:?},{c:?} is order-dependent");
1656 }
1657 }
1658 }
1659 }
1660
1661 static ALT_LAYOUT_CALLS: AtomicUsize = AtomicUsize::new(0);
1664
1665 extern "C" fn alt_layout_callback(_: RefAny, _: LayoutCallbackInfo) -> Dom {
1669 ALT_LAYOUT_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1670 Dom::create_body()
1671 }
1672
1673 #[test]
1674 fn default_layout_callback_returns_body_and_does_not_panic() {
1675 let fx = Fixture::new();
1676 let rd = fx.ref_data();
1677 let info = LayoutCallbackInfo::new(&rd, win(0.0, 0.0, 0), WindowTheme::DarkMode);
1678
1679 let dom = default_layout_callback(RefAny::new(0u32), info);
1681 assert_eq!(dom, Dom::create_body());
1682 }
1683
1684 #[test]
1685 fn layout_callback_create_stores_the_given_fn_and_null_ctx() {
1686 let from_default = LayoutCallback::create(default_layout_callback as LayoutCallbackType);
1687 assert!(
1688 from_default.ctx.is_none(),
1689 "native-Rust create() must leave the FFI ctx empty"
1690 );
1691 assert_eq!(from_default, LayoutCallback::default());
1692
1693 let from_alt = LayoutCallback::create(alt_layout_callback as LayoutCallbackType);
1696 assert!(from_alt.ctx.is_none());
1697 assert_ne!(
1698 from_alt, from_default,
1699 "create() ignored its argument (or the two fns were ICF-folded)"
1700 );
1701
1702 let fx = Fixture::new();
1704 let rd = fx.ref_data();
1705 let before = ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst);
1706 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
1707 let _ = (from_alt.cb)(RefAny::new(()), info);
1708 assert_eq!(ALT_LAYOUT_CALLS.load(AtomicOrdering::SeqCst), before + 1);
1709 }
1710
1711 extern "C" fn vv_keep_current_cb(_: RefAny, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
1714 VirtualViewReturn::keep_current(
1715 info.scroll_size,
1716 info.scroll_offset,
1717 info.virtual_scroll_size,
1718 info.virtual_scroll_offset,
1719 )
1720 }
1721
1722 #[test]
1723 fn virtual_view_callback_create_round_trips_through_the_fn_ptr() {
1724 let cb = VirtualViewCallback::create(vv_keep_current_cb as VirtualViewCallbackType);
1725 assert!(cb.ctx.is_none());
1726
1727 let fonts = FcFontCache::default();
1728 let images = ImageCache::default();
1729 let info = vv_info(&fonts, &images, bounds_1x1());
1730
1731 let ret = (cb.cb)(RefAny::new(0u8), info);
1732 assert!(ret.dom.is_none());
1733 assert_eq!(ret.scroll_size, LogicalSize::new(100.0, 200.0));
1734 assert_eq!(ret.scroll_offset, LogicalPosition::new(1.0, 2.0));
1735 assert_eq!(ret.virtual_scroll_size, LogicalSize::new(1000.0, 2000.0));
1736 assert_eq!(ret.virtual_scroll_offset, LogicalPosition::new(3.0, 4.0));
1737 }
1738
1739 #[test]
1742 fn virtual_view_callback_info_new_holds_its_fields() {
1743 let fonts = FcFontCache::default();
1744 let images = ImageCache::default();
1745 let bounds = HidpiAdjustedBounds::from_bounds(
1746 LayoutSize::new(800, 600),
1747 DpiScaleFactor::new(2.0),
1748 );
1749 let info = vv_info(&fonts, &images, bounds);
1750
1751 assert_eq!(info.reason, VirtualViewCallbackReason::InitialRender);
1752 assert_eq!(info.window_theme, WindowTheme::LightMode);
1753 assert_eq!(info.get_bounds().get_logical_size(), LogicalSize::new(800.0, 600.0));
1754 assert_eq!(info.get_bounds().get_hidpi_factor(), DpiScaleFactor::new(2.0));
1755 assert_eq!(info.scroll_size, LogicalSize::new(100.0, 200.0));
1756
1757 assert!(core::ptr::eq(info.internal_get_system_fonts(), &fonts));
1759 assert!(core::ptr::eq(info.internal_get_image_cache(), &images));
1760
1761 assert!(info.get_ctx().is_none());
1763 assert_eq!(
1764 info.measure_dom(Dom::create_body(), LogicalSize::new(10.0, 10.0)),
1765 LogicalSize::zero()
1766 );
1767
1768 let cloned = info.clone();
1770 assert_eq!(cloned.reason, info.reason);
1771 assert!(core::ptr::eq(cloned.internal_get_system_fonts(), &fonts));
1772 assert!(cloned.get_ctx().is_none());
1773 }
1774
1775 #[test]
1776 fn virtual_view_callback_info_new_survives_nan_and_infinite_geometry() {
1777 let fonts = FcFontCache::default();
1778 let images = ImageCache::default();
1779 let info = VirtualViewCallbackInfo::new(
1780 VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
1781 &fonts,
1782 &images,
1783 WindowTheme::DarkMode,
1784 HidpiAdjustedBounds::from_bounds(
1785 LayoutSize::new(isize::MAX, isize::MIN),
1786 DpiScaleFactor::new(f32::NAN),
1787 ),
1788 LogicalSize::new(f32::NAN, f32::INFINITY),
1789 LogicalPosition::new(f32::NEG_INFINITY, f32::MAX),
1790 LogicalSize::new(f32::MIN, 0.0),
1791 LogicalPosition::new(-0.0, f32::EPSILON),
1792 );
1793
1794 assert!(info.scroll_size.width.is_nan());
1796 assert!(info.scroll_size.height.is_infinite());
1797 assert!(info.scroll_offset.x.is_infinite() && info.scroll_offset.x.is_sign_negative());
1798 assert_eq!(info.virtual_scroll_size.width, f32::MIN);
1799 assert_eq!(info.reason, VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
1800
1801 assert!(info.get_ctx().is_none());
1803 assert!(info.get_bounds().get_logical_size().width > 0.0);
1804 }
1805
1806 #[test]
1807 fn virtual_view_callback_info_get_ctx_clones_without_double_free() {
1808 let fonts = FcFontCache::default();
1809 let images = ImageCache::default();
1810 let mut info = vv_info(&fonts, &images, bounds_1x1());
1811
1812 assert!(info.get_ctx().is_none());
1814
1815 let callable = OptionRefAny::Some(RefAny::new(0xDEAD_BEEF_u32));
1816 info.set_callable_ptr(&callable);
1817
1818 for _ in 0..64 {
1821 let got = info.get_ctx();
1822 assert!(got.is_some());
1823 drop(got);
1824 }
1825
1826 let mut got = info.get_ctx();
1827 match got {
1828 OptionRefAny::Some(ref mut r) => {
1829 let inner = r.downcast_ref::<u32>().expect("ctx should hold a u32");
1830 assert_eq!(*inner, 0xDEAD_BEEF_u32);
1831 }
1832 OptionRefAny::None => panic!("callable_ptr was set, get_ctx() returned None"),
1833 }
1834 drop(got);
1835
1836 let mut orig = callable;
1838 match orig {
1839 OptionRefAny::Some(ref mut r) => {
1840 assert_eq!(*r.downcast_ref::<u32>().unwrap(), 0xDEAD_BEEF_u32);
1841 }
1842 OptionRefAny::None => panic!("original callable was consumed"),
1843 }
1844 }
1845
1846 static MEASURE_CALLS: AtomicUsize = AtomicUsize::new(0);
1849
1850 extern "C" fn test_measure_dom_fn(
1853 ctx: *mut c_void,
1854 dom: *mut Dom,
1855 available: LogicalSize,
1856 ) -> LogicalSize {
1857 MEASURE_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1858 let dom = unsafe { core::ptr::read(dom) };
1861 drop(dom);
1862 if !ctx.is_null() {
1863 unsafe {
1865 *ctx.cast::<u32>() = 0xABCD;
1866 }
1867 }
1868 LogicalSize::new(available.width * 2.0, available.height / 2.0)
1869 }
1870
1871 #[test]
1872 fn measure_dom_without_hook_returns_zero_for_every_input() {
1873 let fonts = FcFontCache::default();
1874 let images = ImageCache::default();
1875 let info = vv_info(&fonts, &images, bounds_1x1());
1876
1877 for available in [
1880 LogicalSize::zero(),
1881 LogicalSize::new(-1.0, -1.0),
1882 LogicalSize::new(f32::NAN, f32::NAN),
1883 LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
1884 LogicalSize::new(f32::MAX, f32::MIN),
1885 LogicalSize::new(1.0, 1_000_000.0),
1886 ] {
1887 assert_eq!(
1888 info.measure_dom(Dom::create_body(), available),
1889 LogicalSize::zero()
1890 );
1891 }
1892 }
1893
1894 #[test]
1895 fn measure_dom_with_hook_forwards_ctx_and_available_and_consumes_the_dom() {
1896 let fonts = FcFontCache::default();
1897 let images = ImageCache::default();
1898 let mut info = vv_info(&fonts, &images, bounds_1x1());
1899
1900 let mut ctx_val: u32 = 0;
1901 info.set_measure_dom_fn(
1902 test_measure_dom_fn,
1903 core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
1904 );
1905
1906 let before = MEASURE_CALLS.load(AtomicOrdering::SeqCst);
1909 let out = info.measure_dom(Dom::create_body(), LogicalSize::new(100.0, 40.0));
1910
1911 assert!(MEASURE_CALLS.load(AtomicOrdering::SeqCst) > before);
1912 assert_eq!(out, LogicalSize::new(200.0, 20.0));
1913 assert_eq!(ctx_val, 0xABCD, "measure ctx pointer was not forwarded");
1914
1915 let natural = info.measure_dom(Dom::create_body(), LogicalSize::new(320.0, 1_000_000.0));
1917 assert_eq!(natural, LogicalSize::new(640.0, 500_000.0));
1918
1919 let nan = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::NAN, 4.0));
1922 assert!(nan.width.is_nan());
1923 assert_eq!(nan.height, 2.0);
1924
1925 let inf = info.measure_dom(Dom::create_body(), LogicalSize::new(f32::INFINITY, 4.0));
1926 assert!(inf.width.is_infinite());
1927 }
1928
1929 #[test]
1930 fn measure_dom_hook_can_be_replaced_and_last_writer_wins() {
1931 let fonts = FcFontCache::default();
1932 let images = ImageCache::default();
1933 let mut info = vv_info(&fonts, &images, bounds_1x1());
1934
1935 info.set_measure_dom_fn(test_measure_dom_fn, core::ptr::null_mut());
1936 let first = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
1938 assert_eq!(first, LogicalSize::new(4.0, 4.0));
1939
1940 let mut ctx_val: u32 = 0;
1941 info.set_measure_dom_fn(
1942 test_measure_dom_fn,
1943 core::ptr::from_mut(&mut ctx_val).cast::<c_void>(),
1944 );
1945 let second = info.measure_dom(Dom::create_body(), LogicalSize::new(2.0, 8.0));
1946 assert_eq!(second, first);
1947 assert_eq!(ctx_val, 0xABCD);
1948 }
1949
1950 #[test]
1953 fn virtual_view_return_with_dom_and_keep_current_hold_their_fields() {
1954 let ss = LogicalSize::new(600.0, 30.0);
1955 let so = LogicalPosition::new(0.0, 300.0);
1956 let vss = LogicalSize::new(600.0, 30_000.0);
1957 let vso = LogicalPosition::zero();
1958
1959 let with = VirtualViewReturn::with_dom(Dom::create_body(), ss, so, vss, vso);
1960 assert!(with.dom.is_some(), "with_dom must produce OptionDom::Some");
1961 assert_eq!(with.scroll_size, ss);
1962 assert_eq!(with.scroll_offset, so);
1963 assert_eq!(with.virtual_scroll_size, vss);
1964 assert_eq!(with.virtual_scroll_offset, vso);
1965 assert_eq!(with.dom, OptionDom::Some(Dom::create_body()));
1966
1967 let keep = VirtualViewReturn::keep_current(ss, so, vss, vso);
1968 assert!(keep.dom.is_none(), "keep_current must produce OptionDom::None");
1969 assert_eq!(keep.scroll_size, ss);
1970 assert_eq!(keep.scroll_offset, so);
1971 assert_eq!(keep.virtual_scroll_size, vss);
1972 assert_eq!(keep.virtual_scroll_offset, vso);
1973
1974 assert_ne!(with, keep);
1976
1977 let d = VirtualViewReturn::default();
1979 assert_eq!(
1980 d,
1981 VirtualViewReturn::keep_current(
1982 LogicalSize::zero(),
1983 LogicalPosition::zero(),
1984 LogicalSize::zero(),
1985 LogicalPosition::zero()
1986 )
1987 );
1988 }
1989
1990 #[test]
1991 fn virtual_view_return_keep_current_passes_extreme_values_through_unclamped() {
1992 let z = VirtualViewReturn::keep_current(
1994 LogicalSize::zero(),
1995 LogicalPosition::zero(),
1996 LogicalSize::zero(),
1997 LogicalPosition::zero(),
1998 );
1999 assert_eq!(z.scroll_size, LogicalSize::zero());
2000 assert_eq!(z.virtual_scroll_size, LogicalSize::zero());
2001
2002 let n = VirtualViewReturn::keep_current(
2004 LogicalSize::new(-1.0, -0.0),
2005 LogicalPosition::new(f32::MIN, f32::MAX),
2006 LogicalSize::new(f32::MAX, f32::MIN_POSITIVE),
2007 LogicalPosition::new(-f32::EPSILON, 0.0),
2008 );
2009 assert_eq!(n.scroll_size.width, -1.0);
2010 assert_eq!(n.scroll_offset.x, f32::MIN);
2011 assert_eq!(n.scroll_offset.y, f32::MAX);
2012 assert_eq!(n.virtual_scroll_size.width, f32::MAX);
2013 assert_eq!(n.virtual_scroll_size.height, f32::MIN_POSITIVE);
2014
2015 let x = VirtualViewReturn::keep_current(
2018 LogicalSize::new(f32::NAN, f32::INFINITY),
2019 LogicalPosition::new(f32::NEG_INFINITY, f32::NAN),
2020 LogicalSize::new(f32::INFINITY, f32::NAN),
2021 LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
2022 );
2023 assert!(x.scroll_size.width.is_nan());
2024 assert!(x.scroll_size.height.is_infinite() && x.scroll_size.height.is_sign_positive());
2025 assert!(x.scroll_offset.x.is_infinite() && x.scroll_offset.x.is_sign_negative());
2026 assert!(x.scroll_offset.y.is_nan());
2027 assert!(x.virtual_scroll_offset.y.is_infinite());
2028 assert!(x.dom.is_none());
2029 }
2030
2031 #[test]
2034 fn timer_callback_return_constructors_match_their_documented_flags() {
2035 let c = TimerCallbackReturn::continue_unchanged();
2036 assert_eq!(c.should_update, Update::DoNothing);
2037 assert_eq!(c.should_terminate, TerminateTimer::Continue);
2038
2039 let cr = TimerCallbackReturn::continue_and_refresh_dom();
2040 assert_eq!(cr.should_update, Update::RefreshDom);
2041 assert_eq!(cr.should_terminate, TerminateTimer::Continue);
2042
2043 let t = TimerCallbackReturn::terminate_unchanged();
2044 assert_eq!(t.should_update, Update::DoNothing);
2045 assert_eq!(t.should_terminate, TerminateTimer::Terminate);
2046
2047 let tr = TimerCallbackReturn::terminate_and_refresh_dom();
2048 assert_eq!(tr.should_update, Update::RefreshDom);
2049 assert_eq!(tr.should_terminate, TerminateTimer::Terminate);
2050
2051 let all = [c, cr, t, tr];
2053 for (i, a) in all.iter().enumerate() {
2054 for (j, b) in all.iter().enumerate() {
2055 assert_eq!(i == j, a == b, "constructors {i} and {j} collide");
2056 }
2057 }
2058
2059 assert_eq!(TimerCallbackReturn::default(), c);
2061 }
2062
2063 #[test]
2064 fn timer_callback_return_create_round_trips_every_flag_combination() {
2065 for u in ALL_UPDATES {
2066 for t in [TerminateTimer::Continue, TerminateTimer::Terminate] {
2067 let r = TimerCallbackReturn::create(u, t);
2068 assert_eq!(r.should_update, u);
2069 assert_eq!(r.should_terminate, t);
2070 }
2071 }
2072
2073 assert_eq!(
2075 TimerCallbackReturn::create(Update::DoNothing, TerminateTimer::Continue),
2076 TimerCallbackReturn::continue_unchanged()
2077 );
2078 assert_eq!(
2079 TimerCallbackReturn::create(Update::RefreshDom, TerminateTimer::Terminate),
2080 TimerCallbackReturn::terminate_and_refresh_dom()
2081 );
2082
2083 let all_windows =
2086 TimerCallbackReturn::create(Update::RefreshDomAllWindows, TerminateTimer::Terminate);
2087 assert_eq!(all_windows.should_update, Update::RefreshDomAllWindows);
2088 }
2089
2090 #[test]
2093 fn layout_callback_info_new_defaults_to_initial_reason_and_holds_fields() {
2094 let fx = Fixture::new();
2095 let rd = fx.ref_data();
2096 let info = LayoutCallbackInfo::new(&rd, win(1280.0, 720.0, 192), WindowTheme::DarkMode);
2097
2098 assert_eq!(info.relayout_reason(), RelayoutReason::Initial);
2099 assert_eq!(info.theme, WindowTheme::DarkMode);
2100 assert_eq!(info.get_window_width(), 1280.0);
2101 assert_eq!(info.get_window_height(), 720.0);
2102 assert_eq!(info.get_dpi_factor(), 2.0);
2103 assert!(info.get_ctx().is_none());
2104
2105 assert!(core::ptr::eq(info.internal_get_image_cache(), &fx.images));
2107 assert!(core::ptr::eq(info.internal_get_system_fonts(), &fx.fonts));
2108 assert!(core::ptr::eq(info.internal_get_gl_context(), &fx.gl));
2109 assert!(info.get_gl_context().is_none());
2110 }
2111
2112 #[test]
2113 fn layout_callback_info_new_with_reason_round_trips_every_reason() {
2114 let fx = Fixture::new();
2115 let rd = fx.ref_data();
2116
2117 for reason in [
2118 RelayoutReason::Initial,
2119 RelayoutReason::RefreshDom,
2120 RelayoutReason::Resize,
2121 RelayoutReason::ThemeChange,
2122 RelayoutReason::RouteChange,
2123 RelayoutReason::Other,
2124 ] {
2125 let info = LayoutCallbackInfo::new_with_reason(
2126 &rd,
2127 WindowSize::default(),
2128 WindowTheme::LightMode,
2129 reason,
2130 );
2131 assert_eq!(info.relayout_reason(), reason);
2132 assert_eq!(info.clone().relayout_reason(), reason);
2134 }
2135
2136 assert_eq!(RelayoutReason::default(), RelayoutReason::Initial);
2137 }
2138
2139 #[test]
2140 fn layout_callback_info_get_system_style_shares_the_arc() {
2141 let fx = Fixture::new();
2142 let rd = fx.ref_data();
2143 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2144
2145 let a = info.get_system_style();
2146 let b = info.get_system_style();
2147 assert!(Arc::ptr_eq(&a, &b));
2149 assert!(Arc::ptr_eq(&a, &fx.style));
2150
2151 let before = Arc::strong_count(&fx.style);
2153 for _ in 0..128 {
2154 drop(info.get_system_style());
2155 }
2156 assert_eq!(Arc::strong_count(&fx.style), before);
2157 }
2158
2159 #[test]
2160 fn layout_callback_info_get_ctx_is_none_until_set_then_clones_safely() {
2161 let fx = Fixture::new();
2162 let rd = fx.ref_data();
2163 let mut info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2164
2165 assert!(info.get_ctx().is_none(), "native path must have a null ctx");
2166
2167 let callable = OptionRefAny::Some(RefAny::new(7u64));
2168 info.set_callable_ptr(&callable);
2169
2170 for _ in 0..64 {
2171 assert!(info.get_ctx().is_some());
2172 }
2173
2174 let mut got = info.get_ctx();
2175 match got {
2176 OptionRefAny::Some(ref mut r) => assert_eq!(*r.downcast_ref::<u64>().unwrap(), 7),
2177 OptionRefAny::None => panic!("get_ctx() lost the callable"),
2178 }
2179 drop(got);
2180
2181 let cloned = info.clone();
2183 assert!(cloned.get_ctx().is_some());
2184 }
2185
2186 #[test]
2187 fn layout_callback_info_get_system_fonts_is_empty_for_an_empty_cache() {
2188 let fx = Fixture::new();
2189 let rd = fx.ref_data();
2190 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2191
2192 let fonts: Vec<AzStringPair> = info.get_system_fonts();
2194 assert!(fonts.is_empty());
2195 assert_eq!(info.get_system_fonts().len(), fonts.len());
2197 }
2198
2199 #[test]
2202 fn get_image_returns_none_for_missing_empty_and_hostile_ids() {
2203 let fx = Fixture::new();
2204 let rd = fx.ref_data();
2205 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2206
2207 assert!(info.get_image(&s("")).is_none());
2208 assert!(info.get_image(&s(" ")).is_none());
2209 assert!(info.get_image(&s("nope")).is_none());
2210 assert!(info.get_image(&s("\u{1F600}\u{0301}")).is_none());
2211 assert!(info.get_image(&s("\0")).is_none());
2212 assert!(info.get_image(&s(&"x".repeat(100_000))).is_none());
2213 }
2214
2215 #[test]
2216 fn get_image_finds_an_inserted_id_and_is_exact_match() {
2217 let mut fx = Fixture::new();
2218 fx.images.add_css_image_id(
2219 s("logo"),
2220 ImageRef::null_image(2, 2, RawImageFormat::RGBA8, Vec::new()),
2221 );
2222 let rd = fx.ref_data();
2223 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2224
2225 assert!(info.get_image(&s("logo")).is_some(), "positive control");
2226
2227 assert!(info.get_image(&s("Logo")).is_none());
2229 assert!(info.get_image(&s(" logo")).is_none());
2230 assert!(info.get_image(&s("logo ")).is_none());
2231 assert!(info.get_image(&s("log")).is_none());
2232 assert!(info.get_image(&s("logos")).is_none());
2233 }
2234
2235 #[test]
2238 fn get_route_param_returns_none_when_no_route_is_active() {
2239 let fx = Fixture::new();
2240 let rd = fx.ref_data();
2241 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2242
2243 assert!(info.get_active_route().is_none());
2244
2245 for key in ["", " ", "\t\n", "id", "\u{1F600}", "\0", "../../etc/passwd"] {
2247 assert!(info.get_route_param(key).is_none(), "key {key:?}");
2248 }
2249 }
2250
2251 #[test]
2252 fn get_route_param_valid_minimal_and_unicode_positive_controls() {
2253 let fx = Fixture::with_route(user_route());
2254 let rd = fx.ref_data();
2255 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2256
2257 let route = info.get_active_route().expect("route was configured");
2258 assert_eq!(route.pattern.as_str(), "/user/:id");
2259
2260 assert_eq!(info.get_route_param("id").map(AzString::as_str), Some("42"));
2262 assert_eq!(
2264 info.get_route_param("\u{1F600}").map(AzString::as_str),
2265 Some("emoji")
2266 );
2267 }
2268
2269 #[test]
2270 fn get_route_param_rejects_malformed_keys_without_trimming_or_folding() {
2271 let fx = Fixture::with_route(user_route());
2272 let rd = fx.ref_data();
2273 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2274
2275 assert!(info.get_route_param("").is_none());
2277 assert!(info.get_route_param(" ").is_none());
2278 assert!(info.get_route_param("\t\n").is_none());
2279
2280 assert!(info.get_route_param(" id").is_none());
2282 assert!(info.get_route_param("id ").is_none());
2283 assert!(info.get_route_param(" id ").is_none());
2284 assert!(info.get_route_param("id;garbage").is_none());
2285 assert!(info.get_route_param("ID").is_none());
2286 assert!(info.get_route_param("Id").is_none());
2287
2288 assert!(info.get_route_param("i").is_none());
2290 assert!(info.get_route_param("idd").is_none());
2291
2292 assert!(info.get_route_param("\0").is_none());
2294 assert!(info.get_route_param("id\0").is_none());
2295 assert!(info.get_route_param("\u{7F}\u{1}\u{2}").is_none());
2296
2297 for key in [
2299 "0",
2300 "-0",
2301 "9223372036854775807",
2302 "-9223372036854775808",
2303 "18446744073709551616",
2304 "NaN",
2305 "inf",
2306 "-inf",
2307 "1e400",
2308 "0.0000000000000000001",
2309 ] {
2310 assert!(info.get_route_param(key).is_none(), "key {key:?}");
2311 }
2312
2313 assert!(info.get_route_param("i\u{0301}d").is_none());
2315 assert!(info.get_route_param("\u{1F600}\u{1F600}").is_none());
2316 }
2317
2318 #[test]
2319 fn get_route_param_handles_pathological_key_sizes_and_nesting() {
2320 let fx = Fixture::with_route(user_route());
2321 let rd = fx.ref_data();
2322 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2323
2324 let huge = "x".repeat(1_000_000);
2326 assert!(info.get_route_param(&huge).is_none());
2327
2328 let long_id = alloc::format!("id{}", "0".repeat(1_000_000));
2330 assert!(info.get_route_param(&long_id).is_none());
2331
2332 let nested = "[".repeat(10_000) + &"]".repeat(10_000);
2335 assert!(info.get_route_param(&nested).is_none());
2336 }
2337
2338 #[test]
2339 fn get_route_param_preserves_huge_and_unicode_values() {
2340 let big = "v".repeat(200_000);
2341 let route = RouteMatch {
2342 pattern: s("/blob/:data"),
2343 params: StringPairVec::from_vec(Vec::from([AzStringPair {
2344 key: s("data"),
2345 value: s(&big),
2346 }])),
2347 };
2348 let fx = Fixture::with_route(route);
2349 let rd = fx.ref_data();
2350 let info = LayoutCallbackInfo::new(&rd, WindowSize::default(), WindowTheme::LightMode);
2351
2352 let got = info.get_route_param("data").expect("param exists");
2353 assert_eq!(got.as_str().len(), 200_000);
2354 }
2355
2356 #[test]
2359 fn window_predicates_obey_trichotomy_and_the_between_identity() {
2360 let fx = Fixture::new();
2361 let rd = fx.ref_data();
2362
2363 let probes = [
2364 0.0f32,
2365 -0.0,
2366 1.0,
2367 -1.0,
2368 640.0,
2369 f32::MIN,
2370 f32::MAX,
2371 f32::MIN_POSITIVE,
2372 f32::INFINITY,
2373 f32::NEG_INFINITY,
2374 ];
2375
2376 for &dim in &probes {
2377 let info = LayoutCallbackInfo::new(&rd, win(dim, dim, 96), WindowTheme::LightMode);
2378
2379 for &px in &probes {
2380 let lt = info.window_width_less_than(px);
2381 let gt = info.window_width_greater_than(px);
2382 let eq = info.get_window_width() == px;
2383
2384 assert_eq!(
2386 u8::from(lt) + u8::from(gt) + u8::from(eq),
2387 1,
2388 "trichotomy broken for width {dim} vs {px}"
2389 );
2390
2391 assert_eq!(info.window_height_less_than(px), lt);
2393 assert_eq!(info.window_height_greater_than(px), gt);
2394
2395 for &px2 in &probes {
2396 assert_eq!(
2398 info.window_width_between(px, px2),
2399 !info.window_width_less_than(px) && !info.window_width_greater_than(px2),
2400 "between identity broken for width {dim} in [{px}, {px2}]"
2401 );
2402 assert_eq!(
2403 info.window_height_between(px, px2),
2404 info.window_width_between(px, px2)
2405 );
2406 }
2407 }
2408 }
2409 }
2410
2411 #[test]
2412 fn window_predicates_with_inverted_and_degenerate_ranges() {
2413 let fx = Fixture::new();
2414 let rd = fx.ref_data();
2415 let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
2416
2417 assert!(!info.window_width_between(1000.0, 100.0));
2419 assert!(!info.window_height_between(1000.0, 100.0));
2420
2421 assert!(info.window_width_between(640.0, 640.0));
2423 assert!(info.window_height_between(480.0, 480.0));
2424 assert!(!info.window_width_between(639.9, 639.95));
2425
2426 assert!(info.window_width_between(640.0, 1000.0));
2428 assert!(info.window_width_between(0.0, 640.0));
2429
2430 assert!(!info.window_width_less_than(640.0));
2432 assert!(!info.window_width_greater_than(640.0));
2433 assert!(info.window_width_less_than(640.001));
2434 assert!(info.window_width_greater_than(639.999));
2435
2436 assert!(info.window_width_between(f32::NEG_INFINITY, f32::INFINITY));
2438 }
2439
2440 #[test]
2441 fn window_predicates_are_all_false_for_nan_probes() {
2442 let fx = Fixture::new();
2443 let rd = fx.ref_data();
2444 let info = LayoutCallbackInfo::new(&rd, win(640.0, 480.0, 96), WindowTheme::LightMode);
2445
2446 assert!(!info.window_width_less_than(f32::NAN));
2448 assert!(!info.window_width_greater_than(f32::NAN));
2449 assert!(!info.window_width_between(f32::NAN, f32::NAN));
2450 assert!(!info.window_width_between(f32::NAN, 10_000.0));
2451 assert!(!info.window_width_between(0.0, f32::NAN));
2452
2453 assert!(!info.window_height_less_than(f32::NAN));
2454 assert!(!info.window_height_greater_than(f32::NAN));
2455 assert!(!info.window_height_between(f32::NAN, f32::NAN));
2456 assert!(!info.window_height_between(f32::NAN, 10_000.0));
2457 assert!(!info.window_height_between(0.0, f32::NAN));
2458 }
2459
2460 #[test]
2461 fn window_predicates_are_all_false_for_a_nan_sized_window() {
2462 let fx = Fixture::new();
2463 let rd = fx.ref_data();
2464 let info = LayoutCallbackInfo::new(
2465 &rd,
2466 win(f32::NAN, f32::NAN, 96),
2467 WindowTheme::LightMode,
2468 );
2469
2470 assert!(info.get_window_width().is_nan());
2471 assert!(info.get_window_height().is_nan());
2472
2473 for px in [0.0f32, 640.0, f32::MAX, f32::INFINITY, f32::NEG_INFINITY] {
2475 assert!(!info.window_width_less_than(px));
2476 assert!(!info.window_width_greater_than(px));
2477 assert!(!info.window_height_less_than(px));
2478 assert!(!info.window_height_greater_than(px));
2479 assert!(!info.window_width_between(f32::NEG_INFINITY, px));
2480 assert!(!info.window_height_between(px, f32::INFINITY));
2481 }
2482 }
2483
2484 #[test]
2485 fn get_dpi_factor_at_zero_and_u32_limits() {
2486 let fx = Fixture::new();
2487 let rd = fx.ref_data();
2488
2489 let base = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 96), WindowTheme::LightMode);
2491 assert_eq!(base.get_dpi_factor(), 1.0);
2492
2493 let hidpi = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 192), WindowTheme::LightMode);
2494 assert_eq!(hidpi.get_dpi_factor(), 2.0);
2495
2496 let zero = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 0), WindowTheme::LightMode);
2498 assert_eq!(zero.get_dpi_factor(), 0.0);
2499
2500 let max = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, u32::MAX), WindowTheme::LightMode);
2502 let f = max.get_dpi_factor();
2503 assert!(f.is_finite() && f > 0.0, "dpi factor {f} is not finite");
2504 assert_eq!(f, (u32::MAX as f32) / 96.0);
2505
2506 let one = LayoutCallbackInfo::new(&rd, win(1.0, 1.0, 1), WindowTheme::LightMode);
2508 assert!(one.get_dpi_factor() > 0.0);
2509 }
2510
2511 #[test]
2514 fn hidpi_adjusted_bounds_from_bounds_holds_its_fields() {
2515 let b = HidpiAdjustedBounds::from_bounds(
2516 LayoutSize::new(800, 600),
2517 DpiScaleFactor::new(1.5),
2518 );
2519 assert_eq!(b.get_logical_size(), LogicalSize::new(800.0, 600.0));
2520 assert_eq!(b.get_hidpi_factor(), DpiScaleFactor::new(1.5));
2521 assert_eq!(b.logical_size, b.get_logical_size());
2522 assert_eq!(b.hidpi_factor, b.get_hidpi_factor());
2523
2524 let p = b.get_physical_size();
2525 assert_eq!(p.width, 1200);
2526 assert_eq!(p.height, 900);
2527 }
2528
2529 #[test]
2530 fn hidpi_adjusted_bounds_at_zero() {
2531 let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(0, 0), DpiScaleFactor::new(1.0));
2532 assert_eq!(b.get_logical_size(), LogicalSize::zero());
2533 let p = b.get_physical_size();
2534 assert_eq!(p.width, 0);
2535 assert_eq!(p.height, 0);
2536
2537 let z = HidpiAdjustedBounds::from_bounds(
2539 LayoutSize::new(1920, 1080),
2540 DpiScaleFactor::new(0.0),
2541 );
2542 let zp = z.get_physical_size();
2543 assert_eq!(zp.width, 0);
2544 assert_eq!(zp.height, 0);
2545 }
2546
2547 #[test]
2552 fn hidpi_adjusted_bounds_physical_size_saturates_on_negative_input() {
2553 let b = HidpiAdjustedBounds::from_bounds(
2554 LayoutSize::new(-100, -50),
2555 DpiScaleFactor::new(1.0),
2556 );
2557 assert_eq!(b.get_logical_size(), LogicalSize::new(-100.0, -50.0));
2558
2559 let p = b.get_physical_size();
2560 assert_eq!(p.width, 0, "negative logical width must clamp to 0, not wrap");
2561 assert_eq!(p.height, 0, "negative logical height must clamp to 0, not wrap");
2562
2563 let neg_scale = HidpiAdjustedBounds::from_bounds(
2565 LayoutSize::new(100, 100),
2566 DpiScaleFactor::new(-2.0),
2567 );
2568 let np = neg_scale.get_physical_size();
2569 assert_eq!(np.width, 0);
2570 assert_eq!(np.height, 0);
2571 }
2572
2573 #[test]
2574 fn hidpi_adjusted_bounds_physical_size_saturates_at_the_upper_limit() {
2575 let b = HidpiAdjustedBounds::from_bounds(
2577 LayoutSize::new(isize::MAX, isize::MAX),
2578 DpiScaleFactor::new(1.0),
2579 );
2580 let p = b.get_physical_size();
2581 assert_eq!(p.width, u32::MAX);
2582 assert_eq!(p.height, u32::MAX);
2583
2584 let min = HidpiAdjustedBounds::from_bounds(
2586 LayoutSize::new(isize::MIN, isize::MIN),
2587 DpiScaleFactor::new(1.0),
2588 );
2589 let mp = min.get_physical_size();
2590 assert_eq!(mp.width, 0);
2591 assert_eq!(mp.height, 0);
2592
2593 let huge_scale = HidpiAdjustedBounds::from_bounds(
2595 LayoutSize::new(1000, 1000),
2596 DpiScaleFactor::new(f32::MAX),
2597 );
2598 let hp = huge_scale.get_physical_size();
2599 assert_eq!(hp.width, u32::MAX);
2600 assert_eq!(hp.height, u32::MAX);
2601 }
2602
2603 #[test]
2607 fn hidpi_adjusted_bounds_physical_size_with_nan_and_infinite_scale() {
2608 let nan = HidpiAdjustedBounds::from_bounds(
2609 LayoutSize::new(100, 100),
2610 DpiScaleFactor::new(f32::NAN),
2611 );
2612 assert_eq!(nan.get_hidpi_factor().inner.get(), 0.0);
2614 let np = nan.get_physical_size();
2615 assert_eq!(np.width, 0);
2616 assert_eq!(np.height, 0);
2617
2618 let inf = HidpiAdjustedBounds::from_bounds(
2619 LayoutSize::new(100, 100),
2620 DpiScaleFactor::new(f32::INFINITY),
2621 );
2622 assert!(inf.get_hidpi_factor().inner.get().is_finite());
2624 let ip = inf.get_physical_size();
2625 assert_eq!(ip.width, u32::MAX);
2626 assert_eq!(ip.height, u32::MAX);
2627
2628 let neg_inf = HidpiAdjustedBounds::from_bounds(
2629 LayoutSize::new(100, 100),
2630 DpiScaleFactor::new(f32::NEG_INFINITY),
2631 );
2632 let nip = neg_inf.get_physical_size();
2633 assert_eq!(nip.width, 0);
2634 assert_eq!(nip.height, 0);
2635 }
2636
2637 #[test]
2638 fn hidpi_adjusted_bounds_physical_size_rounds_to_nearest() {
2639 let b = HidpiAdjustedBounds::from_bounds(LayoutSize::new(3, 3), DpiScaleFactor::new(1.5));
2641 let p = b.get_physical_size();
2642 assert_eq!(p.width, 5, "3 * 1.5 = 4.5 must round to 5");
2643 assert_eq!(p.height, 5);
2644
2645 let p2 = b.get_physical_size();
2647 assert_eq!(p.width, p2.width);
2648 assert_eq!(p.height, p2.height);
2649 }
2650
2651 fn cb_data(cb: usize) -> CoreCallbackData {
2654 CoreCallbackData {
2655 event: EventFilter::Hover(HoverEventFilter::MouseOver),
2656 callback: CoreCallback::from(cb),
2657 refany: RefAny::new(cb),
2658 }
2659 }
2660
2661 #[test]
2662 fn core_callback_data_vec_as_container_on_empty_vecs_does_not_panic() {
2663 let empty = CoreCallbackDataVec::new();
2666 assert_eq!(empty.as_container().len(), 0);
2667 assert!(empty.as_container().internal.is_empty());
2668
2669 let from_empty_vec = CoreCallbackDataVec::from_vec(Vec::new());
2670 assert_eq!(from_empty_vec.as_container().len(), 0);
2671
2672 let mut mut_empty = CoreCallbackDataVec::from_vec(Vec::new());
2673 assert!(mut_empty.as_container_mut().internal.is_empty());
2674 }
2675
2676 #[test]
2677 fn core_callback_data_vec_as_container_matches_the_backing_vec() {
2678 let v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2), cb_data(3)]));
2679
2680 let c = v.as_container();
2681 assert_eq!(c.len(), 3);
2682 assert_eq!(c.len(), v.len());
2683 assert_eq!(c.internal[0].callback.cb, 1);
2684 assert_eq!(c.internal[2].callback.cb, 3);
2685
2686 assert!(core::ptr::eq(c.internal.as_ptr(), v.as_slice().as_ptr()));
2688 }
2689
2690 #[test]
2691 fn core_callback_data_vec_as_container_mut_writes_through() {
2692 let mut v = CoreCallbackDataVec::from_vec(Vec::from([cb_data(1), cb_data(2)]));
2693
2694 {
2695 let mut c = v.as_container_mut();
2696 assert_eq!(c.internal.len(), 2);
2697 c.internal[0].callback.cb = 99;
2698 c.internal[1].event = EventFilter::Hover(HoverEventFilter::MouseDown);
2699 }
2700
2701 let c = v.as_container();
2703 assert_eq!(c.internal[0].callback.cb, 99);
2704 assert_eq!(
2705 c.internal[1].event,
2706 EventFilter::Hover(HoverEventFilter::MouseDown)
2707 );
2708 assert_eq!(c.len(), 2);
2709 }
2710}
2711
2712#[cfg(test)]
2716#[allow(clippy::float_cmp)]
2717mod size_query_tests {
2718 use super::*;
2719 use crate::geom::LogicalSize;
2720
2721 fn win(width: f32, height: f32) -> WindowSize {
2722 WindowSize {
2723 dimensions: LogicalSize::new(width, height),
2724 ..WindowSize::default()
2725 }
2726 }
2727
2728 fn info_at(rd: &LayoutCallbackInfoRefData<'_>, w: f32, h: f32) -> LayoutCallbackInfo {
2729 LayoutCallbackInfo::new(rd, win(w, h), WindowTheme::LightMode)
2730 }
2731
2732 fn drain() -> (alloc::vec::Vec<SizeQuery>, bool) {
2733 take_recorded_size_queries()
2734 }
2735
2736 struct Rd {
2739 image_cache: crate::resources::ImageCache,
2740 gl: crate::gl::OptionGlContextPtr,
2741 fonts: rust_fontconfig::FcFontCache,
2742 style: alloc::sync::Arc<azul_css::system::SystemStyle>,
2743 }
2744 impl Rd {
2745 fn new() -> Self {
2746 Self {
2747 image_cache: crate::resources::ImageCache::default(),
2748 gl: crate::gl::OptionGlContextPtr::None,
2749 fonts: rust_fontconfig::FcFontCache::default(),
2750 style: alloc::sync::Arc::new(azul_css::system::SystemStyle::default()),
2751 }
2752 }
2753 fn ref_data(&self) -> LayoutCallbackInfoRefData<'_> {
2754 LayoutCallbackInfoRefData {
2755 image_cache: &self.image_cache,
2756 gl_context: &self.gl,
2757 system_fonts: &self.fonts,
2758 system_style: self.style.clone(),
2759 active_route: None,
2760 monitors: crate::window::MonitorVec::from_const_slice(&[]),
2761 }
2762 }
2763 }
2764
2765 #[test]
2766 fn every_responsive_helper_records_with_its_exact_operator() {
2767 let rd = Rd::new();
2768 let rd = rd.ref_data();
2769 let _ = drain();
2770
2771 let info = info_at(&rd, 800.0, 600.0);
2772 assert!(!info.window_width_less_than(800.0), "strict <: boundary is false");
2773 assert!(!info.window_width_greater_than(800.0), "strict >: boundary is false");
2774 assert!(info.window_width_between(800.0, 1024.0), "between is inclusive");
2775 assert!(info.window_height_less_than(601.0));
2776 assert!(!info.window_height_greater_than(600.0));
2777 assert!(info.window_height_between(0.0, 600.0));
2778
2779 let (recorded, overflowed) = drain();
2780 assert_eq!(recorded.len(), 8, "every call recorded; between records two bounds");
2783 assert!(!overflowed);
2784 assert_eq!(recorded[0].op, SizeQueryOp::LessThan);
2785 assert_eq!(recorded[1].op, SizeQueryOp::GreaterThan);
2786 assert_eq!(recorded[2].op, SizeQueryOp::GreaterOrEqual);
2787 assert_eq!(recorded[3].op, SizeQueryOp::LessOrEqual);
2788 }
2789
2790 #[test]
2791 fn flips_at_detects_exactly_the_crossings() {
2792 let rd = Rd::new();
2793 let rd = rd.ref_data();
2794 let _ = drain();
2795
2796 let info = info_at(&rd, 800.0, 600.0);
2797 let mobile = info.window_width_less_than(640.0); assert!(!mobile);
2799 let (recorded, _) = drain();
2800 let q = recorded[0];
2801
2802 assert!(!q.flips_at(LogicalSize::new(700.0, 600.0)));
2804 assert!(!q.flips_at(LogicalSize::new(640.0, 600.0)), "strict <: 640 is still false");
2805 assert!(q.flips_at(LogicalSize::new(639.9, 600.0)));
2807 assert!(q.flips_at(LogicalSize::new(320.0, 600.0)));
2808 assert!(!q.flips_at(LogicalSize::new(700.0, 10.0)));
2810 }
2811
2812 #[test]
2815 fn between_flips_on_either_bound_with_inclusive_semantics() {
2816 let rd = Rd::new();
2817 let rd = rd.ref_data();
2818 let _ = drain();
2819
2820 let info = info_at(&rd, 800.0, 600.0);
2821 assert!(info.window_width_between(768.0, 1024.0));
2822 let (recorded, _) = drain();
2823 let lower = recorded[0];
2824 let upper = recorded[1];
2825
2826 assert!(!lower.flips_at(LogicalSize::new(768.0, 600.0)), ">= 768: boundary holds");
2827 assert!(lower.flips_at(LogicalSize::new(767.9, 600.0)));
2828 assert!(!upper.flips_at(LogicalSize::new(1024.0, 600.0)), "<= 1024: boundary holds");
2829 assert!(upper.flips_at(LogicalSize::new(1024.1, 600.0)));
2830 }
2831
2832 #[test]
2833 fn drain_resets_the_recording() {
2834 let rd = Rd::new();
2835 let rd = rd.ref_data();
2836 let _ = drain();
2837
2838 let info = info_at(&rd, 1024.0, 768.0);
2839 let _ = info.window_width_greater_than(640.0);
2840 let (first, _) = drain();
2841 assert_eq!(first.len(), 1);
2842 let (second, overflowed) = drain();
2843 assert!(second.is_empty(), "drain must reset");
2844 assert!(!overflowed);
2845 }
2846
2847 #[test]
2848 fn overflow_latches_and_reports_rather_than_dropping_silently() {
2849 let rd = Rd::new();
2850 let rd = rd.ref_data();
2851 let _ = drain();
2852
2853 let info = info_at(&rd, 1024.0, 768.0);
2854 for i in 0..(size_query_recorder::SIZE_QUERY_CAP + 10) {
2855 let _ = info.window_width_greater_than(i as f32);
2856 }
2857 let (recorded, overflowed) = drain();
2858 assert_eq!(recorded.len(), size_query_recorder::SIZE_QUERY_CAP);
2859 assert!(
2860 overflowed,
2861 "past the cap the drain MUST say the list is incomplete — silence \
2862 here is a resize skipping a layout() that would have branched"
2863 );
2864 let (_, overflowed2) = drain();
2866 assert!(!overflowed2);
2867 }
2868}