get_scrollbar_gutter_property, get_scrollbar_info_from_layout, get_scrollbar_style, get_selection_style,
get_style_border_radius, get_visibility, get_z_index, is_forced_page_break, BorderInfo, CaretStyle,
writeln!(json, " \"border_radius\": {{ \"tl\": {:.1}, \"tr\": {:.1}, \"bl\": {:.1}, \"br\": {:.1} }},",
writeln!(json, " \"clip_bounds\": {{ \"x\": {:.1}, \"y\": {:.1}, \"w\": {:.1}, \"h\": {:.1} }},",
Self::Text { glyphs: g2, font_hash: fh2, font_size_px: fs2, color: c2, clip_rect: cr2, .. }) => {
Self::Strikethrough { bounds: b2, color: c2, thickness: t2 }) => b1 == b2 && c1 == c2 && t1 == t2,
pub fn push_rect(&mut self, bounds: LogicalRect, color: ColorU, border_radius: BorderRadius) {
pub fn push_image_mask_clip(&mut self, bounds: LogicalRect, mask_image: ImageRef, mask_rect: LogicalRect) {
pub fn push_image(&mut self, bounds: LogicalRect, image: ImageRef, border_radius: BorderRadius) {
// +spec:stacking-contexts:33d435 - CSS 2.2 painting order: build stacking context tree then traverse in z-order
// +spec:stacking-contexts:887766 - CSS2 §9.9 stacking contexts, z-index layering, and painting order
// +spec:display-property:9a419c - root element always forms a stacking context (it's the tree root)
// +spec:stacking-contexts:9e85a3 - Stacking context tree: hierarchical, nested, atomic painting order
fn get_styled_node_state(&self, dom_id: NodeId) -> azul_core::styled_dom::StyledNodeState {
let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
let style = get_selection_style(self.ctx.styled_dom, Some(dom_id), self.ctx.system_style.as_ref());
let is_contenteditable = super::getters::is_node_contenteditable_inherited(self.ctx.styled_dom, dom_id);
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let is_selectable = super::getters::is_text_selectable(self.ctx.styled_dom, dom_id, node_state);
let Some(layout) = self.positioned_tree.tree.get_inline_layout_for_node(node_index) else {
*cd == self.ctx.styled_dom.dom_id && (*cn == dom_id || self.positioned_tree.tree.children(node_index).iter().any(|&child_idx| {
for (i, (cursor_dom_id, cursor_node_id, cursor)) in self.ctx.cursor_locations.iter().enumerate() {
// +spec:writing-modes:a86a28 - preorder depth-first traversal of the rendering tree in logical order
// +spec:box-model:de94ab - stacking context painting order (negative z, in-flow, z=0, positive z)
// +spec:display-property:337069 - CSS 2.2 E.2 painting order: stacking contexts sorted by z-index, in-flow children in tree order
// +spec:display-property:7b0a87 - CSS 2.2 E.2 painting order: negative z-index, in-flow, z-index 0/auto, positive z-index
// +spec:stacking-contexts:3ded3a - CSS 2.2 Appendix E painting order: definitions and tree order traversal
// +spec:stacking-contexts:973368 - CSS 2.2 Appendix E.2 painting order: bg/border, negative z, in-flow, zero z, positive z
// +spec:stacking-contexts:464bb7 - CSS 2.2 §9.9.1 painting order: negative z-index, in-flow, z-index 0, positive z-index (recursive)
// +spec:display-property:39e879 - CSS 2.2 E.2 painting order for block-level and inline-level elements
// +spec:display-property:de4c66 - CSS 2.2 E.2 stacking context paint order (canvas bg, negative z, in-flow, floats, inline, positive z)
// +spec:overflow:6e48b4 - CSS 2.2 Appendix E painting order: bg/border, negative z-index, in-flow, floats, z-index 0/auto, positive z-index
// +spec:stacking-contexts:55ca96 - CSS 2.2 E.2 painting order: backgrounds, negative z-index, in-flow, z-index 0/auto, positive z-index
.map(|dom_id| get_position_type(self.ctx.styled_dom, Some(dom_id)) == LayoutPosition::Fixed)
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
// +spec:box-model:84b238 - CSS 2.2 E.2 painting order: bg/border, negative z, in-flow, z=0, positive z
// +spec:overflow:40052b - backgrounds paint at border-box, scrollbars overlay on top (scrollbar-extended background positioning area)
// +spec:display-contents:434de8 - E.2 painting order: negative z-index, in-flow, z-index 0/auto, positive z-index
// +spec:stacking-contexts:9a4eb3 - z-index:auto/0 positioned descendants painted in tree order
// +spec:stacking-contexts:198fa4 - positive z-index stacking contexts painted in z-index order then tree order
// +spec:display-property:86a3de - inline-level boxes painted in document order; z-index does not apply
// +spec:floats:b8c494 - E.2 painting order: non-positioned floats painted after block-level descendants, in tree order
self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
// +spec:positioning:1bcbb5 - floats rendered in front of non-positioned in-flow blocks, but behind in-flow inlines
self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
self.paint_in_flow_descendants(builder, child_index, self.positioned_tree.tree.children(child_index))?;
// +spec:overflow:531bd2 - ancestor clips accumulate via push_clip/pop_clip stack (cumulative intersection)
// +spec:overflow:8098ec - overflow clipping/scrolling; abs-pos elements with containing block outside scroller are not scrolled
// +spec:overflow:6890f2 - text-overflow: clip inline content at end line box edge when overflow != visible
// +spec:overflow:77d7ce - clipping region defines visible portion of border box; default is not clipped
// +spec:overflow:449d69 - when one axis is clip and the other is visible, clipping region is not rounded
// +spec:overflow:449d69 - when one axis is clip and the other is visible, clipping region is not rounded
let ox_clip = overflow_x.is_clipped() && !overflow_x.is_scroll() && !overflow_x.is_auto_overflow();
let oy_clip = overflow_y.is_clipped() && !overflow_y.is_scroll() && !overflow_y.is_auto_overflow();
// +spec:overflow:9207bc - clip rect computed from border-box edges (analogous to CSS 2.2 clip: rect() offsets)
// +spec:overflow:917dae - scrollable overflow rect is a rectangle in box's own coordinate system
fn pop_node_clips(&self, builder: &mut DisplayListBuilder, node: &LayoutNodeHot) -> Result<()> {
// Skip inline and inline-block elements ONLY if they participate in an IFC (Inline Formatting Context).
// In Flex or Grid containers, inline-block elements are treated as flex/grid items and must be painted here.
// Also check parent_formatting_context field which stores parent's FC during tree construction.
.and_then(|w| w.parent_formatting_context.as_ref().map(|fc| matches!(fc, FormattingContext::Flex | FormattingContext::Grid)))
"[paint_node] node {} has display={:?}, parent_formatting_context={:?}, parent_is_flex_or_grid={}",
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
// +spec:overflow:bb4308 - box shadows are ink overflow: painted outside border box, not affecting layout
// +spec:box-model:124815 - Table layer background painting order (6 layers: table, col-group, col, row-group, row, cell)
// +spec:table-layout:7370dc - Table layers and transparency: 6-layer background painting order
// +spec:table-layout:7a5909 - table layers: 6-layer background paint order (table/colgroup/col/rowgroup/row/cell)
/// Emits drawing commands for the foreground content, including hit-test areas and scrollbars.
let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
self.paint_inline_content(builder, content_box_rect, viewport_clip_rect, inline_layout, node_index)?;
fn paint_scrollbars(&self, builder: &mut DisplayListBuilder, node_index: usize) -> Result<()> {
// For VirtualView nodes, the virtual_scroll_size (propagated through ScrollPosition.children_rect)
let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && v_geom.button_size > 0.0 {
let (button_decrement_bounds, button_increment_bounds) = if scrollbar_style.show_scroll_buttons && h_geom.button_size > 0.0 {
// NOTE: Text decorations (underline, strikethrough, overline) are handled in push_text_layout_to_display_list
// +spec:overflow:7807b1 - text-overflow ellipsis side depends on direction (RTL clips left, LTR clips right); not yet implemented
// +spec:inline-block:a60a89 - inline-block painted atomically as pseudo-stacking-context per E.2
let margins = if let Some(indices) = self.positioned_tree.tree.dom_to_layout.get(&node_id) {
// +spec:overflow:47b791 - z-index applies to positioned boxes; z-index:auto does not establish stacking context
// +spec:positioning:8c6efd - Stacking contexts: positioned elements with z-index != auto establish new stacking context
// +spec:positioning:b84cfa - z-index stacking context creation: integer z-index on positioned elements creates SC; auto on fixed/root creates SC
// +spec:positioning:d06368 - relative/absolute with z-index:auto do not form stacking context but are painted as if they did
let z_auto = crate::solver3::getters::is_z_index_auto(self.ctx.styled_dom, Some(dom_id));
/// Expands `clip_rect` outward by the `overflow-clip-margin` value on axes that use `overflow: clip`.
fn get_scroll_content_size(node: &LayoutNodeHot, warm: Option<&LayoutNodeWarm>) -> LogicalSize {
} => clip_selection_rect_item(bounds.into_inner(), *border_radius, *color, page_top, page_bottom),
} => clip_virtual_view_item(*child_dom_id, bounds.into_inner(), clip_rect.into_inner(), page_top, page_bottom),
clipped_info.track_bounds = offset_rect_y(clipped_info.track_bounds.into_inner(), y_offset).into();
clipped_info.thumb_bounds = offset_rect_y(clipped_info.thumb_bounds.into_inner(), y_offset).into();
if let Some(unified_layout) = layout.downcast_ref::<crate::text3::cache::UnifiedLayout>() {
clip_rect_bounds(bounds, page_top, page_bottom).map(|clipped| DisplayListItem::HitTestArea {
fn clip_rect_bounds(bounds: LogicalRect, page_top: f32, page_bottom: f32) -> Option<LogicalRect> {
/// page breaks at positions specified by CSS `break-before: always` and `break-after: always`.
offset_info.track_bounds = offset_rect_y(offset_info.track_bounds.into_inner(), y_offset).into();
offset_info.thumb_bounds = offset_rect_y(offset_info.thumb_bounds.into_inner(), y_offset).into();
// +spec:overflow:f175b9 - bidi ellipsis: characters visually at the end edge of the line are hidden for ellipsis
/// Rasterize an `SvgMultiPolygon` clip path into an R8 image mask at the given paint rect size.