Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264//! 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<usize>, 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<StackingBox>,}
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<NodeId>) { let mut rects: BTreeMap<NodeId, (f64, f64, f64, f64)> = 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<NodeId> = document .elements_in_document_order() .into_iter() .filter(|node| rects.contains_key(node)) .collect(); let index_of: BTreeMap<NodeId, usize> = 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<usize>,}
/// 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<usize>) -> 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 from_layout_flattens_in_dom_order_with_dom_parents() { let html = "<!doctype html><html><head><style>\ * { margin: 0; padding: 0; border: 0; box-sizing: content-box; }\ html, body { display: block; }\ .c { width: 300px; opacity: 0.5; }\ .a { position: absolute; left: 10px; top: 10px; width: 50px; height: 50px; z-index: 3; }\ .b { width: 100px; height: 40px; }\ </style></head><body><div class=\"c\"><div class=\"a\"></div><div class=\"b\"></div></div></body></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 (inputs, nodes) = StackingInputs::from_layout(&document, &root);
assert_eq!(inputs.boxes.len(), nodes.len()); let tags: Vec<String> = nodes.iter().map(|n| n.tag()).collect(); let c = tags .iter() .position(|t| t.eq_ignore_ascii_case("div")) .unwrap_or_else(|| panic!("the container div is laid out; got {tags:?}")); assert_eq!(tags, ["HTML", "BODY", "DIV", "DIV", "DIV"], "head is never laid out; divs follow in tree order"); let (c_box, a_box, b_box) = (inputs.boxes[c], inputs.boxes[c + 1], inputs.boxes[c + 2]); assert_eq!(a_box.parent, Some(c), "the absolute child keeps its DOM parent"); assert_eq!(b_box.parent, Some(c)); assert_eq!(a_box.position, Position::Absolute); assert_eq!(a_box.z_index, ZIndex::Integer(3)); assert_eq!(c_box.opacity, 0.5); assert_eq!(b_box.position, Position::Static); assert_eq!(b_box.z_index, ZIndex::Auto); assert_eq!((a_box.x, a_box.y, a_box.width, a_box.height), (10.0, 10.0, 50.0, 50.0)); assert_eq!((b_box.x, b_box.y, b_box.width, b_box.height), (0.0, 0.0, 100.0, 40.0)); assert_eq!(c_box.width, 300.0); for b in &inputs.boxes { assert_eq!(b.id, inputs.boxes.iter().position(|x| x == b).unwrap()); } assert_eq!(inputs.boxes.iter().filter(|b| b.parent.is_none()).count(), 1, "one root"); }
#[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 = "<!doctype html><html><head><style>\ * { margin: 0; padding: 0; border: 0; box-sizing: content-box; }\ html, body { display: block; }\ .c { width: 300px; opacity: 0.5; }\ .a { position: absolute; left: 10px; top: 10px; width: 50px; height: 50px; z-index: 3; }\ .b { width: 100px; height: 40px; }\ </style></head><body><div class=\"c\"><div class=\"a\"></div><div class=\"b\"></div></div></body></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<String> = 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"); }}