//! CSS 2.1 ยง9.9 and Appendix E: paint order for stacking contexts -- the first generated //! seam whose output is an ordering, not a rectangle (`tasks/stacking-order.md`). One //! plain-data function a generated candidate implements: //! //! - [`resolve`]-shaped ([`StackingResolver`]): given every box in the document as one //! flat, already-laid-out list ([`StackingInputs`]), each carrying its own tree //! position, `position`, `z-index`, `opacity` and absolute (viewport) rect, returns //! the ids of every box in paint order, back to front ([`StackingOutputs`]). //! //! Scope, mirrored from `tasks/stacking-order.md`: block-level boxes in normal flow and //! `position: absolute` boxes (the two kinds `crates/layout/src/fragment.rs` lays out), //! `position` with `z-index: auto` or an integer, stacking contexts established by //! positioned boxes with a non-`auto` `z-index` and by the root, the Appendix E order //! within one context (that context's own background, its negative-`z-index` children, //! its in-flow blocks, its positioned descendants at `z-index: auto`/`0` in tree order, //! then its positive-`z-index` children), and `opacity < 1` as a stacking-context //! trigger only. Out of scope: transforms, `isolation`, `mix-blend-mode`, `will-change`, //! filters, floats, inline content, `pointer-events`, overflow clipping, `visibility`. //! Every box the corpus generates is opaque, non-overflowing and untransformed, so the //! oracle's `elementsFromPoint` probe (`docs/dump-format.md` -> "Stacking probe") reports //! paint order and nothing else. //! //! This module does not change layout: it reads the fragment tree `layout()` already //! produces (`crates/layout/src/fragment.rs` is not edited by this seam), so a box's //! rect here is exactly the border-box rect `fragment::layout` already computed for it. //! [`StackingInputs::from_layout`] is that flattening, and `crate::paint_order` is the //! entry point that runs it and the default resolver together. //! //! [`resolve_baseline`] is the seam's deliberately wrong starting candidate //! (`tasks/stacking-order.md`): every box paints in plain tree order, ignoring //! `position`, `z-index` and `opacity` entirely -- the same "pretend the seam does not //! exist yet" baseline `crate::absolute::resolve_baseline` is for the absolute-positioning //! seam, adapted to an ordering output instead of a rectangle one. use std::collections::BTreeMap; pub use dom::computed::{Position, ZIndex}; use dom::node::NodeId; use dom::Document; use crate::fragment::Fragment; /// One box's plain-data facts, flattened out of the DOM and fragment trees by /// [`StackingInputs::from_layout`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct StackingBox { /// This box's index in the flat list -- also its identity for /// [`StackingOutputs::order`], and stable document-tree order (the order every box /// in the document appears in, depth-first, parent before child -- the same order /// `crates/harness/src/cases.rs` `write_body` assigns per-box classes in, so a /// candidate never needs to see anything about a box beyond what this struct /// already carries to place it correctly). pub id: usize, /// The index (into this same flat list) of this box's parent, or `None` for the /// root. A box's stacking context is always either itself (if it establishes one) /// or its nearest ancestor that does -- a correct [`resolve`] walks `parent` links /// to find it, never assumes flat sibling order already groups boxes by context. pub parent: Option, pub position: Position, pub z_index: ZIndex, pub opacity: f64, /// Border-box rect in absolute (viewport) coordinates, exactly as /// `crate::fragment::Fragment::walk_absolute` already produces for every seam's own /// geometry comparison -- never parent-relative here, since paint order is compared /// across boxes that do not share a parent. pub x: f64, pub y: f64, pub width: f64, pub height: f64, } /// Every box in one document, as a flat list. Order within the list carries no meaning /// of its own beyond what `id`/`parent` already state explicitly -- unlike /// `crate::absolute::AbsoluteInputs`, which is one box's own inputs, this seam's inputs /// are the whole document at once, since paint order is a property of a set of boxes /// competing for the same points, not any one box in isolation. #[derive(Debug, Clone, PartialEq)] pub struct StackingInputs { pub boxes: Vec, } impl StackingInputs { /// Flattens the fragment tree `crate::fragment::layout` produced for `document` into /// one box per laid-out element, in document tree order, returning alongside it the /// element each box index stands for. Tree order and `parent` links follow the DOM /// (`NodeId::dom_parent`), not the fragment tree: an absolutely positioned box is /// laid out under its containing block, but CSS 2.1 Appendix E assigns it to the /// stacking context of its nearest *element* ancestor that establishes one -- an /// `opacity < 1` ancestor that is not positioned, for instance, still captures it. /// Rects are the absolute (viewport) border boxes `Fragment::walk_absolute` reports. /// An element without a fragment (never laid out) gets no box. pub fn from_layout(document: &Document, root: &Fragment) -> (StackingInputs, Vec) { let mut rects: BTreeMap = BTreeMap::new(); root.walk_absolute(0.0, 0.0, &mut |node, x, y, w, h| { rects.entry(node).or_insert((x, y, w, h)); }); let nodes: Vec = document .elements_in_document_order() .into_iter() .filter(|node| rects.contains_key(node)) .collect(); let index_of: BTreeMap = nodes.iter().enumerate().map(|(i, &node)| (node, i)).collect(); let boxes = nodes .iter() .enumerate() .map(|(id, &node)| { let mut parent = node.dom_parent(); while let Some(p) = parent { if index_of.contains_key(&p) { break; } parent = p.dom_parent(); } let (x, y, width, height) = rects[&node]; StackingBox { id, parent: parent.map(|p| index_of[&p]), position: dom::computed::position(document, node), z_index: dom::computed::z_index(document, node), opacity: dom::computed::opacity(document, node), x, y, width, height, } }) .collect(); (StackingInputs { boxes }, nodes) } } /// The paint order every box in [`StackingInputs::boxes`] resolves to, back to front: /// `order[0]` paints first (furthest back), `order.last()` paints last (topmost, in /// front of everything before it). Every `StackingBox::id` appears exactly once. #[derive(Debug, Clone, PartialEq)] pub struct StackingOutputs { pub order: Vec, } /// One candidate's implementation of the seam: plain data in, plain data out. Every /// candidate module the generation loop compiles exports a function of this type named /// `resolve`. pub type StackingResolver = fn(&StackingInputs) -> StackingOutputs; /// The seam's deliberately wrong starting candidate (this module's own doc comment): /// paints every box in plain tree order (the order it already appears in /// [`StackingInputs::boxes`]), ignoring `position`, `z-index` and `opacity` outright -- /// correct only for the cases where tree order already happens to be paint order (no /// positioning, no z-index, no opacity-triggered context in play). pub fn resolve_baseline(inputs: &StackingInputs) -> StackingOutputs { StackingOutputs { order: inputs.boxes.iter().map(|b| b.id).collect() } } #[cfg(test)] mod tests { use super::*; fn plain_box(id: usize, parent: Option) -> StackingBox { StackingBox { id, parent, position: Position::Static, z_index: ZIndex::Auto, opacity: 1.0, x: 0.0, y: 0.0, width: 10.0, height: 10.0, } } #[test] fn baseline_paints_in_plain_tree_order() { let inputs = StackingInputs { boxes: vec![plain_box(0, None), plain_box(1, Some(0)), plain_box(2, Some(0))], }; let out = resolve_baseline(&inputs); assert_eq!(out.order, vec![0, 1, 2]); } #[test] fn paint_order_defaults_to_the_admitted_candidate() { // Same document as `from_layout_flattens_in_dom_order_with_dom_parents`: the // opacity container establishes a context at level 0 under the root; inside it // the static child paints before the z-index:3 absolute child, whatever their // tree order. Back to front: html, body, container, static child, absolute child. let html = "
"; let document = Document::parse(html); let tree = crate::boxtree::BoxTree::build(&document); let root = crate::fragment::layout(&document, &tree, (800.0, 600.0)); let order = crate::paint_order(&document, &root); let classes: Vec = order .iter() .map(|n| { document.nodes()[n.index()] .as_element() .and_then(|e| e.attr("class")) .unwrap_or(&n.tag()) .to_string() }) .collect(); assert_eq!(classes, ["HTML", "BODY", "c", "b", "a"]); let baseline = crate::paint_order_with(&document, &root, resolve_baseline); assert_ne!(order, baseline, "the admitted candidate reorders what the baseline would not"); } #[test] fn baseline_ignores_z_index_and_still_uses_tree_order() { let mut boxes = vec![plain_box(0, None), plain_box(1, Some(0)), plain_box(2, Some(0))]; boxes[1].position = Position::Absolute; boxes[1].z_index = ZIndex::Integer(5); let inputs = StackingInputs { boxes }; let out = resolve_baseline(&inputs); assert_eq!(out.order, vec![0, 1, 2], "baseline never reorders on z-index"); } }