1pub mod cache;
6pub mod calc;
7pub mod counters;
8pub mod display_list;
9pub mod fc;
10pub mod geometry;
11pub mod getters;
12pub mod layout_tree;
13pub mod paged_layout;
14pub mod pagination;
15pub mod positioning;
16pub mod scrollbar;
17pub mod sizing;
18pub mod taffy_bridge;
19
20#[macro_export]
22macro_rules! debug_info {
23 ($ctx:expr, $($arg:tt)*) => {
24 if $ctx.debug_messages.is_some() {
25 $ctx.debug_info_inner(format!($($arg)*));
26 }
27 };
28}
29
30#[macro_export]
32macro_rules! debug_warning {
33 ($ctx:expr, $($arg:tt)*) => {
34 if $ctx.debug_messages.is_some() {
35 $ctx.debug_warning_inner(format!($($arg)*));
36 }
37 };
38}
39
40#[macro_export]
42macro_rules! debug_error {
43 ($ctx:expr, $($arg:tt)*) => {
44 if $ctx.debug_messages.is_some() {
45 $ctx.debug_error_inner(format!($($arg)*));
46 }
47 };
48}
49
50#[macro_export]
52macro_rules! debug_log {
53 ($ctx:expr, $($arg:tt)*) => {
54 if $ctx.debug_messages.is_some() {
55 $ctx.debug_log_inner(format!($($arg)*));
56 }
57 };
58}
59
60#[macro_export]
62macro_rules! debug_box_props {
63 ($ctx:expr, $($arg:tt)*) => {
64 if $ctx.debug_messages.is_some() {
65 $ctx.debug_box_props_inner(format!($($arg)*));
66 }
67 };
68}
69
70#[macro_export]
72macro_rules! debug_css_getter {
73 ($ctx:expr, $($arg:tt)*) => {
74 if $ctx.debug_messages.is_some() {
75 $ctx.debug_css_getter_inner(format!($($arg)*));
76 }
77 };
78}
79
80#[macro_export]
82macro_rules! debug_bfc_layout {
83 ($ctx:expr, $($arg:tt)*) => {
84 if $ctx.debug_messages.is_some() {
85 $ctx.debug_bfc_layout_inner(format!($($arg)*));
86 }
87 };
88}
89
90#[macro_export]
92macro_rules! debug_ifc_layout {
93 ($ctx:expr, $($arg:tt)*) => {
94 if $ctx.debug_messages.is_some() {
95 $ctx.debug_ifc_layout_inner(format!($($arg)*));
96 }
97 };
98}
99
100#[macro_export]
102macro_rules! debug_table_layout {
103 ($ctx:expr, $($arg:tt)*) => {
104 if $ctx.debug_messages.is_some() {
105 $ctx.debug_table_layout_inner(format!($($arg)*));
106 }
107 };
108}
109
110#[macro_export]
112macro_rules! debug_display_type {
113 ($ctx:expr, $($arg:tt)*) => {
114 if $ctx.debug_messages.is_some() {
115 $ctx.debug_display_type_inner(format!($($arg)*));
116 }
117 };
118}
119
120use std::{collections::{BTreeMap, HashMap}, sync::Arc};
121
122use azul_core::{
123 dom::{DomId, NodeId},
124 geom::{LogicalPosition, LogicalRect, LogicalSize},
125 hit_test::{DocumentId, ScrollPosition},
126 resources::RendererResources,
127 selection::{TextCursor, TextSelection},
128 styled_dom::StyledDom,
129};
130
131pub(crate) const POSITION_UNSET: LogicalPosition = LogicalPosition { x: f32::MIN, y: f32::MIN };
133
134const MAX_SCROLLBAR_REFLOW_ITERATIONS: usize = 10;
138
139pub type PositionVec = Vec<LogicalPosition>;
142
143#[inline]
149#[must_use] pub fn pos_get(positions: &PositionVec, idx: usize) -> Option<LogicalPosition> {
150 positions.get(idx).copied().filter(|p| p.x != f32::MIN)
151}
152
153#[inline]
155pub fn pos_set(positions: &mut PositionVec, idx: usize, pos: LogicalPosition) {
156 if idx >= positions.len() {
157 positions.resize(idx + 1, POSITION_UNSET);
158 }
159 positions[idx] = pos;
160}
161
162#[inline]
164#[must_use] pub fn pos_contains(positions: &PositionVec, idx: usize) -> bool {
165 positions.get(idx).is_some_and(|p| p.x != f32::MIN)
166}
167use azul_css::{
168 props::property::{CssProperty, CssPropertyCategory},
169 LayoutDebugMessage, LayoutDebugMessageType,
170};
171
172use self::{
173 display_list::generate_display_list,
174 geometry::IntrinsicSizes,
175 getters::get_writing_mode,
176 layout_tree::{generate_layout_tree, LayoutTree},
177 sizing::calculate_intrinsic_sizes,
178};
179#[cfg(feature = "text_layout")]
180pub use crate::font_traits::TextLayoutCache;
181use crate::{
182 font_traits::ParsedFontTrait,
183 solver3::{
184 cache::LayoutCache,
185 display_list::DisplayList,
186 fc::LayoutConstraints,
187 layout_tree::DirtyFlag,
188 },
189};
190
191#[derive(Debug)]
193pub struct LayoutContext<'a, T: ParsedFontTrait> {
194 pub styled_dom: &'a StyledDom,
195 #[cfg(feature = "text_layout")]
196 pub font_manager: &'a crate::font_traits::FontManager<T>,
197 #[cfg(not(feature = "text_layout"))]
198 pub font_manager: core::marker::PhantomData<&'a T>,
199 pub text_selections: &'a BTreeMap<DomId, TextSelection>,
201 pub debug_messages: &'a mut Option<Vec<LayoutDebugMessage>>,
202 pub counters: &'a mut HashMap<(usize, String), i32>,
203 pub viewport_size: LogicalSize,
204 pub fragmentation_context: Option<&'a mut crate::paged::FragmentationContext>,
207 pub cursor_is_visible: bool,
211 pub cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
215 pub preedit_text: Option<String>,
218 pub dirty_text_overrides: BTreeMap<(DomId, NodeId), String>,
223 pub cache_map: cache::LayoutCacheMap,
227 pub image_cache: &'a azul_core::resources::ImageCache,
229 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
232 pub get_system_time_fn: azul_core::task::GetSystemTimeCallback,
236 pub scrollbar_style_cache:
249 core::cell::RefCell<HashMap<NodeId, getters::ComputedScrollbarStyle>>,
250}
251
252impl<T: ParsedFontTrait> LayoutContext<'_, T> {
253 #[inline]
255 pub fn debug_log_inner(&mut self, message: String) {
256 if let Some(messages) = self.debug_messages.as_mut() {
257 messages.push(LayoutDebugMessage {
258 message: message.into(),
259 location: "solver3".into(),
260 message_type: LayoutDebugMessageType::default(),
261 });
262 }
263 }
264
265 #[inline]
267 pub fn debug_info_inner(&mut self, message: String) {
268 if let Some(messages) = self.debug_messages.as_mut() {
269 messages.push(LayoutDebugMessage::info(message));
270 }
271 }
272
273 #[inline]
275 pub fn debug_warning_inner(&mut self, message: String) {
276 if let Some(messages) = self.debug_messages.as_mut() {
277 messages.push(LayoutDebugMessage::warning(message));
278 }
279 }
280
281 #[inline]
283 pub fn debug_error_inner(&mut self, message: String) {
284 if let Some(messages) = self.debug_messages.as_mut() {
285 messages.push(LayoutDebugMessage::error(message));
286 }
287 }
288
289 #[inline]
291 pub fn debug_box_props_inner(&mut self, message: String) {
292 if let Some(messages) = self.debug_messages.as_mut() {
293 messages.push(LayoutDebugMessage::box_props(message));
294 }
295 }
296
297 #[inline]
299 pub fn debug_css_getter_inner(&mut self, message: String) {
300 if let Some(messages) = self.debug_messages.as_mut() {
301 messages.push(LayoutDebugMessage::css_getter(message));
302 }
303 }
304
305 #[inline]
307 pub fn debug_bfc_layout_inner(&mut self, message: String) {
308 if let Some(messages) = self.debug_messages.as_mut() {
309 messages.push(LayoutDebugMessage::bfc_layout(message));
310 }
311 }
312
313 #[inline]
315 pub fn debug_ifc_layout_inner(&mut self, message: String) {
316 if let Some(messages) = self.debug_messages.as_mut() {
317 messages.push(LayoutDebugMessage::ifc_layout(message));
318 }
319 }
320
321 #[inline]
323 pub fn debug_table_layout_inner(&mut self, message: String) {
324 if let Some(messages) = self.debug_messages.as_mut() {
325 messages.push(LayoutDebugMessage::table_layout(message));
326 }
327 }
328
329 #[inline]
331 pub fn debug_display_type_inner(&mut self, message: String) {
332 if let Some(messages) = self.debug_messages.as_mut() {
333 messages.push(LayoutDebugMessage::display_type(message));
334 }
335 }
336}
337
338#[cfg(feature = "text_layout")]
345pub static SKIP_DISPLAY_LIST: core::sync::atomic::AtomicBool =
359 core::sync::atomic::AtomicBool::new(false);
360
361pub fn set_skip_display_list(skip: bool) {
370 SKIP_DISPLAY_LIST.store(skip, core::sync::atomic::Ordering::Relaxed);
371}
372
373#[inline(never)]
380#[allow(clippy::cast_possible_truncation)]
382#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn layout_document<T: ParsedFontTrait + Sync + 'static>(
387 cache: &mut LayoutCache,
388 text_cache: &mut TextLayoutCache,
389 new_dom: &StyledDom,
390 viewport: LogicalRect,
391 font_manager: &crate::font_traits::FontManager<T>,
392 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
393 text_selections: &BTreeMap<DomId, TextSelection>,
394 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
395 gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
396 renderer_resources: &azul_core::resources::RendererResources,
397 id_namespace: azul_core::resources::IdNamespace,
398 dom_id: DomId,
399 cursor_is_visible: bool,
400 cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
401 preedit_text: Option<String>,
402 image_cache: &azul_core::resources::ImageCache,
403 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
404 get_system_time_fn: azul_core::task::GetSystemTimeCallback,
405) -> Result<DisplayList> {
406 use crate::window::LayoutWindow;
407
408 fn collect_anon_children_by_parent(
416 tree: &LayoutTree,
417 ) -> HashMap<usize, Vec<usize>> {
418 let mut map: HashMap<usize, Vec<usize>> =
419 HashMap::new();
420 for (idx, node) in tree.nodes.iter().enumerate() {
421 if node.dom_node_id.is_some() {
422 continue;
423 }
424 if let Some(parent) = node.parent {
425 map.entry(parent).or_default().push(idx);
426 }
427 }
428 map
429 }
430
431 layout_tree::IfcId::reset_counter();
434 { let _ = (0xDD00_0001u32); }
437 if let Some(msgs) = debug_messages.as_mut() {
440 msgs.push(LayoutDebugMessage::info(format!(
441 "[Layout] layout_document called - viewport: ({:.1}, {:.1}) size ({:.1}x{:.1})",
442 viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height
443 )));
444 msgs.push(LayoutDebugMessage::info(format!(
445 "[Layout] DOM has {} nodes",
446 new_dom.node_data.len()
447 )));
448 }
449
450 let mut counter_values = HashMap::new();
452 let mut ctx_temp = LayoutContext {
453 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
454 styled_dom: new_dom,
455 font_manager,
456 text_selections,
457 debug_messages,
458 counters: &mut counter_values,
459 viewport_size: viewport.size,
460 fragmentation_context: None,
461 cursor_is_visible,
462 cursor_locations: cursor_locations.clone(),
463 preedit_text: preedit_text.clone(),
464 dirty_text_overrides: BTreeMap::new(),
465 cache_map: cache::LayoutCacheMap::default(), image_cache,
467 system_style: system_style.clone(),
468 get_system_time_fn,
469 };
470
471 crate::probe::sample_peak_rss("rss:enter_layout_document");
472
473 let dom_ptr = std::ptr::from_ref::<StyledDom>(new_dom) as usize;
490 cache.prev_dom_ptr = dom_ptr;
491 cache.prev_viewport = viewport;
492
493 crate::probe::reset_peak();
495 let (new_tree_val, mut recon_result) =
496 cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?;
497 let mut new_tree = Box::new(new_tree_val);
504 { let _ = (0xDD00_0002u32); }
505 unsafe { crate::az_mark(0x60704_u32, (0x71u32)); }
507 unsafe { crate::az_mark(0x60740_u32, (new_tree.nodes.len() as u32)); }
511 crate::probe::sample_peak_rss("rss:after_reconcile");
512 crate::probe::sample_phase_peak("rss:peak_during_reconcile");
513
514 if let Some((cached_hash, cached_viewport, cached_dl)) = &cache.cached_display_list {
525 let new_root_hash = new_tree
526 .cold(new_tree.root)
527 .map(|c| c.subtree_hash);
528 if new_root_hash == Some(*cached_hash) && *cached_viewport == viewport {
529 let _p = crate::probe::Probe::span("display_list_cache_hit");
530 return Ok(cached_dl.clone());
531 }
532 }
533
534 for &node_idx in &recon_result.intrinsic_dirty {
536 if let Some(warm) = new_tree.warm_mut(node_idx) {
537 warm.taffy_cache.clear();
538 }
539 }
540
541 {
545 let _p = crate::probe::Probe::span("compute_counters");
546 cache::compute_counters(new_dom, &new_tree, &mut counter_values);
547 }
548 unsafe { crate::az_mark(0x60704_u32, (0x72u32)); }
550
551 let mut cache_map = std::mem::take(&mut cache.cache_map);
567 let probe_cache_remap = Some(crate::probe::Probe::span("cache_map_remap"));
568 if let Some(old_tree) = cache.tree.as_ref() {
569 let mut remapped = cache::LayoutCacheMap::default();
570 remapped.entries.resize_with(new_tree.nodes.len(), Default::default);
571
572 for (dom_id, new_indices) in &new_tree.dom_to_layout {
575 let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
576 continue;
577 };
578 for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
579 let Some(&old_layout_idx) = old_indices.get(pair_idx) else {
580 continue;
581 };
582 if old_layout_idx >= cache_map.entries.len()
583 || new_layout_idx >= remapped.entries.len()
584 {
585 continue;
586 }
587 remapped.entries[new_layout_idx] =
588 core::mem::take(&mut cache_map.entries[old_layout_idx]);
589 }
590 }
591
592 let old_anon_by_parent = collect_anon_children_by_parent(old_tree);
595 let new_anon_by_parent = collect_anon_children_by_parent(&new_tree);
596
597 let mut new_to_old_layout_idx: HashMap<usize, usize> =
602 HashMap::new();
603 for (dom_id, new_indices) in &new_tree.dom_to_layout {
604 let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
605 continue;
606 };
607 for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
608 if let Some(&old_layout_idx) = old_indices.get(pair_idx) {
609 new_to_old_layout_idx.insert(new_layout_idx, old_layout_idx);
610 }
611 }
612 }
613
614 for (new_parent_idx, new_anon_children) in new_anon_by_parent {
615 let Some(&old_parent_idx) = new_to_old_layout_idx.get(&new_parent_idx) else {
616 continue;
617 };
618 let Some(old_anon_children) = old_anon_by_parent.get(&old_parent_idx) else {
619 continue;
620 };
621 for (ord, &new_anon_idx) in new_anon_children.iter().enumerate() {
622 let Some(&old_anon_idx) = old_anon_children.get(ord) else {
623 continue;
624 };
625 if old_anon_idx >= cache_map.entries.len()
626 || new_anon_idx >= remapped.entries.len()
627 {
628 continue;
629 }
630 remapped.entries[new_anon_idx] =
631 core::mem::take(&mut cache_map.entries[old_anon_idx]);
632 }
633 }
634
635 cache_map = remapped;
636 } else {
637 cache_map.resize_to_tree(new_tree.nodes.len());
638 }
639 drop(probe_cache_remap);
640 crate::probe::sample_peak_rss("rss:after_cache_remap");
641 for &node_idx in &recon_result.intrinsic_dirty {
642 cache_map.mark_dirty(node_idx, &new_tree.nodes);
643 }
644 for &node_idx in &recon_result.layout_roots {
645 cache_map.mark_dirty(node_idx, &new_tree.nodes);
646 }
647
648 let mut ctx = LayoutContext {
650 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
651 styled_dom: new_dom,
652 font_manager,
653 text_selections,
654 debug_messages,
655 counters: &mut counter_values,
656 viewport_size: viewport.size,
657 fragmentation_context: None,
658 cursor_is_visible,
659 cursor_locations,
660 preedit_text,
661 dirty_text_overrides: BTreeMap::new(),
662 cache_map, image_cache,
664 system_style,
665 get_system_time_fn,
666 };
667
668 if recon_result.is_clean() && cache.tree.is_some() {
677 debug_log!(ctx, "No changes, returning existing display list");
678 let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
679
680 let scroll_ids = if cache.scroll_ids.is_empty() {
682 use crate::window::LayoutWindow;
683 let (scroll_ids, scroll_id_to_node_id) =
684 LayoutWindow::compute_scroll_ids(tree, new_dom);
685 cache.scroll_ids.clone_from(&scroll_ids);
686 cache.scroll_id_to_node_id = scroll_id_to_node_id;
687 scroll_ids
688 } else {
689 cache.scroll_ids.clone()
690 };
691
692 if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
693 return Ok(DisplayList::default());
694 }
695 return generate_display_list(
696 &mut ctx,
697 tree,
698 &cache.calculated_positions,
699 scroll_offsets,
700 &scroll_ids,
701 gpu_value_cache,
702 renderer_resources,
703 id_namespace,
704 dom_id,
705 );
706 }
707
708 { let _ = (0xDD00_0003u32); }
709 unsafe { crate::az_mark(0x60704_u32, (0x80u32)); }
711 cache.tree = Some((*new_tree).clone());
717 unsafe {
722 crate::az_mark(0x607C0_u32, (new_tree.nodes.len() as u32));
723 crate::az_mark(0x607C4_u32, (cache.tree.as_ref().map_or(999, |t| t.nodes.len()) as u32));
724 }
725
726 let mut calculated_positions = cache.calculated_positions.clone();
728 let mut loop_count = 0;
729 loop {
730 loop_count += 1;
731 if loop_count > MAX_SCROLLBAR_REFLOW_ITERATIONS {
732 debug_warning!(ctx, "Scrollbar reflow loop hit limit of {} iterations, breaking to avoid infinite loop", MAX_SCROLLBAR_REFLOW_ITERATIONS);
733 break;
734 }
735
736 calculated_positions = {
737 let _p = crate::probe::Probe::span("clone_calculated_positions");
738 cache.calculated_positions.clone()
739 };
740 unsafe { crate::az_mark(0x60780_u32, (new_tree.nodes.len() as u32)); }
742 let mut reflow_needed_for_scrollbars = false;
743
744 {
745 crate::probe::reset_peak();
746 unsafe { crate::az_mark(0x60784_u32, (new_tree.nodes.len() as u32)); }
748 let _p = crate::probe::Probe::span("calc_intrinsic_sizes");
749 unsafe { crate::az_mark(0x60788_u32, (new_tree.nodes.len() as u32)); }
751 unsafe {
763 crate::az_mark(0x60748_u32, (new_tree.nodes.len() as u32));
764 crate::az_mark(0x6074C_u32, (cache.tree.as_ref().map_or(999, |t| t.nodes.len()) as u32));
765 }
766 calculate_intrinsic_sizes(
767 &mut ctx,
768 &mut new_tree,
769 text_cache,
770 &recon_result.intrinsic_dirty,
771 )?;
772 }
773 crate::probe::sample_peak_rss("rss:after_calc_intrinsic");
774 crate::probe::sample_phase_peak("rss:peak_during_intrinsic");
775 { let _ = (0xDD00_0005u32); }
777
778 for &root_idx in &recon_result.layout_roots {
779 let (cb_pos, cb_size) = get_containing_block_for_node(
780 &new_tree,
781 new_dom,
782 root_idx,
783 &calculated_positions,
784 viewport,
785 );
786 { let _ = (0xDD00_0053u32); }
789 let root_node = &new_tree.nodes[root_idx];
797 let root_bp = root_node.box_props.unpack();
798 { let _ = (0xDD00_0054u32); }
799
800 let is_root_with_margin = root_node.parent.is_none()
801 && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
802
803 let adjusted_cb_pos = if is_root_with_margin {
804 LogicalPosition::new(
805 cb_pos.x + root_bp.margin.left,
806 cb_pos.y + root_bp.margin.top,
807 )
808 } else {
809 cb_pos
810 };
811 { let _ = (0xDD00_0056u32); }
812
813 if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
815 let dom_name = root_node
816 .dom_node_id
817 .and_then(|id| new_dom.node_data.as_container().internal.get(id.index())).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
818
819 debug_msgs.push(LayoutDebugMessage::new(
820 LayoutDebugMessageType::PositionCalculation,
821 format!(
822 "[LAYOUT ROOT {}] {} - CB pos=({:.2}, {:.2}), adjusted=({:.2}, {:.2}), \
823 CB size=({:.2}x{:.2}), viewport=({:.2}x{:.2}), margin=({:.2}, {:.2})",
824 root_idx,
825 dom_name,
826 cb_pos.x,
827 cb_pos.y,
828 adjusted_cb_pos.x,
829 adjusted_cb_pos.y,
830 cb_size.width,
831 cb_size.height,
832 viewport.size.width,
833 viewport.size.height,
834 root_bp.margin.left,
835 root_bp.margin.top
836 ),
837 ));
838 }
839
840 crate::probe::hint_purge_allocator();
843 crate::probe::sample_peak_rss("rss:before_root_layout");
844 crate::probe::reset_peak();
845 { let _ = (0xDD00_0055u32); }
847 let clr = {
853 let _p = crate::probe::Probe::span("root_layout_pass");
854 cache::calculate_layout_for_subtree(
855 &mut ctx,
856 &mut new_tree,
857 text_cache,
858 root_idx,
859 adjusted_cb_pos,
860 cb_size,
861 &mut calculated_positions,
862 &mut reflow_needed_for_scrollbars,
863 &mut cache.float_cache,
864 cache::ComputeMode::PerformLayout,
865 )
866 };
867 { let _ = (if clr.is_ok() { 0xDD00_0057u32 } else { 0xDD00_005Eu32 }); }
868 crate::probe::sample_peak_rss("rss:after_root_layout");
869 crate::probe::sample_phase_peak("rss:peak_during_root_layout");
870
871 if !pos_contains(&calculated_positions, root_idx) {
879 let root_node = &new_tree.nodes[root_idx];
880 let root_bp2 = root_node.box_props.unpack();
881
882 let root_position = LogicalPosition::new(
886 cb_pos.x + root_bp2.margin.left,
887 cb_pos.y + root_bp2.margin.top,
888 );
889
890 if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
892 let dom_name = root_node
893 .dom_node_id
894 .and_then(|id| new_dom.node_data.as_container().internal.get(id.index())).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
895
896 debug_msgs.push(LayoutDebugMessage::new(
897 LayoutDebugMessageType::PositionCalculation,
898 format!(
899 "[ROOT POSITION {}] {} - Inserting position=({:.2}, {:.2}) (viewport origin + margin), \
900 margin=({:.2}, {:.2}, {:.2}, {:.2})",
901 root_idx,
902 dom_name,
903 root_position.x,
904 root_position.y,
905 root_bp2.margin.top,
906 root_bp2.margin.right,
907 root_bp2.margin.bottom,
908 root_bp2.margin.left
909 ),
910 ));
911 }
912
913 pos_set(&mut calculated_positions, root_idx, root_position);
914 }
915 }
916 { let _ = (0xDD00_0006u32); }
918
919 {
920 let _p = crate::probe::Probe::span("reposition_clean_subtrees");
921 cache::reposition_clean_subtrees(
922 new_dom,
923 &new_tree,
924 &recon_result.layout_roots,
925 &mut calculated_positions,
926 );
927 }
928
929 if reflow_needed_for_scrollbars {
930 debug_log!(ctx,
931 "Scrollbars changed container size, starting full reflow (loop {})",
932 loop_count
933 );
934 recon_result.layout_roots.clear();
935 recon_result.layout_roots.insert(new_tree.root);
936 recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
937 continue;
938 }
939
940 break;
941 }
942
943 {
958 let _p = crate::probe::Probe::span("adjust_relative_positions");
959 positioning::adjust_relative_positions(
960 &mut ctx,
961 &new_tree,
962 &mut calculated_positions,
963 viewport,
964 );
965 }
966
967 {
973 let _p = crate::probe::Probe::span("adjust_sticky_positions");
974 positioning::adjust_sticky_positions(
975 &mut ctx,
976 &new_tree,
977 &mut calculated_positions,
978 scroll_offsets,
979 viewport,
980 );
981 }
982
983 {
988 let _p = crate::probe::Probe::span("position_out_of_flow");
989 positioning::position_out_of_flow_elements(
990 &mut ctx,
991 &mut new_tree,
992 text_cache,
993 &mut calculated_positions,
994 viewport,
995 );
996 }
997
998 let (scroll_ids, scroll_id_to_node_id) = {
1001 let _p = crate::probe::Probe::span("compute_scroll_ids");
1002 LayoutWindow::compute_scroll_ids(&new_tree, new_dom)
1003 };
1004
1005 crate::probe::sample_peak_rss("rss:before_display_list");
1006 crate::probe::reset_peak();
1007 let display_list = if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
1009 DisplayList::default()
1011 } else {
1012 let _p = crate::probe::Probe::span("generate_display_list");
1013 generate_display_list(
1014 &mut ctx,
1015 &new_tree,
1016 &calculated_positions,
1017 scroll_offsets,
1018 &scroll_ids,
1019 gpu_value_cache,
1020 renderer_resources,
1021 id_namespace,
1022 dom_id,
1023 )?
1024 };
1025 crate::probe::sample_phase_peak("rss:peak_during_display_list");
1026
1027 let _p_writeback = crate::probe::Probe::span("cache_writeback");
1029 let cache_map_back = std::mem::take(&mut ctx.cache_map);
1030
1031 let root_subtree_hash = new_tree
1036 .cold(new_tree.root)
1037 .map_or(layout_tree::SubtreeHash(0), |c| c.subtree_hash);
1038 cache.cached_display_list = Some((root_subtree_hash, viewport, display_list.clone()));
1039
1040 cache.tree = Some(*new_tree); cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
1042 cache.viewport = Some(viewport);
1043 cache.scroll_ids = scroll_ids;
1044 cache.scroll_id_to_node_id = scroll_id_to_node_id;
1045 { let _ = (0xDD00_0004u32 | ((cache.calculated_positions.len() as u32 & 0xfff) << 4)); }
1047 cache.counters = counter_values;
1048 cache.cache_map = cache_map_back;
1049 crate::probe::sample_peak_rss("rss:after_layout_document");
1050
1051 Ok(display_list)
1052}
1053
1054pub(super) fn get_containing_block_for_node(
1067 tree: &LayoutTree,
1068 styled_dom: &StyledDom,
1069 node_idx: usize,
1070 calculated_positions: &PositionVec,
1071 viewport: LogicalRect,
1072) -> (LogicalPosition, LogicalSize) {
1073 if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
1074 if let Some(parent_node) = tree.get(parent_idx) {
1075 let pos = pos_get(calculated_positions, parent_idx)
1076 .unwrap_or(viewport.origin);
1077 let size = parent_node.used_size.unwrap_or_default();
1078 let pbp = parent_node.box_props.unpack();
1081 let content_pos = LogicalPosition::new(
1082 pos.x + pbp.border.left + pbp.padding.left,
1083 pos.y + pbp.border.top + pbp.padding.top,
1084 );
1085
1086 if let Some(dom_id) = parent_node.dom_node_id {
1087 let styled_node_state = &styled_dom
1088 .styled_nodes
1089 .as_container()
1090 .get(dom_id)
1091 .map(|n| &n.styled_node_state)
1092 .copied()
1093 .unwrap_or_default();
1094 let writing_mode =
1096 get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
1097 let content_size = pbp.inner_size(size, writing_mode);
1098 return (content_pos, content_size);
1099 }
1100
1101 return (content_pos, size);
1102 }
1103 }
1104
1105 (viewport.origin, viewport.size)
1122}
1123
1124#[allow(variant_size_differences)] #[derive(Debug)]
1132#[repr(C, u8)]
1133pub enum LayoutError {
1134 InvalidTree,
1135 SizingFailed,
1136 PositioningFailed,
1137 DisplayListFailed,
1138 Text(crate::font_traits::LayoutError),
1139}
1140
1141impl std::fmt::Display for LayoutError {
1142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1143 match self {
1144 Self::InvalidTree => write!(f, "Invalid layout tree"),
1145 Self::SizingFailed => write!(f, "Sizing calculation failed"),
1146 Self::PositioningFailed => write!(f, "Position calculation failed"),
1147 Self::DisplayListFailed => write!(f, "Display list generation failed"),
1148 Self::Text(e) => write!(f, "Text layout error: {e:?}"),
1149 }
1150 }
1151}
1152
1153impl From<crate::font_traits::LayoutError> for LayoutError {
1154 fn from(err: crate::font_traits::LayoutError) -> Self {
1155 Self::Text(err)
1156 }
1157}
1158
1159impl std::error::Error for LayoutError {}
1160
1161pub type Result<T> = std::result::Result<T, LayoutError>;
1162
1163#[cfg(test)]
1164#[allow(clippy::float_cmp, clippy::too_many_lines)]
1165mod autotest_generated {
1166 use azul_core::dom::{Dom, FormattingContext};
1167
1168 use super::*;
1169 use crate::solver3::{
1170 geometry::{EdgeSizes, PackedBoxProps, ResolvedBoxProps},
1171 layout_tree::{LayoutNodeCold, LayoutNodeHot, LayoutNodeWarm},
1172 };
1173
1174 fn pos(x: f32, y: f32) -> LogicalPosition {
1179 LogicalPosition::new(x, y)
1180 }
1181
1182 fn size(w: f32, h: f32) -> LogicalSize {
1183 LogicalSize::new(w, h)
1184 }
1185
1186 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
1187 LogicalRect {
1188 origin: pos(x, y),
1189 size: size(w, h),
1190 }
1191 }
1192
1193 fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
1194 EdgeSizes {
1195 top,
1196 right,
1197 bottom,
1198 left,
1199 }
1200 }
1201
1202 fn bp(padding: EdgeSizes, border: EdgeSizes) -> ResolvedBoxProps {
1204 ResolvedBoxProps {
1205 margin: EdgeSizes::default(),
1206 padding,
1207 border,
1208 ..ResolvedBoxProps::default()
1209 }
1210 }
1211
1212 fn hot(
1213 parent: Option<usize>,
1214 dom_node_id: Option<NodeId>,
1215 used_size: Option<LogicalSize>,
1216 props: &ResolvedBoxProps,
1217 ) -> LayoutNodeHot {
1218 LayoutNodeHot {
1219 box_props: PackedBoxProps::pack(props),
1220 dom_node_id,
1221 used_size,
1222 formatting_context: FormattingContext::default(),
1223 parent,
1224 }
1225 }
1226
1227 fn tree_of(nodes: Vec<LayoutNodeHot>) -> LayoutTree {
1230 let n = nodes.len();
1231 LayoutTree {
1232 nodes,
1233 warm: vec![LayoutNodeWarm::default(); n],
1234 cold: vec![LayoutNodeCold::default(); n],
1235 root: 0,
1236 dom_to_layout: BTreeMap::new(),
1237 children_arena: Vec::new(),
1238 children_offsets: vec![(0, 0); n],
1239 subtree_needs_intrinsic: vec![false; n],
1240 }
1241 }
1242
1243 fn close(a: f32, b: f32) -> bool {
1246 (a - b).abs() < 1e-3
1247 }
1248
1249 fn body_dom() -> StyledDom {
1251 let mut dom = Dom::create_body();
1252 let (css, _warnings) = azul_css::parser2::new_from_str("");
1253 StyledDom::create(&mut dom, css)
1254 }
1255
1256 #[test]
1261 fn position_unset_sets_both_components_to_f32_min() {
1262 assert_eq!(POSITION_UNSET.x, f32::MIN);
1265 assert_eq!(POSITION_UNSET.y, f32::MIN);
1266 }
1267
1268 #[test]
1273 fn pos_get_on_an_empty_vec_is_none_for_every_index() {
1274 let positions: PositionVec = Vec::new();
1275 for idx in [0usize, 1, 7, 1_000, usize::MAX / 2, usize::MAX] {
1276 assert!(pos_get(&positions, idx).is_none(), "idx {idx}");
1277 assert!(!pos_contains(&positions, idx), "idx {idx}");
1278 }
1279 }
1280
1281 #[test]
1282 fn pos_get_past_the_end_is_none_and_never_panics() {
1283 let mut positions: PositionVec = Vec::new();
1284 pos_set(&mut positions, 3, pos(1.0, 2.0));
1285 assert_eq!(positions.len(), 4);
1286
1287 for idx in [4usize, 5, 100, usize::MAX] {
1288 assert!(pos_get(&positions, idx).is_none(), "idx {idx}");
1289 assert!(!pos_contains(&positions, idx), "idx {idx}");
1290 }
1291 }
1292
1293 #[test]
1294 fn pos_get_at_zero_round_trips() {
1295 let mut positions: PositionVec = Vec::new();
1296 pos_set(&mut positions, 0, pos(0.0, 0.0));
1297
1298 let got = pos_get(&positions, 0).expect("index 0 was set");
1299 assert_eq!(got.x, 0.0);
1300 assert_eq!(got.y, 0.0);
1301 assert!(pos_contains(&positions, 0));
1302 }
1303
1304 #[test]
1305 fn an_explicitly_written_sentinel_reads_back_as_unset() {
1306 let mut positions: PositionVec = Vec::new();
1310 pos_set(&mut positions, 0, POSITION_UNSET);
1311
1312 assert_eq!(positions.len(), 1);
1313 assert!(pos_get(&positions, 0).is_none());
1314 assert!(!pos_contains(&positions, 0));
1315 }
1316
1317 #[test]
1318 fn a_position_whose_x_is_f32_min_reads_back_as_unset_even_with_a_real_y() {
1319 let mut positions: PositionVec = Vec::new();
1322 pos_set(&mut positions, 0, pos(f32::MIN, 42.0));
1323
1324 assert!(pos_get(&positions, 0).is_none());
1325 assert!(!pos_contains(&positions, 0));
1326 assert_eq!(positions[0].y, 42.0);
1328 }
1329
1330 #[test]
1331 fn negative_f32_max_is_the_same_bit_pattern_as_the_sentinel() {
1332 assert_eq!(f32::MIN, -f32::MAX);
1335
1336 let mut positions: PositionVec = Vec::new();
1337 pos_set(&mut positions, 0, pos(-f32::MAX, 0.0));
1338 assert!(pos_get(&positions, 0).is_none());
1339 }
1340
1341 #[test]
1342 fn a_position_whose_y_is_f32_min_is_still_reported_as_set() {
1343 let mut positions: PositionVec = Vec::new();
1344 pos_set(&mut positions, 0, pos(0.0, f32::MIN));
1345
1346 let got = pos_get(&positions, 0).expect("x is not the sentinel, so it is set");
1347 assert_eq!(got.x, 0.0);
1348 assert_eq!(got.y, f32::MIN);
1349 assert!(pos_contains(&positions, 0));
1350 }
1351
1352 #[test]
1353 fn nan_positions_are_considered_set_and_survive_the_round_trip() {
1354 let mut positions: PositionVec = Vec::new();
1357 pos_set(&mut positions, 0, pos(f32::NAN, f32::NAN));
1358
1359 let got = pos_get(&positions, 0).expect("NaN passes the sentinel filter");
1360 assert!(got.x.is_nan());
1361 assert!(got.y.is_nan());
1362 assert!(pos_contains(&positions, 0));
1363 }
1364
1365 #[test]
1366 fn infinite_and_extreme_positions_round_trip_unchanged() {
1367 let cases = [
1368 pos(f32::INFINITY, f32::NEG_INFINITY),
1369 pos(f32::MAX, -f32::MIN_POSITIVE),
1370 pos(-0.0, 0.0),
1371 pos(-1e30, 1e30),
1372 pos(f32::MIN_POSITIVE, f32::EPSILON),
1373 ];
1374 for (idx, case) in cases.iter().enumerate() {
1375 let mut positions: PositionVec = Vec::new();
1376 pos_set(&mut positions, idx, *case);
1377
1378 let got = pos_get(&positions, idx).expect("non-sentinel x is set");
1379 assert_eq!(got.x.to_bits(), case.x.to_bits(), "case {idx} x");
1380 assert_eq!(got.y.to_bits(), case.y.to_bits(), "case {idx} y");
1381 assert!(pos_contains(&positions, idx), "case {idx}");
1382 }
1383 }
1384
1385 #[test]
1386 fn pos_contains_always_agrees_with_pos_get() {
1387 let values = [
1390 pos(0.0, 0.0),
1391 pos(-0.0, -0.0),
1392 POSITION_UNSET,
1393 pos(f32::MIN, 1.0),
1394 pos(1.0, f32::MIN),
1395 pos(f32::NAN, 0.0),
1396 pos(f32::INFINITY, f32::INFINITY),
1397 pos(f32::NEG_INFINITY, 0.0),
1398 pos(f32::MAX, f32::MIN_POSITIVE),
1399 pos(-f32::MAX, 0.0),
1400 ];
1401 let mut positions: PositionVec = Vec::new();
1402 for (idx, v) in values.iter().enumerate() {
1403 pos_set(&mut positions, idx, *v);
1404 }
1405 for idx in 0..values.len() + 4 {
1406 assert_eq!(
1407 pos_contains(&positions, idx),
1408 pos_get(&positions, idx).is_some(),
1409 "idx {idx} disagrees"
1410 );
1411 }
1412 }
1413
1414 #[test]
1419 fn pos_set_beyond_the_end_grows_and_fills_the_gap_with_the_sentinel() {
1420 let mut positions: PositionVec = Vec::new();
1421 pos_set(&mut positions, 3, pos(10.0, 20.0));
1422
1423 assert_eq!(positions.len(), 4, "grows to exactly idx + 1");
1424 for idx in 0..3 {
1425 assert!(pos_get(&positions, idx).is_none(), "gap idx {idx} must be unset");
1426 assert!(!pos_contains(&positions, idx), "gap idx {idx}");
1427 assert_eq!(positions[idx].x, POSITION_UNSET.x);
1428 assert_eq!(positions[idx].y, POSITION_UNSET.y);
1429 }
1430 let got = pos_get(&positions, 3).expect("idx 3 was set");
1431 assert_eq!(got.x, 10.0);
1432 assert_eq!(got.y, 20.0);
1433 }
1434
1435 #[test]
1436 fn pos_set_inside_the_vec_neither_grows_nor_shrinks_it() {
1437 let mut positions: PositionVec = vec![POSITION_UNSET; 5];
1438 pos_set(&mut positions, 0, pos(1.0, 1.0));
1439 assert_eq!(positions.len(), 5);
1440
1441 pos_set(&mut positions, 4, pos(2.0, 2.0));
1442 assert_eq!(positions.len(), 5);
1443 assert!(pos_contains(&positions, 0));
1444 assert!(pos_contains(&positions, 4));
1445 assert!(!pos_contains(&positions, 2), "untouched slots stay unset");
1446 }
1447
1448 #[test]
1449 fn pos_set_overwrites_an_existing_entry_in_place() {
1450 let mut positions: PositionVec = Vec::new();
1451 pos_set(&mut positions, 1, pos(1.0, 1.0));
1452 pos_set(&mut positions, 1, pos(-5.5, -6.5));
1453
1454 assert_eq!(positions.len(), 2);
1455 let got = pos_get(&positions, 1).expect("still set");
1456 assert_eq!(got.x, -5.5);
1457 assert_eq!(got.y, -6.5);
1458 }
1459
1460 #[test]
1461 fn pos_set_can_reset_an_entry_back_to_unset() {
1462 let mut positions: PositionVec = Vec::new();
1463 pos_set(&mut positions, 0, pos(3.0, 4.0));
1464 assert!(pos_contains(&positions, 0));
1465
1466 pos_set(&mut positions, 0, POSITION_UNSET);
1467 assert!(!pos_contains(&positions, 0));
1468 assert!(pos_get(&positions, 0).is_none());
1469 }
1470
1471 #[test]
1472 fn repeated_growth_preserves_every_earlier_entry() {
1473 let mut positions: PositionVec = Vec::new();
1474 for idx in (0..64).rev() {
1475 pos_set(&mut positions, idx, pos(idx as f32, -(idx as f32)));
1478 }
1479 assert_eq!(positions.len(), 64);
1480 for idx in 0..64 {
1481 let got = pos_get(&positions, idx).expect("all 64 were written");
1482 assert_eq!(got.x, idx as f32);
1483 assert_eq!(got.y, -(idx as f32));
1484 }
1485
1486 pos_set(&mut positions, 4_095, pos(1.0, 1.0));
1488 assert_eq!(positions.len(), 4_096);
1489 for idx in 0..64 {
1490 assert!(pos_contains(&positions, idx), "idx {idx} lost after regrow");
1491 }
1492 for idx in 64..4_095 {
1493 assert!(!pos_contains(&positions, idx), "new slot {idx} must be unset");
1494 }
1495 assert!(pos_contains(&positions, 4_095));
1496 }
1497
1498 #[test]
1503 fn the_scrollbar_reflow_bound_is_a_usable_positive_limit() {
1504 const _: () = assert!(
1507 MAX_SCROLLBAR_REFLOW_ITERATIONS >= 1 && MAX_SCROLLBAR_REFLOW_ITERATIONS <= 64,
1508 "the scrollbar reflow bound must be a usable positive limit; an absurd bound = a hung frame"
1509 );
1510 }
1511
1512 #[test]
1517 fn a_root_node_gets_the_viewport_as_its_containing_block() {
1518 let dom = body_dom();
1519 let tree = tree_of(vec![hot(None, Some(NodeId::ZERO), Some(size(10.0, 10.0)), &ResolvedBoxProps::default())]);
1520 let viewport = rect(7.0, 9.0, 800.0, 600.0);
1521
1522 let (cb_pos, cb_size) =
1523 get_containing_block_for_node(&tree, &dom, 0, &Vec::new(), viewport);
1524
1525 assert_eq!(cb_pos.x, 7.0);
1526 assert_eq!(cb_pos.y, 9.0);
1527 assert_eq!(cb_size.width, 800.0);
1528 assert_eq!(cb_size.height, 600.0);
1529 }
1530
1531 #[test]
1532 fn an_out_of_range_node_index_falls_back_to_the_viewport() {
1533 let dom = body_dom();
1534 let tree = tree_of(vec![hot(None, None, Some(size(10.0, 10.0)), &ResolvedBoxProps::default())]);
1535 let viewport = rect(0.0, 0.0, 800.0, 600.0);
1536
1537 for idx in [1usize, 99, usize::MAX] {
1538 let (cb_pos, cb_size) =
1539 get_containing_block_for_node(&tree, &dom, idx, &Vec::new(), viewport);
1540 assert_eq!(cb_pos.x, 0.0, "idx {idx}");
1541 assert_eq!(cb_size.width, 800.0, "idx {idx}");
1542 assert_eq!(cb_size.height, 600.0, "idx {idx}");
1543 }
1544 }
1545
1546 #[test]
1547 fn a_dangling_parent_index_falls_back_to_the_viewport() {
1548 let dom = body_dom();
1551 let tree = tree_of(vec![
1552 hot(None, None, Some(size(10.0, 10.0)), &ResolvedBoxProps::default()),
1553 hot(Some(999), None, Some(size(10.0, 10.0)), &ResolvedBoxProps::default()),
1554 ]);
1555 let viewport = rect(1.0, 2.0, 300.0, 400.0);
1556
1557 let (cb_pos, cb_size) =
1558 get_containing_block_for_node(&tree, &dom, 1, &Vec::new(), viewport);
1559 assert_eq!(cb_pos.x, 1.0);
1560 assert_eq!(cb_pos.y, 2.0);
1561 assert_eq!(cb_size.width, 300.0);
1562 assert_eq!(cb_size.height, 400.0);
1563 }
1564
1565 #[test]
1566 fn a_dom_backed_parent_shrinks_the_containing_block_by_border_and_padding() {
1567 let dom = body_dom();
1568 let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), edges(5.0, 5.0, 5.0, 5.0));
1569 let tree = tree_of(vec![
1570 hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
1571 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1572 ]);
1573 let mut positions: PositionVec = Vec::new();
1574 pos_set(&mut positions, 0, pos(30.0, 40.0));
1575
1576 let (cb_pos, cb_size) = get_containing_block_for_node(
1577 &tree,
1578 &dom,
1579 1,
1580 &positions,
1581 rect(0.0, 0.0, 800.0, 600.0),
1582 );
1583
1584 assert!(close(cb_pos.x, 45.0), "x was {}", cb_pos.x);
1586 assert!(close(cb_pos.y, 55.0), "y was {}", cb_pos.y);
1587 assert!(close(cb_size.width, 170.0), "width was {}", cb_size.width);
1589 assert!(close(cb_size.height, 70.0), "height was {}", cb_size.height);
1590 }
1591
1592 #[test]
1593 fn an_anonymous_parent_offsets_the_origin_but_keeps_the_border_box_size() {
1594 let dom = body_dom();
1598 let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), edges(5.0, 5.0, 5.0, 5.0));
1599 let tree = tree_of(vec![
1600 hot(None, None, Some(size(200.0, 100.0)), &parent_props),
1601 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1602 ]);
1603 let mut positions: PositionVec = Vec::new();
1604 pos_set(&mut positions, 0, pos(0.0, 0.0));
1605
1606 let (cb_pos, cb_size) = get_containing_block_for_node(
1607 &tree,
1608 &dom,
1609 1,
1610 &positions,
1611 rect(0.0, 0.0, 800.0, 600.0),
1612 );
1613
1614 assert!(close(cb_pos.x, 15.0), "x was {}", cb_pos.x);
1615 assert!(close(cb_pos.y, 15.0), "y was {}", cb_pos.y);
1616 assert_eq!(cb_size.width, 200.0, "anonymous arm does not subtract padding/border");
1617 assert_eq!(cb_size.height, 100.0);
1618 }
1619
1620 #[test]
1621 fn an_unpositioned_parent_falls_back_to_the_viewport_origin() {
1622 let dom = body_dom();
1623 let parent_props = bp(edges(1.0, 0.0, 0.0, 2.0), edges(3.0, 0.0, 0.0, 4.0));
1624 let tree = tree_of(vec![
1625 hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
1626 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1627 ]);
1628 let viewport = rect(100.0, 200.0, 800.0, 600.0);
1629
1630 let (cb_pos, _) = get_containing_block_for_node(&tree, &dom, 1, &Vec::new(), viewport);
1632
1633 assert!(close(cb_pos.x, 106.0), "x was {}", cb_pos.x);
1635 assert!(close(cb_pos.y, 204.0), "y was {}", cb_pos.y);
1636 }
1637
1638 #[test]
1639 fn a_parent_with_a_sentinel_position_is_treated_as_unpositioned() {
1640 let dom = body_dom();
1643 let tree = tree_of(vec![
1644 hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &ResolvedBoxProps::default()),
1645 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1646 ]);
1647 let mut positions: PositionVec = Vec::new();
1648 pos_set(&mut positions, 0, POSITION_UNSET);
1649
1650 let (cb_pos, _) = get_containing_block_for_node(
1651 &tree,
1652 &dom,
1653 1,
1654 &positions,
1655 rect(11.0, 13.0, 800.0, 600.0),
1656 );
1657 assert_eq!(cb_pos.x, 11.0);
1658 assert_eq!(cb_pos.y, 13.0);
1659 }
1660
1661 #[test]
1662 fn a_parent_without_a_used_size_yields_a_zero_sized_containing_block() {
1663 let dom = body_dom();
1664 let tree = tree_of(vec![
1665 hot(None, Some(NodeId::ZERO), None, &ResolvedBoxProps::default()),
1666 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1667 ]);
1668 let mut positions: PositionVec = Vec::new();
1669 pos_set(&mut positions, 0, pos(0.0, 0.0));
1670
1671 let (_, cb_size) = get_containing_block_for_node(
1672 &tree,
1673 &dom,
1674 1,
1675 &positions,
1676 rect(0.0, 0.0, 800.0, 600.0),
1677 );
1678 assert_eq!(cb_size.width, 0.0);
1679 assert_eq!(cb_size.height, 0.0);
1680 }
1681
1682 #[test]
1683 fn huge_parent_padding_saturates_instead_of_wrapping_the_origin() {
1684 let dom = body_dom();
1687 let parent_props = bp(edges(1e30, 1e30, 1e30, 1e30), edges(1e30, 1e30, 1e30, 1e30));
1688 let tree = tree_of(vec![
1689 hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
1690 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1691 ]);
1692 let mut positions: PositionVec = Vec::new();
1693 pos_set(&mut positions, 0, pos(0.0, 0.0));
1694
1695 let (cb_pos, cb_size) = get_containing_block_for_node(
1696 &tree,
1697 &dom,
1698 1,
1699 &positions,
1700 rect(0.0, 0.0, 800.0, 600.0),
1701 );
1702
1703 assert!(cb_pos.x.is_finite() && cb_pos.y.is_finite());
1704 assert!(cb_pos.x > 0.0, "saturated padding must stay positive, got {}", cb_pos.x);
1705 assert!(cb_pos.x <= 2.0 * 3276.7 + 1.0, "clamped to the i16 ×10 range");
1706 assert_eq!(cb_size.width, 0.0);
1708 assert_eq!(cb_size.height, 0.0);
1709 }
1710
1711 #[test]
1712 fn a_nan_parent_position_propagates_but_does_not_panic_or_corrupt_the_size() {
1713 let dom = body_dom();
1714 let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), EdgeSizes::default());
1715 let tree = tree_of(vec![
1716 hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
1717 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1718 ]);
1719 let mut positions: PositionVec = Vec::new();
1720 pos_set(&mut positions, 0, pos(f32::NAN, f32::NAN));
1721
1722 let (cb_pos, cb_size) = get_containing_block_for_node(
1723 &tree,
1724 &dom,
1725 1,
1726 &positions,
1727 rect(0.0, 0.0, 800.0, 600.0),
1728 );
1729
1730 assert!(cb_pos.x.is_nan() && cb_pos.y.is_nan(), "NaN flows through unchanged");
1731 assert!(close(cb_size.width, 180.0), "width was {}", cb_size.width);
1733 assert!(close(cb_size.height, 80.0), "height was {}", cb_size.height);
1734 }
1735
1736 #[test]
1737 fn a_nan_parent_used_size_floors_the_containing_block_at_zero() {
1738 let dom = body_dom();
1741 let tree = tree_of(vec![
1742 hot(None, Some(NodeId::ZERO), Some(size(f32::NAN, f32::NAN)), &ResolvedBoxProps::default()),
1743 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1744 ]);
1745 let mut positions: PositionVec = Vec::new();
1746 pos_set(&mut positions, 0, pos(0.0, 0.0));
1747
1748 let (_, cb_size) = get_containing_block_for_node(
1749 &tree,
1750 &dom,
1751 1,
1752 &positions,
1753 rect(0.0, 0.0, 800.0, 600.0),
1754 );
1755 assert!(!cb_size.width.is_nan() && !cb_size.height.is_nan());
1756 assert_eq!(cb_size.width, 0.0);
1757 assert_eq!(cb_size.height, 0.0);
1758 }
1759
1760 #[test]
1761 fn an_infinite_parent_used_size_stays_infinite_rather_than_becoming_nan() {
1762 let dom = body_dom();
1763 let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), edges(5.0, 5.0, 5.0, 5.0));
1764 let tree = tree_of(vec![
1765 hot(
1766 None,
1767 Some(NodeId::ZERO),
1768 Some(size(f32::INFINITY, f32::INFINITY)),
1769 &parent_props,
1770 ),
1771 hot(Some(0), None, None, &ResolvedBoxProps::default()),
1772 ]);
1773 let mut positions: PositionVec = Vec::new();
1774 pos_set(&mut positions, 0, pos(0.0, 0.0));
1775
1776 let (_, cb_size) = get_containing_block_for_node(
1777 &tree,
1778 &dom,
1779 1,
1780 &positions,
1781 rect(0.0, 0.0, 800.0, 600.0),
1782 );
1783 assert!(cb_size.width.is_infinite() && cb_size.width.is_sign_positive());
1784 assert!(cb_size.height.is_infinite() && cb_size.height.is_sign_positive());
1785 }
1786
1787 #[test]
1788 fn degenerate_viewports_pass_through_the_root_arm_untouched() {
1789 let dom = body_dom();
1792 let tree = tree_of(vec![hot(None, None, None, &ResolvedBoxProps::default())]);
1793
1794 let (p, s) = get_containing_block_for_node(
1795 &tree,
1796 &dom,
1797 0,
1798 &Vec::new(),
1799 rect(0.0, 0.0, 0.0, 0.0),
1800 );
1801 assert_eq!(s.width, 0.0);
1802 assert_eq!(s.height, 0.0);
1803 assert_eq!(p.x, 0.0);
1804
1805 let (_, s) = get_containing_block_for_node(
1806 &tree,
1807 &dom,
1808 0,
1809 &Vec::new(),
1810 rect(0.0, 0.0, -800.0, -600.0),
1811 );
1812 assert_eq!(s.width, -800.0, "negative viewport is not clamped");
1813
1814 let (p, s) = get_containing_block_for_node(
1815 &tree,
1816 &dom,
1817 0,
1818 &Vec::new(),
1819 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
1820 );
1821 assert!(p.x.is_nan() && s.width.is_nan(), "NaN viewport is not sanitised");
1822
1823 let (_, s) = get_containing_block_for_node(
1824 &tree,
1825 &dom,
1826 0,
1827 &Vec::new(),
1828 rect(0.0, 0.0, f32::MAX, f32::MAX),
1829 );
1830 assert_eq!(s.width, f32::MAX);
1831 }
1832
1833 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
1838 #[test]
1839 fn every_layout_error_variant_renders_a_distinct_non_empty_message() {
1840 let variants = [
1841 LayoutError::InvalidTree,
1842 LayoutError::SizingFailed,
1843 LayoutError::PositioningFailed,
1844 LayoutError::DisplayListFailed,
1845 LayoutError::Text(crate::font_traits::LayoutError::BidiError("boom".to_string())),
1846 ];
1847 let rendered: Vec<String> = variants.iter().map(ToString::to_string).collect();
1848
1849 for msg in &rendered {
1850 assert!(!msg.is_empty(), "empty Display output");
1851 assert!(!msg.trim().is_empty(), "whitespace-only Display output");
1852 }
1853 for i in 0..rendered.len() {
1854 for j in (i + 1)..rendered.len() {
1855 assert_ne!(rendered[i], rendered[j], "variants {i} and {j} render alike");
1856 }
1857 }
1858 }
1859
1860 #[test]
1861 fn layout_error_display_matches_the_documented_wording() {
1862 assert_eq!(LayoutError::InvalidTree.to_string(), "Invalid layout tree");
1863 assert_eq!(LayoutError::SizingFailed.to_string(), "Sizing calculation failed");
1864 assert_eq!(
1865 LayoutError::PositioningFailed.to_string(),
1866 "Position calculation failed"
1867 );
1868 assert_eq!(
1869 LayoutError::DisplayListFailed.to_string(),
1870 "Display list generation failed"
1871 );
1872 }
1873
1874 #[test]
1875 fn layout_error_display_ignores_width_and_precision_specifiers() {
1876 let e = LayoutError::InvalidTree;
1879 assert_eq!(format!("{e:>60}"), "Invalid layout tree");
1880 assert_eq!(format!("{e:.3}"), "Invalid layout tree");
1881 assert_eq!(format!("{e:^5}"), "Invalid layout tree");
1882 }
1883
1884 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
1885 #[test]
1886 fn the_text_variant_embeds_the_inner_error_and_survives_hostile_payloads() {
1887 let payloads = [
1888 String::new(),
1889 "\u{202e}rtl-override \u{0}nul".to_string(),
1890 "日本語のエラー 🎉".to_string(),
1891 "x".repeat(100_000),
1892 "\"quotes\" and \\backslashes\\".to_string(),
1893 ];
1894 for payload in payloads {
1895 let err = LayoutError::Text(crate::font_traits::LayoutError::ShapingError(
1896 payload.clone(),
1897 ));
1898 let msg = err.to_string();
1899 assert!(
1900 msg.starts_with("Text layout error: "),
1901 "unexpected prefix for {} byte payload",
1902 payload.len()
1903 );
1904 assert!(msg.len() >= "Text layout error: ".len() + payload.len());
1907 }
1908 }
1909
1910 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
1911 #[test]
1912 fn the_text_variant_renders_a_default_font_selector_without_panicking() {
1913 let err = LayoutError::Text(crate::font_traits::LayoutError::FontNotFound(
1914 crate::font_traits::FontSelector::default(),
1915 ));
1916 let msg = err.to_string();
1917 assert!(msg.starts_with("Text layout error: "));
1918 assert!(msg.contains("serif"), "the default family should show up: {msg}");
1919 }
1920
1921 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
1922 #[test]
1923 fn from_text_layout_error_wraps_into_the_text_variant() {
1924 let inner = crate::font_traits::LayoutError::InvalidText("bad".to_string());
1925 let wrapped: LayoutError = inner.into();
1926
1927 assert!(matches!(wrapped, LayoutError::Text(_)));
1928 assert!(wrapped.to_string().contains("bad"));
1929
1930 fn propagates() -> Result<()> {
1932 let failed: std::result::Result<(), crate::font_traits::LayoutError> = Err(
1933 crate::font_traits::LayoutError::HyphenationError("nope".to_string()),
1934 );
1935 failed?;
1936 Ok(())
1937 }
1938 assert!(matches!(propagates(), Err(LayoutError::Text(_))));
1939 }
1940
1941 #[test]
1942 fn layout_error_is_a_std_error_without_a_source() {
1943 use std::error::Error;
1944 let e = LayoutError::SizingFailed;
1945 assert!(e.source().is_none());
1946 assert!(!format!("{e:?}").is_empty());
1948 }
1949
1950 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
1955 mod debug_sinks {
1956 use std::collections::{BTreeMap, HashMap};
1957
1958 use azul_core::{dom::DomId, selection::TextSelection, styled_dom::StyledDom};
1959 use azul_css::{props::basic::FontRef, LayoutDebugMessage, LayoutDebugMessageType};
1960
1961 use super::{body_dom, size};
1962 use crate::{
1963 font_traits::FontManager,
1964 solver3::{cache, LayoutContext},
1965 };
1966
1967 struct Env {
1970 styled_dom: StyledDom,
1971 font_manager: FontManager<FontRef>,
1972 text_selections: BTreeMap<DomId, TextSelection>,
1973 counters: HashMap<(usize, String), i32>,
1974 image_cache: azul_core::resources::ImageCache,
1975 debug_messages: Option<Vec<LayoutDebugMessage>>,
1976 }
1977
1978 impl Env {
1979 fn new(debug_messages: Option<Vec<LayoutDebugMessage>>) -> Self {
1980 Self {
1981 styled_dom: body_dom(),
1982 font_manager: FontManager::new(rust_fontconfig::FcFontCache::default())
1983 .expect("FontManager over an empty font cache"),
1984 text_selections: BTreeMap::new(),
1985 counters: HashMap::new(),
1986 image_cache: azul_core::resources::ImageCache::default(),
1987 debug_messages,
1988 }
1989 }
1990
1991 fn ctx(&mut self) -> LayoutContext<'_, FontRef> {
1992 LayoutContext {
1993 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
1994 styled_dom: &self.styled_dom,
1995 font_manager: &self.font_manager,
1996 text_selections: &self.text_selections,
1997 debug_messages: &mut self.debug_messages,
1998 counters: &mut self.counters,
1999 viewport_size: size(800.0, 600.0),
2000 fragmentation_context: None,
2001 cursor_is_visible: true,
2002 cursor_locations: Vec::new(),
2003 preedit_text: None,
2004 dirty_text_overrides: BTreeMap::new(),
2005 cache_map: cache::LayoutCacheMap::default(),
2006 image_cache: &self.image_cache,
2007 system_style: None,
2008 get_system_time_fn: azul_core::task::GetSystemTimeCallback {
2009 cb: azul_core::task::get_system_time_libstd,
2010 },
2011 }
2012 }
2013 }
2014
2015 #[test]
2016 fn each_debug_sink_appends_exactly_one_message_of_its_own_type() {
2017 let mut env = Env::new(Some(Vec::new()));
2018 {
2019 let mut ctx = env.ctx();
2020 ctx.debug_log_inner("log".to_string());
2021 ctx.debug_info_inner("info".to_string());
2022 ctx.debug_warning_inner("warning".to_string());
2023 ctx.debug_error_inner("error".to_string());
2024 ctx.debug_box_props_inner("box_props".to_string());
2025 ctx.debug_css_getter_inner("css_getter".to_string());
2026 ctx.debug_bfc_layout_inner("bfc".to_string());
2027 ctx.debug_ifc_layout_inner("ifc".to_string());
2028 ctx.debug_table_layout_inner("table".to_string());
2029 ctx.debug_display_type_inner("display".to_string());
2030 }
2031
2032 let msgs = env.debug_messages.expect("Some(vec) was passed in");
2033 let expected = [
2034 ("log", LayoutDebugMessageType::Info),
2035 ("info", LayoutDebugMessageType::Info),
2036 ("warning", LayoutDebugMessageType::Warning),
2037 ("error", LayoutDebugMessageType::Error),
2038 ("box_props", LayoutDebugMessageType::BoxProps),
2039 ("css_getter", LayoutDebugMessageType::CssGetter),
2040 ("bfc", LayoutDebugMessageType::BfcLayout),
2041 ("ifc", LayoutDebugMessageType::IfcLayout),
2042 ("table", LayoutDebugMessageType::TableLayout),
2043 ("display", LayoutDebugMessageType::DisplayType),
2044 ];
2045 assert_eq!(msgs.len(), expected.len(), "one message per call, in order");
2046 for (msg, (text, ty)) in msgs.iter().zip(expected) {
2047 assert_eq!(msg.message.as_str(), text);
2048 assert_eq!(msg.message_type, ty);
2049 assert!(!msg.location.as_str().is_empty(), "location must be recorded");
2050 }
2051 }
2052
2053 #[test]
2054 fn debug_log_inner_tags_the_message_with_the_solver3_location() {
2055 let mut env = Env::new(Some(Vec::new()));
2059 {
2060 let mut ctx = env.ctx();
2061 ctx.debug_log_inner("hello".to_string());
2062 ctx.debug_info_inner("hello".to_string());
2063 }
2064
2065 let msgs = env.debug_messages.expect("Some(vec)");
2066 assert_eq!(msgs[0].location.as_str(), "solver3");
2067 assert!(
2068 msgs[1].location.as_str().contains(".rs:"),
2069 "track_caller location, got {:?}",
2070 msgs[1].location.as_str()
2071 );
2072 }
2073
2074 #[test]
2075 fn the_debug_sinks_are_no_ops_when_debug_messages_is_none() {
2076 let mut env = Env::new(None);
2079 {
2080 let mut ctx = env.ctx();
2081 ctx.debug_log_inner("log".to_string());
2082 ctx.debug_error_inner("error".to_string());
2083 ctx.debug_table_layout_inner("table".to_string());
2084 }
2085 assert!(env.debug_messages.is_none(), "must not materialise a Vec");
2086 }
2087
2088 #[test]
2089 fn debug_messages_preserve_hostile_payloads_byte_for_byte() {
2090 let payloads = [
2091 String::new(),
2092 "\u{0}\u{7}\u{1b}[31m".to_string(),
2093 "日本語 🎉 \u{202e}reversed".to_string(),
2094 "line\nbreak\ttab\r\n".to_string(),
2095 "{}{{}} {:?} %s %n".to_string(), "x".repeat(200_000),
2097 ];
2098 let mut env = Env::new(Some(Vec::new()));
2099 {
2100 let mut ctx = env.ctx();
2101 for p in &payloads {
2102 ctx.debug_info_inner(p.clone());
2103 }
2104 }
2105
2106 let msgs = env.debug_messages.expect("Some(vec)");
2107 assert_eq!(msgs.len(), payloads.len());
2108 for (msg, payload) in msgs.iter().zip(&payloads) {
2109 assert_eq!(msg.message.as_str(), payload.as_str());
2110 }
2111 }
2112
2113 #[test]
2114 fn the_debug_macros_push_one_message_each_when_capturing() {
2115 let mut env = Env::new(Some(Vec::new()));
2116 {
2117 let mut ctx = env.ctx();
2118 debug_log!(ctx, "log {}", 1);
2119 debug_info!(ctx, "info {}", 2);
2120 debug_warning!(ctx, "warning {}", 3);
2121 debug_error!(ctx, "error {}", 4);
2122 debug_box_props!(ctx, "box_props {}", 5);
2123 debug_css_getter!(ctx, "css_getter {}", 6);
2124 debug_bfc_layout!(ctx, "bfc {}", 7);
2125 debug_ifc_layout!(ctx, "ifc {}", 8);
2126 debug_table_layout!(ctx, "table {}", 9);
2127 debug_display_type!(ctx, "display {}", 10);
2128 }
2129
2130 let msgs = env.debug_messages.expect("Some(vec)");
2131 assert_eq!(msgs.len(), 10);
2132 assert_eq!(msgs[0].message.as_str(), "log 1");
2133 assert_eq!(msgs[9].message.as_str(), "display 10");
2134 }
2135
2136 #[test]
2137 fn the_debug_macros_do_not_evaluate_their_arguments_when_not_capturing() {
2138 let evaluations = core::cell::Cell::new(0u32);
2141 let bump = |c: &core::cell::Cell<u32>| {
2142 c.set(c.get() + 1);
2143 c.get()
2144 };
2145
2146 let mut env = Env::new(None);
2147 {
2148 let mut ctx = env.ctx();
2149 debug_log!(ctx, "{}", bump(&evaluations));
2150 debug_info!(ctx, "{}", bump(&evaluations));
2151 debug_warning!(ctx, "{}", bump(&evaluations));
2152 debug_error!(ctx, "{}", bump(&evaluations));
2153 debug_box_props!(ctx, "{}", bump(&evaluations));
2154 debug_css_getter!(ctx, "{}", bump(&evaluations));
2155 debug_bfc_layout!(ctx, "{}", bump(&evaluations));
2156 debug_ifc_layout!(ctx, "{}", bump(&evaluations));
2157 debug_table_layout!(ctx, "{}", bump(&evaluations));
2158 debug_display_type!(ctx, "{}", bump(&evaluations));
2159 }
2160 assert_eq!(evaluations.get(), 0, "format args must stay unevaluated");
2161
2162 let mut env = Env::new(Some(Vec::new()));
2164 {
2165 let mut ctx = env.ctx();
2166 debug_log!(ctx, "{}", bump(&evaluations));
2167 }
2168 assert_eq!(evaluations.get(), 1);
2169 assert_eq!(env.debug_messages.as_ref().map(Vec::len), Some(1));
2170 }
2171 }
2172
2173 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
2178 #[test]
2179 fn set_skip_display_list_round_trips_and_is_idempotent() {
2180 use core::sync::atomic::Ordering;
2181
2182 let previous = SKIP_DISPLAY_LIST.load(Ordering::Relaxed);
2183
2184 set_skip_display_list(true);
2185 assert!(SKIP_DISPLAY_LIST.load(Ordering::Relaxed));
2186 set_skip_display_list(true);
2187 assert!(SKIP_DISPLAY_LIST.load(Ordering::Relaxed), "double-set is idempotent");
2188
2189 set_skip_display_list(false);
2190 assert!(!SKIP_DISPLAY_LIST.load(Ordering::Relaxed));
2191
2192 set_skip_display_list(previous);
2194 assert_eq!(SKIP_DISPLAY_LIST.load(Ordering::Relaxed), previous);
2195 }
2196
2197 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
2202 mod document {
2203 use std::collections::BTreeMap;
2204
2205 use azul_core::{
2206 dom::{Dom, DomId},
2207 geom::{LogicalPosition, LogicalRect, LogicalSize},
2208 resources::RendererResources,
2209 styled_dom::StyledDom,
2210 };
2211 use azul_css::props::basic::FontRef;
2212
2213 use crate::{
2214 font_traits::{FontManager, TextLayoutCache},
2215 solver3::{cache::LayoutCache, display_list::DisplayList, layout_document, Result},
2216 };
2217
2218 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
2219 LogicalRect {
2220 origin: LogicalPosition::new(x, y),
2221 size: LogicalSize::new(w, h),
2222 }
2223 }
2224
2225 fn simple_dom() -> StyledDom {
2228 let mut dom = Dom::create_body().with_child(Dom::create_div());
2229 let (css, _warnings) =
2230 azul_css::parser2::new_from_str("div { width: 50px; height: 20px; }");
2231 StyledDom::create(&mut dom, css)
2232 }
2233
2234 fn run(cache: &mut LayoutCache, dom: &StyledDom, viewport: LogicalRect) -> Result<DisplayList> {
2235 let mut text_cache = TextLayoutCache::new();
2236 let font_manager: FontManager<FontRef> =
2237 FontManager::new(rust_fontconfig::FcFontCache::default())
2238 .expect("FontManager over an empty font cache");
2239 let renderer_resources = RendererResources::default();
2240 let image_cache = azul_core::resources::ImageCache::default();
2241 let mut debug_messages = None;
2242
2243 layout_document(
2244 cache,
2245 &mut text_cache,
2246 dom,
2247 viewport,
2248 &font_manager,
2249 &BTreeMap::new(),
2250 &BTreeMap::new(),
2251 &mut debug_messages,
2252 None,
2253 &renderer_resources,
2254 azul_core::resources::IdNamespace(0),
2255 DomId::ROOT_ID,
2256 false,
2257 Vec::new(),
2258 None,
2259 &image_cache,
2260 None,
2261 azul_core::task::GetSystemTimeCallback {
2262 cb: azul_core::task::get_system_time_libstd,
2263 },
2264 )
2265 }
2266
2267 #[test]
2268 fn a_zero_sized_viewport_lays_out_without_panicking() {
2269 let dom = simple_dom();
2270 let mut cache = LayoutCache::default();
2271
2272 if run(&mut cache, &dom, rect(0.0, 0.0, 0.0, 0.0)).is_ok() {
2275 for p in &cache.calculated_positions {
2276 assert!(!p.x.is_nan() && !p.y.is_nan(), "NaN position from a 0×0 viewport");
2277 }
2278 }
2279 }
2280
2281 #[test]
2282 fn degenerate_viewports_never_panic_or_hang() {
2283 let viewports = [
2286 rect(0.0, 0.0, f32::NAN, f32::NAN),
2287 rect(f32::NAN, f32::NAN, 800.0, 600.0),
2288 rect(0.0, 0.0, -800.0, -600.0),
2289 rect(0.0, 0.0, f32::MAX, f32::MAX),
2290 rect(0.0, 0.0, f32::INFINITY, f32::INFINITY),
2291 rect(-1e30, -1e30, 1.0, 1.0),
2292 rect(0.0, 0.0, f32::MIN_POSITIVE, f32::MIN_POSITIVE),
2293 ];
2294 for viewport in viewports {
2295 let dom = simple_dom();
2296 let mut cache = LayoutCache::default();
2297 let _ = run(&mut cache, &dom, viewport);
2298 }
2299 }
2300
2301 #[test]
2302 fn laying_out_the_same_dom_twice_is_stable_and_populates_the_cache() {
2303 let dom = simple_dom();
2304 let mut cache = LayoutCache::default();
2305 let viewport = rect(0.0, 0.0, 800.0, 600.0);
2306
2307 let first = run(&mut cache, &dom, viewport);
2308 if first.is_err() {
2309 return;
2311 }
2312 assert!(cache.cached_display_list.is_some(), "cold pass must seed the DL cache");
2313 assert_eq!(cache.viewport, Some(viewport));
2314 let positions_after_first = cache.calculated_positions.clone();
2315
2316 let second = run(&mut cache, &dom, viewport);
2319 assert!(second.is_ok(), "a warm relayout of an unchanged DOM must succeed");
2320 assert_eq!(
2321 cache.calculated_positions.len(),
2322 positions_after_first.len(),
2323 "warm pass changed the node count"
2324 );
2325 for (warm, cold) in cache.calculated_positions.iter().zip(&positions_after_first) {
2326 assert_eq!(warm.x.to_bits(), cold.x.to_bits(), "warm pass moved a node");
2327 assert_eq!(warm.y.to_bits(), cold.y.to_bits(), "warm pass moved a node");
2328 }
2329 }
2330 }
2331}