//! CSS 2.1 8.3.1 margin collapsing, isolated beside `width.rs`'s seam (`roadmap.md` -> //! "Phase 1 -- Engine bootstrap" step 4, the bootstrap order's second category). Two //! plain-data functions a generated candidate implements: //! //! - [`classify`]-shaped ([`MarginClassifyResolver`]): decides, for one box, whether it //! is self-collapsing (CSS 2.1 8.3.1 case 4 -- its own top and bottom margins merge //! into one, since nothing separates them) and whether its top/bottom margin is free //! to adjoin with whatever precedes or follows it -- a parent's own margin (cases 1 //! and 2), a preceding in-flow sibling's margin (case 3), or nothing at all, contained //! by this box's own border/padding or blocked by a new block formatting context //! (`crates/logic/src/facts.rs`'s `establishes_bfc`). //! - [`combine`]-shaped ([`MarginCombineResolver`]): reduces one group of adjoining raw //! margin values to the single value CSS 2.1 8.3.1 says is used -- the largest positive //! margin plus the most negative one. //! //! `crates/layout/src/fragment.rs` owns the structural walk that decides *which* boxes //! belong to the same adjoining group (mirroring `crates/logic/src/facts.rs`'s //! tree-shape facts, computed in plain Rust); these two functions own the CSS-semantic //! judgment calls the walk defers to (mirroring `crates/logic/src/rules.rs`, which //! `docs/margin-collapse-experiment.md` validated the same eight-condition/six-arm shape //! for -- `crates/logic/src/mutants.rs` `Mutant::ALL`). //! //! [`classify_baseline`]/[`combine_baseline`] are the hand rule `fragment.rs` used before //! this extraction: no adjoining, no self-collapsing, nothing escapes -- every margin is //! used exactly as declared. This seam's baseline candidate, not a spec-complete //! implementation (mirrors [`crate::width::resolve_block_width_baseline`]). /// Plain-data inputs to one box's margin-collapsing classification, narrowed to what CSS /// 2.1 8.3.1 reads. `height_is_auto` and `height_is_explicit_zero` are kept separate /// rather than pre-merged into one "zero or auto" flag, since 8.3.1 treats an explicit /// `height: 0` the same as `auto` for self-collapsing /// (`docs/margin-collapse-experiment.md`'s `zero-height-not-self-collapsing` mutant is /// exactly a candidate that only recognises the `auto` case). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MarginCollapseInputs { pub has_top_border_or_padding: bool, pub has_bottom_border_or_padding: bool, /// Whether this box establishes a new block formatting context (non-`visible` /// `overflow-x`/`overflow-y`), which blocks its own margins from collapsing with an /// in-flow child's (`crates/logic/src/facts.rs`'s `establishes_bfc`; the corpus's /// `recipe_collapse_bfc_root`). pub establishes_bfc: bool, pub height_is_auto: bool, pub height_is_explicit_zero: bool, /// True when this box has no in-flow children, or every in-flow child's own /// [`MarginCollapseOutputs::self_collapsing`] came back true -- `fragment.rs` /// computes this from the children's own classification, always available first /// since children are laid out before their parent. pub no_in_flow_content: bool, } /// One box's margin-collapsing classification. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MarginCollapseOutputs { /// CSS 2.1 8.3.1 case 4: this box's own top and bottom margins collapse into one, /// and the box itself takes zero height. pub self_collapsing: bool, /// Whether this box's margin-top is free to adjoin with its own parent's margin-top /// (case 1, only consulted when this box is its parent's first in-flow child) -- /// i.e. whether this box permits its own first in-flow child's margin-top to escape /// past it, from the *parent*'s point of view when this box is acting as that parent. pub top_open: bool, /// Same as `top_open`, for the last in-flow child and the box's own margin-bottom /// (case 2). pub bottom_open: bool, /// Whether this box's margin-top is free to adjoin with a preceding in-flow /// sibling's margin-bottom (case 3). Unconditionally true in a correct /// implementation on this corpus (no floats, no clearance), but exposed as its own /// decision so a candidate can get it wrong /// (`crates/logic/src/mutants.rs` `Mutant::SiblingAdjoinOff`). pub sibling_adjoin: bool, } /// One candidate's classification function: plain data in, plain data out. Every margin /// candidate module the generation loop compiles exports a function of this type named /// `classify`. pub type MarginClassifyResolver = fn(&MarginCollapseInputs) -> MarginCollapseOutputs; /// One candidate's combining function: the raw margin values (signed) entering one /// adjoining group, reduced to the single value CSS 2.1 8.3.1 uses -- the largest /// positive margin plus the most negative one (zero when a side has none). Always called /// with the group's *entire* raw value list at once, never folded pairwise -- folding /// partial results back through this same function is not associative (a positive value /// already summed into a partial result loses its own identity as "a positive" the next /// time the pair is reduced), so `fragment.rs` always collects a whole group before /// calling this once. Every margin candidate module exports a function of this type /// named `combine`. pub type MarginCombineResolver = fn(&[f64]) -> f64; /// The hand rule `fragment.rs` used before this seam existed: nothing adjoins, nothing /// self-collapses, nothing escapes past a box's own border/padding. This seam's baseline /// candidate, not a spec-complete implementation. pub fn classify_baseline(_inputs: &MarginCollapseInputs) -> MarginCollapseOutputs { MarginCollapseOutputs { self_collapsing: false, top_open: false, bottom_open: false, sibling_adjoin: false } } /// [`classify_baseline`]'s counterpart. Under `classify_baseline` no group ever grows /// past one value (nothing ever adjoins), so any function that returns a lone value /// unchanged reproduces the pre-extraction behavior; summing is the simplest one that /// also behaves sensibly if a caller ever mixes this with a `classify` that does let /// groups grow. pub fn combine_baseline(values: &[f64]) -> f64 { values.iter().sum() } #[cfg(test)] mod tests { use super::*; fn inputs() -> MarginCollapseInputs { MarginCollapseInputs { has_top_border_or_padding: false, has_bottom_border_or_padding: false, establishes_bfc: false, height_is_auto: true, height_is_explicit_zero: false, no_in_flow_content: true, } } #[test] fn baseline_never_collapses_or_opens() { let out = classify_baseline(&inputs()); assert!(!out.self_collapsing); assert!(!out.top_open); assert!(!out.bottom_open); assert!(!out.sibling_adjoin); } #[test] fn baseline_combine_passes_a_lone_value_through() { assert_eq!(combine_baseline(&[12.5]), 12.5); assert_eq!(combine_baseline(&[]), 0.0); } }