pub mod cache;
pub mod calc;
pub mod counters;
pub mod display_list;
pub mod fc;
pub mod geometry;
pub mod getters;
pub mod layout_tree;
pub mod paged_layout;
pub mod pagination;
pub mod positioning;
pub mod scrollbar;
pub mod sizing;
pub mod taffy_bridge;
#[macro_export]
macro_rules! debug_info {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_info_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_warning {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_warning_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_error {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_error_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_log {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_log_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_box_props {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_box_props_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_css_getter {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_css_getter_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_bfc_layout {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_bfc_layout_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_ifc_layout {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_ifc_layout_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_table_layout {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_table_layout_inner(format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! debug_display_type {
($ctx:expr, $($arg:tt)*) => {
if $ctx.debug_messages.is_some() {
$ctx.debug_display_type_inner(format!($($arg)*));
}
};
}
use std::{collections::{BTreeMap, HashMap}, sync::Arc};
use azul_core::{
dom::{DomId, NodeId},
geom::{LogicalPosition, LogicalRect, LogicalSize},
hit_test::{DocumentId, ScrollPosition},
resources::RendererResources,
selection::{TextCursor, TextSelection},
styled_dom::StyledDom,
};
pub(crate) const POSITION_UNSET: LogicalPosition = LogicalPosition { x: f32::MIN, y: f32::MIN };
const MAX_SCROLLBAR_REFLOW_ITERATIONS: usize = 10;
pub type PositionVec = Vec<LogicalPosition>;
#[inline]
#[must_use] pub fn pos_get(positions: &PositionVec, idx: usize) -> Option<LogicalPosition> {
positions.get(idx).copied().filter(|p| p.x != f32::MIN)
}
#[inline]
pub fn pos_set(positions: &mut PositionVec, idx: usize, pos: LogicalPosition) {
if idx >= positions.len() {
positions.resize(idx + 1, POSITION_UNSET);
}
positions[idx] = pos;
}
#[inline]
#[must_use] pub fn pos_contains(positions: &PositionVec, idx: usize) -> bool {
positions.get(idx).is_some_and(|p| p.x != f32::MIN)
}
use azul_css::{
props::property::{CssProperty, CssPropertyCategory},
LayoutDebugMessage, LayoutDebugMessageType,
};
use self::{
display_list::generate_display_list,
geometry::IntrinsicSizes,
getters::get_writing_mode,
layout_tree::{generate_layout_tree, LayoutTree},
sizing::calculate_intrinsic_sizes,
};
#[cfg(feature = "text_layout")]
pub use crate::font_traits::TextLayoutCache;
use crate::{
font_traits::ParsedFontTrait,
solver3::{
cache::LayoutCache,
display_list::DisplayList,
fc::LayoutConstraints,
layout_tree::DirtyFlag,
},
};
#[derive(Debug)]
pub struct LayoutContext<'a, T: ParsedFontTrait> {
pub styled_dom: &'a StyledDom,
#[cfg(feature = "text_layout")]
pub font_manager: &'a crate::font_traits::FontManager<T>,
#[cfg(not(feature = "text_layout"))]
pub font_manager: core::marker::PhantomData<&'a T>,
pub text_selections: &'a BTreeMap<DomId, TextSelection>,
pub debug_messages: &'a mut Option<Vec<LayoutDebugMessage>>,
pub counters: &'a mut HashMap<(usize, String), i32>,
pub viewport_size: LogicalSize,
pub fragmentation_context: Option<&'a mut crate::paged::FragmentationContext>,
pub cursor_is_visible: bool,
pub cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
pub preedit_text: Option<String>,
pub dirty_text_overrides: BTreeMap<(DomId, NodeId), String>,
pub cache_map: cache::LayoutCacheMap,
pub image_cache: &'a azul_core::resources::ImageCache,
pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
pub get_system_time_fn: azul_core::task::GetSystemTimeCallback,
pub scrollbar_style_cache:
core::cell::RefCell<HashMap<NodeId, getters::ComputedScrollbarStyle>>,
}
impl<T: ParsedFontTrait> LayoutContext<'_, T> {
#[inline]
pub fn debug_log_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage {
message: message.into(),
location: "solver3".into(),
message_type: LayoutDebugMessageType::default(),
});
}
}
#[inline]
pub fn debug_info_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::info(message));
}
}
#[inline]
pub fn debug_warning_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::warning(message));
}
}
#[inline]
pub fn debug_error_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::error(message));
}
}
#[inline]
pub fn debug_box_props_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::box_props(message));
}
}
#[inline]
pub fn debug_css_getter_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::css_getter(message));
}
}
#[inline]
pub fn debug_bfc_layout_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::bfc_layout(message));
}
}
#[inline]
pub fn debug_ifc_layout_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::ifc_layout(message));
}
}
#[inline]
pub fn debug_table_layout_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::table_layout(message));
}
}
#[inline]
pub fn debug_display_type_inner(&mut self, message: String) {
if let Some(messages) = self.debug_messages.as_mut() {
messages.push(LayoutDebugMessage::display_type(message));
}
}
}
#[cfg(feature = "text_layout")]
pub static SKIP_DISPLAY_LIST: core::sync::atomic::AtomicBool =
core::sync::atomic::AtomicBool::new(false);
pub fn set_skip_display_list(skip: bool) {
SKIP_DISPLAY_LIST.store(skip, core::sync::atomic::Ordering::Relaxed);
}
#[inline(never)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn layout_document<T: ParsedFontTrait + Sync + 'static>(
cache: &mut LayoutCache,
text_cache: &mut TextLayoutCache,
new_dom: &StyledDom,
viewport: LogicalRect,
font_manager: &crate::font_traits::FontManager<T>,
scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
text_selections: &BTreeMap<DomId, TextSelection>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
renderer_resources: &azul_core::resources::RendererResources,
id_namespace: azul_core::resources::IdNamespace,
dom_id: DomId,
cursor_is_visible: bool,
cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
preedit_text: Option<String>,
image_cache: &azul_core::resources::ImageCache,
system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
get_system_time_fn: azul_core::task::GetSystemTimeCallback,
) -> Result<DisplayList> {
use crate::window::LayoutWindow;
fn collect_anon_children_by_parent(
tree: &LayoutTree,
) -> HashMap<usize, Vec<usize>> {
let mut map: HashMap<usize, Vec<usize>> =
HashMap::new();
for (idx, node) in tree.nodes.iter().enumerate() {
if node.dom_node_id.is_some() {
continue;
}
if let Some(parent) = node.parent {
map.entry(parent).or_default().push(idx);
}
}
map
}
layout_tree::IfcId::reset_counter();
{ let _ = (0xDD00_0001u32); }
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[Layout] layout_document called - viewport: ({:.1}, {:.1}) size ({:.1}x{:.1})",
viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height
)));
msgs.push(LayoutDebugMessage::info(format!(
"[Layout] DOM has {} nodes",
new_dom.node_data.len()
)));
}
let mut counter_values = HashMap::new();
let mut ctx_temp = LayoutContext {
scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
styled_dom: new_dom,
font_manager,
text_selections,
debug_messages,
counters: &mut counter_values,
viewport_size: viewport.size,
fragmentation_context: None,
cursor_is_visible,
cursor_locations: cursor_locations.clone(),
preedit_text: preedit_text.clone(),
dirty_text_overrides: BTreeMap::new(),
cache_map: cache::LayoutCacheMap::default(), image_cache,
system_style: system_style.clone(),
get_system_time_fn,
};
crate::probe::sample_peak_rss("rss:enter_layout_document");
let dom_ptr = std::ptr::from_ref::<StyledDom>(new_dom) as usize;
cache.prev_dom_ptr = dom_ptr;
cache.prev_viewport = viewport;
crate::probe::reset_peak();
let (new_tree_val, mut recon_result) =
cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?;
let mut new_tree = Box::new(new_tree_val);
{ let _ = (0xDD00_0002u32); }
unsafe { crate::az_mark(0x60704_u32, (0x71u32)); }
unsafe { crate::az_mark(0x60740_u32, (new_tree.nodes.len() as u32)); }
crate::probe::sample_peak_rss("rss:after_reconcile");
crate::probe::sample_phase_peak("rss:peak_during_reconcile");
if let Some((cached_hash, cached_viewport, cached_dl)) = &cache.cached_display_list {
let new_root_hash = new_tree
.cold(new_tree.root)
.map(|c| c.subtree_hash);
if new_root_hash == Some(*cached_hash) && *cached_viewport == viewport {
let _p = crate::probe::Probe::span("display_list_cache_hit");
return Ok(cached_dl.clone());
}
}
for &node_idx in &recon_result.intrinsic_dirty {
if let Some(warm) = new_tree.warm_mut(node_idx) {
warm.taffy_cache.clear();
}
}
{
let _p = crate::probe::Probe::span("compute_counters");
cache::compute_counters(new_dom, &new_tree, &mut counter_values);
}
unsafe { crate::az_mark(0x60704_u32, (0x72u32)); }
let mut cache_map = std::mem::take(&mut cache.cache_map);
let probe_cache_remap = Some(crate::probe::Probe::span("cache_map_remap"));
if let Some(old_tree) = cache.tree.as_ref() {
let mut remapped = cache::LayoutCacheMap::default();
remapped.entries.resize_with(new_tree.nodes.len(), Default::default);
for (dom_id, new_indices) in &new_tree.dom_to_layout {
let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
continue;
};
for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
let Some(&old_layout_idx) = old_indices.get(pair_idx) else {
continue;
};
if old_layout_idx >= cache_map.entries.len()
|| new_layout_idx >= remapped.entries.len()
{
continue;
}
remapped.entries[new_layout_idx] =
core::mem::take(&mut cache_map.entries[old_layout_idx]);
}
}
let old_anon_by_parent = collect_anon_children_by_parent(old_tree);
let new_anon_by_parent = collect_anon_children_by_parent(&new_tree);
let mut new_to_old_layout_idx: HashMap<usize, usize> =
HashMap::new();
for (dom_id, new_indices) in &new_tree.dom_to_layout {
let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
continue;
};
for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
if let Some(&old_layout_idx) = old_indices.get(pair_idx) {
new_to_old_layout_idx.insert(new_layout_idx, old_layout_idx);
}
}
}
for (new_parent_idx, new_anon_children) in new_anon_by_parent {
let Some(&old_parent_idx) = new_to_old_layout_idx.get(&new_parent_idx) else {
continue;
};
let Some(old_anon_children) = old_anon_by_parent.get(&old_parent_idx) else {
continue;
};
for (ord, &new_anon_idx) in new_anon_children.iter().enumerate() {
let Some(&old_anon_idx) = old_anon_children.get(ord) else {
continue;
};
if old_anon_idx >= cache_map.entries.len()
|| new_anon_idx >= remapped.entries.len()
{
continue;
}
remapped.entries[new_anon_idx] =
core::mem::take(&mut cache_map.entries[old_anon_idx]);
}
}
cache_map = remapped;
} else {
cache_map.resize_to_tree(new_tree.nodes.len());
}
drop(probe_cache_remap);
crate::probe::sample_peak_rss("rss:after_cache_remap");
for &node_idx in &recon_result.intrinsic_dirty {
cache_map.mark_dirty(node_idx, &new_tree.nodes);
}
for &node_idx in &recon_result.layout_roots {
cache_map.mark_dirty(node_idx, &new_tree.nodes);
}
let mut ctx = LayoutContext {
scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
styled_dom: new_dom,
font_manager,
text_selections,
debug_messages,
counters: &mut counter_values,
viewport_size: viewport.size,
fragmentation_context: None,
cursor_is_visible,
cursor_locations,
preedit_text,
dirty_text_overrides: BTreeMap::new(),
cache_map, image_cache,
system_style,
get_system_time_fn,
};
if recon_result.is_clean() && cache.tree.is_some() {
debug_log!(ctx, "No changes, returning existing display list");
let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
let scroll_ids = if cache.scroll_ids.is_empty() {
use crate::window::LayoutWindow;
let (scroll_ids, scroll_id_to_node_id) =
LayoutWindow::compute_scroll_ids(tree, new_dom);
cache.scroll_ids.clone_from(&scroll_ids);
cache.scroll_id_to_node_id = scroll_id_to_node_id;
scroll_ids
} else {
cache.scroll_ids.clone()
};
if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
return Ok(DisplayList::default());
}
return generate_display_list(
&mut ctx,
tree,
&cache.calculated_positions,
scroll_offsets,
&scroll_ids,
gpu_value_cache,
renderer_resources,
id_namespace,
dom_id,
);
}
{ let _ = (0xDD00_0003u32); }
unsafe { crate::az_mark(0x60704_u32, (0x80u32)); }
cache.tree = Some((*new_tree).clone());
unsafe {
crate::az_mark(0x607C0_u32, (new_tree.nodes.len() as u32));
crate::az_mark(0x607C4_u32, (cache.tree.as_ref().map_or(999, |t| t.nodes.len()) as u32));
}
let mut calculated_positions = cache.calculated_positions.clone();
let mut loop_count = 0;
loop {
loop_count += 1;
if loop_count > MAX_SCROLLBAR_REFLOW_ITERATIONS {
debug_warning!(ctx, "Scrollbar reflow loop hit limit of {} iterations, breaking to avoid infinite loop", MAX_SCROLLBAR_REFLOW_ITERATIONS);
break;
}
calculated_positions = {
let _p = crate::probe::Probe::span("clone_calculated_positions");
cache.calculated_positions.clone()
};
unsafe { crate::az_mark(0x60780_u32, (new_tree.nodes.len() as u32)); }
let mut reflow_needed_for_scrollbars = false;
{
crate::probe::reset_peak();
unsafe { crate::az_mark(0x60784_u32, (new_tree.nodes.len() as u32)); }
let _p = crate::probe::Probe::span("calc_intrinsic_sizes");
unsafe { crate::az_mark(0x60788_u32, (new_tree.nodes.len() as u32)); }
unsafe {
crate::az_mark(0x60748_u32, (new_tree.nodes.len() as u32));
crate::az_mark(0x6074C_u32, (cache.tree.as_ref().map_or(999, |t| t.nodes.len()) as u32));
}
calculate_intrinsic_sizes(
&mut ctx,
&mut new_tree,
text_cache,
&recon_result.intrinsic_dirty,
)?;
}
crate::probe::sample_peak_rss("rss:after_calc_intrinsic");
crate::probe::sample_phase_peak("rss:peak_during_intrinsic");
{ let _ = (0xDD00_0005u32); }
for &root_idx in &recon_result.layout_roots {
let (cb_pos, cb_size) = get_containing_block_for_node(
&new_tree,
new_dom,
root_idx,
&calculated_positions,
viewport,
);
{ let _ = (0xDD00_0053u32); }
let root_node = &new_tree.nodes[root_idx];
let root_bp = root_node.box_props.unpack();
{ let _ = (0xDD00_0054u32); }
let is_root_with_margin = root_node.parent.is_none()
&& (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
let adjusted_cb_pos = if is_root_with_margin {
LogicalPosition::new(
cb_pos.x + root_bp.margin.left,
cb_pos.y + root_bp.margin.top,
)
} else {
cb_pos
};
{ let _ = (0xDD00_0056u32); }
if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
let dom_name = root_node
.dom_node_id
.and_then(|id| new_dom.node_data.as_container().internal.get(id.index())).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
debug_msgs.push(LayoutDebugMessage::new(
LayoutDebugMessageType::PositionCalculation,
format!(
"[LAYOUT ROOT {}] {} - CB pos=({:.2}, {:.2}), adjusted=({:.2}, {:.2}), \
CB size=({:.2}x{:.2}), viewport=({:.2}x{:.2}), margin=({:.2}, {:.2})",
root_idx,
dom_name,
cb_pos.x,
cb_pos.y,
adjusted_cb_pos.x,
adjusted_cb_pos.y,
cb_size.width,
cb_size.height,
viewport.size.width,
viewport.size.height,
root_bp.margin.left,
root_bp.margin.top
),
));
}
crate::probe::hint_purge_allocator();
crate::probe::sample_peak_rss("rss:before_root_layout");
crate::probe::reset_peak();
{ let _ = (0xDD00_0055u32); }
let clr = {
let _p = crate::probe::Probe::span("root_layout_pass");
cache::calculate_layout_for_subtree(
&mut ctx,
&mut new_tree,
text_cache,
root_idx,
adjusted_cb_pos,
cb_size,
&mut calculated_positions,
&mut reflow_needed_for_scrollbars,
&mut cache.float_cache,
cache::ComputeMode::PerformLayout,
)
};
{ let _ = (if clr.is_ok() { 0xDD00_0057u32 } else { 0xDD00_005Eu32 }); }
crate::probe::sample_peak_rss("rss:after_root_layout");
crate::probe::sample_phase_peak("rss:peak_during_root_layout");
if !pos_contains(&calculated_positions, root_idx) {
let root_node = &new_tree.nodes[root_idx];
let root_bp2 = root_node.box_props.unpack();
let root_position = LogicalPosition::new(
cb_pos.x + root_bp2.margin.left,
cb_pos.y + root_bp2.margin.top,
);
if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
let dom_name = root_node
.dom_node_id
.and_then(|id| new_dom.node_data.as_container().internal.get(id.index())).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
debug_msgs.push(LayoutDebugMessage::new(
LayoutDebugMessageType::PositionCalculation,
format!(
"[ROOT POSITION {}] {} - Inserting position=({:.2}, {:.2}) (viewport origin + margin), \
margin=({:.2}, {:.2}, {:.2}, {:.2})",
root_idx,
dom_name,
root_position.x,
root_position.y,
root_bp2.margin.top,
root_bp2.margin.right,
root_bp2.margin.bottom,
root_bp2.margin.left
),
));
}
pos_set(&mut calculated_positions, root_idx, root_position);
}
}
{ let _ = (0xDD00_0006u32); }
{
let _p = crate::probe::Probe::span("reposition_clean_subtrees");
cache::reposition_clean_subtrees(
new_dom,
&new_tree,
&recon_result.layout_roots,
&mut calculated_positions,
);
}
if reflow_needed_for_scrollbars {
debug_log!(ctx,
"Scrollbars changed container size, starting full reflow (loop {})",
loop_count
);
recon_result.layout_roots.clear();
recon_result.layout_roots.insert(new_tree.root);
recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
continue;
}
break;
}
{
let _p = crate::probe::Probe::span("adjust_relative_positions");
positioning::adjust_relative_positions(
&mut ctx,
&new_tree,
&mut calculated_positions,
viewport,
);
}
{
let _p = crate::probe::Probe::span("adjust_sticky_positions");
positioning::adjust_sticky_positions(
&mut ctx,
&new_tree,
&mut calculated_positions,
scroll_offsets,
viewport,
);
}
{
let _p = crate::probe::Probe::span("position_out_of_flow");
positioning::position_out_of_flow_elements(
&mut ctx,
&mut new_tree,
text_cache,
&mut calculated_positions,
viewport,
);
}
let (scroll_ids, scroll_id_to_node_id) = {
let _p = crate::probe::Probe::span("compute_scroll_ids");
LayoutWindow::compute_scroll_ids(&new_tree, new_dom)
};
crate::probe::sample_peak_rss("rss:before_display_list");
crate::probe::reset_peak();
let display_list = if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
DisplayList::default()
} else {
let _p = crate::probe::Probe::span("generate_display_list");
generate_display_list(
&mut ctx,
&new_tree,
&calculated_positions,
scroll_offsets,
&scroll_ids,
gpu_value_cache,
renderer_resources,
id_namespace,
dom_id,
)?
};
crate::probe::sample_phase_peak("rss:peak_during_display_list");
let _p_writeback = crate::probe::Probe::span("cache_writeback");
let cache_map_back = std::mem::take(&mut ctx.cache_map);
let root_subtree_hash = new_tree
.cold(new_tree.root)
.map_or(layout_tree::SubtreeHash(0), |c| c.subtree_hash);
cache.cached_display_list = Some((root_subtree_hash, viewport, display_list.clone()));
cache.tree = Some(*new_tree); cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
cache.viewport = Some(viewport);
cache.scroll_ids = scroll_ids;
cache.scroll_id_to_node_id = scroll_id_to_node_id;
{ let _ = (0xDD00_0004u32 | ((cache.calculated_positions.len() as u32 & 0xfff) << 4)); }
cache.counters = counter_values;
cache.cache_map = cache_map_back;
crate::probe::sample_peak_rss("rss:after_layout_document");
Ok(display_list)
}
pub(super) fn get_containing_block_for_node(
tree: &LayoutTree,
styled_dom: &StyledDom,
node_idx: usize,
calculated_positions: &PositionVec,
viewport: LogicalRect,
) -> (LogicalPosition, LogicalSize) {
if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
if let Some(parent_node) = tree.get(parent_idx) {
let pos = pos_get(calculated_positions, parent_idx)
.unwrap_or(viewport.origin);
let size = parent_node.used_size.unwrap_or_default();
let pbp = parent_node.box_props.unpack();
let content_pos = LogicalPosition::new(
pos.x + pbp.border.left + pbp.padding.left,
pos.y + pbp.border.top + pbp.padding.top,
);
if let Some(dom_id) = parent_node.dom_node_id {
let styled_node_state = &styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| &n.styled_node_state)
.copied()
.unwrap_or_default();
let writing_mode =
get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
let content_size = pbp.inner_size(size, writing_mode);
return (content_pos, content_size);
}
return (content_pos, size);
}
}
(viewport.origin, viewport.size)
}
#[allow(variant_size_differences)] #[derive(Debug)]
#[repr(C, u8)]
pub enum LayoutError {
InvalidTree,
SizingFailed,
PositioningFailed,
DisplayListFailed,
Text(crate::font_traits::LayoutError),
}
impl std::fmt::Display for LayoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidTree => write!(f, "Invalid layout tree"),
Self::SizingFailed => write!(f, "Sizing calculation failed"),
Self::PositioningFailed => write!(f, "Position calculation failed"),
Self::DisplayListFailed => write!(f, "Display list generation failed"),
Self::Text(e) => write!(f, "Text layout error: {e:?}"),
}
}
}
impl From<crate::font_traits::LayoutError> for LayoutError {
fn from(err: crate::font_traits::LayoutError) -> Self {
Self::Text(err)
}
}
impl std::error::Error for LayoutError {}
pub type Result<T> = std::result::Result<T, LayoutError>;
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
use azul_core::dom::{Dom, FormattingContext};
use super::*;
use crate::solver3::{
geometry::{EdgeSizes, PackedBoxProps, ResolvedBoxProps},
layout_tree::{LayoutNodeCold, LayoutNodeHot, LayoutNodeWarm},
};
fn pos(x: f32, y: f32) -> LogicalPosition {
LogicalPosition::new(x, y)
}
fn size(w: f32, h: f32) -> LogicalSize {
LogicalSize::new(w, h)
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect {
origin: pos(x, y),
size: size(w, h),
}
}
fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
EdgeSizes {
top,
right,
bottom,
left,
}
}
fn bp(padding: EdgeSizes, border: EdgeSizes) -> ResolvedBoxProps {
ResolvedBoxProps {
margin: EdgeSizes::default(),
padding,
border,
..ResolvedBoxProps::default()
}
}
fn hot(
parent: Option<usize>,
dom_node_id: Option<NodeId>,
used_size: Option<LogicalSize>,
props: &ResolvedBoxProps,
) -> LayoutNodeHot {
LayoutNodeHot {
box_props: PackedBoxProps::pack(props),
dom_node_id,
used_size,
formatting_context: FormattingContext::default(),
parent,
}
}
fn tree_of(nodes: Vec<LayoutNodeHot>) -> LayoutTree {
let n = nodes.len();
LayoutTree {
nodes,
warm: vec![LayoutNodeWarm::default(); n],
cold: vec![LayoutNodeCold::default(); n],
root: 0,
dom_to_layout: BTreeMap::new(),
children_arena: Vec::new(),
children_offsets: vec![(0, 0); n],
subtree_needs_intrinsic: vec![false; n],
}
}
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
fn body_dom() -> StyledDom {
let mut dom = Dom::create_body();
let (css, _warnings) = azul_css::parser2::new_from_str("");
StyledDom::create(&mut dom, css)
}
#[test]
fn position_unset_sets_both_components_to_f32_min() {
assert_eq!(POSITION_UNSET.x, f32::MIN);
assert_eq!(POSITION_UNSET.y, f32::MIN);
}
#[test]
fn pos_get_on_an_empty_vec_is_none_for_every_index() {
let positions: PositionVec = Vec::new();
for idx in [0usize, 1, 7, 1_000, usize::MAX / 2, usize::MAX] {
assert!(pos_get(&positions, idx).is_none(), "idx {idx}");
assert!(!pos_contains(&positions, idx), "idx {idx}");
}
}
#[test]
fn pos_get_past_the_end_is_none_and_never_panics() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 3, pos(1.0, 2.0));
assert_eq!(positions.len(), 4);
for idx in [4usize, 5, 100, usize::MAX] {
assert!(pos_get(&positions, idx).is_none(), "idx {idx}");
assert!(!pos_contains(&positions, idx), "idx {idx}");
}
}
#[test]
fn pos_get_at_zero_round_trips() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, 0.0));
let got = pos_get(&positions, 0).expect("index 0 was set");
assert_eq!(got.x, 0.0);
assert_eq!(got.y, 0.0);
assert!(pos_contains(&positions, 0));
}
#[test]
fn an_explicitly_written_sentinel_reads_back_as_unset() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, POSITION_UNSET);
assert_eq!(positions.len(), 1);
assert!(pos_get(&positions, 0).is_none());
assert!(!pos_contains(&positions, 0));
}
#[test]
fn a_position_whose_x_is_f32_min_reads_back_as_unset_even_with_a_real_y() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(f32::MIN, 42.0));
assert!(pos_get(&positions, 0).is_none());
assert!(!pos_contains(&positions, 0));
assert_eq!(positions[0].y, 42.0);
}
#[test]
fn negative_f32_max_is_the_same_bit_pattern_as_the_sentinel() {
assert_eq!(f32::MIN, -f32::MAX);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(-f32::MAX, 0.0));
assert!(pos_get(&positions, 0).is_none());
}
#[test]
fn a_position_whose_y_is_f32_min_is_still_reported_as_set() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, f32::MIN));
let got = pos_get(&positions, 0).expect("x is not the sentinel, so it is set");
assert_eq!(got.x, 0.0);
assert_eq!(got.y, f32::MIN);
assert!(pos_contains(&positions, 0));
}
#[test]
fn nan_positions_are_considered_set_and_survive_the_round_trip() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(f32::NAN, f32::NAN));
let got = pos_get(&positions, 0).expect("NaN passes the sentinel filter");
assert!(got.x.is_nan());
assert!(got.y.is_nan());
assert!(pos_contains(&positions, 0));
}
#[test]
fn infinite_and_extreme_positions_round_trip_unchanged() {
let cases = [
pos(f32::INFINITY, f32::NEG_INFINITY),
pos(f32::MAX, -f32::MIN_POSITIVE),
pos(-0.0, 0.0),
pos(-1e30, 1e30),
pos(f32::MIN_POSITIVE, f32::EPSILON),
];
for (idx, case) in cases.iter().enumerate() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, idx, *case);
let got = pos_get(&positions, idx).expect("non-sentinel x is set");
assert_eq!(got.x.to_bits(), case.x.to_bits(), "case {idx} x");
assert_eq!(got.y.to_bits(), case.y.to_bits(), "case {idx} y");
assert!(pos_contains(&positions, idx), "case {idx}");
}
}
#[test]
fn pos_contains_always_agrees_with_pos_get() {
let values = [
pos(0.0, 0.0),
pos(-0.0, -0.0),
POSITION_UNSET,
pos(f32::MIN, 1.0),
pos(1.0, f32::MIN),
pos(f32::NAN, 0.0),
pos(f32::INFINITY, f32::INFINITY),
pos(f32::NEG_INFINITY, 0.0),
pos(f32::MAX, f32::MIN_POSITIVE),
pos(-f32::MAX, 0.0),
];
let mut positions: PositionVec = Vec::new();
for (idx, v) in values.iter().enumerate() {
pos_set(&mut positions, idx, *v);
}
for idx in 0..values.len() + 4 {
assert_eq!(
pos_contains(&positions, idx),
pos_get(&positions, idx).is_some(),
"idx {idx} disagrees"
);
}
}
#[test]
fn pos_set_beyond_the_end_grows_and_fills_the_gap_with_the_sentinel() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 3, pos(10.0, 20.0));
assert_eq!(positions.len(), 4, "grows to exactly idx + 1");
for idx in 0..3 {
assert!(pos_get(&positions, idx).is_none(), "gap idx {idx} must be unset");
assert!(!pos_contains(&positions, idx), "gap idx {idx}");
assert_eq!(positions[idx].x, POSITION_UNSET.x);
assert_eq!(positions[idx].y, POSITION_UNSET.y);
}
let got = pos_get(&positions, 3).expect("idx 3 was set");
assert_eq!(got.x, 10.0);
assert_eq!(got.y, 20.0);
}
#[test]
fn pos_set_inside_the_vec_neither_grows_nor_shrinks_it() {
let mut positions: PositionVec = vec![POSITION_UNSET; 5];
pos_set(&mut positions, 0, pos(1.0, 1.0));
assert_eq!(positions.len(), 5);
pos_set(&mut positions, 4, pos(2.0, 2.0));
assert_eq!(positions.len(), 5);
assert!(pos_contains(&positions, 0));
assert!(pos_contains(&positions, 4));
assert!(!pos_contains(&positions, 2), "untouched slots stay unset");
}
#[test]
fn pos_set_overwrites_an_existing_entry_in_place() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 1, pos(1.0, 1.0));
pos_set(&mut positions, 1, pos(-5.5, -6.5));
assert_eq!(positions.len(), 2);
let got = pos_get(&positions, 1).expect("still set");
assert_eq!(got.x, -5.5);
assert_eq!(got.y, -6.5);
}
#[test]
fn pos_set_can_reset_an_entry_back_to_unset() {
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(3.0, 4.0));
assert!(pos_contains(&positions, 0));
pos_set(&mut positions, 0, POSITION_UNSET);
assert!(!pos_contains(&positions, 0));
assert!(pos_get(&positions, 0).is_none());
}
#[test]
fn repeated_growth_preserves_every_earlier_entry() {
let mut positions: PositionVec = Vec::new();
for idx in (0..64).rev() {
pos_set(&mut positions, idx, pos(idx as f32, -(idx as f32)));
}
assert_eq!(positions.len(), 64);
for idx in 0..64 {
let got = pos_get(&positions, idx).expect("all 64 were written");
assert_eq!(got.x, idx as f32);
assert_eq!(got.y, -(idx as f32));
}
pos_set(&mut positions, 4_095, pos(1.0, 1.0));
assert_eq!(positions.len(), 4_096);
for idx in 0..64 {
assert!(pos_contains(&positions, idx), "idx {idx} lost after regrow");
}
for idx in 64..4_095 {
assert!(!pos_contains(&positions, idx), "new slot {idx} must be unset");
}
assert!(pos_contains(&positions, 4_095));
}
#[test]
fn the_scrollbar_reflow_bound_is_a_usable_positive_limit() {
const _: () = assert!(
MAX_SCROLLBAR_REFLOW_ITERATIONS >= 1 && MAX_SCROLLBAR_REFLOW_ITERATIONS <= 64,
"the scrollbar reflow bound must be a usable positive limit; an absurd bound = a hung frame"
);
}
#[test]
fn a_root_node_gets_the_viewport_as_its_containing_block() {
let dom = body_dom();
let tree = tree_of(vec![hot(None, Some(NodeId::ZERO), Some(size(10.0, 10.0)), &ResolvedBoxProps::default())]);
let viewport = rect(7.0, 9.0, 800.0, 600.0);
let (cb_pos, cb_size) =
get_containing_block_for_node(&tree, &dom, 0, &Vec::new(), viewport);
assert_eq!(cb_pos.x, 7.0);
assert_eq!(cb_pos.y, 9.0);
assert_eq!(cb_size.width, 800.0);
assert_eq!(cb_size.height, 600.0);
}
#[test]
fn an_out_of_range_node_index_falls_back_to_the_viewport() {
let dom = body_dom();
let tree = tree_of(vec![hot(None, None, Some(size(10.0, 10.0)), &ResolvedBoxProps::default())]);
let viewport = rect(0.0, 0.0, 800.0, 600.0);
for idx in [1usize, 99, usize::MAX] {
let (cb_pos, cb_size) =
get_containing_block_for_node(&tree, &dom, idx, &Vec::new(), viewport);
assert_eq!(cb_pos.x, 0.0, "idx {idx}");
assert_eq!(cb_size.width, 800.0, "idx {idx}");
assert_eq!(cb_size.height, 600.0, "idx {idx}");
}
}
#[test]
fn a_dangling_parent_index_falls_back_to_the_viewport() {
let dom = body_dom();
let tree = tree_of(vec![
hot(None, None, Some(size(10.0, 10.0)), &ResolvedBoxProps::default()),
hot(Some(999), None, Some(size(10.0, 10.0)), &ResolvedBoxProps::default()),
]);
let viewport = rect(1.0, 2.0, 300.0, 400.0);
let (cb_pos, cb_size) =
get_containing_block_for_node(&tree, &dom, 1, &Vec::new(), viewport);
assert_eq!(cb_pos.x, 1.0);
assert_eq!(cb_pos.y, 2.0);
assert_eq!(cb_size.width, 300.0);
assert_eq!(cb_size.height, 400.0);
}
#[test]
fn a_dom_backed_parent_shrinks_the_containing_block_by_border_and_padding() {
let dom = body_dom();
let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), edges(5.0, 5.0, 5.0, 5.0));
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(30.0, 40.0));
let (cb_pos, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert!(close(cb_pos.x, 45.0), "x was {}", cb_pos.x);
assert!(close(cb_pos.y, 55.0), "y was {}", cb_pos.y);
assert!(close(cb_size.width, 170.0), "width was {}", cb_size.width);
assert!(close(cb_size.height, 70.0), "height was {}", cb_size.height);
}
#[test]
fn an_anonymous_parent_offsets_the_origin_but_keeps_the_border_box_size() {
let dom = body_dom();
let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), edges(5.0, 5.0, 5.0, 5.0));
let tree = tree_of(vec![
hot(None, None, Some(size(200.0, 100.0)), &parent_props),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, 0.0));
let (cb_pos, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert!(close(cb_pos.x, 15.0), "x was {}", cb_pos.x);
assert!(close(cb_pos.y, 15.0), "y was {}", cb_pos.y);
assert_eq!(cb_size.width, 200.0, "anonymous arm does not subtract padding/border");
assert_eq!(cb_size.height, 100.0);
}
#[test]
fn an_unpositioned_parent_falls_back_to_the_viewport_origin() {
let dom = body_dom();
let parent_props = bp(edges(1.0, 0.0, 0.0, 2.0), edges(3.0, 0.0, 0.0, 4.0));
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let viewport = rect(100.0, 200.0, 800.0, 600.0);
let (cb_pos, _) = get_containing_block_for_node(&tree, &dom, 1, &Vec::new(), viewport);
assert!(close(cb_pos.x, 106.0), "x was {}", cb_pos.x);
assert!(close(cb_pos.y, 204.0), "y was {}", cb_pos.y);
}
#[test]
fn a_parent_with_a_sentinel_position_is_treated_as_unpositioned() {
let dom = body_dom();
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &ResolvedBoxProps::default()),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, POSITION_UNSET);
let (cb_pos, _) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(11.0, 13.0, 800.0, 600.0),
);
assert_eq!(cb_pos.x, 11.0);
assert_eq!(cb_pos.y, 13.0);
}
#[test]
fn a_parent_without_a_used_size_yields_a_zero_sized_containing_block() {
let dom = body_dom();
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), None, &ResolvedBoxProps::default()),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, 0.0));
let (_, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert_eq!(cb_size.width, 0.0);
assert_eq!(cb_size.height, 0.0);
}
#[test]
fn huge_parent_padding_saturates_instead_of_wrapping_the_origin() {
let dom = body_dom();
let parent_props = bp(edges(1e30, 1e30, 1e30, 1e30), edges(1e30, 1e30, 1e30, 1e30));
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, 0.0));
let (cb_pos, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert!(cb_pos.x.is_finite() && cb_pos.y.is_finite());
assert!(cb_pos.x > 0.0, "saturated padding must stay positive, got {}", cb_pos.x);
assert!(cb_pos.x <= 2.0 * 3276.7 + 1.0, "clamped to the i16 ×10 range");
assert_eq!(cb_size.width, 0.0);
assert_eq!(cb_size.height, 0.0);
}
#[test]
fn a_nan_parent_position_propagates_but_does_not_panic_or_corrupt_the_size() {
let dom = body_dom();
let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), EdgeSizes::default());
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), Some(size(200.0, 100.0)), &parent_props),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(f32::NAN, f32::NAN));
let (cb_pos, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert!(cb_pos.x.is_nan() && cb_pos.y.is_nan(), "NaN flows through unchanged");
assert!(close(cb_size.width, 180.0), "width was {}", cb_size.width);
assert!(close(cb_size.height, 80.0), "height was {}", cb_size.height);
}
#[test]
fn a_nan_parent_used_size_floors_the_containing_block_at_zero() {
let dom = body_dom();
let tree = tree_of(vec![
hot(None, Some(NodeId::ZERO), Some(size(f32::NAN, f32::NAN)), &ResolvedBoxProps::default()),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, 0.0));
let (_, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert!(!cb_size.width.is_nan() && !cb_size.height.is_nan());
assert_eq!(cb_size.width, 0.0);
assert_eq!(cb_size.height, 0.0);
}
#[test]
fn an_infinite_parent_used_size_stays_infinite_rather_than_becoming_nan() {
let dom = body_dom();
let parent_props = bp(edges(10.0, 10.0, 10.0, 10.0), edges(5.0, 5.0, 5.0, 5.0));
let tree = tree_of(vec![
hot(
None,
Some(NodeId::ZERO),
Some(size(f32::INFINITY, f32::INFINITY)),
&parent_props,
),
hot(Some(0), None, None, &ResolvedBoxProps::default()),
]);
let mut positions: PositionVec = Vec::new();
pos_set(&mut positions, 0, pos(0.0, 0.0));
let (_, cb_size) = get_containing_block_for_node(
&tree,
&dom,
1,
&positions,
rect(0.0, 0.0, 800.0, 600.0),
);
assert!(cb_size.width.is_infinite() && cb_size.width.is_sign_positive());
assert!(cb_size.height.is_infinite() && cb_size.height.is_sign_positive());
}
#[test]
fn degenerate_viewports_pass_through_the_root_arm_untouched() {
let dom = body_dom();
let tree = tree_of(vec![hot(None, None, None, &ResolvedBoxProps::default())]);
let (p, s) = get_containing_block_for_node(
&tree,
&dom,
0,
&Vec::new(),
rect(0.0, 0.0, 0.0, 0.0),
);
assert_eq!(s.width, 0.0);
assert_eq!(s.height, 0.0);
assert_eq!(p.x, 0.0);
let (_, s) = get_containing_block_for_node(
&tree,
&dom,
0,
&Vec::new(),
rect(0.0, 0.0, -800.0, -600.0),
);
assert_eq!(s.width, -800.0, "negative viewport is not clamped");
let (p, s) = get_containing_block_for_node(
&tree,
&dom,
0,
&Vec::new(),
rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
);
assert!(p.x.is_nan() && s.width.is_nan(), "NaN viewport is not sanitised");
let (_, s) = get_containing_block_for_node(
&tree,
&dom,
0,
&Vec::new(),
rect(0.0, 0.0, f32::MAX, f32::MAX),
);
assert_eq!(s.width, f32::MAX);
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
#[test]
fn every_layout_error_variant_renders_a_distinct_non_empty_message() {
let variants = [
LayoutError::InvalidTree,
LayoutError::SizingFailed,
LayoutError::PositioningFailed,
LayoutError::DisplayListFailed,
LayoutError::Text(crate::font_traits::LayoutError::BidiError("boom".to_string())),
];
let rendered: Vec<String> = variants.iter().map(ToString::to_string).collect();
for msg in &rendered {
assert!(!msg.is_empty(), "empty Display output");
assert!(!msg.trim().is_empty(), "whitespace-only Display output");
}
for i in 0..rendered.len() {
for j in (i + 1)..rendered.len() {
assert_ne!(rendered[i], rendered[j], "variants {i} and {j} render alike");
}
}
}
#[test]
fn layout_error_display_matches_the_documented_wording() {
assert_eq!(LayoutError::InvalidTree.to_string(), "Invalid layout tree");
assert_eq!(LayoutError::SizingFailed.to_string(), "Sizing calculation failed");
assert_eq!(
LayoutError::PositioningFailed.to_string(),
"Position calculation failed"
);
assert_eq!(
LayoutError::DisplayListFailed.to_string(),
"Display list generation failed"
);
}
#[test]
fn layout_error_display_ignores_width_and_precision_specifiers() {
let e = LayoutError::InvalidTree;
assert_eq!(format!("{e:>60}"), "Invalid layout tree");
assert_eq!(format!("{e:.3}"), "Invalid layout tree");
assert_eq!(format!("{e:^5}"), "Invalid layout tree");
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
#[test]
fn the_text_variant_embeds_the_inner_error_and_survives_hostile_payloads() {
let payloads = [
String::new(),
"\u{202e}rtl-override \u{0}nul".to_string(),
"日本語のエラー 🎉".to_string(),
"x".repeat(100_000),
"\"quotes\" and \\backslashes\\".to_string(),
];
for payload in payloads {
let err = LayoutError::Text(crate::font_traits::LayoutError::ShapingError(
payload.clone(),
));
let msg = err.to_string();
assert!(
msg.starts_with("Text layout error: "),
"unexpected prefix for {} byte payload",
payload.len()
);
assert!(msg.len() >= "Text layout error: ".len() + payload.len());
}
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
#[test]
fn the_text_variant_renders_a_default_font_selector_without_panicking() {
let err = LayoutError::Text(crate::font_traits::LayoutError::FontNotFound(
crate::font_traits::FontSelector::default(),
));
let msg = err.to_string();
assert!(msg.starts_with("Text layout error: "));
assert!(msg.contains("serif"), "the default family should show up: {msg}");
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
#[test]
fn from_text_layout_error_wraps_into_the_text_variant() {
let inner = crate::font_traits::LayoutError::InvalidText("bad".to_string());
let wrapped: LayoutError = inner.into();
assert!(matches!(wrapped, LayoutError::Text(_)));
assert!(wrapped.to_string().contains("bad"));
fn propagates() -> Result<()> {
let failed: std::result::Result<(), crate::font_traits::LayoutError> = Err(
crate::font_traits::LayoutError::HyphenationError("nope".to_string()),
);
failed?;
Ok(())
}
assert!(matches!(propagates(), Err(LayoutError::Text(_))));
}
#[test]
fn layout_error_is_a_std_error_without_a_source() {
use std::error::Error;
let e = LayoutError::SizingFailed;
assert!(e.source().is_none());
assert!(!format!("{e:?}").is_empty());
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
mod debug_sinks {
use std::collections::{BTreeMap, HashMap};
use azul_core::{dom::DomId, selection::TextSelection, styled_dom::StyledDom};
use azul_css::{props::basic::FontRef, LayoutDebugMessage, LayoutDebugMessageType};
use super::{body_dom, size};
use crate::{
font_traits::FontManager,
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(debug_messages: Option<Vec<LayoutDebugMessage>>) -> Self {
Self {
styled_dom: body_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,
}
}
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: size(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,
},
}
}
}
#[test]
fn each_debug_sink_appends_exactly_one_message_of_its_own_type() {
let mut env = Env::new(Some(Vec::new()));
{
let mut ctx = env.ctx();
ctx.debug_log_inner("log".to_string());
ctx.debug_info_inner("info".to_string());
ctx.debug_warning_inner("warning".to_string());
ctx.debug_error_inner("error".to_string());
ctx.debug_box_props_inner("box_props".to_string());
ctx.debug_css_getter_inner("css_getter".to_string());
ctx.debug_bfc_layout_inner("bfc".to_string());
ctx.debug_ifc_layout_inner("ifc".to_string());
ctx.debug_table_layout_inner("table".to_string());
ctx.debug_display_type_inner("display".to_string());
}
let msgs = env.debug_messages.expect("Some(vec) was passed in");
let expected = [
("log", LayoutDebugMessageType::Info),
("info", LayoutDebugMessageType::Info),
("warning", LayoutDebugMessageType::Warning),
("error", LayoutDebugMessageType::Error),
("box_props", LayoutDebugMessageType::BoxProps),
("css_getter", LayoutDebugMessageType::CssGetter),
("bfc", LayoutDebugMessageType::BfcLayout),
("ifc", LayoutDebugMessageType::IfcLayout),
("table", LayoutDebugMessageType::TableLayout),
("display", LayoutDebugMessageType::DisplayType),
];
assert_eq!(msgs.len(), expected.len(), "one message per call, in order");
for (msg, (text, ty)) in msgs.iter().zip(expected) {
assert_eq!(msg.message.as_str(), text);
assert_eq!(msg.message_type, ty);
assert!(!msg.location.as_str().is_empty(), "location must be recorded");
}
}
#[test]
fn debug_log_inner_tags_the_message_with_the_solver3_location() {
let mut env = Env::new(Some(Vec::new()));
{
let mut ctx = env.ctx();
ctx.debug_log_inner("hello".to_string());
ctx.debug_info_inner("hello".to_string());
}
let msgs = env.debug_messages.expect("Some(vec)");
assert_eq!(msgs[0].location.as_str(), "solver3");
assert!(
msgs[1].location.as_str().contains(".rs:"),
"track_caller location, got {:?}",
msgs[1].location.as_str()
);
}
#[test]
fn the_debug_sinks_are_no_ops_when_debug_messages_is_none() {
let mut env = Env::new(None);
{
let mut ctx = env.ctx();
ctx.debug_log_inner("log".to_string());
ctx.debug_error_inner("error".to_string());
ctx.debug_table_layout_inner("table".to_string());
}
assert!(env.debug_messages.is_none(), "must not materialise a Vec");
}
#[test]
fn debug_messages_preserve_hostile_payloads_byte_for_byte() {
let payloads = [
String::new(),
"\u{0}\u{7}\u{1b}[31m".to_string(),
"日本語 🎉 \u{202e}reversed".to_string(),
"line\nbreak\ttab\r\n".to_string(),
"{}{{}} {:?} %s %n".to_string(), "x".repeat(200_000),
];
let mut env = Env::new(Some(Vec::new()));
{
let mut ctx = env.ctx();
for p in &payloads {
ctx.debug_info_inner(p.clone());
}
}
let msgs = env.debug_messages.expect("Some(vec)");
assert_eq!(msgs.len(), payloads.len());
for (msg, payload) in msgs.iter().zip(&payloads) {
assert_eq!(msg.message.as_str(), payload.as_str());
}
}
#[test]
fn the_debug_macros_push_one_message_each_when_capturing() {
let mut env = Env::new(Some(Vec::new()));
{
let mut ctx = env.ctx();
debug_log!(ctx, "log {}", 1);
debug_info!(ctx, "info {}", 2);
debug_warning!(ctx, "warning {}", 3);
debug_error!(ctx, "error {}", 4);
debug_box_props!(ctx, "box_props {}", 5);
debug_css_getter!(ctx, "css_getter {}", 6);
debug_bfc_layout!(ctx, "bfc {}", 7);
debug_ifc_layout!(ctx, "ifc {}", 8);
debug_table_layout!(ctx, "table {}", 9);
debug_display_type!(ctx, "display {}", 10);
}
let msgs = env.debug_messages.expect("Some(vec)");
assert_eq!(msgs.len(), 10);
assert_eq!(msgs[0].message.as_str(), "log 1");
assert_eq!(msgs[9].message.as_str(), "display 10");
}
#[test]
fn the_debug_macros_do_not_evaluate_their_arguments_when_not_capturing() {
let evaluations = core::cell::Cell::new(0u32);
let bump = |c: &core::cell::Cell<u32>| {
c.set(c.get() + 1);
c.get()
};
let mut env = Env::new(None);
{
let mut ctx = env.ctx();
debug_log!(ctx, "{}", bump(&evaluations));
debug_info!(ctx, "{}", bump(&evaluations));
debug_warning!(ctx, "{}", bump(&evaluations));
debug_error!(ctx, "{}", bump(&evaluations));
debug_box_props!(ctx, "{}", bump(&evaluations));
debug_css_getter!(ctx, "{}", bump(&evaluations));
debug_bfc_layout!(ctx, "{}", bump(&evaluations));
debug_ifc_layout!(ctx, "{}", bump(&evaluations));
debug_table_layout!(ctx, "{}", bump(&evaluations));
debug_display_type!(ctx, "{}", bump(&evaluations));
}
assert_eq!(evaluations.get(), 0, "format args must stay unevaluated");
let mut env = Env::new(Some(Vec::new()));
{
let mut ctx = env.ctx();
debug_log!(ctx, "{}", bump(&evaluations));
}
assert_eq!(evaluations.get(), 1);
assert_eq!(env.debug_messages.as_ref().map(Vec::len), Some(1));
}
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
#[test]
fn set_skip_display_list_round_trips_and_is_idempotent() {
use core::sync::atomic::Ordering;
let previous = SKIP_DISPLAY_LIST.load(Ordering::Relaxed);
set_skip_display_list(true);
assert!(SKIP_DISPLAY_LIST.load(Ordering::Relaxed));
set_skip_display_list(true);
assert!(SKIP_DISPLAY_LIST.load(Ordering::Relaxed), "double-set is idempotent");
set_skip_display_list(false);
assert!(!SKIP_DISPLAY_LIST.load(Ordering::Relaxed));
set_skip_display_list(previous);
assert_eq!(SKIP_DISPLAY_LIST.load(Ordering::Relaxed), previous);
}
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
mod document {
use std::collections::BTreeMap;
use azul_core::{
dom::{Dom, DomId},
geom::{LogicalPosition, LogicalRect, LogicalSize},
resources::RendererResources,
styled_dom::StyledDom,
};
use azul_css::props::basic::FontRef;
use crate::{
font_traits::{FontManager, TextLayoutCache},
solver3::{cache::LayoutCache, display_list::DisplayList, layout_document, Result},
};
fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect {
origin: LogicalPosition::new(x, y),
size: LogicalSize::new(w, h),
}
}
fn simple_dom() -> StyledDom {
let mut dom = Dom::create_body().with_child(Dom::create_div());
let (css, _warnings) =
azul_css::parser2::new_from_str("div { width: 50px; height: 20px; }");
StyledDom::create(&mut dom, css)
}
fn run(cache: &mut LayoutCache, dom: &StyledDom, viewport: LogicalRect) -> Result<DisplayList> {
let mut text_cache = TextLayoutCache::new();
let font_manager: FontManager<FontRef> =
FontManager::new(rust_fontconfig::FcFontCache::default())
.expect("FontManager over an empty font cache");
let renderer_resources = RendererResources::default();
let image_cache = azul_core::resources::ImageCache::default();
let mut debug_messages = None;
layout_document(
cache,
&mut text_cache,
dom,
viewport,
&font_manager,
&BTreeMap::new(),
&BTreeMap::new(),
&mut debug_messages,
None,
&renderer_resources,
azul_core::resources::IdNamespace(0),
DomId::ROOT_ID,
false,
Vec::new(),
None,
&image_cache,
None,
azul_core::task::GetSystemTimeCallback {
cb: azul_core::task::get_system_time_libstd,
},
)
}
#[test]
fn a_zero_sized_viewport_lays_out_without_panicking() {
let dom = simple_dom();
let mut cache = LayoutCache::default();
if run(&mut cache, &dom, rect(0.0, 0.0, 0.0, 0.0)).is_ok() {
for p in &cache.calculated_positions {
assert!(!p.x.is_nan() && !p.y.is_nan(), "NaN position from a 0×0 viewport");
}
}
}
#[test]
fn degenerate_viewports_never_panic_or_hang() {
let viewports = [
rect(0.0, 0.0, f32::NAN, f32::NAN),
rect(f32::NAN, f32::NAN, 800.0, 600.0),
rect(0.0, 0.0, -800.0, -600.0),
rect(0.0, 0.0, f32::MAX, f32::MAX),
rect(0.0, 0.0, f32::INFINITY, f32::INFINITY),
rect(-1e30, -1e30, 1.0, 1.0),
rect(0.0, 0.0, f32::MIN_POSITIVE, f32::MIN_POSITIVE),
];
for viewport in viewports {
let dom = simple_dom();
let mut cache = LayoutCache::default();
let _ = run(&mut cache, &dom, viewport);
}
}
#[test]
fn laying_out_the_same_dom_twice_is_stable_and_populates_the_cache() {
let dom = simple_dom();
let mut cache = LayoutCache::default();
let viewport = rect(0.0, 0.0, 800.0, 600.0);
let first = run(&mut cache, &dom, viewport);
if first.is_err() {
return;
}
assert!(cache.cached_display_list.is_some(), "cold pass must seed the DL cache");
assert_eq!(cache.viewport, Some(viewport));
let positions_after_first = cache.calculated_positions.clone();
let second = run(&mut cache, &dom, viewport);
assert!(second.is_ok(), "a warm relayout of an unchanged DOM must succeed");
assert_eq!(
cache.calculated_positions.len(),
positions_after_first.len(),
"warm pass changed the node count"
);
for (warm, cold) in cache.calculated_positions.iter().zip(&positions_after_first) {
assert_eq!(warm.x.to_bits(), cold.x.to_bits(), "warm pass moved a node");
assert_eq!(warm.y.to_bits(), cold.y.to_bits(), "warm pass moved a node");
}
}
}
}