1use crate::debug_log;
5use std::collections::BTreeMap;
6
7use azul_core::{
8 dom::{NodeId, NodeType},
9 geom::{LogicalPosition, LogicalRect, LogicalSize},
10 hit_test::ScrollPosition,
11 resources::RendererResources,
12 styled_dom::StyledDom,
13};
14use azul_css::{
15 corety::LayoutDebugMessage,
16 css::CssPropertyValue,
17 props::{
18 basic::pixel::PixelValue,
19 layout::{LayoutPosition, LayoutWritingMode},
20 property::{CssProperty, CssPropertyType},
21 },
22};
23
24use crate::{
25 font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
26 solver3::{
27 fc::{layout_formatting_context, FloatingContext, LayoutConstraints, TextAlign},
28 getters::{
29 get_aspect_ratio_property, get_direction_property, get_display_property, get_writing_mode, get_position, MultiValue,
30 get_css_top, get_css_bottom, get_css_left, get_css_right,
31 get_css_height, get_css_width,
32 },
33 layout_tree::LayoutTree,
34 LayoutContext, LayoutError, Result,
35 },
36};
37
38#[derive(Debug, Default)]
39pub(crate) struct PositionOffsets {
40 pub(crate) top: Option<f32>,
41 pub(crate) right: Option<f32>,
42 pub(crate) bottom: Option<f32>,
43 pub(crate) left: Option<f32>,
44}
45
46#[must_use] pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
50 let Some(id) = dom_id else {
51 return LayoutPosition::Static;
52 };
53 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
54 get_position(styled_dom, id, node_state).unwrap_or_default()
55}
56
57#[allow(clippy::field_reassign_with_default)] pub(crate) fn resolve_position_offsets(
65 styled_dom: &StyledDom,
66 dom_id: Option<NodeId>,
67 cb_size: LogicalSize,
68 viewport_size: LogicalSize,
69) -> PositionOffsets {
70 use azul_css::props::basic::pixel::{PhysicalSize, PropertyContext, ResolutionContext};
71
72 use crate::solver3::getters::{
73 get_element_font_size, get_parent_font_size, get_root_font_size,
74 };
75
76 let Some(id) = dom_id else {
77 return PositionOffsets::default();
78 };
79 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
80
81 let element_font_size = get_element_font_size(styled_dom, id, node_state);
83 let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
84 let root_font_size = get_root_font_size(styled_dom, node_state);
85
86 let containing_block_size = PhysicalSize::new(cb_size.width, cb_size.height);
87
88 let resolution_context = ResolutionContext {
89 element_font_size,
90 parent_font_size,
91 root_font_size,
92 containing_block_size,
93 element_size: None, viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
95 };
96
97 let mut offsets = PositionOffsets::default();
98
99 offsets.top = match get_css_top(styled_dom, id, node_state) {
103 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
104 _ => None,
105 };
106
107 offsets.bottom = match get_css_bottom(styled_dom, id, node_state) {
108 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
109 _ => None,
110 };
111
112 offsets.left = match get_css_left(styled_dom, id, node_state) {
114 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
115 _ => None,
116 };
117
118 offsets.right = match get_css_right(styled_dom, id, node_state) {
119 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
120 _ => None,
121 };
122
123 offsets
124}
125
126#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
143 ctx: &mut LayoutContext<'_, T>,
144 tree: &mut LayoutTree,
145 text_cache: &mut TextLayoutCache,
146 calculated_positions: &mut super::PositionVec,
147 viewport: LogicalRect,
148) {
149 use azul_css::props::style::StyleDirection;
150 for node_index in 0..tree.nodes.len() {
153 let node = &tree.nodes[node_index];
154 let Some(dom_id) = node.dom_node_id else {
155 continue;
156 };
157
158 let position_type = get_position_type(ctx.styled_dom, Some(dom_id));
159
160 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
165 {
169 use azul_core::dom::FormattingContext;
170 let parent_is_flex_or_grid = node.parent.and_then(|p| tree.get(p)).is_some_and(|pn| {
171 matches!(pn.formatting_context, FormattingContext::Flex | FormattingContext::Grid)
172 });
173 if parent_is_flex_or_grid {
174 continue;
175 }
176 }
177
178 let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
180 let node = &tree.nodes[node_index];
181 node.parent.and_then(|parent_idx| {
182 let parent_node = tree.get(parent_idx)?;
183 let parent_dom_id = parent_node.dom_node_id?;
184 let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
185 if parent_position == LayoutPosition::Absolute
186 || parent_position == LayoutPosition::Fixed
187 {
188 calculated_positions.get(parent_idx).map(|parent_pos| {
189 let pbp = parent_node.box_props.unpack();
190 (
191 parent_idx,
192 *parent_pos,
193 pbp.border.left,
194 pbp.border.top,
195 pbp.padding.left,
196 pbp.padding.top,
197 )
198 })
199 } else {
200 None
201 }
202 })
203 };
204
205 let containing_block_rect = if position_type == LayoutPosition::Fixed {
222 viewport
223 } else {
224 match find_absolute_containing_block_rect(
228 tree,
229 node_index,
230 ctx.styled_dom,
231 calculated_positions,
232 viewport,
233 ) {
234 Ok(r) => r,
235 Err(_) => continue,
236 }
237 };
238
239 let node = &tree.nodes[node_index];
241
242 let element_size = if let Some(size) = node.used_size {
245 size
246 } else {
247 let intrinsic = tree.warm(node_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
249 let Ok(size) = crate::solver3::sizing::calculate_used_size_for_node(
250 ctx.styled_dom,
251 Some(dom_id),
252 &containing_block_rect.size,
253 intrinsic,
254 &node.box_props.unpack(),
255 &ctx.viewport_size,
256 ) else {
257 continue;
258 };
259
260 if let Some(node_mut) = tree.get_mut(node_index) {
262 node_mut.used_size = Some(size);
263 }
264
265 size
266 };
267
268 let offsets =
272 resolve_position_offsets(ctx.styled_dom, Some(dom_id), containing_block_rect.size, viewport.size);
273
274 let mut static_pos = calculated_positions
278 .get(node_index)
279 .copied()
280 .unwrap_or_default();
281
282 if position_type == LayoutPosition::Fixed {
287 if let Some((_, parent_pos, border_left, border_top, padding_left, padding_top)) =
288 parent_info
289 {
290 static_pos = LogicalPosition::new(
292 parent_pos.x + border_left + padding_left,
293 parent_pos.y + border_top + padding_top,
294 );
295 }
296 }
297
298 let mut final_pos = LogicalPosition::zero();
299
300 let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
308
309 let (margin_top_val, margin_bottom_val, margin_auto,
311 margin_left_val, margin_right_val, margin_left_auto_flag, margin_right_auto_flag) = {
312 let node = &tree.nodes[node_index];
313 let nbp = node.box_props.unpack();
314 (nbp.margin.top, nbp.margin.bottom,
315 nbp.margin_auto,
316 nbp.margin.left, nbp.margin.right,
317 nbp.margin_auto.left, nbp.margin_auto.right)
318 };
319 let cb_height = containing_block_rect.size.height;
321
322 let css_height = get_css_height(ctx.styled_dom, dom_id, node_state);
323 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
327 let is_replaced = matches!(node_data.node_type, NodeType::Image(_))
328 || node_data.is_virtual_view_node();
329 let height_is_auto = css_height.is_auto() && !is_replaced;
330 let top_is_auto = offsets.top.is_none();
332 let bottom_is_auto = offsets.bottom.is_none();
333
334 let mut used_height = element_size.height;
340 let mut used_margin_top = if margin_auto.top { 0.0 } else { margin_top_val };
343 let mut used_margin_bottom = if margin_auto.bottom { 0.0 } else { margin_bottom_val };
344
345 if top_is_auto && height_is_auto && bottom_is_auto {
353 final_pos.y = static_pos.y;
360 } else if !top_is_auto && !height_is_auto && !bottom_is_auto {
361 let top_val = offsets.top.unwrap();
366 let bottom_val = offsets.bottom.unwrap();
367 if margin_auto.top && margin_auto.bottom {
368 let available = cb_height - top_val - used_height - bottom_val;
370 let each = available / 2.0;
371 used_margin_top = each;
372 used_margin_bottom = each;
373 } else if margin_auto.top {
374 used_margin_top = cb_height - top_val - used_height - used_margin_bottom - bottom_val;
375 } else if margin_auto.bottom {
376 used_margin_bottom = cb_height - top_val - used_height - used_margin_top - bottom_val;
377 }
378 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
380 } else if top_is_auto && height_is_auto && !bottom_is_auto {
381 let bottom_val = offsets.bottom.unwrap();
384 let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
385 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
386 } else if top_is_auto && bottom_is_auto && !height_is_auto {
387 final_pos.y = static_pos.y;
389 } else if height_is_auto && bottom_is_auto && !top_is_auto {
390 let top_val = offsets.top.unwrap();
392 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
393 } else if top_is_auto && !height_is_auto && !bottom_is_auto {
394 let bottom_val = offsets.bottom.unwrap();
397 let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
398 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
399 } else if height_is_auto && !top_is_auto && !bottom_is_auto {
400 let has_aspect_ratio = matches!(
403 get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
404 MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
405 );
406 let top_val = offsets.top.unwrap();
407 let bottom_val = offsets.bottom.unwrap();
408 if !has_aspect_ratio {
409 used_height = (cb_height - top_val - used_margin_top - used_margin_bottom - bottom_val).max(0.0);
413 }
414 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
416 if let Some(node_mut) = tree.get_mut(node_index) {
418 if let Some(ref mut size) = node_mut.used_size {
419 size.height = used_height;
420 }
421 }
422 } else if bottom_is_auto && !top_is_auto && !height_is_auto {
423 let top_val = offsets.top.unwrap();
425 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
426 } else {
427 final_pos.y = static_pos.y;
429 }
430
431 {
440 let margin_left = margin_left_val;
441 let margin_right = margin_right_val;
442 let margin_left_auto = margin_left_auto_flag;
443 let margin_right_auto = margin_right_auto_flag;
444 let cb_width = containing_block_rect.size.width;
445 let border_box_width = element_size.width;
446 let left_val = offsets.left;
447 let right_val = offsets.right;
448 let left_is_auto = left_val.is_none();
449 let right_is_auto = right_val.is_none();
450
451 let cb_direction = {
453 let cb_dom_id = if position_type == LayoutPosition::Fixed {
454 None } else {
456 let mut parent = tree.nodes[node_index].parent;
457 let mut found = None;
458 while let Some(pidx) = parent {
459 if let Some(pnode) = tree.get(pidx) {
460 if get_position_type(ctx.styled_dom, pnode.dom_node_id).is_positioned() {
461 found = pnode.dom_node_id;
462 break;
463 }
464 parent = pnode.parent;
465 } else {
466 break;
467 }
468 }
469 found
470 };
471 match cb_dom_id {
472 Some(cb_id) => {
473 let cb_ns = &ctx.styled_dom.styled_nodes.as_container()[cb_id].styled_node_state;
474 match get_direction_property(ctx.styled_dom, cb_id, cb_ns) {
475 MultiValue::Exact(v) => v,
476 _ => StyleDirection::Ltr,
477 }
478 }
479 None => StyleDirection::Ltr,
480 }
481 };
482
483 let width_is_auto = get_css_width(ctx.styled_dom, dom_id, node_state).is_auto() && !is_replaced;
486
487 if !left_is_auto && !width_is_auto && !right_is_auto {
488 let left = left_val.unwrap();
493 let right = right_val.unwrap();
494 let remaining = cb_width - left - border_box_width - right;
495
496 if margin_left_auto && margin_right_auto {
498 let each_margin = remaining / 2.0;
501 if each_margin < 0.0 {
502 match cb_direction {
503 StyleDirection::Ltr => {
504 final_pos.x = containing_block_rect.origin.x + left;
505 }
506 StyleDirection::Rtl => {
507 final_pos.x = containing_block_rect.origin.x + left + remaining;
508 }
509 }
510 } else {
511 final_pos.x = containing_block_rect.origin.x + left + each_margin;
512 }
513 } else if margin_left_auto {
514 let solved_margin_left = remaining - margin_right;
515 final_pos.x = containing_block_rect.origin.x + left + solved_margin_left;
516 } else if margin_right_auto {
517 final_pos.x = containing_block_rect.origin.x + left + margin_left;
518 } else {
519 match cb_direction {
521 StyleDirection::Ltr => {
522 final_pos.x = containing_block_rect.origin.x + left + margin_left;
523 }
524 StyleDirection::Rtl => {
525 let solved_left = cb_width - margin_left - border_box_width - margin_right - right;
526 final_pos.x = containing_block_rect.origin.x + solved_left + margin_left;
527 }
528 }
529 }
530 } else {
531 let m_left = if margin_left_auto { 0.0 } else { margin_left };
538 let m_right = if margin_right_auto { 0.0 } else { margin_right };
539
540 if left_is_auto && width_is_auto && right_is_auto {
543 match cb_direction {
544 StyleDirection::Ltr => {
545 final_pos.x = static_pos.x;
547 }
548 StyleDirection::Rtl => {
549 let static_offset = static_pos.x - containing_block_rect.origin.x;
551 let right_static = (cb_width - static_offset - border_box_width).max(0.0);
552 let solved_left = cb_width - m_left - border_box_width - m_right - right_static;
553 final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
554 }
555 }
556 } else if left_is_auto && width_is_auto && !right_is_auto {
557 let right = right_val.unwrap();
559 let solved_left = cb_width - m_left - border_box_width - m_right - right;
560 final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
561 } else if left_is_auto && !width_is_auto && right_is_auto {
562 final_pos.x = static_pos.x;
564 } else if !left_is_auto && width_is_auto && right_is_auto {
565 let left = left_val.unwrap();
567 final_pos.x = containing_block_rect.origin.x + left + m_left;
568 } else if left_is_auto && !width_is_auto && !right_is_auto {
569 let right = right_val.unwrap();
571 let solved_left = cb_width - m_left - border_box_width - m_right - right;
572 final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
573 } else if !left_is_auto && width_is_auto && !right_is_auto {
574 let has_aspect_ratio = matches!(
577 get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
578 MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
579 );
580 let left = left_val.unwrap();
581 let right = right_val.unwrap();
582 if !has_aspect_ratio {
583 let used_width = (cb_width - left - m_left - m_right - right).max(0.0);
585 if let Some(node_mut) = tree.get_mut(node_index) {
586 if let Some(ref mut size) = node_mut.used_size {
587 size.width = used_width;
588 }
589 }
590 }
591 final_pos.x = containing_block_rect.origin.x + left + m_left;
593 } else if !left_is_auto && !width_is_auto && right_is_auto {
594 let left = left_val.unwrap();
596 final_pos.x = containing_block_rect.origin.x + left + m_left;
597 } else {
598 final_pos.x = static_pos.x;
599 }
600 }
601 }
602
603 super::pos_set(calculated_positions, node_index, final_pos);
604
605 if height_is_auto {
616 let (used_size, inner, child_collapsed) = {
617 let n = &tree.nodes[node_index];
618 let used = n.used_size.unwrap_or_default();
619 let inner = n.box_props.inner_size(used, LayoutWritingMode::HorizontalTb);
620 let collapsed = inner.height > 1.0
621 && tree.children(node_index).iter().any(|&c| {
622 tree.get(c)
623 .and_then(|cn| cn.used_size)
624 .is_none_or(|s| s.height < 1.0)
625 });
626 (used, inner, collapsed)
627 };
628 let _ = used_size;
629 if child_collapsed {
630 let constraints = LayoutConstraints {
631 available_size: inner,
632 writing_mode: LayoutWritingMode::HorizontalTb,
633 writing_mode_ctx: super::geometry::WritingModeContext::default(),
634 bfc_state: None,
635 text_align: TextAlign::Start,
636 containing_block_size: inner,
637 available_width_type:
638 crate::text3::cache::AvailableSpace::Definite(inner.width),
639 };
640 let mut reflow_float_cache: std::collections::HashMap<usize, FloatingContext> =
641 std::collections::HashMap::new();
642 drop(layout_formatting_context(
643 ctx,
644 tree,
645 text_cache,
646 node_index,
647 &constraints,
648 &mut reflow_float_cache,
649 ));
650 }
651 }
652 }
653 }
654}
655
656#[allow(clippy::too_many_lines)] pub fn adjust_relative_positions<T: ParsedFontTrait>(
672 ctx: &mut LayoutContext<'_, T>,
673 tree: &LayoutTree,
674 calculated_positions: &mut super::PositionVec,
675 viewport: LogicalRect, ) {
677 use azul_css::props::style::StyleDirection;
678 for node_index in 0..tree.nodes.len() {
685 let node = &tree.nodes[node_index];
686 let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
687
688 if position_type != LayoutPosition::Relative && position_type != LayoutPosition::Sticky {
692 continue;
693 }
694
695 {
698 use azul_css::props::layout::LayoutDisplay;
699 let display = get_display_property(ctx.styled_dom, node.dom_node_id);
700 if let MultiValue::Exact(d) = display {
701 if matches!(
706 d,
707 LayoutDisplay::TableColumnGroup
708 | LayoutDisplay::TableColumn
709 | LayoutDisplay::TableCell
710 | LayoutDisplay::TableCaption
711 ) {
712 continue;
713 }
714 }
715 }
716
717 let containing_block_size = node.parent
720 .and_then(|parent_idx| tree.get(parent_idx))
721 .map_or(viewport.size, |parent_node| {
722 let parent_wm = parent_node.dom_node_id
724 .map(|pid| {
725 let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
726 get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
727 })
728 .unwrap_or_default();
729 let parent_used_size = parent_node.used_size.unwrap_or_default();
730 parent_node.box_props.inner_size(parent_used_size, parent_wm)
731 });
732
733 let offsets =
735 resolve_position_offsets(ctx.styled_dom, node.dom_node_id, containing_block_size, viewport.size);
736
737 let Some(current_pos) = calculated_positions.get_mut(node_index) else {
739 continue;
740 };
741
742 let initial_pos = *current_pos;
743
744 let mut delta_x = 0.0;
748 let mut delta_y = 0.0;
749
750 if let Some(top) = offsets.top {
768 delta_y = top;
769 } else if let Some(bottom) = offsets.bottom {
770 delta_y = -bottom;
771 }
772
773 let cb_direction = node.parent
777 .and_then(|parent_idx| tree.get(parent_idx))
778 .and_then(|parent_node| {
779 let parent_dom_id = parent_node.dom_node_id?;
780 let parent_state =
781 &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
782 match get_direction_property(ctx.styled_dom, parent_dom_id, parent_state) {
783 MultiValue::Exact(v) => Some(v),
784 _ => None,
785 }
786 })
787 .unwrap_or(StyleDirection::Ltr);
788 match cb_direction {
790 StyleDirection::Ltr => {
791 if let Some(left) = offsets.left {
792 delta_x = left;
793 } else if let Some(right) = offsets.right {
794 delta_x = -right;
796 }
797 }
798 StyleDirection::Rtl => {
799 if let Some(right) = offsets.right {
800 delta_x = -right;
801 } else if let Some(left) = offsets.left {
802 delta_x = left;
803 }
804 }
805 }
806
807 if delta_x != 0.0 || delta_y != 0.0 {
810 current_pos.x += delta_x;
811 current_pos.y += delta_y;
812
813 debug_log!(ctx, "Adjusted relative element #{} from {:?} to {:?} (delta: {}, {})",
814 node_index, initial_pos, *current_pos, delta_x, delta_y);
815
816 {
820 use azul_css::props::layout::LayoutDisplay;
821 let display = get_display_property(ctx.styled_dom, node.dom_node_id);
822 let is_table_row_like = matches!(
823 display,
824 MultiValue::Exact(
825 LayoutDisplay::TableRowGroup
826 | LayoutDisplay::TableHeaderGroup
827 | LayoutDisplay::TableFooterGroup
828 | LayoutDisplay::TableRow
829 )
830 );
831 if is_table_row_like {
832 let mut stack = tree.children(node_index).to_vec();
834 while let Some(child_idx) = stack.pop() {
835 if let Some(child_pos) = calculated_positions.get_mut(child_idx) {
836 child_pos.x += delta_x;
837 child_pos.y += delta_y;
838 }
839 stack.extend_from_slice(tree.children(child_idx));
840 }
841 }
842 }
843 }
844 }
845}
846
847fn find_nearest_scrollport(
852 tree: &LayoutTree,
853 node_index: usize,
854 styled_dom: &StyledDom,
855 calculated_positions: &super::PositionVec,
856 viewport: LogicalRect,
857) -> LogicalRect {
858 use crate::solver3::getters::{get_overflow_x, get_overflow_y};
859 use azul_css::props::layout::LayoutOverflow;
860
861 let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
862
863 while let Some(parent_index) = current_parent_idx {
864 let Some(parent_node) = tree.get(parent_index) else {
865 break;
866 };
867 let Some(parent_dom_id) = parent_node.dom_node_id else {
868 current_parent_idx = parent_node.parent;
869 continue;
870 };
871
872 let node_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
873 let ox = get_overflow_x(styled_dom, parent_dom_id, node_state);
874 let oy = get_overflow_y(styled_dom, parent_dom_id, node_state);
875
876 let is_scrollport = matches!(
877 ox,
878 MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
879 ) || matches!(
880 oy,
881 MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
882 );
883
884 if is_scrollport {
885 let margin_box_pos = calculated_positions
886 .get(parent_index)
887 .copied()
888 .unwrap_or_default();
889 let border_box_size = parent_node.used_size.unwrap_or_default();
890
891 let pbp = parent_node.box_props.unpack();
893 let content_pos = LogicalPosition::new(
894 margin_box_pos.x
895 + pbp.border.left
896 + pbp.padding.left,
897 margin_box_pos.y
898 + pbp.border.top
899 + pbp.padding.top,
900 );
901 let content_size = LogicalSize::new(
902 (border_box_size.width
903 - pbp.border.left
904 - pbp.border.right
905 - pbp.padding.left
906 - pbp.padding.right)
907 .max(0.0),
908 (border_box_size.height
909 - pbp.border.top
910 - pbp.border.bottom
911 - pbp.padding.top
912 - pbp.padding.bottom)
913 .max(0.0),
914 );
915 return LogicalRect::new(content_pos, content_size);
916 }
917
918 current_parent_idx = parent_node.parent;
919 }
920
921 viewport
922}
923
924fn find_nearest_scroll_offset(
927 tree: &LayoutTree,
928 node_index: usize,
929 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
930) -> LogicalPosition {
931 let mut parent = tree.get(node_index).and_then(|n| n.parent);
932 while let Some(pidx) = parent {
933 if let Some(pnode) = tree.get(pidx) {
934 if let Some(dom_id) = pnode.dom_node_id {
935 if let Some(scroll_pos) = scroll_offsets.get(&dom_id) {
936 let offset_x = scroll_pos.children_rect.origin.x - scroll_pos.parent_rect.origin.x;
937 let offset_y = scroll_pos.children_rect.origin.y - scroll_pos.parent_rect.origin.y;
938 return LogicalPosition::new(offset_x, offset_y);
939 }
940 }
941 parent = pnode.parent;
942 } else {
943 break;
944 }
945 }
946 LogicalPosition::zero()
947}
948
949#[allow(clippy::too_many_lines)] pub fn adjust_sticky_positions<T: ParsedFontTrait>(
962 ctx: &mut LayoutContext<'_, T>,
963 tree: &LayoutTree,
964 calculated_positions: &mut super::PositionVec,
965 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
966 viewport: LogicalRect,
967) {
968 for node_index in 0..tree.nodes.len() {
971 let node = &tree.nodes[node_index];
972 let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
973
974 if position_type != LayoutPosition::Sticky {
975 continue;
976 }
977
978 let Some(dom_id) = node.dom_node_id else {
979 continue;
980 };
981
982 let scrollport = find_nearest_scrollport(
984 tree,
985 node_index,
986 ctx.styled_dom,
987 calculated_positions,
988 viewport,
989 );
990
991 let containing_block = node.parent
993 .and_then(|parent_idx| {
994 let parent_node = tree.get(parent_idx)?;
995 let parent_pos = calculated_positions.get(parent_idx).copied().unwrap_or_default();
996 let parent_size = parent_node.used_size.unwrap_or_default();
997 let parent_wm = parent_node.dom_node_id
998 .map(|pid| {
999 let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
1000 get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
1001 })
1002 .unwrap_or_default();
1003 let pbp = parent_node.box_props.unpack();
1004 let content_size = pbp.inner_size(parent_size, parent_wm);
1005 let content_origin = LogicalPosition::new(
1006 parent_pos.x + pbp.border.left + pbp.padding.left,
1007 parent_pos.y + pbp.border.top + pbp.padding.top,
1008 );
1009 Some(LogicalRect::new(content_origin, content_size))
1010 })
1011 .unwrap_or(viewport);
1012
1013 let offsets = resolve_position_offsets(ctx.styled_dom, Some(dom_id), scrollport.size, viewport.size);
1015
1016 let scroll_offset = find_nearest_scroll_offset(tree, node_index, scroll_offsets);
1018
1019 let Some(current_pos) = calculated_positions.get_mut(node_index) else {
1020 continue;
1021 };
1022
1023 let static_pos = *current_pos;
1024 let element_size = node.used_size.unwrap_or_default();
1025 let nbp = node.box_props.unpack();
1026 let margin = &nbp.margin;
1027
1028 let mut shift_x = 0.0f32;
1029 let mut shift_y = 0.0f32;
1030
1031 if let Some(top_inset) = offsets.top {
1035 let sticky_edge = scrollport.origin.y + scroll_offset.y + top_inset;
1036 let border_top = current_pos.y;
1037 if border_top < sticky_edge {
1038 shift_y = shift_y.max(sticky_edge - border_top);
1039 }
1040 }
1041
1042 if let Some(bottom_inset) = offsets.bottom {
1043 let sticky_edge = scrollport.origin.y + scroll_offset.y + scrollport.size.height - bottom_inset;
1044 let border_bottom = current_pos.y + element_size.height;
1045 if border_bottom > sticky_edge {
1046 shift_y = shift_y.min(sticky_edge - border_bottom);
1047 }
1048 }
1049
1050 if let Some(left_inset) = offsets.left {
1051 let sticky_edge = scrollport.origin.x + scroll_offset.x + left_inset;
1052 let border_left = current_pos.x;
1053 if border_left < sticky_edge {
1054 shift_x = shift_x.max(sticky_edge - border_left);
1055 }
1056 }
1057
1058 if let Some(right_inset) = offsets.right {
1059 let sticky_edge = scrollport.origin.x + scroll_offset.x + scrollport.size.width - right_inset;
1060 let border_right = current_pos.x + element_size.width;
1061 if border_right > sticky_edge {
1062 shift_x = shift_x.min(sticky_edge - border_right);
1063 }
1064 }
1065
1066 if shift_y != 0.0 {
1068 let margin_box_top = current_pos.y - margin.top + shift_y;
1069 let margin_box_bottom = current_pos.y + element_size.height + margin.bottom + shift_y;
1070 if margin_box_top < containing_block.origin.y {
1071 shift_y += containing_block.origin.y - margin_box_top;
1072 }
1073 let cb_bottom = containing_block.origin.y + containing_block.size.height;
1074 if margin_box_bottom > cb_bottom {
1075 shift_y -= margin_box_bottom - cb_bottom;
1076 }
1077 }
1078
1079 if shift_x != 0.0 {
1080 let margin_box_left = current_pos.x - margin.left + shift_x;
1081 let margin_box_right = current_pos.x + element_size.width + margin.right + shift_x;
1082 if margin_box_left < containing_block.origin.x {
1083 shift_x += containing_block.origin.x - margin_box_left;
1084 }
1085 let cb_right = containing_block.origin.x + containing_block.size.width;
1086 if margin_box_right > cb_right {
1087 shift_x -= margin_box_right - cb_right;
1088 }
1089 }
1090
1091 if shift_x != 0.0 || shift_y != 0.0 {
1092 current_pos.x += shift_x;
1093 current_pos.y += shift_y;
1094
1095 debug_log!(ctx, "Adjusted sticky element #{} from {:?} to {:?}",
1096 node_index, static_pos, *current_pos);
1097 }
1098 }
1099}
1100
1101pub(crate) fn find_absolute_containing_block_rect(
1138 tree: &LayoutTree,
1139 node_index: usize,
1140 styled_dom: &StyledDom,
1141 calculated_positions: &super::PositionVec,
1142 viewport: LogicalRect,
1143) -> Result<LogicalRect> {
1144 let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
1146
1147 while let Some(parent_index) = current_parent_idx {
1149 let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;
1150
1151 if get_position_type(styled_dom, parent_node.dom_node_id).is_positioned() {
1152 let margin_box_pos = calculated_positions
1154 .get(parent_index)
1155 .copied()
1156 .unwrap_or_default();
1157 let border_box_size = parent_node.used_size.unwrap_or_default();
1159
1160 let pbp = parent_node.box_props.unpack();
1164 let padding_box_pos = LogicalPosition::new(
1165 margin_box_pos.x + pbp.border.left,
1166 margin_box_pos.y + pbp.border.top,
1167 );
1168
1169 let padding_box_size = LogicalSize::new(
1171 (border_box_size.width
1172 - pbp.border.left
1173 - pbp.border.right)
1174 .max(0.0),
1175 (border_box_size.height
1176 - pbp.border.top
1177 - pbp.border.bottom)
1178 .max(0.0),
1179 );
1180
1181 return Ok(LogicalRect::new(padding_box_pos, padding_box_size));
1182 }
1183 current_parent_idx = parent_node.parent;
1184 }
1185
1186 Ok(viewport)
1195}
1196
1197#[cfg(test)]
1198#[allow(clippy::float_cmp, clippy::too_many_lines)]
1199mod autotest_generated {
1200 use azul_core::dom::{Dom, FormattingContext, IdOrClass};
1201
1202 use super::*;
1203 use crate::solver3::{
1204 geometry::{EdgeSizes, MarginAuto, PackedBoxProps, ResolvedBoxProps},
1205 layout_tree::{LayoutNodeCold, LayoutNodeHot, LayoutNodeWarm},
1206 pos_set, PositionVec, POSITION_UNSET,
1207 };
1208
1209 fn close(a: f32, b: f32, eps: f32) -> bool {
1214 (a - b).abs() <= eps
1215 }
1216
1217 fn viewport() -> LogicalRect {
1218 LogicalRect::new(
1219 LogicalPosition::new(0.0, 0.0),
1220 LogicalSize::new(800.0, 600.0),
1221 )
1222 }
1223
1224 fn styled(dom: Dom, css_str: &str) -> StyledDom {
1225 let mut dom = dom;
1226 let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
1227 StyledDom::create(&mut dom, css)
1228 }
1229
1230 fn div_class(class: &str) -> Dom {
1231 Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
1232 }
1233
1234 fn body_class(class: &str) -> Dom {
1235 Dom::create_body().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
1236 }
1237
1238 fn node_by_class(sd: &StyledDom, class: &str) -> NodeId {
1240 let container = sd.node_data.as_container();
1241 for i in 0..sd.node_data.len() {
1242 let id = NodeId::new(i);
1243 let ids_and_classes = container[id].get_ids_and_classes();
1244 let hit = ids_and_classes
1245 .as_ref()
1246 .iter()
1247 .any(|ioc| matches!(ioc, IdOrClass::Class(c) if c.as_str() == class));
1248 if hit {
1249 return id;
1250 }
1251 }
1252 panic!("no node with class {class:?}");
1253 }
1254
1255 fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
1256 EdgeSizes {
1257 top,
1258 right,
1259 bottom,
1260 left,
1261 }
1262 }
1263
1264 fn uniform(v: f32) -> EdgeSizes {
1265 edges(v, v, v, v)
1266 }
1267
1268 fn bp(margin: EdgeSizes, padding: EdgeSizes, border: EdgeSizes) -> PackedBoxProps {
1269 PackedBoxProps::pack(&ResolvedBoxProps {
1270 margin,
1271 padding,
1272 border,
1273 margin_auto: MarginAuto::default(),
1274 })
1275 }
1276
1277 fn bp_auto_margins(margin_auto: MarginAuto) -> PackedBoxProps {
1278 PackedBoxProps::pack(&ResolvedBoxProps {
1279 margin: uniform(0.0),
1280 padding: uniform(0.0),
1281 border: uniform(0.0),
1282 margin_auto,
1283 })
1284 }
1285
1286 fn hot(parent: Option<usize>, dom_node_id: Option<NodeId>) -> LayoutNodeHot {
1287 LayoutNodeHot {
1288 box_props: PackedBoxProps::default(),
1289 dom_node_id,
1290 used_size: None,
1291 formatting_context: FormattingContext::Block {
1292 establishes_new_context: false,
1293 },
1294 parent,
1295 }
1296 }
1297
1298 fn raw_tree(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
1301 let n = nodes.len();
1302 let mut children_arena: Vec<usize> = Vec::new();
1303 let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
1304 for cl in child_lists {
1305 let start = u32::try_from(children_arena.len()).unwrap();
1306 children_arena.extend_from_slice(cl);
1307 children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
1308 }
1309 while children_offsets.len() < n {
1310 children_offsets.push((0, 0));
1311 }
1312 LayoutTree {
1313 nodes,
1314 warm: vec![LayoutNodeWarm::default(); n],
1315 cold: vec![LayoutNodeCold::default(); n],
1316 root: 0,
1317 dom_to_layout: BTreeMap::new(),
1318 children_arena,
1319 children_offsets,
1320 subtree_needs_intrinsic: Vec::new(),
1321 }
1322 }
1323
1324 fn two_level(css: &str) -> (StyledDom, LayoutTree) {
1326 let sd = styled(body_class("root").with_child(div_class("child")), css);
1327 let root = node_by_class(&sd, "root");
1328 let child = node_by_class(&sd, "child");
1329 let tree = raw_tree(
1330 vec![hot(None, Some(root)), hot(Some(0), Some(child))],
1331 &[vec![1], vec![]],
1332 );
1333 (sd, tree)
1334 }
1335
1336 fn three_level(css: &str) -> (StyledDom, LayoutTree) {
1338 let sd = styled(
1339 body_class("root").with_child(div_class("mid").with_child(div_class("child"))),
1340 css,
1341 );
1342 let root = node_by_class(&sd, "root");
1343 let mid = node_by_class(&sd, "mid");
1344 let child = node_by_class(&sd, "child");
1345 let tree = raw_tree(
1346 vec![
1347 hot(None, Some(root)),
1348 hot(Some(0), Some(mid)),
1349 hot(Some(1), Some(child)),
1350 ],
1351 &[vec![1], vec![2], vec![]],
1352 );
1353 (sd, tree)
1354 }
1355
1356 fn positions(list: &[(f32, f32)]) -> PositionVec {
1357 list.iter()
1358 .map(|&(x, y)| LogicalPosition::new(x, y))
1359 .collect()
1360 }
1361
1362 #[test]
1367 fn get_position_type_none_dom_id_is_static() {
1368 let (sd, _tree) = two_level("");
1369 assert_eq!(get_position_type(&sd, None), LayoutPosition::Static);
1370 }
1371
1372 #[test]
1373 fn get_position_type_unstyled_node_is_static() {
1374 let (sd, _tree) = two_level("");
1375 let child = node_by_class(&sd, "child");
1376 assert_eq!(get_position_type(&sd, Some(child)), LayoutPosition::Static);
1377 }
1378
1379 #[test]
1380 fn get_position_type_reads_every_keyword() {
1381 let sd = styled(
1382 body_class("root")
1383 .with_child(div_class("st"))
1384 .with_child(div_class("rel"))
1385 .with_child(div_class("abs"))
1386 .with_child(div_class("fix"))
1387 .with_child(div_class("sticky")),
1388 ".st { position: static; } .rel { position: relative; } \
1389 .abs { position: absolute; } .fix { position: fixed; } \
1390 .sticky { position: sticky; }",
1391 );
1392 for (class, expected) in [
1393 ("st", LayoutPosition::Static),
1394 ("rel", LayoutPosition::Relative),
1395 ("abs", LayoutPosition::Absolute),
1396 ("fix", LayoutPosition::Fixed),
1397 ("sticky", LayoutPosition::Sticky),
1398 ] {
1399 let id = node_by_class(&sd, class);
1400 assert_eq!(get_position_type(&sd, Some(id)), expected, "class {class}");
1401 }
1402 }
1403
1404 #[test]
1405 fn get_position_type_garbage_value_falls_back_to_static() {
1406 let (sd, _tree) = two_level(".child { position: rubbish-42; }");
1409 let child = node_by_class(&sd, "child");
1410 assert_eq!(get_position_type(&sd, Some(child)), LayoutPosition::Static);
1411 }
1412
1413 #[test]
1414 fn get_position_type_is_pure_and_stable_across_calls() {
1415 let (sd, _tree) = two_level(".child { position: sticky; }");
1416 let child = node_by_class(&sd, "child");
1417 let a = get_position_type(&sd, Some(child));
1418 let b = get_position_type(&sd, Some(child));
1419 assert_eq!(a, b);
1420 assert_eq!(a, LayoutPosition::Sticky);
1421 assert!(a.is_positioned());
1423 }
1424
1425 #[test]
1430 fn resolve_position_offsets_none_dom_id_is_all_none() {
1431 let (sd, _tree) = two_level(".child { top: 10px; }");
1432 let o = resolve_position_offsets(
1433 &sd,
1434 None,
1435 LogicalSize::new(100.0, 100.0),
1436 LogicalSize::new(800.0, 600.0),
1437 );
1438 assert!(o.top.is_none() && o.right.is_none() && o.bottom.is_none() && o.left.is_none());
1439 }
1440
1441 #[test]
1442 fn resolve_position_offsets_unset_insets_are_none_not_zero() {
1443 let (sd, _tree) = two_level("");
1446 let child = node_by_class(&sd, "child");
1447 let o = resolve_position_offsets(
1448 &sd,
1449 Some(child),
1450 LogicalSize::new(100.0, 100.0),
1451 LogicalSize::new(800.0, 600.0),
1452 );
1453 assert!(o.top.is_none() && o.right.is_none() && o.bottom.is_none() && o.left.is_none());
1454 }
1455
1456 #[test]
1457 fn resolve_position_offsets_zero_px_is_some_zero() {
1458 let (sd, _tree) = two_level(".child { top: 0px; left: 0px; }");
1459 let child = node_by_class(&sd, "child");
1460 let o = resolve_position_offsets(
1461 &sd,
1462 Some(child),
1463 LogicalSize::new(0.0, 0.0),
1464 LogicalSize::new(0.0, 0.0),
1465 );
1466 assert_eq!(o.top, Some(0.0));
1467 assert_eq!(o.left, Some(0.0));
1468 assert!(o.right.is_none() && o.bottom.is_none());
1469 }
1470
1471 #[test]
1472 fn resolve_position_offsets_px_values_round_trip() {
1473 let (sd, _tree) =
1474 two_level(".child { top: 11px; right: 22px; bottom: 33px; left: 44px; }");
1475 let child = node_by_class(&sd, "child");
1476 let o = resolve_position_offsets(
1477 &sd,
1478 Some(child),
1479 LogicalSize::new(200.0, 100.0),
1480 LogicalSize::new(800.0, 600.0),
1481 );
1482 assert_eq!(o.top, Some(11.0));
1483 assert_eq!(o.right, Some(22.0));
1484 assert_eq!(o.bottom, Some(33.0));
1485 assert_eq!(o.left, Some(44.0));
1486 }
1487
1488 #[test]
1489 fn resolve_position_offsets_percent_uses_the_correct_axis() {
1490 let (sd, _tree) =
1493 two_level(".child { top: 50%; bottom: 25%; left: 50%; right: 10%; }");
1494 let child = node_by_class(&sd, "child");
1495 let o = resolve_position_offsets(
1496 &sd,
1497 Some(child),
1498 LogicalSize::new(400.0, 200.0),
1499 LogicalSize::new(800.0, 600.0),
1500 );
1501 assert_eq!(o.top, Some(100.0), "50% of CB height 200");
1502 assert_eq!(o.bottom, Some(50.0), "25% of CB height 200");
1503 assert_eq!(o.left, Some(200.0), "50% of CB width 400");
1504 assert_eq!(o.right, Some(40.0), "10% of CB width 400");
1505 }
1506
1507 #[test]
1508 fn resolve_position_offsets_percent_of_zero_containing_block_is_zero() {
1509 let (sd, _tree) = two_level(".child { top: 75%; left: 75%; }");
1510 let child = node_by_class(&sd, "child");
1511 let o = resolve_position_offsets(
1512 &sd,
1513 Some(child),
1514 LogicalSize::new(0.0, 0.0),
1515 LogicalSize::new(800.0, 600.0),
1516 );
1517 assert_eq!(o.top, Some(0.0));
1518 assert_eq!(o.left, Some(0.0));
1519 }
1520
1521 #[test]
1522 fn resolve_position_offsets_negative_values_stay_negative() {
1523 let (sd, _tree) = two_level(".child { top: -40px; left: -25%; }");
1524 let child = node_by_class(&sd, "child");
1525 let o = resolve_position_offsets(
1526 &sd,
1527 Some(child),
1528 LogicalSize::new(400.0, 200.0),
1529 LogicalSize::new(800.0, 600.0),
1530 );
1531 assert_eq!(o.top, Some(-40.0));
1532 assert_eq!(o.left, Some(-100.0), "-25% of CB width 400");
1533 }
1534
1535 #[test]
1536 fn resolve_position_offsets_em_uses_element_font_size_rem_uses_root() {
1537 let sd = styled(
1538 body_class("root").with_child(div_class("child")),
1539 ".root { font-size: 10px; } .child { font-size: 20px; top: 2em; left: 3rem; }",
1540 );
1541 let child = node_by_class(&sd, "child");
1542 let o = resolve_position_offsets(
1543 &sd,
1544 Some(child),
1545 LogicalSize::new(400.0, 200.0),
1546 LogicalSize::new(800.0, 600.0),
1547 );
1548 assert_eq!(o.top, Some(40.0), "2em of the element's own 20px font");
1549 assert_eq!(o.left, Some(30.0), "3rem of the 10px root font");
1550 }
1551
1552 #[test]
1553 fn resolve_position_offsets_viewport_units_use_the_viewport_not_the_containing_block() {
1554 let (sd, _tree) = two_level(".child { top: 10vh; left: 10vw; }");
1555 let child = node_by_class(&sd, "child");
1556 let o = resolve_position_offsets(
1557 &sd,
1558 Some(child),
1559 LogicalSize::new(50.0, 50.0), LogicalSize::new(800.0, 600.0),
1561 );
1562 assert_eq!(o.top, Some(60.0), "10vh of a 600px viewport");
1563 assert_eq!(o.left, Some(80.0), "10vw of an 800px viewport");
1564 }
1565
1566 #[test]
1567 fn resolve_position_offsets_huge_px_bypasses_the_i16_compact_cache_intact() {
1568 let (sd, _tree) = two_level(".child { top: 100000px; left: -100000px; }");
1573 let child = node_by_class(&sd, "child");
1574 let o = resolve_position_offsets(
1575 &sd,
1576 Some(child),
1577 LogicalSize::new(400.0, 200.0),
1578 LogicalSize::new(800.0, 600.0),
1579 );
1580 assert_eq!(o.top, Some(100_000.0));
1581 assert_eq!(o.left, Some(-100_000.0));
1582 }
1583
1584 #[test]
1585 fn resolve_position_offsets_around_the_i16_cache_boundary_agree_within_a_tenth_px() {
1586 let (sd, _tree) = two_level(".child { top: 3276.3px; bottom: 3276.4px; }");
1589 let child = node_by_class(&sd, "child");
1590 let o = resolve_position_offsets(
1591 &sd,
1592 Some(child),
1593 LogicalSize::new(400.0, 200.0),
1594 LogicalSize::new(800.0, 600.0),
1595 );
1596 let top = o.top.expect("top is set");
1597 let bottom = o.bottom.expect("bottom is set");
1598 assert!(close(top, 3276.3, 0.1), "top was {top}");
1599 assert!(close(bottom, 3276.4, 0.1), "bottom was {bottom}");
1600 }
1601
1602 #[test]
1603 fn resolve_position_offsets_sub_tenth_px_precision_loss_is_bounded() {
1604 let (sd, _tree) = two_level(".child { top: 10.567px; }");
1607 let child = node_by_class(&sd, "child");
1608 let o = resolve_position_offsets(
1609 &sd,
1610 Some(child),
1611 LogicalSize::new(400.0, 200.0),
1612 LogicalSize::new(800.0, 600.0),
1613 );
1614 let top = o.top.expect("top is set");
1615 assert!(close(top, 10.567, 0.05), "top was {top}");
1616 }
1617
1618 #[test]
1619 fn resolve_position_offsets_nan_containing_block_yields_nan_not_a_panic() {
1620 let (sd, _tree) = two_level(".child { top: 50%; left: 50%; }");
1621 let child = node_by_class(&sd, "child");
1622 let o = resolve_position_offsets(
1623 &sd,
1624 Some(child),
1625 LogicalSize::new(f32::NAN, f32::NAN),
1626 LogicalSize::new(800.0, 600.0),
1627 );
1628 assert!(o.top.expect("top is set").is_nan());
1629 assert!(o.left.expect("left is set").is_nan());
1630 }
1631
1632 #[test]
1633 fn resolve_position_offsets_infinite_containing_block_yields_infinity_not_a_panic() {
1634 let (sd, _tree) = two_level(".child { top: 50%; left: 50%; }");
1635 let child = node_by_class(&sd, "child");
1636 let o = resolve_position_offsets(
1637 &sd,
1638 Some(child),
1639 LogicalSize::new(f32::INFINITY, f32::INFINITY),
1640 LogicalSize::new(800.0, 600.0),
1641 );
1642 assert_eq!(o.top, Some(f32::INFINITY));
1643 assert_eq!(o.left, Some(f32::INFINITY));
1644 }
1645
1646 #[test]
1647 fn resolve_position_offsets_at_f32_max_containing_block_does_not_panic() {
1648 let (sd, _tree) = two_level(".child { top: 100%; left: 100%; }");
1649 let child = node_by_class(&sd, "child");
1650 let o = resolve_position_offsets(
1651 &sd,
1652 Some(child),
1653 LogicalSize::new(f32::MAX, f32::MAX),
1654 LogicalSize::new(f32::MAX, f32::MAX),
1655 );
1656 assert_eq!(o.top, Some(f32::MAX));
1658 assert_eq!(o.left, Some(f32::MAX));
1659 }
1660
1661 #[test]
1666 fn find_absolute_cb_rect_root_without_parent_is_the_viewport() {
1667 let (sd, tree) = two_level(".root { position: relative; }");
1668 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1669 let got = find_absolute_containing_block_rect(&tree, 0, &sd, &pos, viewport())
1670 .expect("root resolves to the initial CB");
1671 assert_eq!(got, viewport());
1672 }
1673
1674 #[test]
1675 fn find_absolute_cb_rect_out_of_range_index_is_the_viewport_not_a_panic() {
1676 let (sd, tree) = two_level(".root { position: relative; }");
1677 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1678 let got = find_absolute_containing_block_rect(&tree, 9_999, &sd, &pos, viewport())
1679 .expect("an out-of-range index falls back to the initial CB");
1680 assert_eq!(got, viewport());
1681 }
1682
1683 #[test]
1684 fn find_absolute_cb_rect_dangling_parent_index_is_an_error_not_a_panic() {
1685 let (sd, mut tree) = two_level(".root { position: relative; }");
1686 tree.nodes[1].parent = Some(9_999); let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1688 let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport());
1689 assert!(matches!(got, Err(LayoutError::InvalidTree)));
1690 }
1691
1692 #[test]
1693 fn find_absolute_cb_rect_static_ancestors_fall_back_to_the_viewport() {
1694 let (sd, mut tree) = three_level("");
1695 tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
1696 tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 100.0));
1697 let pos = positions(&[(0.0, 0.0), (10.0, 10.0), (20.0, 20.0)]);
1698 let got = find_absolute_containing_block_rect(&tree, 2, &sd, &pos, viewport())
1699 .expect("no positioned ancestor → initial CB");
1700 assert_eq!(got, viewport());
1701 }
1702
1703 #[test]
1704 fn find_absolute_cb_rect_is_the_padding_box_of_the_positioned_ancestor() {
1705 let (sd, mut tree) = two_level(".root { position: relative; }");
1707 tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
1708 tree.nodes[0].box_props = bp(uniform(0.0), uniform(5.0), uniform(10.0));
1709 let pos = positions(&[(20.0, 30.0), (0.0, 0.0)]);
1710 let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
1711 .expect("relative parent is the CB");
1712 assert_eq!(got.origin, LogicalPosition::new(30.0, 40.0));
1713 assert_eq!(got.size, LogicalSize::new(380.0, 280.0));
1714 }
1715
1716 #[test]
1717 fn find_absolute_cb_rect_accepts_every_positioned_ancestor_kind() {
1718 for keyword in ["relative", "absolute", "fixed", "sticky"] {
1719 let css = format!(".root {{ position: {keyword}; }}");
1720 let (sd, mut tree) = two_level(&css);
1721 tree.nodes[0].used_size = Some(LogicalSize::new(100.0, 100.0));
1722 let pos = positions(&[(5.0, 5.0), (0.0, 0.0)]);
1723 let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
1724 .expect("positioned ancestor resolves");
1725 assert_eq!(
1726 got,
1727 LogicalRect::new(
1728 LogicalPosition::new(5.0, 5.0),
1729 LogicalSize::new(100.0, 100.0)
1730 ),
1731 "position: {keyword}"
1732 );
1733 }
1734 }
1735
1736 #[test]
1737 fn find_absolute_cb_rect_picks_the_nearest_positioned_ancestor() {
1738 let (sd, mut tree) = three_level(".root { position: relative; } .mid { position: absolute; }");
1739 tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
1740 tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 100.0));
1741 let pos = positions(&[(0.0, 0.0), (50.0, 60.0), (0.0, 0.0)]);
1742 let got = find_absolute_containing_block_rect(&tree, 2, &sd, &pos, viewport())
1743 .expect("nearest positioned ancestor");
1744 assert_eq!(got.origin, LogicalPosition::new(50.0, 60.0), "mid, not root");
1745 assert_eq!(got.size, LogicalSize::new(200.0, 100.0));
1746 }
1747
1748 #[test]
1749 fn find_absolute_cb_rect_saturating_borders_clamp_the_padding_box_to_zero() {
1750 let (sd, mut tree) = two_level(".root { position: relative; }");
1753 tree.nodes[0].used_size = Some(LogicalSize::new(100.0, 100.0));
1754 tree.nodes[0].box_props = bp(uniform(0.0), uniform(0.0), uniform(1e30));
1755 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1756 let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
1757 .expect("saturated borders still resolve");
1758 assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
1759 assert!(got.size.width >= 0.0 && got.size.height >= 0.0);
1760 assert!(got.origin.x.is_finite() && got.origin.y.is_finite());
1761 }
1762
1763 #[test]
1764 fn find_absolute_cb_rect_unsized_ancestor_is_a_zero_sized_padding_box() {
1765 let (sd, tree) = two_level(".root { position: relative; }"); let pos = positions(&[(7.0, 9.0), (0.0, 0.0)]);
1767 let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
1768 .expect("an unsized ancestor still resolves");
1769 assert_eq!(got.origin, LogicalPosition::new(7.0, 9.0));
1770 assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
1771 }
1772
1773 #[test]
1774 fn find_absolute_cb_rect_missing_position_entry_defaults_to_the_origin() {
1775 let (sd, mut tree) = two_level(".root { position: relative; }");
1776 tree.nodes[0].used_size = Some(LogicalSize::new(100.0, 100.0));
1777 let pos: PositionVec = Vec::new(); let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
1779 .expect("an empty position vec still resolves");
1780 assert_eq!(got.origin, LogicalPosition::new(0.0, 0.0));
1781 assert_eq!(got.size, LogicalSize::new(100.0, 100.0));
1782 }
1783
1784 #[test]
1789 fn find_nearest_scrollport_without_a_scroll_ancestor_is_the_viewport() {
1790 let (sd, tree) = two_level("");
1791 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1792 assert_eq!(
1793 find_nearest_scrollport(&tree, 1, &sd, &pos, viewport()),
1794 viewport()
1795 );
1796 }
1797
1798 #[test]
1799 fn find_nearest_scrollport_out_of_range_index_is_the_viewport_not_a_panic() {
1800 let (sd, tree) = two_level(".root { overflow-y: scroll; }");
1801 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1802 assert_eq!(
1803 find_nearest_scrollport(&tree, 9_999, &sd, &pos, viewport()),
1804 viewport()
1805 );
1806 }
1807
1808 #[test]
1809 fn find_nearest_scrollport_returns_the_ancestor_content_box() {
1810 for css in [
1811 ".root { overflow-x: scroll; }",
1812 ".root { overflow-y: scroll; }",
1813 ".root { overflow-x: auto; }",
1814 ".root { overflow-y: auto; }",
1815 ] {
1816 let (sd, mut tree) = two_level(css);
1817 tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 150.0));
1818 tree.nodes[0].box_props = bp(uniform(0.0), uniform(5.0), uniform(10.0));
1819 let pos = positions(&[(20.0, 30.0), (0.0, 0.0)]);
1820 let got = find_nearest_scrollport(&tree, 1, &sd, &pos, viewport());
1821 assert_eq!(got.origin, LogicalPosition::new(35.0, 45.0), "{css}");
1823 assert_eq!(got.size, LogicalSize::new(170.0, 120.0), "{css}");
1824 }
1825 }
1826
1827 #[test]
1828 fn find_nearest_scrollport_ignores_non_scrolling_overflow() {
1829 for css in [
1830 ".root { overflow-x: hidden; }",
1831 ".root { overflow-y: visible; }",
1832 ".root { overflow-x: clip; }",
1833 ] {
1834 let (sd, mut tree) = two_level(css);
1835 tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 150.0));
1836 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1837 assert_eq!(
1838 find_nearest_scrollport(&tree, 1, &sd, &pos, viewport()),
1839 viewport(),
1840 "{css}"
1841 );
1842 }
1843 }
1844
1845 #[test]
1846 fn find_nearest_scrollport_picks_the_nearest_of_two_scroll_ancestors() {
1847 let (sd, mut tree) =
1848 three_level(".root { overflow-y: scroll; } .mid { overflow-y: scroll; }");
1849 tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
1850 tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 100.0));
1851 let pos = positions(&[(0.0, 0.0), (11.0, 12.0), (0.0, 0.0)]);
1852 let got = find_nearest_scrollport(&tree, 2, &sd, &pos, viewport());
1853 assert_eq!(got.origin, LogicalPosition::new(11.0, 12.0), "mid, not root");
1854 assert_eq!(got.size, LogicalSize::new(200.0, 100.0));
1855 }
1856
1857 #[test]
1858 fn find_nearest_scrollport_walks_past_anonymous_boxes() {
1859 let (sd, mut tree) = three_level(".root { overflow-y: scroll; }");
1862 tree.nodes[1].dom_node_id = None; tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
1864 let pos = positions(&[(1.0, 2.0), (0.0, 0.0), (0.0, 0.0)]);
1865 let got = find_nearest_scrollport(&tree, 2, &sd, &pos, viewport());
1866 assert_eq!(got.origin, LogicalPosition::new(1.0, 2.0));
1867 assert_eq!(got.size, LogicalSize::new(400.0, 300.0));
1868 }
1869
1870 #[test]
1871 fn find_nearest_scrollport_clamps_the_content_box_to_zero_when_padding_exceeds_the_box() {
1872 let (sd, mut tree) = two_level(".root { overflow-y: scroll; }");
1873 tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
1874 tree.nodes[0].box_props = bp(uniform(0.0), uniform(1e30), uniform(1e30));
1875 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
1876 let got = find_nearest_scrollport(&tree, 1, &sd, &pos, viewport());
1877 assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
1878 assert!(got.size.width >= 0.0 && got.size.height >= 0.0);
1879 }
1880
1881 #[test]
1882 fn find_nearest_scrollport_unsized_scrollport_is_zero_sized() {
1883 let (sd, tree) = two_level(".root { overflow-y: scroll; }"); let pos: PositionVec = Vec::new();
1885 let got = find_nearest_scrollport(&tree, 1, &sd, &pos, viewport());
1886 assert_eq!(got.origin, LogicalPosition::new(0.0, 0.0));
1887 assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
1888 }
1889
1890 fn scroll_at(parent: (f32, f32), children: (f32, f32)) -> ScrollPosition {
1895 ScrollPosition {
1896 parent_rect: LogicalRect::new(
1897 LogicalPosition::new(parent.0, parent.1),
1898 LogicalSize::new(100.0, 100.0),
1899 ),
1900 children_rect: LogicalRect::new(
1901 LogicalPosition::new(children.0, children.1),
1902 LogicalSize::new(100.0, 400.0),
1903 ),
1904 }
1905 }
1906
1907 #[test]
1908 fn find_nearest_scroll_offset_empty_map_is_zero() {
1909 let (_sd, tree) = two_level("");
1910 let offsets: BTreeMap<NodeId, ScrollPosition> = BTreeMap::new();
1911 assert_eq!(
1912 find_nearest_scroll_offset(&tree, 1, &offsets),
1913 LogicalPosition::zero()
1914 );
1915 }
1916
1917 #[test]
1918 fn find_nearest_scroll_offset_out_of_range_index_is_zero_not_a_panic() {
1919 let (sd, tree) = two_level("");
1920 let mut offsets = BTreeMap::new();
1921 offsets.insert(node_by_class(&sd, "root"), scroll_at((0.0, 0.0), (0.0, -50.0)));
1922 assert_eq!(
1923 find_nearest_scroll_offset(&tree, 9_999, &offsets),
1924 LogicalPosition::zero()
1925 );
1926 }
1927
1928 #[test]
1929 fn find_nearest_scroll_offset_ignores_the_nodes_own_entry() {
1930 let (sd, tree) = two_level("");
1933 let mut offsets = BTreeMap::new();
1934 offsets.insert(
1935 node_by_class(&sd, "child"),
1936 scroll_at((0.0, 0.0), (0.0, -50.0)),
1937 );
1938 assert_eq!(
1939 find_nearest_scroll_offset(&tree, 1, &offsets),
1940 LogicalPosition::zero()
1941 );
1942 }
1943
1944 #[test]
1945 fn find_nearest_scroll_offset_is_children_origin_minus_parent_origin() {
1946 let (sd, tree) = two_level("");
1947 let mut offsets = BTreeMap::new();
1948 offsets.insert(
1949 node_by_class(&sd, "root"),
1950 scroll_at((10.0, 20.0), (-5.0, -80.0)),
1951 );
1952 assert_eq!(
1953 find_nearest_scroll_offset(&tree, 1, &offsets),
1954 LogicalPosition::new(-15.0, -100.0)
1955 );
1956 }
1957
1958 #[test]
1959 fn find_nearest_scroll_offset_picks_the_nearest_ancestor() {
1960 let (sd, tree) = three_level("");
1961 let mut offsets = BTreeMap::new();
1962 offsets.insert(node_by_class(&sd, "root"), scroll_at((0.0, 0.0), (0.0, -999.0)));
1963 offsets.insert(node_by_class(&sd, "mid"), scroll_at((0.0, 0.0), (0.0, -7.0)));
1964 assert_eq!(
1965 find_nearest_scroll_offset(&tree, 2, &offsets),
1966 LogicalPosition::new(0.0, -7.0),
1967 "mid wins over root"
1968 );
1969 }
1970
1971 #[test]
1972 fn find_nearest_scroll_offset_walks_past_anonymous_ancestors() {
1973 let (sd, mut tree) = three_level("");
1974 tree.nodes[1].dom_node_id = None;
1975 let mut offsets = BTreeMap::new();
1976 offsets.insert(node_by_class(&sd, "root"), scroll_at((0.0, 0.0), (0.0, -30.0)));
1977 assert_eq!(
1978 find_nearest_scroll_offset(&tree, 2, &offsets),
1979 LogicalPosition::new(0.0, -30.0)
1980 );
1981 }
1982
1983 #[test]
1984 fn find_nearest_scroll_offset_at_f32_extremes_stays_deterministic() {
1985 let (sd, tree) = two_level("");
1986 let mut offsets = BTreeMap::new();
1987 offsets.insert(
1988 node_by_class(&sd, "root"),
1989 scroll_at((f32::MAX, f32::MAX), (f32::MIN, f32::MIN)),
1990 );
1991 let got = find_nearest_scroll_offset(&tree, 1, &offsets);
1992 assert!(!got.x.is_nan() && !got.y.is_nan());
1995 assert_eq!(got.x, f32::NEG_INFINITY);
1996 assert_eq!(got.y, f32::NEG_INFINITY);
1997 }
1998
1999 #[cfg(all(feature = "text_layout", feature = "font_loading"))]
2003 mod with_ctx {
2004 use std::collections::HashMap;
2005
2006 use azul_core::{dom::DomId, selection::TextSelection};
2007 use azul_css::props::basic::FontRef;
2008
2009 use super::*;
2010 use crate::{
2011 font_traits::{FontManager, TextLayoutCache},
2012 solver3::{cache, LayoutContext},
2013 };
2014
2015 struct Env {
2017 styled_dom: StyledDom,
2018 font_manager: FontManager<FontRef>,
2019 text_selections: BTreeMap<DomId, TextSelection>,
2020 counters: HashMap<(usize, String), i32>,
2021 image_cache: azul_core::resources::ImageCache,
2022 debug_messages: Option<Vec<LayoutDebugMessage>>,
2023 }
2024
2025 impl Env {
2026 fn new(styled_dom: StyledDom) -> Self {
2027 Self {
2028 styled_dom,
2029 font_manager: FontManager::new(rust_fontconfig::FcFontCache::default())
2030 .expect("FontManager over an empty font cache"),
2031 text_selections: BTreeMap::new(),
2032 counters: HashMap::new(),
2033 image_cache: azul_core::resources::ImageCache::default(),
2034 debug_messages: None,
2035 }
2036 }
2037
2038 fn ctx(&mut self) -> LayoutContext<'_, FontRef> {
2039 LayoutContext {
2040 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
2041 styled_dom: &self.styled_dom,
2042 font_manager: &self.font_manager,
2043 text_selections: &self.text_selections,
2044 debug_messages: &mut self.debug_messages,
2045 counters: &mut self.counters,
2046 viewport_size: LogicalSize::new(800.0, 600.0),
2047 fragmentation_context: None,
2048 cursor_is_visible: true,
2049 cursor_locations: Vec::new(),
2050 preedit_text: None,
2051 dirty_text_overrides: BTreeMap::new(),
2052 cache_map: cache::LayoutCacheMap::default(),
2053 image_cache: &self.image_cache,
2054 system_style: None,
2055 get_system_time_fn: azul_core::task::GetSystemTimeCallback {
2056 cb: azul_core::task::get_system_time_libstd,
2057 },
2058 }
2059 }
2060 }
2061
2062 fn abs_fixture(css: &str) -> (Env, LayoutTree, PositionVec) {
2066 let (sd, mut tree) = two_level(css);
2067 tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
2068 tree.nodes[0].box_props = bp(uniform(0.0), uniform(5.0), uniform(10.0));
2069 tree.nodes[1].used_size = Some(LogicalSize::new(50.0, 50.0));
2070 let pos = positions(&[(20.0, 30.0), (0.0, 0.0)]);
2071 (Env::new(sd), tree, pos)
2072 }
2073
2074 fn run_oof(env: &mut Env, tree: &mut LayoutTree, pos: &mut PositionVec, vp: LogicalRect) {
2075 let mut text_cache = TextLayoutCache::default();
2076 let mut ctx = env.ctx();
2077 position_out_of_flow_elements(&mut ctx, tree, &mut text_cache, pos, vp);
2078 }
2079
2080 #[test]
2085 fn out_of_flow_top_left_offset_from_the_ancestor_padding_box() {
2086 let (mut env, mut tree, mut pos) = abs_fixture(
2087 ".root { position: relative; } \
2088 .child { position: absolute; top: 25px; left: 15px; }",
2089 );
2090 run_oof(&mut env, &mut tree, &mut pos, viewport());
2091 assert_eq!(pos[1], LogicalPosition::new(45.0, 65.0));
2092 }
2093
2094 #[test]
2095 fn out_of_flow_zero_insets_land_exactly_on_the_padding_box_origin() {
2096 let (mut env, mut tree, mut pos) = abs_fixture(
2097 ".root { position: relative; } .child { position: absolute; top: 0px; left: 0px; }",
2098 );
2099 run_oof(&mut env, &mut tree, &mut pos, viewport());
2100 assert_eq!(pos[1], LogicalPosition::new(30.0, 40.0));
2101 }
2102
2103 #[test]
2104 fn out_of_flow_all_auto_keeps_the_static_position() {
2105 let (mut env, mut tree, mut pos) =
2107 abs_fixture(".root { position: relative; } .child { position: absolute; }");
2108 pos_set(&mut pos, 1, LogicalPosition::new(7.0, 9.0));
2109 run_oof(&mut env, &mut tree, &mut pos, viewport());
2110 assert_eq!(pos[1], LogicalPosition::new(7.0, 9.0));
2111 }
2112
2113 #[test]
2114 fn out_of_flow_fixed_resolves_against_the_viewport_not_the_ancestor() {
2115 let (mut env, mut tree, mut pos) = abs_fixture(
2116 ".root { position: relative; } .child { position: fixed; top: 25px; left: 15px; }",
2117 );
2118 run_oof(&mut env, &mut tree, &mut pos, viewport());
2119 assert_eq!(pos[1], LogicalPosition::new(15.0, 25.0));
2120 }
2121
2122 #[test]
2123 fn out_of_flow_over_constrained_ignores_the_end_insets_in_ltr() {
2124 let (mut env, mut tree, mut pos) = abs_fixture(
2126 ".root { position: relative; } \
2127 .child { position: absolute; top: 10px; bottom: 10px; left: 10px; \
2128 right: 10px; width: 50px; height: 50px; }",
2129 );
2130 run_oof(&mut env, &mut tree, &mut pos, viewport());
2131 assert_eq!(pos[1], LogicalPosition::new(40.0, 50.0));
2132 }
2133
2134 #[test]
2135 fn out_of_flow_auto_margins_center_the_box_in_both_axes() {
2136 let (mut env, mut tree, mut pos) = abs_fixture(
2138 ".root { position: relative; } \
2139 .child { position: absolute; top: 0px; bottom: 0px; left: 0px; \
2140 right: 0px; width: 100px; height: 100px; }",
2141 );
2142 tree.nodes[1].used_size = Some(LogicalSize::new(100.0, 100.0));
2143 tree.nodes[1].box_props = bp_auto_margins(MarginAuto {
2144 top: true,
2145 bottom: true,
2146 left: true,
2147 right: true,
2148 });
2149 run_oof(&mut env, &mut tree, &mut pos, viewport());
2150 assert_eq!(pos[1], LogicalPosition::new(170.0, 130.0));
2152 }
2153
2154 #[test]
2155 fn out_of_flow_negative_free_space_with_auto_margins_pins_to_the_start_edge_in_ltr() {
2156 let (mut env, mut tree, mut pos) = abs_fixture(
2158 ".root { position: relative; } \
2159 .child { position: absolute; left: 0px; right: 0px; width: 500px; }",
2160 );
2161 tree.nodes[1].used_size = Some(LogicalSize::new(500.0, 50.0));
2162 tree.nodes[1].box_props = bp_auto_margins(MarginAuto {
2163 left: true,
2164 right: true,
2165 top: false,
2166 bottom: false,
2167 });
2168 run_oof(&mut env, &mut tree, &mut pos, viewport());
2169 assert_eq!(pos[1].x, 30.0);
2171 }
2172
2173 #[test]
2174 fn out_of_flow_over_constrained_ignores_the_left_inset_in_rtl() {
2175 let (mut env, mut tree, mut pos) = abs_fixture(
2176 ".root { position: relative; direction: rtl; } \
2177 .child { position: absolute; left: 10px; right: 10px; width: 50px; }",
2178 );
2179 run_oof(&mut env, &mut tree, &mut pos, viewport());
2180 assert_eq!(pos[1].x, 350.0);
2182 }
2183
2184 #[test]
2185 fn out_of_flow_auto_height_and_width_stretch_between_the_insets() {
2186 let (mut env, mut tree, mut pos) = abs_fixture(
2188 ".root { position: relative; } \
2189 .child { position: absolute; top: 10px; bottom: 20px; left: 30px; right: 40px; }",
2190 );
2191 run_oof(&mut env, &mut tree, &mut pos, viewport());
2192 assert_eq!(pos[1], LogicalPosition::new(60.0, 50.0));
2193 let used = tree.nodes[1].used_size.expect("size was resolved");
2194 assert_eq!(used, LogicalSize::new(310.0, 250.0));
2195 }
2196
2197 #[test]
2198 fn out_of_flow_insets_larger_than_the_containing_block_clamp_the_size_to_zero() {
2199 let (mut env, mut tree, mut pos) = abs_fixture(
2200 ".root { position: relative; } \
2201 .child { position: absolute; top: 500px; bottom: 500px; \
2202 left: 500px; right: 500px; }",
2203 );
2204 run_oof(&mut env, &mut tree, &mut pos, viewport());
2205 let used = tree.nodes[1].used_size.expect("size was resolved");
2206 assert_eq!(used, LogicalSize::new(0.0, 0.0), "never negative");
2207 assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
2208 }
2209
2210 #[test]
2211 fn out_of_flow_huge_insets_bypass_the_i16_cache_and_stay_finite() {
2212 let (mut env, mut tree, mut pos) = abs_fixture(
2213 ".root { position: relative; } \
2214 .child { position: absolute; top: 3300px; left: 100000px; }",
2215 );
2216 run_oof(&mut env, &mut tree, &mut pos, viewport());
2217 assert_eq!(pos[1], LogicalPosition::new(100_030.0, 3340.0));
2218 assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
2219 }
2220
2221 #[test]
2222 fn out_of_flow_negative_insets_move_the_box_outside_the_containing_block() {
2223 let (mut env, mut tree, mut pos) = abs_fixture(
2224 ".root { position: relative; } \
2225 .child { position: absolute; top: -100px; left: -200px; }",
2226 );
2227 run_oof(&mut env, &mut tree, &mut pos, viewport());
2228 assert_eq!(pos[1], LogicalPosition::new(-170.0, -60.0));
2229 }
2230
2231 #[test]
2232 fn out_of_flow_nan_viewport_clamps_the_stretch_height_to_zero_and_keeps_the_position_finite()
2233 {
2234 let (mut env, mut tree, mut pos) = abs_fixture(
2237 ".root { position: relative; } \
2238 .child { position: fixed; top: 10px; bottom: 20px; }",
2239 );
2240 let nan_vp = LogicalRect::new(
2241 LogicalPosition::new(0.0, 0.0),
2242 LogicalSize::new(f32::NAN, f32::NAN),
2243 );
2244 run_oof(&mut env, &mut tree, &mut pos, nan_vp);
2245 let used = tree.nodes[1].used_size.expect("size was resolved");
2246 assert_eq!(used.height, 0.0);
2247 assert_eq!(pos[1].y, 10.0);
2248 assert!(pos[1].y.is_finite());
2249 }
2250
2251 #[test]
2252 fn out_of_flow_infinite_viewport_keeps_the_position_finite() {
2253 let (mut env, mut tree, mut pos) = abs_fixture(
2254 ".root { position: relative; } \
2255 .child { position: fixed; top: 10px; bottom: 20px; }",
2256 );
2257 let inf_vp = LogicalRect::new(
2258 LogicalPosition::new(0.0, 0.0),
2259 LogicalSize::new(f32::INFINITY, f32::INFINITY),
2260 );
2261 run_oof(&mut env, &mut tree, &mut pos, inf_vp);
2262 assert_eq!(pos[1].y, 10.0);
2263 let used = tree.nodes[1].used_size.expect("size was resolved");
2264 assert!(used.height.is_infinite() && used.height > 0.0);
2265 }
2266
2267 #[test]
2268 fn out_of_flow_every_auto_combination_of_top_height_bottom_is_panic_free() {
2269 for top in ["", "top: 10px;"] {
2273 for bottom in ["", "bottom: 20px;"] {
2274 for height in ["", "height: 30px;"] {
2275 for left in ["", "left: 10px;"] {
2276 for right in ["", "right: 20px;"] {
2277 for width in ["", "width: 30px;"] {
2278 let css = format!(
2279 ".root {{ position: relative; }} \
2280 .child {{ position: absolute; {top}{bottom}{height}\
2281 {left}{right}{width} }}"
2282 );
2283 let (mut env, mut tree, mut pos) = abs_fixture(&css);
2284 run_oof(&mut env, &mut tree, &mut pos, viewport());
2285 assert!(
2286 pos[1].x.is_finite() && pos[1].y.is_finite(),
2287 "non-finite position for {css}"
2288 );
2289 }
2290 }
2291 }
2292 }
2293 }
2294 }
2295 }
2296
2297 #[test]
2298 fn out_of_flow_skips_children_of_flex_and_grid_parents() {
2299 for fc in [FormattingContext::Flex, FormattingContext::Grid] {
2302 let (mut env, mut tree, mut pos) = abs_fixture(
2303 ".root { position: relative; } \
2304 .child { position: absolute; top: 25px; left: 15px; }",
2305 );
2306 tree.nodes[0].formatting_context = fc;
2307 pos_set(&mut pos, 1, LogicalPosition::new(3.0, 4.0));
2308 run_oof(&mut env, &mut tree, &mut pos, viewport());
2309 assert_eq!(pos[1], LogicalPosition::new(3.0, 4.0), "{fc:?}");
2310 }
2311 }
2312
2313 #[test]
2314 fn out_of_flow_leaves_static_and_relative_nodes_alone() {
2315 for keyword in ["static", "relative", "sticky"] {
2316 let css = format!(
2317 ".root {{ position: relative; }} \
2318 .child {{ position: {keyword}; top: 25px; left: 15px; }}"
2319 );
2320 let (mut env, mut tree, mut pos) = abs_fixture(&css);
2321 pos_set(&mut pos, 1, LogicalPosition::new(3.0, 4.0));
2322 run_oof(&mut env, &mut tree, &mut pos, viewport());
2323 assert_eq!(pos[1], LogicalPosition::new(3.0, 4.0), "{keyword}");
2324 }
2325 }
2326
2327 #[test]
2328 fn out_of_flow_short_position_vec_grows_instead_of_panicking() {
2329 let (mut env, mut tree, _pos) = abs_fixture(
2330 ".root { position: relative; } \
2331 .child { position: absolute; top: 25px; left: 15px; }",
2332 );
2333 let mut pos: PositionVec = Vec::new(); run_oof(&mut env, &mut tree, &mut pos, viewport());
2335 assert_eq!(pos.len(), 2, "pos_set grew the vec");
2336 assert_eq!(pos[1], LogicalPosition::new(25.0, 35.0));
2338 }
2339
2340 #[test]
2341 fn out_of_flow_unsized_node_is_sized_on_the_fly_without_panicking() {
2342 let (mut env, mut tree, mut pos) = abs_fixture(
2343 ".root { position: relative; } \
2344 .child { position: absolute; top: 10px; left: 10px; }",
2345 );
2346 tree.nodes[1].used_size = None; run_oof(&mut env, &mut tree, &mut pos, viewport());
2348 assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
2349 }
2350
2351 fn rel_fixture(css: &str) -> (Env, LayoutTree, PositionVec) {
2358 let (sd, mut tree) = two_level(css);
2359 tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 100.0));
2360 tree.nodes[0].box_props = bp(uniform(0.0), uniform(10.0), uniform(0.0));
2361 tree.nodes[1].used_size = Some(LogicalSize::new(50.0, 20.0));
2362 let pos = positions(&[(0.0, 0.0), (100.0, 100.0)]);
2363 (Env::new(sd), tree, pos)
2364 }
2365
2366 fn run_rel(env: &mut Env, tree: &LayoutTree, pos: &mut PositionVec) {
2367 let mut ctx = env.ctx();
2368 adjust_relative_positions(&mut ctx, tree, pos, viewport());
2369 }
2370
2371 #[test]
2372 fn relative_px_offsets_shift_from_the_static_position() {
2373 let (mut env, tree, mut pos) =
2374 rel_fixture(".child { position: relative; top: 10px; left: 5px; }");
2375 run_rel(&mut env, &tree, &mut pos);
2376 assert_eq!(pos[1], LogicalPosition::new(105.0, 110.0));
2377 }
2378
2379 #[test]
2380 fn relative_percentages_resolve_against_the_parent_content_box() {
2381 let (mut env, tree, mut pos) =
2382 rel_fixture(".child { position: relative; top: 50%; left: 50%; }");
2383 run_rel(&mut env, &tree, &mut pos);
2384 assert_eq!(pos[1], LogicalPosition::new(190.0, 140.0));
2386 }
2387
2388 #[test]
2389 fn relative_top_wins_over_bottom() {
2390 let (mut env, tree, mut pos) =
2392 rel_fixture(".child { position: relative; top: 10px; bottom: 30px; }");
2393 run_rel(&mut env, &tree, &mut pos);
2394 assert_eq!(pos[1].y, 110.0);
2395 }
2396
2397 #[test]
2398 fn relative_bottom_alone_is_the_negation_of_top() {
2399 let (mut env, tree, mut pos) =
2400 rel_fixture(".child { position: relative; bottom: 30px; }");
2401 run_rel(&mut env, &tree, &mut pos);
2402 assert_eq!(pos[1].y, 70.0);
2403 }
2404
2405 #[test]
2406 fn relative_right_alone_is_the_negation_of_left() {
2407 let (mut env, tree, mut pos) =
2409 rel_fixture(".child { position: relative; right: 20px; }");
2410 run_rel(&mut env, &tree, &mut pos);
2411 assert_eq!(pos[1].x, 80.0);
2412 }
2413
2414 #[test]
2415 fn relative_left_wins_in_ltr_and_right_wins_in_rtl() {
2416 let (mut env, tree, mut pos) =
2418 rel_fixture(".child { position: relative; left: 5px; right: 20px; }");
2419 run_rel(&mut env, &tree, &mut pos);
2420 assert_eq!(pos[1].x, 105.0, "ltr: left wins");
2421
2422 let (mut env, tree, mut pos) = rel_fixture(
2423 ".root { direction: rtl; } \
2424 .child { position: relative; left: 5px; right: 20px; }",
2425 );
2426 run_rel(&mut env, &tree, &mut pos);
2427 assert_eq!(pos[1].x, 80.0, "rtl: right wins → -20");
2428 }
2429
2430 #[test]
2431 fn relative_zero_offsets_are_a_no_op() {
2432 let (mut env, tree, mut pos) =
2433 rel_fixture(".child { position: relative; top: 0px; left: 0px; }");
2434 run_rel(&mut env, &tree, &mut pos);
2435 assert_eq!(pos[1], LogicalPosition::new(100.0, 100.0));
2436 }
2437
2438 #[test]
2439 fn relative_leaves_static_absolute_and_fixed_nodes_untouched() {
2440 for keyword in ["static", "absolute", "fixed"] {
2441 let css =
2442 format!(".child {{ position: {keyword}; top: 10px; left: 5px; }}");
2443 let (mut env, tree, mut pos) = rel_fixture(&css);
2444 run_rel(&mut env, &tree, &mut pos);
2445 assert_eq!(pos[1], LogicalPosition::new(100.0, 100.0), "{keyword}");
2446 }
2447 }
2448
2449 #[test]
2450 fn relative_also_offsets_sticky_boxes() {
2451 let (mut env, tree, mut pos) =
2455 rel_fixture(".child { position: relative; top: 10px; }");
2456 run_rel(&mut env, &tree, &mut pos);
2457 let relative_y = pos[1].y;
2458
2459 let (mut env, tree, mut pos) =
2460 rel_fixture(".child { position: sticky; top: 10px; }");
2461 run_rel(&mut env, &tree, &mut pos);
2462 assert_eq!(pos[1].y, relative_y);
2463 }
2464
2465 #[test]
2466 fn relative_is_undefined_for_table_cells_and_captions_so_they_are_skipped() {
2467 for display in ["table-cell", "table-caption", "table-column"] {
2468 let css = format!(
2469 ".child {{ position: relative; display: {display}; top: 10px; left: 5px; }}"
2470 );
2471 let (mut env, tree, mut pos) = rel_fixture(&css);
2472 run_rel(&mut env, &tree, &mut pos);
2473 assert_eq!(pos[1], LogicalPosition::new(100.0, 100.0), "{display}");
2474 }
2475 }
2476
2477 #[test]
2478 fn relative_table_rows_drag_their_whole_subtree() {
2479 let (sd, mut tree) = three_level(
2481 ".mid { position: relative; display: table-row; top: 10px; left: 5px; } \
2482 .child { display: table-cell; }",
2483 );
2484 tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 100.0));
2485 tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 50.0));
2486 tree.nodes[2].used_size = Some(LogicalSize::new(100.0, 50.0));
2487 let mut pos = positions(&[(0.0, 0.0), (10.0, 20.0), (10.0, 20.0)]);
2488 let mut env = Env::new(sd);
2489 run_rel(&mut env, &tree, &mut pos);
2490 assert_eq!(pos[1], LogicalPosition::new(15.0, 30.0), "the row itself");
2491 assert_eq!(pos[2], LogicalPosition::new(15.0, 30.0), "the cell follows");
2492 }
2493
2494 #[test]
2495 fn relative_short_position_vec_is_skipped_not_panicked_on() {
2496 let (mut env, tree, _pos) =
2497 rel_fixture(".child { position: relative; top: 10px; left: 5px; }");
2498 let mut pos: PositionVec = Vec::new();
2499 run_rel(&mut env, &tree, &mut pos);
2500 assert!(pos.is_empty(), "nothing to shift, nothing added");
2501 }
2502
2503 #[test]
2504 fn relative_huge_and_negative_offsets_stay_finite() {
2505 let (mut env, tree, mut pos) =
2506 rel_fixture(".child { position: relative; top: 100000px; left: -100000px; }");
2507 run_rel(&mut env, &tree, &mut pos);
2508 assert_eq!(pos[1], LogicalPosition::new(-99_900.0, 100_100.0));
2509 assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
2510 }
2511
2512 #[test]
2513 fn relative_unset_sentinel_position_is_not_silently_shifted_into_a_real_one() {
2514 let (mut env, tree, mut pos) =
2518 rel_fixture(".child { position: relative; top: 10px; left: 5px; }");
2519 pos[1] = POSITION_UNSET;
2520 run_rel(&mut env, &tree, &mut pos);
2521 assert!(pos[1].x < -1e30 && pos[1].y < -1e30);
2522 }
2523
2524 fn sticky_fixture(css: &str) -> (Env, LayoutTree, PositionVec) {
2530 let (sd, mut tree) = two_level(css);
2531 tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 200.0));
2532 tree.nodes[1].used_size = Some(LogicalSize::new(50.0, 20.0));
2533 let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
2534 (Env::new(sd), tree, pos)
2535 }
2536
2537 fn run_sticky(
2538 env: &mut Env,
2539 tree: &LayoutTree,
2540 pos: &mut PositionVec,
2541 offsets: &BTreeMap<NodeId, ScrollPosition>,
2542 ) {
2543 let mut ctx = env.ctx();
2544 adjust_sticky_positions(&mut ctx, tree, pos, offsets, viewport());
2545 }
2546
2547 #[test]
2548 fn sticky_top_inset_pins_the_box_to_the_scrollport_edge() {
2549 let (mut env, tree, mut pos) = sticky_fixture(
2550 ".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
2551 );
2552 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2553 assert_eq!(pos[1], LogicalPosition::new(0.0, 10.0));
2554 }
2555
2556 #[test]
2557 fn sticky_without_insets_does_not_move() {
2558 let (mut env, tree, mut pos) =
2559 sticky_fixture(".root { overflow-y: scroll; } .child { position: sticky; }");
2560 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2561 assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0));
2562 }
2563
2564 #[test]
2565 fn sticky_ignores_non_sticky_positions() {
2566 for keyword in ["static", "relative", "absolute", "fixed"] {
2567 let css = format!(
2568 ".root {{ overflow-y: scroll; }} \
2569 .child {{ position: {keyword}; top: 10px; }}"
2570 );
2571 let (mut env, tree, mut pos) = sticky_fixture(&css);
2572 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2573 assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0), "{keyword}");
2574 }
2575 }
2576
2577 #[test]
2578 fn sticky_edge_moves_with_the_scroll_offset_of_the_nearest_container() {
2579 let (mut env, tree, mut pos) = sticky_fixture(
2580 ".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
2581 );
2582 let root = node_by_class(&env.styled_dom, "root");
2583 let mut offsets = BTreeMap::new();
2584 offsets.insert(root, scroll_at((0.0, 0.0), (0.0, 50.0)));
2585 run_sticky(&mut env, &tree, &mut pos, &offsets);
2586 assert_eq!(pos[1].y, 60.0);
2588 }
2589
2590 #[test]
2591 fn sticky_percentage_inset_resolves_against_the_scrollport() {
2592 let (mut env, tree, mut pos) = sticky_fixture(
2593 ".root { overflow-y: scroll; } .child { position: sticky; top: 10%; }",
2594 );
2595 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2596 assert_eq!(pos[1].y, 20.0, "10% of the 200px scrollport");
2597 }
2598
2599 #[test]
2600 fn sticky_bottom_inset_pulls_the_box_back_up_into_the_scrollport() {
2601 let (mut env, tree, mut pos) = sticky_fixture(
2602 ".root { overflow-y: scroll; } .child { position: sticky; bottom: 10px; }",
2603 );
2604 pos_set(&mut pos, 1, LogicalPosition::new(0.0, 250.0));
2605 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2606 assert_eq!(pos[1].y, 170.0);
2608 }
2609
2610 #[test]
2611 fn sticky_shift_is_clamped_by_the_containing_block() {
2612 let (sd, mut tree) = three_level(
2615 ".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
2616 );
2617 tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 200.0));
2618 tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 25.0)); tree.nodes[2].used_size = Some(LogicalSize::new(50.0, 20.0));
2620 let mut pos = positions(&[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]);
2621 let mut env = Env::new(sd);
2622 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2623 assert_eq!(pos[2].y, 5.0);
2625 }
2626
2627 #[test]
2628 fn sticky_huge_inset_clamps_to_the_containing_block_instead_of_flying_away() {
2629 let (mut env, tree, mut pos) = sticky_fixture(
2630 ".root { overflow-y: scroll; } .child { position: sticky; top: 100000px; }",
2631 );
2632 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2633 assert_eq!(pos[1].y, 180.0);
2636 assert!(pos[1].y.is_finite());
2637 }
2638
2639 #[test]
2640 fn sticky_negative_inset_is_deterministic_and_finite() {
2641 let (mut env, tree, mut pos) = sticky_fixture(
2642 ".root { overflow-y: scroll; } .child { position: sticky; top: -50px; }",
2643 );
2644 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2645 assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0));
2647 }
2648
2649 #[test]
2650 fn sticky_without_a_scroll_ancestor_falls_back_to_the_viewport() {
2651 let (mut env, tree, mut pos) =
2652 sticky_fixture(".child { position: sticky; top: 10px; }"); run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2654 assert_eq!(pos[1].y, 10.0);
2657 }
2658
2659 #[test]
2660 fn sticky_left_and_right_insets_shift_the_inline_axis() {
2661 let (mut env, tree, mut pos) = sticky_fixture(
2662 ".root { overflow-x: scroll; } .child { position: sticky; left: 15px; }",
2663 );
2664 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2665 assert_eq!(pos[1].x, 15.0);
2666
2667 let (mut env, tree, mut pos) = sticky_fixture(
2668 ".root { overflow-x: scroll; } .child { position: sticky; right: 10px; }",
2669 );
2670 pos_set(&mut pos, 1, LogicalPosition::new(300.0, 0.0));
2671 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2672 assert_eq!(pos[1].x, 140.0);
2674 }
2675
2676 #[test]
2677 fn sticky_short_position_vec_is_skipped_not_panicked_on() {
2678 let (mut env, tree, _pos) = sticky_fixture(
2679 ".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
2680 );
2681 let mut pos: PositionVec = Vec::new();
2682 run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
2683 assert!(pos.is_empty());
2684 }
2685
2686 #[test]
2687 fn sticky_nan_scroll_offset_never_panics() {
2688 let (mut env, tree, mut pos) = sticky_fixture(
2689 ".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
2690 );
2691 let root = node_by_class(&env.styled_dom, "root");
2692 let mut offsets = BTreeMap::new();
2693 offsets.insert(root, scroll_at((f32::NAN, f32::NAN), (f32::NAN, f32::NAN)));
2694 run_sticky(&mut env, &tree, &mut pos, &offsets);
2695 assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0));
2697 }
2698 }
2699}