use crate::debug_log;
use std::collections::BTreeMap;
use azul_core::{
dom::{NodeId, NodeType},
geom::{LogicalPosition, LogicalRect, LogicalSize},
hit_test::ScrollPosition,
resources::RendererResources,
styled_dom::StyledDom,
};
use azul_css::{
corety::LayoutDebugMessage,
css::CssPropertyValue,
props::{
basic::pixel::PixelValue,
layout::{LayoutPosition, LayoutWritingMode},
property::{CssProperty, CssPropertyType},
},
};
use crate::{
font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
solver3::{
fc::{layout_formatting_context, FloatingContext, LayoutConstraints, TextAlign},
getters::{
get_aspect_ratio_property, get_direction_property, get_display_property, get_writing_mode, get_position, MultiValue,
get_css_top, get_css_bottom, get_css_left, get_css_right,
get_css_height, get_css_width,
},
layout_tree::LayoutTree,
LayoutContext, LayoutError, Result,
},
};
#[derive(Debug, Default)]
pub(crate) struct PositionOffsets {
pub(crate) top: Option<f32>,
pub(crate) right: Option<f32>,
pub(crate) bottom: Option<f32>,
pub(crate) left: Option<f32>,
}
#[must_use] pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
let Some(id) = dom_id else {
return LayoutPosition::Static;
};
let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
get_position(styled_dom, id, node_state).unwrap_or_default()
}
#[allow(clippy::field_reassign_with_default)] pub(crate) fn resolve_position_offsets(
styled_dom: &StyledDom,
dom_id: Option<NodeId>,
cb_size: LogicalSize,
viewport_size: LogicalSize,
) -> PositionOffsets {
use azul_css::props::basic::pixel::{PhysicalSize, PropertyContext, ResolutionContext};
use crate::solver3::getters::{
get_element_font_size, get_parent_font_size, get_root_font_size,
};
let Some(id) = dom_id else {
return PositionOffsets::default();
};
let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
let element_font_size = get_element_font_size(styled_dom, id, node_state);
let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
let root_font_size = get_root_font_size(styled_dom, node_state);
let containing_block_size = PhysicalSize::new(cb_size.width, cb_size.height);
let resolution_context = ResolutionContext {
element_font_size,
parent_font_size,
root_font_size,
containing_block_size,
element_size: None, viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
};
let mut offsets = PositionOffsets::default();
offsets.top = match get_css_top(styled_dom, id, node_state) {
MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
_ => None,
};
offsets.bottom = match get_css_bottom(styled_dom, id, node_state) {
MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
_ => None,
};
offsets.left = match get_css_left(styled_dom, id, node_state) {
MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
_ => None,
};
offsets.right = match get_css_right(styled_dom, id, node_state) {
MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
_ => None,
};
offsets
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut TextLayoutCache,
calculated_positions: &mut super::PositionVec,
viewport: LogicalRect,
) {
use azul_css::props::style::StyleDirection;
for node_index in 0..tree.nodes.len() {
let node = &tree.nodes[node_index];
let Some(dom_id) = node.dom_node_id else {
continue;
};
let position_type = get_position_type(ctx.styled_dom, Some(dom_id));
if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
{
use azul_core::dom::FormattingContext;
let parent_is_flex_or_grid = node.parent.and_then(|p| tree.get(p)).is_some_and(|pn| {
matches!(pn.formatting_context, FormattingContext::Flex | FormattingContext::Grid)
});
if parent_is_flex_or_grid {
continue;
}
}
let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
let node = &tree.nodes[node_index];
node.parent.and_then(|parent_idx| {
let parent_node = tree.get(parent_idx)?;
let parent_dom_id = parent_node.dom_node_id?;
let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
if parent_position == LayoutPosition::Absolute
|| parent_position == LayoutPosition::Fixed
{
calculated_positions.get(parent_idx).map(|parent_pos| {
let pbp = parent_node.box_props.unpack();
(
parent_idx,
*parent_pos,
pbp.border.left,
pbp.border.top,
pbp.padding.left,
pbp.padding.top,
)
})
} else {
None
}
})
};
let containing_block_rect = if position_type == LayoutPosition::Fixed {
viewport
} else {
match find_absolute_containing_block_rect(
tree,
node_index,
ctx.styled_dom,
calculated_positions,
viewport,
) {
Ok(r) => r,
Err(_) => continue,
}
};
let node = &tree.nodes[node_index];
let element_size = if let Some(size) = node.used_size {
size
} else {
let intrinsic = tree.warm(node_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
let Ok(size) = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
Some(dom_id),
&containing_block_rect.size,
intrinsic,
&node.box_props.unpack(),
&ctx.viewport_size,
) else {
continue;
};
if let Some(node_mut) = tree.get_mut(node_index) {
node_mut.used_size = Some(size);
}
size
};
let offsets =
resolve_position_offsets(ctx.styled_dom, Some(dom_id), containing_block_rect.size, viewport.size);
let mut static_pos = calculated_positions
.get(node_index)
.copied()
.unwrap_or_default();
if position_type == LayoutPosition::Fixed {
if let Some((_, parent_pos, border_left, border_top, padding_left, padding_top)) =
parent_info
{
static_pos = LogicalPosition::new(
parent_pos.x + border_left + padding_left,
parent_pos.y + border_top + padding_top,
);
}
}
let mut final_pos = LogicalPosition::zero();
let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let (margin_top_val, margin_bottom_val, margin_auto,
margin_left_val, margin_right_val, margin_left_auto_flag, margin_right_auto_flag) = {
let node = &tree.nodes[node_index];
let nbp = node.box_props.unpack();
(nbp.margin.top, nbp.margin.bottom,
nbp.margin_auto,
nbp.margin.left, nbp.margin.right,
nbp.margin_auto.left, nbp.margin_auto.right)
};
let cb_height = containing_block_rect.size.height;
let css_height = get_css_height(ctx.styled_dom, dom_id, node_state);
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let is_replaced = matches!(node_data.node_type, NodeType::Image(_))
|| node_data.is_virtual_view_node();
let height_is_auto = css_height.is_auto() && !is_replaced;
let top_is_auto = offsets.top.is_none();
let bottom_is_auto = offsets.bottom.is_none();
let mut used_height = element_size.height;
let mut used_margin_top = if margin_auto.top { 0.0 } else { margin_top_val };
let mut used_margin_bottom = if margin_auto.bottom { 0.0 } else { margin_bottom_val };
if top_is_auto && height_is_auto && bottom_is_auto {
final_pos.y = static_pos.y;
} else if !top_is_auto && !height_is_auto && !bottom_is_auto {
let top_val = offsets.top.unwrap();
let bottom_val = offsets.bottom.unwrap();
if margin_auto.top && margin_auto.bottom {
let available = cb_height - top_val - used_height - bottom_val;
let each = available / 2.0;
used_margin_top = each;
used_margin_bottom = each;
} else if margin_auto.top {
used_margin_top = cb_height - top_val - used_height - used_margin_bottom - bottom_val;
} else if margin_auto.bottom {
used_margin_bottom = cb_height - top_val - used_height - used_margin_top - bottom_val;
}
final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
} else if top_is_auto && height_is_auto && !bottom_is_auto {
let bottom_val = offsets.bottom.unwrap();
let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
} else if top_is_auto && bottom_is_auto && !height_is_auto {
final_pos.y = static_pos.y;
} else if height_is_auto && bottom_is_auto && !top_is_auto {
let top_val = offsets.top.unwrap();
final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
} else if top_is_auto && !height_is_auto && !bottom_is_auto {
let bottom_val = offsets.bottom.unwrap();
let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
} else if height_is_auto && !top_is_auto && !bottom_is_auto {
let has_aspect_ratio = matches!(
get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
);
let top_val = offsets.top.unwrap();
let bottom_val = offsets.bottom.unwrap();
if !has_aspect_ratio {
used_height = (cb_height - top_val - used_margin_top - used_margin_bottom - bottom_val).max(0.0);
}
final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
if let Some(node_mut) = tree.get_mut(node_index) {
if let Some(ref mut size) = node_mut.used_size {
size.height = used_height;
}
}
} else if bottom_is_auto && !top_is_auto && !height_is_auto {
let top_val = offsets.top.unwrap();
final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
} else {
final_pos.y = static_pos.y;
}
{
let margin_left = margin_left_val;
let margin_right = margin_right_val;
let margin_left_auto = margin_left_auto_flag;
let margin_right_auto = margin_right_auto_flag;
let cb_width = containing_block_rect.size.width;
let border_box_width = element_size.width;
let left_val = offsets.left;
let right_val = offsets.right;
let left_is_auto = left_val.is_none();
let right_is_auto = right_val.is_none();
let cb_direction = {
let cb_dom_id = if position_type == LayoutPosition::Fixed {
None } else {
let mut parent = tree.nodes[node_index].parent;
let mut found = None;
while let Some(pidx) = parent {
if let Some(pnode) = tree.get(pidx) {
if get_position_type(ctx.styled_dom, pnode.dom_node_id).is_positioned() {
found = pnode.dom_node_id;
break;
}
parent = pnode.parent;
} else {
break;
}
}
found
};
match cb_dom_id {
Some(cb_id) => {
let cb_ns = &ctx.styled_dom.styled_nodes.as_container()[cb_id].styled_node_state;
match get_direction_property(ctx.styled_dom, cb_id, cb_ns) {
MultiValue::Exact(v) => v,
_ => StyleDirection::Ltr,
}
}
None => StyleDirection::Ltr,
}
};
let width_is_auto = get_css_width(ctx.styled_dom, dom_id, node_state).is_auto() && !is_replaced;
if !left_is_auto && !width_is_auto && !right_is_auto {
let left = left_val.unwrap();
let right = right_val.unwrap();
let remaining = cb_width - left - border_box_width - right;
if margin_left_auto && margin_right_auto {
let each_margin = remaining / 2.0;
if each_margin < 0.0 {
match cb_direction {
StyleDirection::Ltr => {
final_pos.x = containing_block_rect.origin.x + left;
}
StyleDirection::Rtl => {
final_pos.x = containing_block_rect.origin.x + left + remaining;
}
}
} else {
final_pos.x = containing_block_rect.origin.x + left + each_margin;
}
} else if margin_left_auto {
let solved_margin_left = remaining - margin_right;
final_pos.x = containing_block_rect.origin.x + left + solved_margin_left;
} else if margin_right_auto {
final_pos.x = containing_block_rect.origin.x + left + margin_left;
} else {
match cb_direction {
StyleDirection::Ltr => {
final_pos.x = containing_block_rect.origin.x + left + margin_left;
}
StyleDirection::Rtl => {
let solved_left = cb_width - margin_left - border_box_width - margin_right - right;
final_pos.x = containing_block_rect.origin.x + solved_left + margin_left;
}
}
}
} else {
let m_left = if margin_left_auto { 0.0 } else { margin_left };
let m_right = if margin_right_auto { 0.0 } else { margin_right };
if left_is_auto && width_is_auto && right_is_auto {
match cb_direction {
StyleDirection::Ltr => {
final_pos.x = static_pos.x;
}
StyleDirection::Rtl => {
let static_offset = static_pos.x - containing_block_rect.origin.x;
let right_static = (cb_width - static_offset - border_box_width).max(0.0);
let solved_left = cb_width - m_left - border_box_width - m_right - right_static;
final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
}
}
} else if left_is_auto && width_is_auto && !right_is_auto {
let right = right_val.unwrap();
let solved_left = cb_width - m_left - border_box_width - m_right - right;
final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
} else if left_is_auto && !width_is_auto && right_is_auto {
final_pos.x = static_pos.x;
} else if !left_is_auto && width_is_auto && right_is_auto {
let left = left_val.unwrap();
final_pos.x = containing_block_rect.origin.x + left + m_left;
} else if left_is_auto && !width_is_auto && !right_is_auto {
let right = right_val.unwrap();
let solved_left = cb_width - m_left - border_box_width - m_right - right;
final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
} else if !left_is_auto && width_is_auto && !right_is_auto {
let has_aspect_ratio = matches!(
get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
);
let left = left_val.unwrap();
let right = right_val.unwrap();
if !has_aspect_ratio {
let used_width = (cb_width - left - m_left - m_right - right).max(0.0);
if let Some(node_mut) = tree.get_mut(node_index) {
if let Some(ref mut size) = node_mut.used_size {
size.width = used_width;
}
}
}
final_pos.x = containing_block_rect.origin.x + left + m_left;
} else if !left_is_auto && !width_is_auto && right_is_auto {
let left = left_val.unwrap();
final_pos.x = containing_block_rect.origin.x + left + m_left;
} else {
final_pos.x = static_pos.x;
}
}
}
super::pos_set(calculated_positions, node_index, final_pos);
if height_is_auto {
let (used_size, inner, child_collapsed) = {
let n = &tree.nodes[node_index];
let used = n.used_size.unwrap_or_default();
let inner = n.box_props.inner_size(used, LayoutWritingMode::HorizontalTb);
let collapsed = inner.height > 1.0
&& tree.children(node_index).iter().any(|&c| {
tree.get(c)
.and_then(|cn| cn.used_size)
.is_none_or(|s| s.height < 1.0)
});
(used, inner, collapsed)
};
let _ = used_size;
if child_collapsed {
let constraints = LayoutConstraints {
available_size: inner,
writing_mode: LayoutWritingMode::HorizontalTb,
writing_mode_ctx: super::geometry::WritingModeContext::default(),
bfc_state: None,
text_align: TextAlign::Start,
containing_block_size: inner,
available_width_type:
crate::text3::cache::AvailableSpace::Definite(inner.width),
};
let mut reflow_float_cache: std::collections::HashMap<usize, FloatingContext> =
std::collections::HashMap::new();
drop(layout_formatting_context(
ctx,
tree,
text_cache,
node_index,
&constraints,
&mut reflow_float_cache,
));
}
}
}
}
}
#[allow(clippy::too_many_lines)] pub fn adjust_relative_positions<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &LayoutTree,
calculated_positions: &mut super::PositionVec,
viewport: LogicalRect, ) {
use azul_css::props::style::StyleDirection;
for node_index in 0..tree.nodes.len() {
let node = &tree.nodes[node_index];
let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
if position_type != LayoutPosition::Relative && position_type != LayoutPosition::Sticky {
continue;
}
{
use azul_css::props::layout::LayoutDisplay;
let display = get_display_property(ctx.styled_dom, node.dom_node_id);
if let MultiValue::Exact(d) = display {
if matches!(
d,
LayoutDisplay::TableColumnGroup
| LayoutDisplay::TableColumn
| LayoutDisplay::TableCell
| LayoutDisplay::TableCaption
) {
continue;
}
}
}
let containing_block_size = node.parent
.and_then(|parent_idx| tree.get(parent_idx))
.map_or(viewport.size, |parent_node| {
let parent_wm = parent_node.dom_node_id
.map(|pid| {
let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
})
.unwrap_or_default();
let parent_used_size = parent_node.used_size.unwrap_or_default();
parent_node.box_props.inner_size(parent_used_size, parent_wm)
});
let offsets =
resolve_position_offsets(ctx.styled_dom, node.dom_node_id, containing_block_size, viewport.size);
let Some(current_pos) = calculated_positions.get_mut(node_index) else {
continue;
};
let initial_pos = *current_pos;
let mut delta_x = 0.0;
let mut delta_y = 0.0;
if let Some(top) = offsets.top {
delta_y = top;
} else if let Some(bottom) = offsets.bottom {
delta_y = -bottom;
}
let cb_direction = node.parent
.and_then(|parent_idx| tree.get(parent_idx))
.and_then(|parent_node| {
let parent_dom_id = parent_node.dom_node_id?;
let parent_state =
&ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
match get_direction_property(ctx.styled_dom, parent_dom_id, parent_state) {
MultiValue::Exact(v) => Some(v),
_ => None,
}
})
.unwrap_or(StyleDirection::Ltr);
match cb_direction {
StyleDirection::Ltr => {
if let Some(left) = offsets.left {
delta_x = left;
} else if let Some(right) = offsets.right {
delta_x = -right;
}
}
StyleDirection::Rtl => {
if let Some(right) = offsets.right {
delta_x = -right;
} else if let Some(left) = offsets.left {
delta_x = left;
}
}
}
if delta_x != 0.0 || delta_y != 0.0 {
current_pos.x += delta_x;
current_pos.y += delta_y;
debug_log!(ctx, "Adjusted relative element #{} from {:?} to {:?} (delta: {}, {})",
node_index, initial_pos, *current_pos, delta_x, delta_y);
{
use azul_css::props::layout::LayoutDisplay;
let display = get_display_property(ctx.styled_dom, node.dom_node_id);
let is_table_row_like = matches!(
display,
MultiValue::Exact(
LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup
| LayoutDisplay::TableRow
)
);
if is_table_row_like {
let mut stack = tree.children(node_index).to_vec();
while let Some(child_idx) = stack.pop() {
if let Some(child_pos) = calculated_positions.get_mut(child_idx) {
child_pos.x += delta_x;
child_pos.y += delta_y;
}
stack.extend_from_slice(tree.children(child_idx));
}
}
}
}
}
}
fn find_nearest_scrollport(
tree: &LayoutTree,
node_index: usize,
styled_dom: &StyledDom,
calculated_positions: &super::PositionVec,
viewport: LogicalRect,
) -> LogicalRect {
use crate::solver3::getters::{get_overflow_x, get_overflow_y};
use azul_css::props::layout::LayoutOverflow;
let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
while let Some(parent_index) = current_parent_idx {
let Some(parent_node) = tree.get(parent_index) else {
break;
};
let Some(parent_dom_id) = parent_node.dom_node_id else {
current_parent_idx = parent_node.parent;
continue;
};
let node_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
let ox = get_overflow_x(styled_dom, parent_dom_id, node_state);
let oy = get_overflow_y(styled_dom, parent_dom_id, node_state);
let is_scrollport = matches!(
ox,
MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
) || matches!(
oy,
MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
);
if is_scrollport {
let margin_box_pos = calculated_positions
.get(parent_index)
.copied()
.unwrap_or_default();
let border_box_size = parent_node.used_size.unwrap_or_default();
let pbp = parent_node.box_props.unpack();
let content_pos = LogicalPosition::new(
margin_box_pos.x
+ pbp.border.left
+ pbp.padding.left,
margin_box_pos.y
+ pbp.border.top
+ pbp.padding.top,
);
let content_size = LogicalSize::new(
(border_box_size.width
- pbp.border.left
- pbp.border.right
- pbp.padding.left
- pbp.padding.right)
.max(0.0),
(border_box_size.height
- pbp.border.top
- pbp.border.bottom
- pbp.padding.top
- pbp.padding.bottom)
.max(0.0),
);
return LogicalRect::new(content_pos, content_size);
}
current_parent_idx = parent_node.parent;
}
viewport
}
fn find_nearest_scroll_offset(
tree: &LayoutTree,
node_index: usize,
scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
) -> LogicalPosition {
let mut parent = tree.get(node_index).and_then(|n| n.parent);
while let Some(pidx) = parent {
if let Some(pnode) = tree.get(pidx) {
if let Some(dom_id) = pnode.dom_node_id {
if let Some(scroll_pos) = scroll_offsets.get(&dom_id) {
let offset_x = scroll_pos.children_rect.origin.x - scroll_pos.parent_rect.origin.x;
let offset_y = scroll_pos.children_rect.origin.y - scroll_pos.parent_rect.origin.y;
return LogicalPosition::new(offset_x, offset_y);
}
}
parent = pnode.parent;
} else {
break;
}
}
LogicalPosition::zero()
}
#[allow(clippy::too_many_lines)] pub fn adjust_sticky_positions<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &LayoutTree,
calculated_positions: &mut super::PositionVec,
scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
viewport: LogicalRect,
) {
for node_index in 0..tree.nodes.len() {
let node = &tree.nodes[node_index];
let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
if position_type != LayoutPosition::Sticky {
continue;
}
let Some(dom_id) = node.dom_node_id else {
continue;
};
let scrollport = find_nearest_scrollport(
tree,
node_index,
ctx.styled_dom,
calculated_positions,
viewport,
);
let containing_block = node.parent
.and_then(|parent_idx| {
let parent_node = tree.get(parent_idx)?;
let parent_pos = calculated_positions.get(parent_idx).copied().unwrap_or_default();
let parent_size = parent_node.used_size.unwrap_or_default();
let parent_wm = parent_node.dom_node_id
.map(|pid| {
let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
})
.unwrap_or_default();
let pbp = parent_node.box_props.unpack();
let content_size = pbp.inner_size(parent_size, parent_wm);
let content_origin = LogicalPosition::new(
parent_pos.x + pbp.border.left + pbp.padding.left,
parent_pos.y + pbp.border.top + pbp.padding.top,
);
Some(LogicalRect::new(content_origin, content_size))
})
.unwrap_or(viewport);
let offsets = resolve_position_offsets(ctx.styled_dom, Some(dom_id), scrollport.size, viewport.size);
let scroll_offset = find_nearest_scroll_offset(tree, node_index, scroll_offsets);
let Some(current_pos) = calculated_positions.get_mut(node_index) else {
continue;
};
let static_pos = *current_pos;
let element_size = node.used_size.unwrap_or_default();
let nbp = node.box_props.unpack();
let margin = &nbp.margin;
let mut shift_x = 0.0f32;
let mut shift_y = 0.0f32;
if let Some(top_inset) = offsets.top {
let sticky_edge = scrollport.origin.y + scroll_offset.y + top_inset;
let border_top = current_pos.y;
if border_top < sticky_edge {
shift_y = shift_y.max(sticky_edge - border_top);
}
}
if let Some(bottom_inset) = offsets.bottom {
let sticky_edge = scrollport.origin.y + scroll_offset.y + scrollport.size.height - bottom_inset;
let border_bottom = current_pos.y + element_size.height;
if border_bottom > sticky_edge {
shift_y = shift_y.min(sticky_edge - border_bottom);
}
}
if let Some(left_inset) = offsets.left {
let sticky_edge = scrollport.origin.x + scroll_offset.x + left_inset;
let border_left = current_pos.x;
if border_left < sticky_edge {
shift_x = shift_x.max(sticky_edge - border_left);
}
}
if let Some(right_inset) = offsets.right {
let sticky_edge = scrollport.origin.x + scroll_offset.x + scrollport.size.width - right_inset;
let border_right = current_pos.x + element_size.width;
if border_right > sticky_edge {
shift_x = shift_x.min(sticky_edge - border_right);
}
}
if shift_y != 0.0 {
let margin_box_top = current_pos.y - margin.top + shift_y;
let margin_box_bottom = current_pos.y + element_size.height + margin.bottom + shift_y;
if margin_box_top < containing_block.origin.y {
shift_y += containing_block.origin.y - margin_box_top;
}
let cb_bottom = containing_block.origin.y + containing_block.size.height;
if margin_box_bottom > cb_bottom {
shift_y -= margin_box_bottom - cb_bottom;
}
}
if shift_x != 0.0 {
let margin_box_left = current_pos.x - margin.left + shift_x;
let margin_box_right = current_pos.x + element_size.width + margin.right + shift_x;
if margin_box_left < containing_block.origin.x {
shift_x += containing_block.origin.x - margin_box_left;
}
let cb_right = containing_block.origin.x + containing_block.size.width;
if margin_box_right > cb_right {
shift_x -= margin_box_right - cb_right;
}
}
if shift_x != 0.0 || shift_y != 0.0 {
current_pos.x += shift_x;
current_pos.y += shift_y;
debug_log!(ctx, "Adjusted sticky element #{} from {:?} to {:?}",
node_index, static_pos, *current_pos);
}
}
}
pub(crate) fn find_absolute_containing_block_rect(
tree: &LayoutTree,
node_index: usize,
styled_dom: &StyledDom,
calculated_positions: &super::PositionVec,
viewport: LogicalRect,
) -> Result<LogicalRect> {
let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
while let Some(parent_index) = current_parent_idx {
let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;
if get_position_type(styled_dom, parent_node.dom_node_id).is_positioned() {
let margin_box_pos = calculated_positions
.get(parent_index)
.copied()
.unwrap_or_default();
let border_box_size = parent_node.used_size.unwrap_or_default();
let pbp = parent_node.box_props.unpack();
let padding_box_pos = LogicalPosition::new(
margin_box_pos.x + pbp.border.left,
margin_box_pos.y + pbp.border.top,
);
let padding_box_size = LogicalSize::new(
(border_box_size.width
- pbp.border.left
- pbp.border.right)
.max(0.0),
(border_box_size.height
- pbp.border.top
- pbp.border.bottom)
.max(0.0),
);
return Ok(LogicalRect::new(padding_box_pos, padding_box_size));
}
current_parent_idx = parent_node.parent;
}
Ok(viewport)
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
use azul_core::dom::{Dom, FormattingContext, IdOrClass};
use super::*;
use crate::solver3::{
geometry::{EdgeSizes, MarginAuto, PackedBoxProps, ResolvedBoxProps},
layout_tree::{LayoutNodeCold, LayoutNodeHot, LayoutNodeWarm},
pos_set, PositionVec, POSITION_UNSET,
};
fn close(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() <= eps
}
fn viewport() -> LogicalRect {
LogicalRect::new(
LogicalPosition::new(0.0, 0.0),
LogicalSize::new(800.0, 600.0),
)
}
fn styled(dom: Dom, css_str: &str) -> StyledDom {
let mut dom = dom;
let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
StyledDom::create(&mut dom, css)
}
fn div_class(class: &str) -> Dom {
Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
}
fn body_class(class: &str) -> Dom {
Dom::create_body().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
}
fn node_by_class(sd: &StyledDom, class: &str) -> NodeId {
let container = sd.node_data.as_container();
for i in 0..sd.node_data.len() {
let id = NodeId::new(i);
let ids_and_classes = container[id].get_ids_and_classes();
let hit = ids_and_classes
.as_ref()
.iter()
.any(|ioc| matches!(ioc, IdOrClass::Class(c) if c.as_str() == class));
if hit {
return id;
}
}
panic!("no node with class {class:?}");
}
fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
EdgeSizes {
top,
right,
bottom,
left,
}
}
fn uniform(v: f32) -> EdgeSizes {
edges(v, v, v, v)
}
fn bp(margin: EdgeSizes, padding: EdgeSizes, border: EdgeSizes) -> PackedBoxProps {
PackedBoxProps::pack(&ResolvedBoxProps {
margin,
padding,
border,
margin_auto: MarginAuto::default(),
})
}
fn bp_auto_margins(margin_auto: MarginAuto) -> PackedBoxProps {
PackedBoxProps::pack(&ResolvedBoxProps {
margin: uniform(0.0),
padding: uniform(0.0),
border: uniform(0.0),
margin_auto,
})
}
fn hot(parent: Option<usize>, dom_node_id: Option<NodeId>) -> LayoutNodeHot {
LayoutNodeHot {
box_props: PackedBoxProps::default(),
dom_node_id,
used_size: None,
formatting_context: FormattingContext::Block {
establishes_new_context: false,
},
parent,
}
}
fn raw_tree(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
let n = nodes.len();
let mut children_arena: Vec<usize> = Vec::new();
let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
for cl in child_lists {
let start = u32::try_from(children_arena.len()).unwrap();
children_arena.extend_from_slice(cl);
children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
}
while children_offsets.len() < n {
children_offsets.push((0, 0));
}
LayoutTree {
nodes,
warm: vec![LayoutNodeWarm::default(); n],
cold: vec![LayoutNodeCold::default(); n],
root: 0,
dom_to_layout: BTreeMap::new(),
children_arena,
children_offsets,
subtree_needs_intrinsic: Vec::new(),
}
}
fn two_level(css: &str) -> (StyledDom, LayoutTree) {
let sd = styled(body_class("root").with_child(div_class("child")), css);
let root = node_by_class(&sd, "root");
let child = node_by_class(&sd, "child");
let tree = raw_tree(
vec![hot(None, Some(root)), hot(Some(0), Some(child))],
&[vec![1], vec![]],
);
(sd, tree)
}
fn three_level(css: &str) -> (StyledDom, LayoutTree) {
let sd = styled(
body_class("root").with_child(div_class("mid").with_child(div_class("child"))),
css,
);
let root = node_by_class(&sd, "root");
let mid = node_by_class(&sd, "mid");
let child = node_by_class(&sd, "child");
let tree = raw_tree(
vec![
hot(None, Some(root)),
hot(Some(0), Some(mid)),
hot(Some(1), Some(child)),
],
&[vec![1], vec![2], vec![]],
);
(sd, tree)
}
fn positions(list: &[(f32, f32)]) -> PositionVec {
list.iter()
.map(|&(x, y)| LogicalPosition::new(x, y))
.collect()
}
#[test]
fn get_position_type_none_dom_id_is_static() {
let (sd, _tree) = two_level("");
assert_eq!(get_position_type(&sd, None), LayoutPosition::Static);
}
#[test]
fn get_position_type_unstyled_node_is_static() {
let (sd, _tree) = two_level("");
let child = node_by_class(&sd, "child");
assert_eq!(get_position_type(&sd, Some(child)), LayoutPosition::Static);
}
#[test]
fn get_position_type_reads_every_keyword() {
let sd = styled(
body_class("root")
.with_child(div_class("st"))
.with_child(div_class("rel"))
.with_child(div_class("abs"))
.with_child(div_class("fix"))
.with_child(div_class("sticky")),
".st { position: static; } .rel { position: relative; } \
.abs { position: absolute; } .fix { position: fixed; } \
.sticky { position: sticky; }",
);
for (class, expected) in [
("st", LayoutPosition::Static),
("rel", LayoutPosition::Relative),
("abs", LayoutPosition::Absolute),
("fix", LayoutPosition::Fixed),
("sticky", LayoutPosition::Sticky),
] {
let id = node_by_class(&sd, class);
assert_eq!(get_position_type(&sd, Some(id)), expected, "class {class}");
}
}
#[test]
fn get_position_type_garbage_value_falls_back_to_static() {
let (sd, _tree) = two_level(".child { position: rubbish-42; }");
let child = node_by_class(&sd, "child");
assert_eq!(get_position_type(&sd, Some(child)), LayoutPosition::Static);
}
#[test]
fn get_position_type_is_pure_and_stable_across_calls() {
let (sd, _tree) = two_level(".child { position: sticky; }");
let child = node_by_class(&sd, "child");
let a = get_position_type(&sd, Some(child));
let b = get_position_type(&sd, Some(child));
assert_eq!(a, b);
assert_eq!(a, LayoutPosition::Sticky);
assert!(a.is_positioned());
}
#[test]
fn resolve_position_offsets_none_dom_id_is_all_none() {
let (sd, _tree) = two_level(".child { top: 10px; }");
let o = resolve_position_offsets(
&sd,
None,
LogicalSize::new(100.0, 100.0),
LogicalSize::new(800.0, 600.0),
);
assert!(o.top.is_none() && o.right.is_none() && o.bottom.is_none() && o.left.is_none());
}
#[test]
fn resolve_position_offsets_unset_insets_are_none_not_zero() {
let (sd, _tree) = two_level("");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(100.0, 100.0),
LogicalSize::new(800.0, 600.0),
);
assert!(o.top.is_none() && o.right.is_none() && o.bottom.is_none() && o.left.is_none());
}
#[test]
fn resolve_position_offsets_zero_px_is_some_zero() {
let (sd, _tree) = two_level(".child { top: 0px; left: 0px; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(0.0, 0.0),
LogicalSize::new(0.0, 0.0),
);
assert_eq!(o.top, Some(0.0));
assert_eq!(o.left, Some(0.0));
assert!(o.right.is_none() && o.bottom.is_none());
}
#[test]
fn resolve_position_offsets_px_values_round_trip() {
let (sd, _tree) =
two_level(".child { top: 11px; right: 22px; bottom: 33px; left: 44px; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(200.0, 100.0),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(11.0));
assert_eq!(o.right, Some(22.0));
assert_eq!(o.bottom, Some(33.0));
assert_eq!(o.left, Some(44.0));
}
#[test]
fn resolve_position_offsets_percent_uses_the_correct_axis() {
let (sd, _tree) =
two_level(".child { top: 50%; bottom: 25%; left: 50%; right: 10%; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(400.0, 200.0),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(100.0), "50% of CB height 200");
assert_eq!(o.bottom, Some(50.0), "25% of CB height 200");
assert_eq!(o.left, Some(200.0), "50% of CB width 400");
assert_eq!(o.right, Some(40.0), "10% of CB width 400");
}
#[test]
fn resolve_position_offsets_percent_of_zero_containing_block_is_zero() {
let (sd, _tree) = two_level(".child { top: 75%; left: 75%; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(0.0, 0.0),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(0.0));
assert_eq!(o.left, Some(0.0));
}
#[test]
fn resolve_position_offsets_negative_values_stay_negative() {
let (sd, _tree) = two_level(".child { top: -40px; left: -25%; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(400.0, 200.0),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(-40.0));
assert_eq!(o.left, Some(-100.0), "-25% of CB width 400");
}
#[test]
fn resolve_position_offsets_em_uses_element_font_size_rem_uses_root() {
let sd = styled(
body_class("root").with_child(div_class("child")),
".root { font-size: 10px; } .child { font-size: 20px; top: 2em; left: 3rem; }",
);
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(400.0, 200.0),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(40.0), "2em of the element's own 20px font");
assert_eq!(o.left, Some(30.0), "3rem of the 10px root font");
}
#[test]
fn resolve_position_offsets_viewport_units_use_the_viewport_not_the_containing_block() {
let (sd, _tree) = two_level(".child { top: 10vh; left: 10vw; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(50.0, 50.0), LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(60.0), "10vh of a 600px viewport");
assert_eq!(o.left, Some(80.0), "10vw of an 800px viewport");
}
#[test]
fn resolve_position_offsets_huge_px_bypasses_the_i16_compact_cache_intact() {
let (sd, _tree) = two_level(".child { top: 100000px; left: -100000px; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(400.0, 200.0),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(100_000.0));
assert_eq!(o.left, Some(-100_000.0));
}
#[test]
fn resolve_position_offsets_around_the_i16_cache_boundary_agree_within_a_tenth_px() {
let (sd, _tree) = two_level(".child { top: 3276.3px; bottom: 3276.4px; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(400.0, 200.0),
LogicalSize::new(800.0, 600.0),
);
let top = o.top.expect("top is set");
let bottom = o.bottom.expect("bottom is set");
assert!(close(top, 3276.3, 0.1), "top was {top}");
assert!(close(bottom, 3276.4, 0.1), "bottom was {bottom}");
}
#[test]
fn resolve_position_offsets_sub_tenth_px_precision_loss_is_bounded() {
let (sd, _tree) = two_level(".child { top: 10.567px; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(400.0, 200.0),
LogicalSize::new(800.0, 600.0),
);
let top = o.top.expect("top is set");
assert!(close(top, 10.567, 0.05), "top was {top}");
}
#[test]
fn resolve_position_offsets_nan_containing_block_yields_nan_not_a_panic() {
let (sd, _tree) = two_level(".child { top: 50%; left: 50%; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(f32::NAN, f32::NAN),
LogicalSize::new(800.0, 600.0),
);
assert!(o.top.expect("top is set").is_nan());
assert!(o.left.expect("left is set").is_nan());
}
#[test]
fn resolve_position_offsets_infinite_containing_block_yields_infinity_not_a_panic() {
let (sd, _tree) = two_level(".child { top: 50%; left: 50%; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(f32::INFINITY, f32::INFINITY),
LogicalSize::new(800.0, 600.0),
);
assert_eq!(o.top, Some(f32::INFINITY));
assert_eq!(o.left, Some(f32::INFINITY));
}
#[test]
fn resolve_position_offsets_at_f32_max_containing_block_does_not_panic() {
let (sd, _tree) = two_level(".child { top: 100%; left: 100%; }");
let child = node_by_class(&sd, "child");
let o = resolve_position_offsets(
&sd,
Some(child),
LogicalSize::new(f32::MAX, f32::MAX),
LogicalSize::new(f32::MAX, f32::MAX),
);
assert_eq!(o.top, Some(f32::MAX));
assert_eq!(o.left, Some(f32::MAX));
}
#[test]
fn find_absolute_cb_rect_root_without_parent_is_the_viewport() {
let (sd, tree) = two_level(".root { position: relative; }");
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 0, &sd, &pos, viewport())
.expect("root resolves to the initial CB");
assert_eq!(got, viewport());
}
#[test]
fn find_absolute_cb_rect_out_of_range_index_is_the_viewport_not_a_panic() {
let (sd, tree) = two_level(".root { position: relative; }");
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 9_999, &sd, &pos, viewport())
.expect("an out-of-range index falls back to the initial CB");
assert_eq!(got, viewport());
}
#[test]
fn find_absolute_cb_rect_dangling_parent_index_is_an_error_not_a_panic() {
let (sd, mut tree) = two_level(".root { position: relative; }");
tree.nodes[1].parent = Some(9_999); let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport());
assert!(matches!(got, Err(LayoutError::InvalidTree)));
}
#[test]
fn find_absolute_cb_rect_static_ancestors_fall_back_to_the_viewport() {
let (sd, mut tree) = three_level("");
tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 100.0));
let pos = positions(&[(0.0, 0.0), (10.0, 10.0), (20.0, 20.0)]);
let got = find_absolute_containing_block_rect(&tree, 2, &sd, &pos, viewport())
.expect("no positioned ancestor → initial CB");
assert_eq!(got, viewport());
}
#[test]
fn find_absolute_cb_rect_is_the_padding_box_of_the_positioned_ancestor() {
let (sd, mut tree) = two_level(".root { position: relative; }");
tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
tree.nodes[0].box_props = bp(uniform(0.0), uniform(5.0), uniform(10.0));
let pos = positions(&[(20.0, 30.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
.expect("relative parent is the CB");
assert_eq!(got.origin, LogicalPosition::new(30.0, 40.0));
assert_eq!(got.size, LogicalSize::new(380.0, 280.0));
}
#[test]
fn find_absolute_cb_rect_accepts_every_positioned_ancestor_kind() {
for keyword in ["relative", "absolute", "fixed", "sticky"] {
let css = format!(".root {{ position: {keyword}; }}");
let (sd, mut tree) = two_level(&css);
tree.nodes[0].used_size = Some(LogicalSize::new(100.0, 100.0));
let pos = positions(&[(5.0, 5.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
.expect("positioned ancestor resolves");
assert_eq!(
got,
LogicalRect::new(
LogicalPosition::new(5.0, 5.0),
LogicalSize::new(100.0, 100.0)
),
"position: {keyword}"
);
}
}
#[test]
fn find_absolute_cb_rect_picks_the_nearest_positioned_ancestor() {
let (sd, mut tree) = three_level(".root { position: relative; } .mid { position: absolute; }");
tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 100.0));
let pos = positions(&[(0.0, 0.0), (50.0, 60.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 2, &sd, &pos, viewport())
.expect("nearest positioned ancestor");
assert_eq!(got.origin, LogicalPosition::new(50.0, 60.0), "mid, not root");
assert_eq!(got.size, LogicalSize::new(200.0, 100.0));
}
#[test]
fn find_absolute_cb_rect_saturating_borders_clamp_the_padding_box_to_zero() {
let (sd, mut tree) = two_level(".root { position: relative; }");
tree.nodes[0].used_size = Some(LogicalSize::new(100.0, 100.0));
tree.nodes[0].box_props = bp(uniform(0.0), uniform(0.0), uniform(1e30));
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
.expect("saturated borders still resolve");
assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
assert!(got.size.width >= 0.0 && got.size.height >= 0.0);
assert!(got.origin.x.is_finite() && got.origin.y.is_finite());
}
#[test]
fn find_absolute_cb_rect_unsized_ancestor_is_a_zero_sized_padding_box() {
let (sd, tree) = two_level(".root { position: relative; }"); let pos = positions(&[(7.0, 9.0), (0.0, 0.0)]);
let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
.expect("an unsized ancestor still resolves");
assert_eq!(got.origin, LogicalPosition::new(7.0, 9.0));
assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
}
#[test]
fn find_absolute_cb_rect_missing_position_entry_defaults_to_the_origin() {
let (sd, mut tree) = two_level(".root { position: relative; }");
tree.nodes[0].used_size = Some(LogicalSize::new(100.0, 100.0));
let pos: PositionVec = Vec::new(); let got = find_absolute_containing_block_rect(&tree, 1, &sd, &pos, viewport())
.expect("an empty position vec still resolves");
assert_eq!(got.origin, LogicalPosition::new(0.0, 0.0));
assert_eq!(got.size, LogicalSize::new(100.0, 100.0));
}
#[test]
fn find_nearest_scrollport_without_a_scroll_ancestor_is_the_viewport() {
let (sd, tree) = two_level("");
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
assert_eq!(
find_nearest_scrollport(&tree, 1, &sd, &pos, viewport()),
viewport()
);
}
#[test]
fn find_nearest_scrollport_out_of_range_index_is_the_viewport_not_a_panic() {
let (sd, tree) = two_level(".root { overflow-y: scroll; }");
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
assert_eq!(
find_nearest_scrollport(&tree, 9_999, &sd, &pos, viewport()),
viewport()
);
}
#[test]
fn find_nearest_scrollport_returns_the_ancestor_content_box() {
for css in [
".root { overflow-x: scroll; }",
".root { overflow-y: scroll; }",
".root { overflow-x: auto; }",
".root { overflow-y: auto; }",
] {
let (sd, mut tree) = two_level(css);
tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 150.0));
tree.nodes[0].box_props = bp(uniform(0.0), uniform(5.0), uniform(10.0));
let pos = positions(&[(20.0, 30.0), (0.0, 0.0)]);
let got = find_nearest_scrollport(&tree, 1, &sd, &pos, viewport());
assert_eq!(got.origin, LogicalPosition::new(35.0, 45.0), "{css}");
assert_eq!(got.size, LogicalSize::new(170.0, 120.0), "{css}");
}
}
#[test]
fn find_nearest_scrollport_ignores_non_scrolling_overflow() {
for css in [
".root { overflow-x: hidden; }",
".root { overflow-y: visible; }",
".root { overflow-x: clip; }",
] {
let (sd, mut tree) = two_level(css);
tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 150.0));
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
assert_eq!(
find_nearest_scrollport(&tree, 1, &sd, &pos, viewport()),
viewport(),
"{css}"
);
}
}
#[test]
fn find_nearest_scrollport_picks_the_nearest_of_two_scroll_ancestors() {
let (sd, mut tree) =
three_level(".root { overflow-y: scroll; } .mid { overflow-y: scroll; }");
tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 100.0));
let pos = positions(&[(0.0, 0.0), (11.0, 12.0), (0.0, 0.0)]);
let got = find_nearest_scrollport(&tree, 2, &sd, &pos, viewport());
assert_eq!(got.origin, LogicalPosition::new(11.0, 12.0), "mid, not root");
assert_eq!(got.size, LogicalSize::new(200.0, 100.0));
}
#[test]
fn find_nearest_scrollport_walks_past_anonymous_boxes() {
let (sd, mut tree) = three_level(".root { overflow-y: scroll; }");
tree.nodes[1].dom_node_id = None; tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
let pos = positions(&[(1.0, 2.0), (0.0, 0.0), (0.0, 0.0)]);
let got = find_nearest_scrollport(&tree, 2, &sd, &pos, viewport());
assert_eq!(got.origin, LogicalPosition::new(1.0, 2.0));
assert_eq!(got.size, LogicalSize::new(400.0, 300.0));
}
#[test]
fn find_nearest_scrollport_clamps_the_content_box_to_zero_when_padding_exceeds_the_box() {
let (sd, mut tree) = two_level(".root { overflow-y: scroll; }");
tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
tree.nodes[0].box_props = bp(uniform(0.0), uniform(1e30), uniform(1e30));
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
let got = find_nearest_scrollport(&tree, 1, &sd, &pos, viewport());
assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
assert!(got.size.width >= 0.0 && got.size.height >= 0.0);
}
#[test]
fn find_nearest_scrollport_unsized_scrollport_is_zero_sized() {
let (sd, tree) = two_level(".root { overflow-y: scroll; }"); let pos: PositionVec = Vec::new();
let got = find_nearest_scrollport(&tree, 1, &sd, &pos, viewport());
assert_eq!(got.origin, LogicalPosition::new(0.0, 0.0));
assert_eq!(got.size, LogicalSize::new(0.0, 0.0));
}
fn scroll_at(parent: (f32, f32), children: (f32, f32)) -> ScrollPosition {
ScrollPosition {
parent_rect: LogicalRect::new(
LogicalPosition::new(parent.0, parent.1),
LogicalSize::new(100.0, 100.0),
),
children_rect: LogicalRect::new(
LogicalPosition::new(children.0, children.1),
LogicalSize::new(100.0, 400.0),
),
}
}
#[test]
fn find_nearest_scroll_offset_empty_map_is_zero() {
let (_sd, tree) = two_level("");
let offsets: BTreeMap<NodeId, ScrollPosition> = BTreeMap::new();
assert_eq!(
find_nearest_scroll_offset(&tree, 1, &offsets),
LogicalPosition::zero()
);
}
#[test]
fn find_nearest_scroll_offset_out_of_range_index_is_zero_not_a_panic() {
let (sd, tree) = two_level("");
let mut offsets = BTreeMap::new();
offsets.insert(node_by_class(&sd, "root"), scroll_at((0.0, 0.0), (0.0, -50.0)));
assert_eq!(
find_nearest_scroll_offset(&tree, 9_999, &offsets),
LogicalPosition::zero()
);
}
#[test]
fn find_nearest_scroll_offset_ignores_the_nodes_own_entry() {
let (sd, tree) = two_level("");
let mut offsets = BTreeMap::new();
offsets.insert(
node_by_class(&sd, "child"),
scroll_at((0.0, 0.0), (0.0, -50.0)),
);
assert_eq!(
find_nearest_scroll_offset(&tree, 1, &offsets),
LogicalPosition::zero()
);
}
#[test]
fn find_nearest_scroll_offset_is_children_origin_minus_parent_origin() {
let (sd, tree) = two_level("");
let mut offsets = BTreeMap::new();
offsets.insert(
node_by_class(&sd, "root"),
scroll_at((10.0, 20.0), (-5.0, -80.0)),
);
assert_eq!(
find_nearest_scroll_offset(&tree, 1, &offsets),
LogicalPosition::new(-15.0, -100.0)
);
}
#[test]
fn find_nearest_scroll_offset_picks_the_nearest_ancestor() {
let (sd, tree) = three_level("");
let mut offsets = BTreeMap::new();
offsets.insert(node_by_class(&sd, "root"), scroll_at((0.0, 0.0), (0.0, -999.0)));
offsets.insert(node_by_class(&sd, "mid"), scroll_at((0.0, 0.0), (0.0, -7.0)));
assert_eq!(
find_nearest_scroll_offset(&tree, 2, &offsets),
LogicalPosition::new(0.0, -7.0),
"mid wins over root"
);
}
#[test]
fn find_nearest_scroll_offset_walks_past_anonymous_ancestors() {
let (sd, mut tree) = three_level("");
tree.nodes[1].dom_node_id = None;
let mut offsets = BTreeMap::new();
offsets.insert(node_by_class(&sd, "root"), scroll_at((0.0, 0.0), (0.0, -30.0)));
assert_eq!(
find_nearest_scroll_offset(&tree, 2, &offsets),
LogicalPosition::new(0.0, -30.0)
);
}
#[test]
fn find_nearest_scroll_offset_at_f32_extremes_stays_deterministic() {
let (sd, tree) = two_level("");
let mut offsets = BTreeMap::new();
offsets.insert(
node_by_class(&sd, "root"),
scroll_at((f32::MAX, f32::MAX), (f32::MIN, f32::MIN)),
);
let got = find_nearest_scroll_offset(&tree, 1, &offsets);
assert!(!got.x.is_nan() && !got.y.is_nan());
assert_eq!(got.x, f32::NEG_INFINITY);
assert_eq!(got.y, f32::NEG_INFINITY);
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
mod with_ctx {
use std::collections::HashMap;
use azul_core::{dom::DomId, selection::TextSelection};
use azul_css::props::basic::FontRef;
use super::*;
use crate::{
font_traits::{FontManager, TextLayoutCache},
solver3::{cache, LayoutContext},
};
struct Env {
styled_dom: StyledDom,
font_manager: FontManager<FontRef>,
text_selections: BTreeMap<DomId, TextSelection>,
counters: HashMap<(usize, String), i32>,
image_cache: azul_core::resources::ImageCache,
debug_messages: Option<Vec<LayoutDebugMessage>>,
}
impl Env {
fn new(styled_dom: StyledDom) -> Self {
Self {
styled_dom,
font_manager: FontManager::new(rust_fontconfig::FcFontCache::default())
.expect("FontManager over an empty font cache"),
text_selections: BTreeMap::new(),
counters: HashMap::new(),
image_cache: azul_core::resources::ImageCache::default(),
debug_messages: None,
}
}
fn ctx(&mut self) -> LayoutContext<'_, FontRef> {
LayoutContext {
scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
styled_dom: &self.styled_dom,
font_manager: &self.font_manager,
text_selections: &self.text_selections,
debug_messages: &mut self.debug_messages,
counters: &mut self.counters,
viewport_size: LogicalSize::new(800.0, 600.0),
fragmentation_context: None,
cursor_is_visible: true,
cursor_locations: Vec::new(),
preedit_text: None,
dirty_text_overrides: BTreeMap::new(),
cache_map: cache::LayoutCacheMap::default(),
image_cache: &self.image_cache,
system_style: None,
get_system_time_fn: azul_core::task::GetSystemTimeCallback {
cb: azul_core::task::get_system_time_libstd,
},
}
}
}
fn abs_fixture(css: &str) -> (Env, LayoutTree, PositionVec) {
let (sd, mut tree) = two_level(css);
tree.nodes[0].used_size = Some(LogicalSize::new(400.0, 300.0));
tree.nodes[0].box_props = bp(uniform(0.0), uniform(5.0), uniform(10.0));
tree.nodes[1].used_size = Some(LogicalSize::new(50.0, 50.0));
let pos = positions(&[(20.0, 30.0), (0.0, 0.0)]);
(Env::new(sd), tree, pos)
}
fn run_oof(env: &mut Env, tree: &mut LayoutTree, pos: &mut PositionVec, vp: LogicalRect) {
let mut text_cache = TextLayoutCache::default();
let mut ctx = env.ctx();
position_out_of_flow_elements(&mut ctx, tree, &mut text_cache, pos, vp);
}
#[test]
fn out_of_flow_top_left_offset_from_the_ancestor_padding_box() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 25px; left: 15px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(45.0, 65.0));
}
#[test]
fn out_of_flow_zero_insets_land_exactly_on_the_padding_box_origin() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } .child { position: absolute; top: 0px; left: 0px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(30.0, 40.0));
}
#[test]
fn out_of_flow_all_auto_keeps_the_static_position() {
let (mut env, mut tree, mut pos) =
abs_fixture(".root { position: relative; } .child { position: absolute; }");
pos_set(&mut pos, 1, LogicalPosition::new(7.0, 9.0));
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(7.0, 9.0));
}
#[test]
fn out_of_flow_fixed_resolves_against_the_viewport_not_the_ancestor() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } .child { position: fixed; top: 25px; left: 15px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(15.0, 25.0));
}
#[test]
fn out_of_flow_over_constrained_ignores_the_end_insets_in_ltr() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 10px; bottom: 10px; left: 10px; \
right: 10px; width: 50px; height: 50px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(40.0, 50.0));
}
#[test]
fn out_of_flow_auto_margins_center_the_box_in_both_axes() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 0px; bottom: 0px; left: 0px; \
right: 0px; width: 100px; height: 100px; }",
);
tree.nodes[1].used_size = Some(LogicalSize::new(100.0, 100.0));
tree.nodes[1].box_props = bp_auto_margins(MarginAuto {
top: true,
bottom: true,
left: true,
right: true,
});
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(170.0, 130.0));
}
#[test]
fn out_of_flow_negative_free_space_with_auto_margins_pins_to_the_start_edge_in_ltr() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; left: 0px; right: 0px; width: 500px; }",
);
tree.nodes[1].used_size = Some(LogicalSize::new(500.0, 50.0));
tree.nodes[1].box_props = bp_auto_margins(MarginAuto {
left: true,
right: true,
top: false,
bottom: false,
});
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1].x, 30.0);
}
#[test]
fn out_of_flow_over_constrained_ignores_the_left_inset_in_rtl() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; direction: rtl; } \
.child { position: absolute; left: 10px; right: 10px; width: 50px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1].x, 350.0);
}
#[test]
fn out_of_flow_auto_height_and_width_stretch_between_the_insets() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 10px; bottom: 20px; left: 30px; right: 40px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(60.0, 50.0));
let used = tree.nodes[1].used_size.expect("size was resolved");
assert_eq!(used, LogicalSize::new(310.0, 250.0));
}
#[test]
fn out_of_flow_insets_larger_than_the_containing_block_clamp_the_size_to_zero() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 500px; bottom: 500px; \
left: 500px; right: 500px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
let used = tree.nodes[1].used_size.expect("size was resolved");
assert_eq!(used, LogicalSize::new(0.0, 0.0), "never negative");
assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
}
#[test]
fn out_of_flow_huge_insets_bypass_the_i16_cache_and_stay_finite() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 3300px; left: 100000px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(100_030.0, 3340.0));
assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
}
#[test]
fn out_of_flow_negative_insets_move_the_box_outside_the_containing_block() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: -100px; left: -200px; }",
);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(-170.0, -60.0));
}
#[test]
fn out_of_flow_nan_viewport_clamps_the_stretch_height_to_zero_and_keeps_the_position_finite()
{
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: fixed; top: 10px; bottom: 20px; }",
);
let nan_vp = LogicalRect::new(
LogicalPosition::new(0.0, 0.0),
LogicalSize::new(f32::NAN, f32::NAN),
);
run_oof(&mut env, &mut tree, &mut pos, nan_vp);
let used = tree.nodes[1].used_size.expect("size was resolved");
assert_eq!(used.height, 0.0);
assert_eq!(pos[1].y, 10.0);
assert!(pos[1].y.is_finite());
}
#[test]
fn out_of_flow_infinite_viewport_keeps_the_position_finite() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: fixed; top: 10px; bottom: 20px; }",
);
let inf_vp = LogicalRect::new(
LogicalPosition::new(0.0, 0.0),
LogicalSize::new(f32::INFINITY, f32::INFINITY),
);
run_oof(&mut env, &mut tree, &mut pos, inf_vp);
assert_eq!(pos[1].y, 10.0);
let used = tree.nodes[1].used_size.expect("size was resolved");
assert!(used.height.is_infinite() && used.height > 0.0);
}
#[test]
fn out_of_flow_every_auto_combination_of_top_height_bottom_is_panic_free() {
for top in ["", "top: 10px;"] {
for bottom in ["", "bottom: 20px;"] {
for height in ["", "height: 30px;"] {
for left in ["", "left: 10px;"] {
for right in ["", "right: 20px;"] {
for width in ["", "width: 30px;"] {
let css = format!(
".root {{ position: relative; }} \
.child {{ position: absolute; {top}{bottom}{height}\
{left}{right}{width} }}"
);
let (mut env, mut tree, mut pos) = abs_fixture(&css);
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert!(
pos[1].x.is_finite() && pos[1].y.is_finite(),
"non-finite position for {css}"
);
}
}
}
}
}
}
}
#[test]
fn out_of_flow_skips_children_of_flex_and_grid_parents() {
for fc in [FormattingContext::Flex, FormattingContext::Grid] {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 25px; left: 15px; }",
);
tree.nodes[0].formatting_context = fc;
pos_set(&mut pos, 1, LogicalPosition::new(3.0, 4.0));
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(3.0, 4.0), "{fc:?}");
}
}
#[test]
fn out_of_flow_leaves_static_and_relative_nodes_alone() {
for keyword in ["static", "relative", "sticky"] {
let css = format!(
".root {{ position: relative; }} \
.child {{ position: {keyword}; top: 25px; left: 15px; }}"
);
let (mut env, mut tree, mut pos) = abs_fixture(&css);
pos_set(&mut pos, 1, LogicalPosition::new(3.0, 4.0));
run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos[1], LogicalPosition::new(3.0, 4.0), "{keyword}");
}
}
#[test]
fn out_of_flow_short_position_vec_grows_instead_of_panicking() {
let (mut env, mut tree, _pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 25px; left: 15px; }",
);
let mut pos: PositionVec = Vec::new(); run_oof(&mut env, &mut tree, &mut pos, viewport());
assert_eq!(pos.len(), 2, "pos_set grew the vec");
assert_eq!(pos[1], LogicalPosition::new(25.0, 35.0));
}
#[test]
fn out_of_flow_unsized_node_is_sized_on_the_fly_without_panicking() {
let (mut env, mut tree, mut pos) = abs_fixture(
".root { position: relative; } \
.child { position: absolute; top: 10px; left: 10px; }",
);
tree.nodes[1].used_size = None; run_oof(&mut env, &mut tree, &mut pos, viewport());
assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
}
fn rel_fixture(css: &str) -> (Env, LayoutTree, PositionVec) {
let (sd, mut tree) = two_level(css);
tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 100.0));
tree.nodes[0].box_props = bp(uniform(0.0), uniform(10.0), uniform(0.0));
tree.nodes[1].used_size = Some(LogicalSize::new(50.0, 20.0));
let pos = positions(&[(0.0, 0.0), (100.0, 100.0)]);
(Env::new(sd), tree, pos)
}
fn run_rel(env: &mut Env, tree: &LayoutTree, pos: &mut PositionVec) {
let mut ctx = env.ctx();
adjust_relative_positions(&mut ctx, tree, pos, viewport());
}
#[test]
fn relative_px_offsets_shift_from_the_static_position() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 10px; left: 5px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(105.0, 110.0));
}
#[test]
fn relative_percentages_resolve_against_the_parent_content_box() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 50%; left: 50%; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(190.0, 140.0));
}
#[test]
fn relative_top_wins_over_bottom() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 10px; bottom: 30px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1].y, 110.0);
}
#[test]
fn relative_bottom_alone_is_the_negation_of_top() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; bottom: 30px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1].y, 70.0);
}
#[test]
fn relative_right_alone_is_the_negation_of_left() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; right: 20px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1].x, 80.0);
}
#[test]
fn relative_left_wins_in_ltr_and_right_wins_in_rtl() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; left: 5px; right: 20px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1].x, 105.0, "ltr: left wins");
let (mut env, tree, mut pos) = rel_fixture(
".root { direction: rtl; } \
.child { position: relative; left: 5px; right: 20px; }",
);
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1].x, 80.0, "rtl: right wins → -20");
}
#[test]
fn relative_zero_offsets_are_a_no_op() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 0px; left: 0px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(100.0, 100.0));
}
#[test]
fn relative_leaves_static_absolute_and_fixed_nodes_untouched() {
for keyword in ["static", "absolute", "fixed"] {
let css =
format!(".child {{ position: {keyword}; top: 10px; left: 5px; }}");
let (mut env, tree, mut pos) = rel_fixture(&css);
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(100.0, 100.0), "{keyword}");
}
}
#[test]
fn relative_also_offsets_sticky_boxes() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 10px; }");
run_rel(&mut env, &tree, &mut pos);
let relative_y = pos[1].y;
let (mut env, tree, mut pos) =
rel_fixture(".child { position: sticky; top: 10px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1].y, relative_y);
}
#[test]
fn relative_is_undefined_for_table_cells_and_captions_so_they_are_skipped() {
for display in ["table-cell", "table-caption", "table-column"] {
let css = format!(
".child {{ position: relative; display: {display}; top: 10px; left: 5px; }}"
);
let (mut env, tree, mut pos) = rel_fixture(&css);
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(100.0, 100.0), "{display}");
}
}
#[test]
fn relative_table_rows_drag_their_whole_subtree() {
let (sd, mut tree) = three_level(
".mid { position: relative; display: table-row; top: 10px; left: 5px; } \
.child { display: table-cell; }",
);
tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 100.0));
tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 50.0));
tree.nodes[2].used_size = Some(LogicalSize::new(100.0, 50.0));
let mut pos = positions(&[(0.0, 0.0), (10.0, 20.0), (10.0, 20.0)]);
let mut env = Env::new(sd);
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(15.0, 30.0), "the row itself");
assert_eq!(pos[2], LogicalPosition::new(15.0, 30.0), "the cell follows");
}
#[test]
fn relative_short_position_vec_is_skipped_not_panicked_on() {
let (mut env, tree, _pos) =
rel_fixture(".child { position: relative; top: 10px; left: 5px; }");
let mut pos: PositionVec = Vec::new();
run_rel(&mut env, &tree, &mut pos);
assert!(pos.is_empty(), "nothing to shift, nothing added");
}
#[test]
fn relative_huge_and_negative_offsets_stay_finite() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 100000px; left: -100000px; }");
run_rel(&mut env, &tree, &mut pos);
assert_eq!(pos[1], LogicalPosition::new(-99_900.0, 100_100.0));
assert!(pos[1].x.is_finite() && pos[1].y.is_finite());
}
#[test]
fn relative_unset_sentinel_position_is_not_silently_shifted_into_a_real_one() {
let (mut env, tree, mut pos) =
rel_fixture(".child { position: relative; top: 10px; left: 5px; }");
pos[1] = POSITION_UNSET;
run_rel(&mut env, &tree, &mut pos);
assert!(pos[1].x < -1e30 && pos[1].y < -1e30);
}
fn sticky_fixture(css: &str) -> (Env, LayoutTree, PositionVec) {
let (sd, mut tree) = two_level(css);
tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 200.0));
tree.nodes[1].used_size = Some(LogicalSize::new(50.0, 20.0));
let pos = positions(&[(0.0, 0.0), (0.0, 0.0)]);
(Env::new(sd), tree, pos)
}
fn run_sticky(
env: &mut Env,
tree: &LayoutTree,
pos: &mut PositionVec,
offsets: &BTreeMap<NodeId, ScrollPosition>,
) {
let mut ctx = env.ctx();
adjust_sticky_positions(&mut ctx, tree, pos, offsets, viewport());
}
#[test]
fn sticky_top_inset_pins_the_box_to_the_scrollport_edge() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1], LogicalPosition::new(0.0, 10.0));
}
#[test]
fn sticky_without_insets_does_not_move() {
let (mut env, tree, mut pos) =
sticky_fixture(".root { overflow-y: scroll; } .child { position: sticky; }");
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0));
}
#[test]
fn sticky_ignores_non_sticky_positions() {
for keyword in ["static", "relative", "absolute", "fixed"] {
let css = format!(
".root {{ overflow-y: scroll; }} \
.child {{ position: {keyword}; top: 10px; }}"
);
let (mut env, tree, mut pos) = sticky_fixture(&css);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0), "{keyword}");
}
}
#[test]
fn sticky_edge_moves_with_the_scroll_offset_of_the_nearest_container() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
);
let root = node_by_class(&env.styled_dom, "root");
let mut offsets = BTreeMap::new();
offsets.insert(root, scroll_at((0.0, 0.0), (0.0, 50.0)));
run_sticky(&mut env, &tree, &mut pos, &offsets);
assert_eq!(pos[1].y, 60.0);
}
#[test]
fn sticky_percentage_inset_resolves_against_the_scrollport() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: 10%; }",
);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1].y, 20.0, "10% of the 200px scrollport");
}
#[test]
fn sticky_bottom_inset_pulls_the_box_back_up_into_the_scrollport() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; bottom: 10px; }",
);
pos_set(&mut pos, 1, LogicalPosition::new(0.0, 250.0));
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1].y, 170.0);
}
#[test]
fn sticky_shift_is_clamped_by_the_containing_block() {
let (sd, mut tree) = three_level(
".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
);
tree.nodes[0].used_size = Some(LogicalSize::new(200.0, 200.0));
tree.nodes[1].used_size = Some(LogicalSize::new(200.0, 25.0)); tree.nodes[2].used_size = Some(LogicalSize::new(50.0, 20.0));
let mut pos = positions(&[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]);
let mut env = Env::new(sd);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[2].y, 5.0);
}
#[test]
fn sticky_huge_inset_clamps_to_the_containing_block_instead_of_flying_away() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: 100000px; }",
);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1].y, 180.0);
assert!(pos[1].y.is_finite());
}
#[test]
fn sticky_negative_inset_is_deterministic_and_finite() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: -50px; }",
);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0));
}
#[test]
fn sticky_without_a_scroll_ancestor_falls_back_to_the_viewport() {
let (mut env, tree, mut pos) =
sticky_fixture(".child { position: sticky; top: 10px; }"); run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1].y, 10.0);
}
#[test]
fn sticky_left_and_right_insets_shift_the_inline_axis() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-x: scroll; } .child { position: sticky; left: 15px; }",
);
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1].x, 15.0);
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-x: scroll; } .child { position: sticky; right: 10px; }",
);
pos_set(&mut pos, 1, LogicalPosition::new(300.0, 0.0));
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert_eq!(pos[1].x, 140.0);
}
#[test]
fn sticky_short_position_vec_is_skipped_not_panicked_on() {
let (mut env, tree, _pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
);
let mut pos: PositionVec = Vec::new();
run_sticky(&mut env, &tree, &mut pos, &BTreeMap::new());
assert!(pos.is_empty());
}
#[test]
fn sticky_nan_scroll_offset_never_panics() {
let (mut env, tree, mut pos) = sticky_fixture(
".root { overflow-y: scroll; } .child { position: sticky; top: 10px; }",
);
let root = node_by_class(&env.styled_dom, "root");
let mut offsets = BTreeMap::new();
offsets.insert(root, scroll_at((f32::NAN, f32::NAN), (f32::NAN, f32::NAN)));
run_sticky(&mut env, &tree, &mut pos, &offsets);
assert_eq!(pos[1], LogicalPosition::new(0.0, 0.0));
}
}
}