Add iframe support: srcdoc/src content fetching, independent pipeline rendering, style isolation, dimension attributes, and UA stylesheet defaults. Includes code review fixes for recursion guard, dimension capping, alpha compositing, spec-compliant u32 parsing, base URL correctness, font loading, and golden test infrastructure that exercises the full iframe rendering pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1456 lines
60 KiB
Rust
1456 lines
60 KiB
Rust
use layout::{BoxType, InlineFragment, LayoutBox, LayoutTree};
|
|
use shared::{BorderStyle, CornerRadii, EdgeColors, EdgeSizes, EdgeStyles, Rect};
|
|
use style::{BorderCollapse, EmptyCells, Float, ListStylePosition, Overflow, Position, Visibility};
|
|
|
|
use crate::{DisplayItem, DisplayList};
|
|
|
|
// =============================================================================
|
|
// Display List Builder
|
|
// =============================================================================
|
|
|
|
/// A positioned or stacking-context descendant collected from the subtree
|
|
/// for rendering at the correct CSS paint-order step.
|
|
struct StackingParticipant<'a> {
|
|
layout_box: &'a LayoutBox,
|
|
/// Accumulated offset of this element's parent in the tree.
|
|
parent_offset: CumulativeOffset,
|
|
/// Clip rects from intermediate overflow:hidden ancestors between
|
|
/// the owning stacking context and this element.
|
|
ancestor_clips: Vec<Rect>,
|
|
}
|
|
|
|
/// Collected stacking context descendants bucketed by CSS 2.1 Appendix E (§E.2) steps.
|
|
struct StackingBuckets<'a> {
|
|
/// Step 2: child stacking contexts with negative z-index
|
|
negative: Vec<StackingParticipant<'a>>,
|
|
/// Step 6: child stacking contexts with z-index:0
|
|
zero: Vec<StackingParticipant<'a>>,
|
|
/// Step 7: child stacking contexts with positive z-index (>0)
|
|
positive: Vec<StackingParticipant<'a>>,
|
|
}
|
|
|
|
/// Cumulative offset for position: relative elements.
|
|
/// This offset is accumulated as we traverse the tree, so children
|
|
/// of a relatively positioned element also get the offset applied.
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub(crate) struct CumulativeOffset {
|
|
pub(crate) x: f32,
|
|
pub(crate) y: f32,
|
|
}
|
|
|
|
impl CumulativeOffset {
|
|
pub(crate) fn add(&self, x: f32, y: f32) -> Self {
|
|
Self {
|
|
x: self.x + x,
|
|
y: self.y + y,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn apply(&self, rect: &Rect) -> Rect {
|
|
if self.x == 0.0 && self.y == 0.0 {
|
|
*rect
|
|
} else {
|
|
Rect::new(
|
|
rect.x() + self.x,
|
|
rect.y() + self.y,
|
|
rect.width(),
|
|
rect.height(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Controls which parts of a box are rendered during CSS 2.1 Appendix E paint ordering.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum RenderPhase {
|
|
/// Render backgrounds, borders, box-shadows, images, markers only (step 3)
|
|
BackgroundsOnly,
|
|
/// Render inline content (IFC fragments, text) only (step 5)
|
|
InlineContentOnly,
|
|
/// Render everything — used for floats (step 4) and other full-render contexts
|
|
Full,
|
|
}
|
|
|
|
pub struct DisplayListBuilder {
|
|
list: DisplayList,
|
|
pub(crate) scroll_offset_y: f32,
|
|
viewport_width: f32,
|
|
pub(crate) viewport_height: f32,
|
|
}
|
|
|
|
impl Default for DisplayListBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl DisplayListBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
list: DisplayList::new(),
|
|
scroll_offset_y: 0.0,
|
|
viewport_width: 0.0,
|
|
viewport_height: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn with_scroll_offset(scroll_y: f32) -> Self {
|
|
Self {
|
|
list: DisplayList::new(),
|
|
scroll_offset_y: scroll_y,
|
|
viewport_width: 0.0,
|
|
viewport_height: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn build(mut self, tree: &LayoutTree) -> DisplayList {
|
|
self.viewport_width = tree.viewport_width;
|
|
self.viewport_height = tree.viewport_height;
|
|
|
|
// Paint canvas background as a full-viewport rect before any layout boxes.
|
|
let viewport_rect = Rect::new(0.0, 0.0, tree.viewport_width, tree.viewport_height);
|
|
if tree.canvas_background_color.a != 0 {
|
|
self.list.push(DisplayItem::SolidRect {
|
|
rect: viewport_rect,
|
|
color: tree.canvas_background_color,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
if let Some(ref gradient) = tree.canvas_background_gradient {
|
|
self.list.push(DisplayItem::LinearGradient {
|
|
rect: viewport_rect,
|
|
gradient: gradient.clone(),
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
if let Some(image_id) = tree.canvas_background_image_id {
|
|
self.list.push(DisplayItem::BackgroundImage {
|
|
clip_rect: viewport_rect,
|
|
image_id,
|
|
position_x: tree.canvas_background_position_x,
|
|
position_y: tree.canvas_background_position_y,
|
|
repeat: tree.canvas_background_repeat,
|
|
attachment: tree.canvas_background_attachment,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
|
|
if let Some(ref root) = tree.root {
|
|
let initial_offset = CumulativeOffset {
|
|
x: 0.0,
|
|
y: -self.scroll_offset_y,
|
|
};
|
|
self.render_layout_box(root, initial_offset, true);
|
|
}
|
|
self.list
|
|
}
|
|
|
|
fn render_layout_box(
|
|
&mut self,
|
|
layout_box: &LayoutBox,
|
|
offset: CumulativeOffset,
|
|
is_root: bool,
|
|
) {
|
|
// Render using stacking context rules
|
|
self.render_stacking_context(layout_box, offset, is_root);
|
|
}
|
|
|
|
/// Render a layout box following CSS 2.1 Appendix E stacking context rules.
|
|
/// Paint order within a stacking context:
|
|
/// 1. Background and borders of the stacking context element
|
|
/// 2. Child stacking contexts with negative z-index (sorted by z-index)
|
|
/// 3. In-flow, non-inline-level, non-positioned descendants (block bg/borders)
|
|
/// 4. Non-positioned floats (painted as mini stacking contexts)
|
|
/// 5. In-flow, inline-level, non-positioned descendants (inline content)
|
|
/// 6. Positioned descendants with z-index: auto or 0
|
|
/// 7. Child stacking contexts with positive z-index (sorted by z-index)
|
|
fn render_stacking_context(
|
|
&mut self,
|
|
layout_box: &LayoutBox,
|
|
parent_offset: CumulativeOffset,
|
|
is_root: bool,
|
|
) {
|
|
// Fixed elements ignore scroll offset - reset to zero
|
|
let base_offset = if layout_box.position == Position::Fixed {
|
|
CumulativeOffset::default()
|
|
} else {
|
|
parent_offset
|
|
};
|
|
// Add this element's render offset + sticky offset to the cumulative offset
|
|
let (sticky_x, sticky_y) = self.compute_sticky_offset(layout_box, base_offset);
|
|
let offset = base_offset.add(
|
|
layout_box.render_offset_x + sticky_x,
|
|
layout_box.render_offset_y + sticky_y,
|
|
);
|
|
|
|
let is_visible = layout_box.visibility == Visibility::Visible;
|
|
|
|
// CSS 2.1 §11.1.2: Apply clip rect for absolutely positioned elements
|
|
let has_clip = layout_box.clip.is_some();
|
|
if let Some(clip) = &layout_box.clip {
|
|
// clip: rect(top, right, bottom, left) — offsets relative to the element's border box
|
|
let border_box = offset.apply(&layout_box.dimensions.border_box());
|
|
let clip_top = clip.top.unwrap_or(0.0);
|
|
let clip_left = clip.left.unwrap_or(0.0);
|
|
let clip_bottom = clip.bottom.unwrap_or(border_box.height());
|
|
let clip_right = clip.right.unwrap_or(border_box.width());
|
|
let clip_rect = Rect::new(
|
|
border_box.x() + clip_left,
|
|
border_box.y() + clip_top,
|
|
(clip_right - clip_left).max(0.0),
|
|
(clip_bottom - clip_top).max(0.0),
|
|
);
|
|
self.list.push(DisplayItem::PushClip { rect: clip_rect });
|
|
}
|
|
|
|
// Step 1: Render container's own background/border (skip if hidden).
|
|
if is_visible {
|
|
self.render_box_shadows(layout_box, offset, false);
|
|
self.render_background(layout_box, offset);
|
|
self.render_borders(layout_box, offset);
|
|
self.render_box_shadows(layout_box, offset, true);
|
|
self.render_image(layout_box, offset);
|
|
self.render_list_marker(layout_box, offset);
|
|
}
|
|
|
|
// Check if we need to clip overflow
|
|
let needs_clip = layout_box.overflow_x != Overflow::Visible
|
|
|| layout_box.overflow_y != Overflow::Visible;
|
|
|
|
if needs_clip {
|
|
let clip_rect = if is_root {
|
|
// CSS 2.1 §11.1.1: overflow on root applies to the viewport, pinned at origin
|
|
Rect::new(0.0, 0.0, self.viewport_width, self.viewport_height)
|
|
} else {
|
|
offset.apply(&layout_box.dimensions.padding_box())
|
|
};
|
|
self.list.push(DisplayItem::PushClip { rect: clip_rect });
|
|
}
|
|
|
|
// Categorize direct children for stacking order.
|
|
// Positioned-auto direct children are handled here (step 6).
|
|
let mut normal_flow_children: Vec<&LayoutBox> = Vec::new();
|
|
let mut positioned_auto_children: Vec<&LayoutBox> = Vec::new();
|
|
let has_ifc = layout_box.inline_context.is_some();
|
|
|
|
// Only collect stacking participants if this box actually creates a
|
|
// stacking context (or is root). Positioned-auto elements do NOT create
|
|
// stacking contexts — their stacking context descendants are hoisted to
|
|
// the nearest ancestor stacking context via collect_stacking_participants.
|
|
let owns_stacking_context = is_root || layout_box.creates_stacking_context();
|
|
|
|
for child in &layout_box.children {
|
|
if child.creates_stacking_context() {
|
|
// Always skip stacking context children from normal categorization.
|
|
// When owns_stacking_context is true, they're collected below via
|
|
// collect_stacking_participants. When false (positioned-auto parent),
|
|
// they've already been collected by the nearest ancestor SC.
|
|
continue;
|
|
} else if child.is_positioned()
|
|
&& !(has_ifc && child.box_type == layout::BoxType::Inline)
|
|
{
|
|
// Positioned-auto block-level children are rendered at step 6.
|
|
// Inline positioned children in an IFC are handled by the IFC
|
|
// fragment system, not extracted to step 6.
|
|
positioned_auto_children.push(child);
|
|
} else if has_ifc
|
|
&& (child.box_type == layout::BoxType::Anonymous
|
|
|| child.box_type == layout::BoxType::Inline)
|
|
{
|
|
// Anonymous and inline children are already represented in the IFC
|
|
// fragments; rendering them as normal flow would double-paint
|
|
// the text (once from the IFC at step 5, once from the child box).
|
|
continue;
|
|
} else {
|
|
// Normal flow (including floats — floats are separated during rendering)
|
|
normal_flow_children.push(child);
|
|
}
|
|
}
|
|
|
|
// Collect stacking context descendants from the entire subtree (tree order).
|
|
// Only done when this box owns a stacking context — positioned-auto elements
|
|
// don't collect because their SC descendants are hoisted to the parent SC.
|
|
let mut buckets = StackingBuckets {
|
|
negative: Vec::new(),
|
|
zero: Vec::new(),
|
|
positive: Vec::new(),
|
|
};
|
|
if owns_stacking_context {
|
|
self.collect_stacking_participants(layout_box, offset, &[], &[], &mut buckets, is_root);
|
|
}
|
|
|
|
// Stable sort preserves tree order for equal z-index
|
|
buckets
|
|
.negative
|
|
.sort_by_key(|p| p.layout_box.z_index.unwrap_or(0));
|
|
buckets
|
|
.positive
|
|
.sort_by_key(|p| p.layout_box.z_index.unwrap_or(0));
|
|
|
|
// Step 2: Render negative z-index stacking contexts
|
|
for p in &buckets.negative {
|
|
let parent_offset = p.parent_offset;
|
|
self.render_with_ancestor_clips(p, |builder| {
|
|
builder.render_stacking_context(p.layout_box, parent_offset, false);
|
|
});
|
|
}
|
|
|
|
// Steps 3-5: Render normal flow with proper float ordering.
|
|
// CSS 2.1 Appendix E: block backgrounds (step 3) → floats (step 4) → inline content (step 5).
|
|
// Pass 1 (step 3): render only backgrounds/borders of normal flow children
|
|
for child in &normal_flow_children {
|
|
self.render_layout_box_normal(child, offset, true, RenderPhase::BackgroundsOnly);
|
|
}
|
|
// Pass 2 (step 4): collect and render all floats (full render)
|
|
// Both direct float children of this stacking context and nested float descendants.
|
|
let mut floats: Vec<(&LayoutBox, CumulativeOffset)> = Vec::new();
|
|
for child in &normal_flow_children {
|
|
if child.float != Float::None {
|
|
// Direct float child — render in float layer
|
|
floats.push((child, offset));
|
|
} else {
|
|
// Non-float child — collect nested float descendants
|
|
self.collect_descendant_floats(child, offset, &mut floats);
|
|
}
|
|
}
|
|
for (float_box, float_offset) in floats {
|
|
self.render_layout_box_normal(float_box, float_offset, false, RenderPhase::Full);
|
|
}
|
|
// For IFC elements, render non-inline descendants (e.g., InlineBlock within
|
|
// inline wrappers) that were skipped by the Inline child exclusion above.
|
|
if has_ifc {
|
|
self.render_non_inline_descendants(
|
|
layout_box,
|
|
offset,
|
|
true,
|
|
RenderPhase::BackgroundsOnly,
|
|
);
|
|
}
|
|
// Pass 3 (step 5): render only inline content of normal flow children
|
|
for child in &normal_flow_children {
|
|
self.render_layout_box_normal(child, offset, true, RenderPhase::InlineContentOnly);
|
|
}
|
|
// For IFC elements, also render inline content of non-inline descendants.
|
|
if has_ifc {
|
|
self.render_non_inline_descendants(
|
|
layout_box,
|
|
offset,
|
|
true,
|
|
RenderPhase::InlineContentOnly,
|
|
);
|
|
}
|
|
// Pass 4 (step 5 cont.): inline content of the stacking context element itself
|
|
if is_visible {
|
|
if let Some(ref ifc) = layout_box.inline_context {
|
|
self.render_inline_fragments(ifc, offset);
|
|
} else {
|
|
self.render_text(layout_box, offset);
|
|
}
|
|
}
|
|
|
|
// Step 6: Render child stacking contexts with z-index:0 AND positioned
|
|
// descendants with z-index:auto, in tree order (CSS 2.1 Appendix E step 6).
|
|
// Collect all step-6 participants and merge in tree order using node_id.
|
|
{
|
|
// Each entry: (node_id for tree ordering, layout_box, offset, ancestor_clips)
|
|
let mut step6: Vec<(usize, &LayoutBox, CumulativeOffset, Vec<Rect>)> = Vec::new();
|
|
|
|
// Direct positioned-auto children
|
|
for child in positioned_auto_children {
|
|
step6.push((child.node_id.0, child, offset, Vec::new()));
|
|
}
|
|
|
|
// Deep positioned-auto descendants from normal flow subtree
|
|
let mut deep_positioned: Vec<(&LayoutBox, CumulativeOffset)> = Vec::new();
|
|
for child in &normal_flow_children {
|
|
if child.float == Float::None {
|
|
self.collect_positioned_auto_descendants(child, offset, &mut deep_positioned);
|
|
}
|
|
}
|
|
if has_ifc {
|
|
self.collect_positioned_auto_descendants(layout_box, offset, &mut deep_positioned);
|
|
}
|
|
for (pos_box, pos_offset) in deep_positioned {
|
|
step6.push((pos_box.node_id.0, pos_box, pos_offset, Vec::new()));
|
|
}
|
|
|
|
// z-index:0 stacking contexts from full subtree
|
|
for p in buckets.zero {
|
|
step6.push((
|
|
p.layout_box.node_id.0,
|
|
p.layout_box,
|
|
p.parent_offset,
|
|
p.ancestor_clips,
|
|
));
|
|
}
|
|
|
|
// Stable sort by node_id to get tree order.
|
|
// Invariant: NodeId values increase in document (DFS) order,
|
|
// so sorting by node_id.0 yields correct tree order.
|
|
step6.sort_by_key(|(nid, _, _, _)| *nid);
|
|
|
|
for (_, lb, item_offset, clips) in &step6 {
|
|
for clip in clips {
|
|
self.list.push(DisplayItem::PushClip { rect: *clip });
|
|
}
|
|
self.render_stacking_context(lb, *item_offset, false);
|
|
for _ in clips {
|
|
self.list.push(DisplayItem::PopClip);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 7: Render positive z-index (>0) stacking contexts
|
|
for p in &buckets.positive {
|
|
let parent_offset = p.parent_offset;
|
|
self.render_with_ancestor_clips(p, |builder| {
|
|
builder.render_stacking_context(p.layout_box, parent_offset, false);
|
|
});
|
|
}
|
|
|
|
// Render collapsed borders (on top of all cell backgrounds)
|
|
if is_visible {
|
|
self.render_collapsed_borders(layout_box, offset);
|
|
}
|
|
|
|
// Step 10 (CSS 2.1 Appendix E): Render outline on top of all content
|
|
if is_visible {
|
|
self.render_outline(layout_box, offset);
|
|
}
|
|
|
|
if needs_clip {
|
|
self.list.push(DisplayItem::PopClip);
|
|
}
|
|
|
|
// Emit scroll indicators for overflow: scroll (always) or auto (when content overflows)
|
|
if !is_root && needs_clip {
|
|
self.emit_scroll_indicators(layout_box, offset);
|
|
}
|
|
|
|
// Pop clip rect for CSS clip property (CSS 2.1 §11.1.2)
|
|
if has_clip {
|
|
self.list.push(DisplayItem::PopClip);
|
|
}
|
|
}
|
|
|
|
/// Render a layout box in normal flow.
|
|
/// When `skip_floats` is true, float children at any depth are skipped
|
|
/// (they will be collected and rendered separately by the stacking context).
|
|
/// The `phase` parameter controls which parts are rendered for Appendix E ordering.
|
|
fn render_layout_box_normal(
|
|
&mut self,
|
|
layout_box: &LayoutBox,
|
|
parent_offset: CumulativeOffset,
|
|
skip_floats: bool,
|
|
phase: RenderPhase,
|
|
) {
|
|
// Skip float children when in skip_floats mode
|
|
if skip_floats && layout_box.float != Float::None {
|
|
return;
|
|
}
|
|
|
|
// Fixed elements ignore scroll offset - reset to zero
|
|
let base_offset = if layout_box.position == Position::Fixed {
|
|
CumulativeOffset::default()
|
|
} else {
|
|
parent_offset
|
|
};
|
|
// Add this element's render offset + sticky offset to the cumulative offset
|
|
let (sticky_x, sticky_y) = self.compute_sticky_offset(layout_box, base_offset);
|
|
let offset = base_offset.add(
|
|
layout_box.render_offset_x + sticky_x,
|
|
layout_box.render_offset_y + sticky_y,
|
|
);
|
|
|
|
let is_visible = layout_box.visibility == Visibility::Visible;
|
|
|
|
// CSS 2.1 §17.6.1.1: empty-cells: hide suppresses backgrounds and borders
|
|
// for empty table cells in the separate border model.
|
|
let hide_empty_cell = layout_box.box_type == layout::BoxType::TableCell
|
|
&& layout_box.empty_cells == EmptyCells::Hide
|
|
&& layout_box.border_collapse == BorderCollapse::Separate
|
|
&& Self::is_cell_empty(layout_box);
|
|
|
|
// Render container's own background/border (skip if hidden or in InlineContentOnly phase).
|
|
if is_visible && phase != RenderPhase::InlineContentOnly && !hide_empty_cell {
|
|
self.render_box_shadows(layout_box, offset, false);
|
|
self.render_background(layout_box, offset);
|
|
self.render_borders(layout_box, offset);
|
|
self.render_box_shadows(layout_box, offset, true);
|
|
self.render_image(layout_box, offset);
|
|
self.render_list_marker(layout_box, offset);
|
|
}
|
|
|
|
// Check if we need to clip overflow
|
|
let needs_clip = layout_box.overflow_x != Overflow::Visible
|
|
|| layout_box.overflow_y != Overflow::Visible;
|
|
|
|
if needs_clip {
|
|
let clip_rect = offset.apply(&layout_box.dimensions.padding_box());
|
|
self.list.push(DisplayItem::PushClip { rect: clip_rect });
|
|
}
|
|
|
|
// Handle inline formatting context
|
|
if let Some(ref ifc) = layout_box.inline_context {
|
|
// Only render inline content if visible and not in BackgroundsOnly phase
|
|
if is_visible && phase != RenderPhase::BackgroundsOnly {
|
|
self.render_inline_fragments(ifc, offset);
|
|
}
|
|
|
|
// Always render block-level and inline-block children
|
|
// (they may override visibility).
|
|
self.render_non_inline_descendants(layout_box, offset, skip_floats, phase);
|
|
} else {
|
|
// No IFC: existing behavior
|
|
if is_visible && phase != RenderPhase::BackgroundsOnly {
|
|
self.render_text(layout_box, offset);
|
|
}
|
|
|
|
for child in &layout_box.children {
|
|
// Skip non-inline stacking-context children — they have been hoisted
|
|
// to the owning stacking context's paint-order steps 2/6/7.
|
|
if skip_floats
|
|
&& child.box_type != layout::BoxType::Inline
|
|
&& child.creates_stacking_context()
|
|
{
|
|
continue;
|
|
}
|
|
// Non-inline positioned-auto elements: skip during phased passes
|
|
// (collected by collect_positioned_auto_descendants for step 6).
|
|
// In Full phase, render via render_stacking_context for Appendix E ordering.
|
|
// Inline positioned elements (e.g., position:relative spans) stay in flow.
|
|
if child.is_positioned()
|
|
&& !child.creates_stacking_context()
|
|
&& child.box_type != layout::BoxType::Inline
|
|
{
|
|
if phase == RenderPhase::Full {
|
|
self.render_stacking_context(child, offset, false);
|
|
}
|
|
// In BackgroundsOnly/InlineContentOnly, skip entirely —
|
|
// they'll be collected by collect_positioned_auto_descendants.
|
|
continue;
|
|
}
|
|
let child_skip = skip_floats && !child.is_positioned();
|
|
self.render_layout_box_normal(child, offset, child_skip, phase);
|
|
}
|
|
}
|
|
|
|
// Render collapsed borders (on top of all cell backgrounds) — part of backgrounds phase
|
|
if is_visible && phase != RenderPhase::InlineContentOnly {
|
|
self.render_collapsed_borders(layout_box, offset);
|
|
}
|
|
|
|
// Outline: rendered after all content (CSS 2.1 Appendix E step 10)
|
|
if is_visible && phase != RenderPhase::BackgroundsOnly {
|
|
self.render_outline(layout_box, offset);
|
|
}
|
|
|
|
if needs_clip {
|
|
self.list.push(DisplayItem::PopClip);
|
|
}
|
|
|
|
// Emit scroll indicators for overflow: scroll (always) or auto (when content overflows)
|
|
// Only emit during InlineContentOnly or Full phase to avoid double-emitting
|
|
if needs_clip && phase != RenderPhase::BackgroundsOnly {
|
|
self.emit_scroll_indicators(layout_box, offset);
|
|
}
|
|
}
|
|
|
|
/// Recursively collect all float descendants from the normal flow tree.
|
|
/// Stops at stacking context boundaries and positioned elements.
|
|
fn collect_descendant_floats<'a>(
|
|
&self,
|
|
layout_box: &'a LayoutBox,
|
|
parent_offset: CumulativeOffset,
|
|
floats: &mut Vec<(&'a LayoutBox, CumulativeOffset)>,
|
|
) {
|
|
let base_offset = if layout_box.position == Position::Fixed {
|
|
CumulativeOffset::default()
|
|
} else {
|
|
parent_offset
|
|
};
|
|
// Don't compute sticky offset here — this is a geometry collection pass.
|
|
// Sticky visual offsets are applied during actual rendering.
|
|
let offset = base_offset.add(layout_box.render_offset_x, layout_box.render_offset_y);
|
|
|
|
for child in &layout_box.children {
|
|
if child.creates_stacking_context() || child.is_positioned() {
|
|
continue;
|
|
}
|
|
if child.float != Float::None {
|
|
floats.push((child, offset));
|
|
} else {
|
|
// Recurse into normal flow children looking for nested floats
|
|
self.collect_descendant_floats(child, offset, floats);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Recursively collect positioned-auto descendants from the normal flow tree.
|
|
/// These need to be rendered at step 6 of the owning stacking context.
|
|
/// Stops at stacking context boundaries (they're handled by collect_stacking_participants).
|
|
fn collect_positioned_auto_descendants<'a>(
|
|
&self,
|
|
layout_box: &'a LayoutBox,
|
|
parent_offset: CumulativeOffset,
|
|
positioned: &mut Vec<(&'a LayoutBox, CumulativeOffset)>,
|
|
) {
|
|
let base_offset = if layout_box.position == Position::Fixed {
|
|
CumulativeOffset::default()
|
|
} else {
|
|
parent_offset
|
|
};
|
|
let offset = base_offset.add(layout_box.render_offset_x, layout_box.render_offset_y);
|
|
|
|
for child in &layout_box.children {
|
|
if child.creates_stacking_context() {
|
|
// Stacking context children are handled by collect_stacking_participants
|
|
continue;
|
|
}
|
|
if child.is_positioned() && child.box_type != layout::BoxType::Inline {
|
|
// Collect non-inline positioned-auto descendants for step 6.
|
|
// Inline positioned elements (e.g., position:relative spans)
|
|
// are part of the IFC and should not be hoisted.
|
|
// Don't recurse — its own positioned descendants are handled
|
|
// within its own stacking context rendering.
|
|
positioned.push((child, offset));
|
|
} else if child.float != Float::None {
|
|
// Floats are handled separately; don't recurse into them
|
|
// for positioned descendants (floats establish their own context).
|
|
continue;
|
|
} else {
|
|
self.collect_positioned_auto_descendants(child, offset, positioned);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Recursively collect stacking-context descendants from the normal-flow subtree
|
|
/// for rendering at the correct CSS 2.1 Appendix E paint-order steps:
|
|
/// negative z-index (step 2), z-index:0 (step 6), positive z-index (step 7).
|
|
/// Recurses through positioned-auto ancestors (they don't create stacking contexts).
|
|
/// Stops at stacking context boundaries and non-SC floats.
|
|
fn collect_stacking_participants<'a>(
|
|
&self,
|
|
layout_box: &'a LayoutBox,
|
|
parent_offset: CumulativeOffset,
|
|
ancestor_clips: &[Rect],
|
|
// Clips that apply to absolutely positioned descendants (only from positioned ancestors)
|
|
positioned_ancestor_clips: &[Rect],
|
|
buckets: &mut StackingBuckets<'a>,
|
|
is_root: bool,
|
|
) {
|
|
let base_offset = if layout_box.position == Position::Fixed {
|
|
CumulativeOffset::default()
|
|
} else {
|
|
parent_offset
|
|
};
|
|
// Don't compute sticky offset here — geometry collection pass only.
|
|
let offset = base_offset.add(layout_box.render_offset_x, layout_box.render_offset_y);
|
|
|
|
let is_positioned = layout_box.is_positioned() || is_root;
|
|
|
|
// Track overflow:hidden and CSS clip from intermediate ancestors.
|
|
// CSS 2.1 §11.1.2: clip applies to the element and its descendants.
|
|
let mut clips = ancestor_clips.to_vec();
|
|
let mut pos_clips = positioned_ancestor_clips.to_vec();
|
|
if layout_box.overflow_x != Overflow::Visible || layout_box.overflow_y != Overflow::Visible
|
|
{
|
|
let clip_rect = if is_root {
|
|
// CSS 2.1 §11.1.1: overflow on root applies to the viewport, pinned at origin
|
|
Rect::new(0.0, 0.0, self.viewport_width, self.viewport_height)
|
|
} else {
|
|
offset.apply(&layout_box.dimensions.padding_box())
|
|
};
|
|
clips.push(clip_rect);
|
|
// CSS 2.1 §11.1.1: overflow clipping on a non-positioned element does NOT
|
|
// clip absolutely positioned descendants whose containing block is above it.
|
|
// Only positioned elements (containing blocks) propagate overflow clips
|
|
// to absolutely positioned descendants.
|
|
if is_positioned {
|
|
pos_clips.push(clip_rect);
|
|
}
|
|
}
|
|
// Propagate CSS clip: rect() from this ancestor to descendant stacking contexts
|
|
if let Some(clip) = &layout_box.clip {
|
|
let border_box = offset.apply(&layout_box.dimensions.border_box());
|
|
let clip_top = clip.top.unwrap_or(0.0);
|
|
let clip_left = clip.left.unwrap_or(0.0);
|
|
let clip_bottom = clip.bottom.unwrap_or(border_box.height());
|
|
let clip_right = clip.right.unwrap_or(border_box.width());
|
|
let clip_rect = Rect::new(
|
|
border_box.x() + clip_left,
|
|
border_box.y() + clip_top,
|
|
(clip_right - clip_left).max(0.0),
|
|
(clip_bottom - clip_top).max(0.0),
|
|
);
|
|
clips.push(clip_rect);
|
|
if is_positioned {
|
|
pos_clips.push(clip_rect);
|
|
}
|
|
}
|
|
|
|
for child in &layout_box.children {
|
|
// Skip simple floats — their stacking-context descendants stay within
|
|
// the float's mini stacking context, not hoisted.
|
|
// But floats that *themselves* create a stacking context must be
|
|
// collected here (CSS 2.1 Appendix E steps 2/6/7), since they are
|
|
// excluded from both `normal_flow_children` and
|
|
// `collect_descendant_floats`.
|
|
if child.float != Float::None && !child.creates_stacking_context() {
|
|
continue;
|
|
}
|
|
|
|
// Inline-level stacking-context elements are handled by the IFC
|
|
// fragment system, not hoisted. Recurse into them like normal flow.
|
|
let is_inline = child.box_type == layout::BoxType::Inline;
|
|
|
|
if !is_inline && child.creates_stacking_context() {
|
|
let z = child.z_index.unwrap_or(0);
|
|
// CSS 2.1 §11.1.1: absolutely positioned children use only clips
|
|
// from positioned ancestors (containing blocks).
|
|
let child_clips = if child.position == Position::Absolute {
|
|
pos_clips.clone()
|
|
} else {
|
|
clips.clone()
|
|
};
|
|
let participant = StackingParticipant {
|
|
layout_box: child,
|
|
parent_offset: offset,
|
|
ancestor_clips: child_clips,
|
|
};
|
|
if z < 0 {
|
|
buckets.negative.push(participant);
|
|
} else if z == 0 {
|
|
// CSS 2.1 Appendix E step 6: z-index:0 stacking contexts
|
|
buckets.zero.push(participant);
|
|
} else {
|
|
// CSS 2.1 Appendix E step 7: positive z-index only
|
|
buckets.positive.push(participant);
|
|
}
|
|
} else if child.is_positioned() {
|
|
// Positioned-auto elements (z-index:auto) do NOT create stacking
|
|
// contexts, so their descendant stacking contexts still participate
|
|
// in the current stacking context. Recurse into them to collect
|
|
// any such descendants (CSS 2.1 Appendix E).
|
|
self.collect_stacking_participants(
|
|
child, offset, &clips, &pos_clips, buckets, false,
|
|
);
|
|
} else {
|
|
// Normal flow, non-positioned, non-float, or inline:
|
|
// recurse deeper
|
|
self.collect_stacking_participants(
|
|
child, offset, &clips, &pos_clips, buckets, false,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Render a stacking participant with its ancestor clip rects.
|
|
fn render_with_ancestor_clips<F>(&mut self, p: &StackingParticipant, render_fn: F)
|
|
where
|
|
F: FnOnce(&mut Self),
|
|
{
|
|
for clip in &p.ancestor_clips {
|
|
self.list.push(DisplayItem::PushClip { rect: *clip });
|
|
}
|
|
render_fn(self);
|
|
for _ in &p.ancestor_clips {
|
|
self.list.push(DisplayItem::PopClip);
|
|
}
|
|
}
|
|
|
|
/// Render inline fragments from an inline formatting context
|
|
fn render_inline_fragments(
|
|
&mut self,
|
|
ifc: &layout::InlineFormattingContext,
|
|
offset: CumulativeOffset,
|
|
) {
|
|
for line_box in &ifc.line_boxes {
|
|
for fragment in &line_box.fragments {
|
|
// Skip atomic inline fragments - they are positioning-only placeholders
|
|
if fragment.is_atomic {
|
|
continue;
|
|
}
|
|
// 1. Render inline element background (if present)
|
|
if let Some(bg_color) = fragment.background_color {
|
|
if bg_color.a > 0 {
|
|
let bg_rect = self.compute_fragment_background_rect(fragment);
|
|
let bg_rect = offset.apply(&bg_rect);
|
|
self.list.push(DisplayItem::SolidRect {
|
|
rect: bg_rect,
|
|
color: bg_color,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// 2. Render inline element borders (if fragment is element start/end)
|
|
self.render_fragment_borders(fragment, offset);
|
|
|
|
// 3. Render text (if present)
|
|
if let Some(ref text) = fragment.text {
|
|
let text_rect = offset.apply(&fragment.rect);
|
|
self.list.push(DisplayItem::Text {
|
|
rect: text_rect,
|
|
text: text.clone(),
|
|
color: fragment.color,
|
|
font_size: fragment.font_size,
|
|
font_weight: fragment.font_weight,
|
|
font_style: fragment.font_style,
|
|
font_variant: fragment.font_variant,
|
|
font_family: fragment.font_family.clone(),
|
|
});
|
|
}
|
|
|
|
// 4. Render text decorations (underline, line-through, overline)
|
|
if !fragment.text_decoration_line.is_none() && fragment.text.is_some() {
|
|
let text_rect = offset.apply(&fragment.rect);
|
|
let thickness = (fragment.font_size / 14.0).max(1.0);
|
|
|
|
if fragment.text_decoration_line.has_underline() {
|
|
let y = text_rect.y() + fragment.baseline_offset + thickness * 2.0;
|
|
self.list.push(DisplayItem::SolidRect {
|
|
rect: Rect::new(text_rect.x(), y, text_rect.width(), thickness),
|
|
color: fragment.color,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
if fragment.text_decoration_line.has_line_through() {
|
|
let y = text_rect.y() + fragment.baseline_offset - fragment.font_size * 0.3;
|
|
self.list.push(DisplayItem::SolidRect {
|
|
rect: Rect::new(text_rect.x(), y, text_rect.width(), thickness),
|
|
color: fragment.color,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
if fragment.text_decoration_line.has_overline() {
|
|
let y = text_rect.y() + fragment.baseline_offset - fragment.font_size * 0.8;
|
|
self.list.push(DisplayItem::SolidRect {
|
|
rect: Rect::new(text_rect.x(), y, text_rect.width(), thickness),
|
|
color: fragment.color,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compute the background rect for a fragment, including padding
|
|
fn compute_fragment_background_rect(&self, fragment: &InlineFragment) -> Rect {
|
|
let padding = fragment.padding.unwrap_or(EdgeSizes::ZERO);
|
|
let border = fragment.border.unwrap_or(EdgeSizes::ZERO);
|
|
|
|
let mut x = fragment.rect.x();
|
|
let mut width = fragment.rect.width();
|
|
|
|
// Include left padding/border if this is the start of the element
|
|
if fragment.is_element_start {
|
|
x -= padding.left + border.left;
|
|
width += padding.left + border.left;
|
|
}
|
|
|
|
// Include right padding/border if this is the end of the element
|
|
if fragment.is_element_end {
|
|
width += padding.right + border.right;
|
|
}
|
|
|
|
Rect::new(
|
|
x,
|
|
fragment.rect.y() - padding.top - border.top,
|
|
width,
|
|
fragment.rect.height() + padding.vertical() + border.vertical(),
|
|
)
|
|
}
|
|
|
|
/// Render borders for an inline fragment with offset support
|
|
fn render_fragment_borders(&mut self, fragment: &InlineFragment, offset: CumulativeOffset) {
|
|
let (Some(border), Some(colors), Some(styles)) = (
|
|
&fragment.border,
|
|
&fragment.border_colors,
|
|
&fragment.border_styles,
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
if !styles.any_visible() {
|
|
return;
|
|
}
|
|
|
|
let mut widths = *border;
|
|
if !fragment.is_element_start {
|
|
widths.left = 0.0;
|
|
}
|
|
if !fragment.is_element_end {
|
|
widths.right = 0.0;
|
|
}
|
|
if !fragment.is_first_line {
|
|
widths.top = 0.0;
|
|
}
|
|
if !fragment.is_last_line {
|
|
widths.bottom = 0.0;
|
|
}
|
|
|
|
if widths.top == 0.0 && widths.right == 0.0 && widths.bottom == 0.0 && widths.left == 0.0 {
|
|
return;
|
|
}
|
|
|
|
let border_rect = self.compute_fragment_background_rect(fragment);
|
|
let border_rect = offset.apply(&border_rect);
|
|
|
|
self.list.push(DisplayItem::Border {
|
|
rect: border_rect,
|
|
widths,
|
|
colors: *colors,
|
|
styles: *styles,
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
|
|
fn render_box_shadows(
|
|
&mut self,
|
|
layout_box: &LayoutBox,
|
|
offset: CumulativeOffset,
|
|
inset: bool,
|
|
) {
|
|
if layout_box.box_shadows.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let border_box = layout_box.dimensions.border_box();
|
|
let border_box = offset.apply(&border_box);
|
|
|
|
// CSS: first shadow in the list paints on top (closest to viewer).
|
|
// Display list paints in order (later items on top), so we emit
|
|
// last-declared shadow first so it ends up underneath.
|
|
for shadow in layout_box.box_shadows.iter().rev() {
|
|
if shadow.inset != inset {
|
|
continue;
|
|
}
|
|
self.list.push(DisplayItem::BoxShadow {
|
|
border_box,
|
|
shadow: *shadow,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// CSS 2.1 §17.6.1.1: A cell is "empty" if it has no in-flow content —
|
|
/// no child boxes (or only anonymous children with whitespace-only text)
|
|
/// and no non-whitespace inline content.
|
|
fn is_cell_empty(layout_box: &LayoutBox) -> bool {
|
|
// Check children: non-anonymous children mean the cell has real content.
|
|
// Anonymous children with only whitespace text are treated as empty.
|
|
for child in &layout_box.children {
|
|
if child.box_type != BoxType::Anonymous {
|
|
return false;
|
|
}
|
|
// Anonymous child: check if it contains only whitespace
|
|
if let Some(ref text) = child.text_content {
|
|
if !text.trim().is_empty() {
|
|
return false;
|
|
}
|
|
}
|
|
// Recursively check anonymous child's inline content
|
|
if let Some(ref ifc) = child.inline_context {
|
|
for line_box in &ifc.line_boxes {
|
|
for fragment in &line_box.fragments {
|
|
if let Some(ref text) = fragment.text {
|
|
if !text.trim().is_empty() {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Anonymous child with non-anonymous grandchildren is non-empty
|
|
if !child.children.is_empty() {
|
|
return false;
|
|
}
|
|
}
|
|
if let Some(ref ifc) = layout_box.inline_context {
|
|
for line_box in &ifc.line_boxes {
|
|
for fragment in &line_box.fragments {
|
|
if let Some(ref text) = fragment.text {
|
|
if !text.trim().is_empty() {
|
|
return false;
|
|
}
|
|
} else {
|
|
// Non-text fragments (e.g., replaced elements) mean the cell is not empty
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(ref text) = layout_box.text_content {
|
|
if !text.trim().is_empty() {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn render_background(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
let border_box = layout_box.dimensions.border_box();
|
|
let rect = offset.apply(&border_box);
|
|
let radii = layout_box.resolved_border_radii();
|
|
|
|
// Render background color (skip transparent)
|
|
if layout_box.background_color.a != 0 {
|
|
self.list.push(DisplayItem::SolidRect {
|
|
rect,
|
|
color: layout_box.background_color,
|
|
radii,
|
|
});
|
|
}
|
|
|
|
// Render background gradient on top of background color
|
|
if let Some(gradient) = &layout_box.background_gradient {
|
|
self.list.push(DisplayItem::LinearGradient {
|
|
rect,
|
|
gradient: gradient.clone(),
|
|
radii,
|
|
});
|
|
}
|
|
|
|
// Render background image on top of background color (and gradient)
|
|
if let Some(bg_image_id) = layout_box.background_image_id {
|
|
let padding_box = layout_box.dimensions.padding_box();
|
|
let clip_rect = offset.apply(&padding_box);
|
|
self.list.push(DisplayItem::BackgroundImage {
|
|
clip_rect,
|
|
image_id: bg_image_id,
|
|
position_x: layout_box.background_position_x,
|
|
position_y: layout_box.background_position_y,
|
|
repeat: layout_box.background_repeat,
|
|
attachment: layout_box.background_attachment,
|
|
radii,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn render_borders(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
let d = &layout_box.dimensions;
|
|
|
|
// Skip if no borders or no visible styles
|
|
if d.border.top == 0.0
|
|
&& d.border.right == 0.0
|
|
&& d.border.bottom == 0.0
|
|
&& d.border.left == 0.0
|
|
{
|
|
return;
|
|
}
|
|
|
|
if !layout_box.border_styles.any_visible() {
|
|
return;
|
|
}
|
|
|
|
let border_box = d.border_box();
|
|
let rect = offset.apply(&border_box);
|
|
|
|
self.list.push(DisplayItem::Border {
|
|
rect,
|
|
widths: d.border,
|
|
colors: layout_box.border_colors,
|
|
styles: layout_box.border_styles,
|
|
radii: layout_box.resolved_border_radii(),
|
|
});
|
|
}
|
|
|
|
/// Render outline outside the border box (CSS 2.1 §18.4).
|
|
/// Outline does not affect layout and is painted after borders.
|
|
fn render_outline(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
if !layout_box.outline_style.is_visible() || layout_box.outline_width <= 0.0 {
|
|
return;
|
|
}
|
|
|
|
let d = &layout_box.dimensions;
|
|
let border_box = d.border_box();
|
|
|
|
// Expand border box outward by outline_offset + outline_width
|
|
let expand = layout_box.outline_offset + layout_box.outline_width;
|
|
let w = border_box.width() + expand * 2.0;
|
|
let h = border_box.height() + expand * 2.0;
|
|
if w <= 0.0 || h <= 0.0 {
|
|
return;
|
|
}
|
|
let outline_rect = Rect::new(border_box.x() - expand, border_box.y() - expand, w, h);
|
|
let rect = offset.apply(&outline_rect);
|
|
|
|
self.list.push(DisplayItem::Outline {
|
|
rect,
|
|
width: layout_box.outline_width,
|
|
style: layout_box.outline_style,
|
|
color: layout_box.outline_color,
|
|
});
|
|
}
|
|
|
|
/// Emit scroll indicator display items for overflow: scroll or auto.
|
|
/// CSS 2.1 §11.1: scroll always shows scrollbar, auto only when content overflows.
|
|
/// Note: scroll indicators overlap the content/padding area (last 8px). Interactive
|
|
/// scrolling (Story 5-5) should account for this by reducing available content width/height.
|
|
fn emit_scroll_indicators(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
let padding_box = offset.apply(&layout_box.dimensions.padding_box());
|
|
let content_rect = &layout_box.dimensions.content;
|
|
|
|
// Compute content extent from children.
|
|
// Skip absolutely positioned children whose containing block is not this element
|
|
// (they escape the overflow container per CSS 2.1 §11.1.1).
|
|
let is_containing_block = layout_box.is_positioned();
|
|
let mut max_child_right: f32 = 0.0;
|
|
let mut max_child_bottom: f32 = 0.0;
|
|
for child in &layout_box.children {
|
|
if child.position == Position::Absolute && !is_containing_block {
|
|
continue;
|
|
}
|
|
let child_bb = child.dimensions.border_box();
|
|
max_child_right =
|
|
max_child_right.max(child_bb.x() + child_bb.width() - content_rect.x());
|
|
max_child_bottom =
|
|
max_child_bottom.max(child_bb.y() + child_bb.height() - content_rect.y());
|
|
}
|
|
|
|
// Also consider inline formatting context dimensions
|
|
if let Some(ref ifc) = layout_box.inline_context {
|
|
max_child_right = max_child_right.max(ifc.max_line_width);
|
|
max_child_bottom = max_child_bottom.max(ifc.total_height);
|
|
}
|
|
|
|
let content_width = content_rect.width();
|
|
let content_height = content_rect.height();
|
|
let overflows_x = max_child_right > content_width;
|
|
let overflows_y = max_child_bottom > content_height;
|
|
|
|
const SCROLLBAR_SIZE: f32 = 8.0;
|
|
const MIN_THUMB: f32 = 16.0;
|
|
|
|
// Determine which scrollbars are shown
|
|
let show_vertical = match layout_box.overflow_y {
|
|
Overflow::Scroll => true,
|
|
Overflow::Auto => overflows_y,
|
|
_ => false,
|
|
};
|
|
let show_horizontal = match layout_box.overflow_x {
|
|
Overflow::Scroll => true,
|
|
Overflow::Auto => overflows_x,
|
|
_ => false,
|
|
};
|
|
|
|
// Vertical scrollbar (shorten track if horizontal scrollbar also present to avoid overlap)
|
|
if show_vertical {
|
|
let track_x = padding_box.x() + padding_box.width() - SCROLLBAR_SIZE;
|
|
let track_y = padding_box.y();
|
|
let track_h = padding_box.height() - if show_horizontal { SCROLLBAR_SIZE } else { 0.0 };
|
|
let track_rect = Rect::new(track_x, track_y, SCROLLBAR_SIZE, track_h);
|
|
|
|
let thumb_h = if overflows_y && max_child_bottom > 0.0 {
|
|
(content_height / max_child_bottom * track_h)
|
|
.max(MIN_THUMB)
|
|
.min(track_h)
|
|
} else {
|
|
track_h // No overflow → thumb fills track
|
|
};
|
|
let thumb_rect = Rect::new(track_x, track_y, SCROLLBAR_SIZE, thumb_h);
|
|
|
|
self.list.push(DisplayItem::ScrollIndicator {
|
|
track_rect,
|
|
thumb_rect,
|
|
vertical: true,
|
|
});
|
|
}
|
|
|
|
// Horizontal scrollbar (shorten track if vertical scrollbar also present to avoid overlap)
|
|
if show_horizontal {
|
|
let track_x = padding_box.x();
|
|
let track_y = padding_box.y() + padding_box.height() - SCROLLBAR_SIZE;
|
|
let track_w = padding_box.width() - if show_vertical { SCROLLBAR_SIZE } else { 0.0 };
|
|
let track_rect = Rect::new(track_x, track_y, track_w, SCROLLBAR_SIZE);
|
|
|
|
let thumb_w = if overflows_x && max_child_right > 0.0 {
|
|
(content_width / max_child_right * track_w)
|
|
.max(MIN_THUMB)
|
|
.min(track_w)
|
|
} else {
|
|
track_w
|
|
};
|
|
let thumb_rect = Rect::new(track_x, track_y, thumb_w, SCROLLBAR_SIZE);
|
|
|
|
self.list.push(DisplayItem::ScrollIndicator {
|
|
track_rect,
|
|
thumb_rect,
|
|
vertical: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn render_collapsed_borders(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
let Some(ref segments) = layout_box.collapsed_borders else {
|
|
return;
|
|
};
|
|
|
|
for seg in segments {
|
|
if !seg.style.is_visible() || seg.width == 0.0 {
|
|
continue;
|
|
}
|
|
|
|
let rect = offset.apply(&seg.rect);
|
|
|
|
// Emit a Border display item. For horizontal segments we set only
|
|
// the top width; for vertical segments only the left width.
|
|
// The rect already has the correct dimensions.
|
|
if seg.is_horizontal {
|
|
self.list.push(DisplayItem::Border {
|
|
rect,
|
|
widths: EdgeSizes::new(seg.width, 0.0, 0.0, 0.0),
|
|
colors: EdgeColors::new(seg.color, seg.color, seg.color, seg.color),
|
|
styles: EdgeStyles::new(
|
|
seg.style,
|
|
BorderStyle::None,
|
|
BorderStyle::None,
|
|
BorderStyle::None,
|
|
),
|
|
radii: CornerRadii::default(),
|
|
});
|
|
} else {
|
|
self.list.push(DisplayItem::Border {
|
|
rect,
|
|
widths: EdgeSizes::new(0.0, 0.0, 0.0, seg.width),
|
|
colors: EdgeColors::new(seg.color, seg.color, seg.color, seg.color),
|
|
styles: EdgeStyles::new(
|
|
BorderStyle::None,
|
|
BorderStyle::None,
|
|
BorderStyle::None,
|
|
seg.style,
|
|
),
|
|
radii: CornerRadii::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_list_marker(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
if let Some(ref marker) = layout_box.list_marker {
|
|
let content = &layout_box.dimensions.content;
|
|
let marker_width = layout_box.list_marker_width;
|
|
|
|
let (x, y) = match layout_box.list_style_position {
|
|
ListStylePosition::Outside => {
|
|
let gap = 4.0;
|
|
(content.x() - marker_width - gap, content.y())
|
|
}
|
|
ListStylePosition::Inside => {
|
|
// Inside: marker is at the start of the content area
|
|
(content.x(), content.y())
|
|
}
|
|
};
|
|
|
|
let rect = Rect::new(x, y, marker_width, layout_box.font_size);
|
|
let rect = offset.apply(&rect);
|
|
self.list.push(DisplayItem::Text {
|
|
rect,
|
|
text: marker.clone(),
|
|
color: layout_box.color,
|
|
font_size: layout_box.font_size,
|
|
font_weight: layout_box.font_weight,
|
|
font_style: layout_box.font_style,
|
|
font_variant: layout_box.font_variant,
|
|
font_family: layout_box.font_family.clone(),
|
|
});
|
|
}
|
|
}
|
|
|
|
fn render_image(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
if let Some(image_id) = layout_box.image_id {
|
|
let content_rect = layout_box.dimensions.content;
|
|
let rect = offset.apply(&content_rect);
|
|
self.list.push(DisplayItem::Image { rect, image_id });
|
|
}
|
|
if let Some(ref iframe) = layout_box.iframe_content {
|
|
let content_rect = layout_box.dimensions.content;
|
|
let rect = offset.apply(&content_rect);
|
|
self.list.push(DisplayItem::IframeContent {
|
|
rect,
|
|
width: iframe.width,
|
|
height: iframe.height,
|
|
pixels: iframe.pixels.clone(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Render block-level and inline-block descendants within an IFC,
|
|
/// traversing through inline wrappers (e.g., `<a>` containing `<img>`)
|
|
/// without re-rendering their backgrounds/borders (already handled by
|
|
/// the IFC fragment system).
|
|
fn render_non_inline_descendants(
|
|
&mut self,
|
|
layout_box: &LayoutBox,
|
|
offset: CumulativeOffset,
|
|
skip_floats: bool,
|
|
phase: RenderPhase,
|
|
) {
|
|
for child in &layout_box.children {
|
|
// Skip non-inline stacking-context children — they have been hoisted
|
|
// to the owning stacking context's paint-order steps 2/7.
|
|
if skip_floats
|
|
&& child.box_type != layout::BoxType::Inline
|
|
&& child.creates_stacking_context()
|
|
{
|
|
continue;
|
|
}
|
|
// Non-inline positioned-auto elements: skip during phased passes
|
|
// (they'll be collected by collect_positioned_auto_descendants for step 6).
|
|
// In Full phase, render via render_stacking_context for Appendix E ordering.
|
|
// Inline positioned elements (e.g., position:relative spans) stay in IFC flow.
|
|
if child.is_positioned()
|
|
&& !child.creates_stacking_context()
|
|
&& child.box_type != layout::BoxType::Inline
|
|
{
|
|
if phase == RenderPhase::Full {
|
|
self.render_stacking_context(child, offset, false);
|
|
}
|
|
continue;
|
|
}
|
|
match child.box_type {
|
|
layout::BoxType::Block
|
|
| layout::BoxType::Flex
|
|
| layout::BoxType::Grid
|
|
| layout::BoxType::Table
|
|
| layout::BoxType::TableRowGroup
|
|
| layout::BoxType::TableRow
|
|
| layout::BoxType::TableCell
|
|
| layout::BoxType::TableCaption
|
|
| layout::BoxType::InlineBlock => {
|
|
// Don't propagate skip_floats into positioned elements
|
|
let child_skip = skip_floats && !child.is_positioned();
|
|
self.render_layout_box_normal(child, offset, child_skip, phase);
|
|
}
|
|
layout::BoxType::Inline => {
|
|
// Absolutely/fixed positioned inline children
|
|
// (e.g. <span style="position:absolute">) get their own
|
|
// formatting context from deferred abs-pos layout.
|
|
// Render them fully so their IFC content appears.
|
|
// Skip those that create stacking contexts (e.g. z-index set)
|
|
// — they are already handled by collect_stacking_participants.
|
|
if (child.position == Position::Absolute || child.position == Position::Fixed)
|
|
&& !child.creates_stacking_context()
|
|
{
|
|
self.render_layout_box_normal(child, offset, false, phase);
|
|
} else {
|
|
self.render_non_inline_descendants(child, offset, skip_floats, phase);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_text(&mut self, layout_box: &LayoutBox, offset: CumulativeOffset) {
|
|
if let Some(ref text) = layout_box.text_content {
|
|
let content_rect = layout_box.dimensions.content;
|
|
let rect = offset.apply(&content_rect);
|
|
self.list.push(DisplayItem::Text {
|
|
rect,
|
|
text: text.clone(),
|
|
color: layout_box.color,
|
|
font_size: layout_box.font_size,
|
|
font_weight: layout_box.font_weight,
|
|
font_style: layout_box.font_style,
|
|
font_variant: layout_box.font_variant,
|
|
font_family: layout_box.font_family.clone(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Compute the sticky positioning offset for a layout box.
|
|
/// `base_offset` is the parent's accumulated offset (includes scroll and ancestor offsets).
|
|
///
|
|
/// Per CSS Position 3 §A.2: when both `top` and `bottom` are specified, `top` wins
|
|
/// for the offset but the result is still clamped by the bottom constraint.
|
|
pub(crate) fn compute_sticky_offset(
|
|
&self,
|
|
layout_box: &LayoutBox,
|
|
base_offset: CumulativeOffset,
|
|
) -> (f32, f32) {
|
|
let Some(ref sc) = layout_box.sticky_constraints else {
|
|
return (0.0, 0.0);
|
|
};
|
|
let margin_box_height = sc.normal_flow_bottom - sc.normal_flow_y;
|
|
let mut offset_y = 0.0;
|
|
|
|
// Top sticky: push element down so its top stays at the threshold
|
|
if let Some(top) = sc.top_threshold {
|
|
let viewport_y = sc.normal_flow_y + base_offset.y;
|
|
if viewport_y < top {
|
|
offset_y = top - viewport_y;
|
|
}
|
|
}
|
|
|
|
// Bottom sticky: push element up so its bottom stays at the threshold.
|
|
// When both top and bottom are set, top wins for the offset value,
|
|
// but bottom still clamps (applied below).
|
|
if let Some(bottom) = sc.bottom_threshold {
|
|
let elem_bottom_viewport = sc.normal_flow_y + margin_box_height + base_offset.y;
|
|
let stick_point = self.viewport_height - bottom;
|
|
if sc.top_threshold.is_none() && elem_bottom_viewport > stick_point {
|
|
offset_y = stick_point - elem_bottom_viewport;
|
|
}
|
|
}
|
|
|
|
// Clamp: for top-sticky, never above normal position (offset >= 0)
|
|
if sc.top_threshold.is_some() {
|
|
offset_y = offset_y.max(0.0);
|
|
}
|
|
|
|
// Clamp: can't push element below containing block bottom
|
|
if offset_y > 0.0 {
|
|
let max_offset = sc.containing_block_bottom - sc.normal_flow_bottom;
|
|
offset_y = offset_y.min(max_offset.max(0.0));
|
|
}
|
|
|
|
// When both top and bottom are set, clamp top-sticky offset so the
|
|
// element's bottom doesn't violate the bottom threshold.
|
|
if let (Some(_top), Some(bottom)) = (sc.top_threshold, sc.bottom_threshold) {
|
|
let elem_bottom_viewport =
|
|
sc.normal_flow_y + margin_box_height + base_offset.y + offset_y;
|
|
let stick_point = self.viewport_height - bottom;
|
|
if elem_bottom_viewport > stick_point {
|
|
let excess = elem_bottom_viewport - stick_point;
|
|
offset_y = (offset_y - excess).max(0.0);
|
|
}
|
|
}
|
|
|
|
(0.0, offset_y)
|
|
}
|
|
|
|
/// Apply render offset (for position: relative) to a rect - used by tests
|
|
pub fn apply_render_offset(&self, rect: &Rect, layout_box: &LayoutBox) -> Rect {
|
|
if layout_box.render_offset_x == 0.0 && layout_box.render_offset_y == 0.0 {
|
|
*rect
|
|
} else {
|
|
Rect::new(
|
|
rect.x() + layout_box.render_offset_x,
|
|
rect.y() + layout_box.render_offset_y,
|
|
rect.width(),
|
|
rect.height(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Convenience functions
|
|
// =============================================================================
|
|
|
|
pub fn build_display_list(tree: &LayoutTree) -> DisplayList {
|
|
DisplayListBuilder::new().build(tree)
|
|
}
|
|
|
|
/// Build a display list with a vertical scroll offset applied.
|
|
///
|
|
/// This is more efficient than rebuilding the layout tree when only the scroll
|
|
/// position changes - we skip the HTML/CSS parsing and layout phases.
|
|
pub fn build_display_list_with_scroll(tree: &LayoutTree, scroll_y: f32) -> DisplayList {
|
|
DisplayListBuilder::with_scroll_offset(scroll_y).build(tree)
|
|
}
|