//! CSS 2.1 §10.1's four rules for which box is a given box's containing block, isolated //! as a seam the way `crate::absolute` isolates §10.3.7/§10.6.4's equations. One //! plain-data function a generated candidate implements: //! //! - [`resolve`]-shaped ([`ContainingBlockResolver`]): given a box's own `position`, its //! ancestors nearest-first (each with its own `position`, whether it is a block //! container, and its content-box and padding-box rects), and the viewport rect, //! returns the rect that box's containing block covers, which §10.1 rule chose it, and //! which ancestor it belongs to. //! //! The four rules, as §10.1 states them: //! //! 1. The root element's containing block is the initial containing block -- a rectangle //! the size of the viewport, anchored at the canvas origin. //! 2. A `static` or `relative` box's containing block is the content edge of its nearest //! block-container ancestor. //! 3. A `fixed` box's containing block is the viewport. //! 4. An `absolute` box's containing block is the padding edge of the nearest ancestor //! whose `position` is not `static`; if there is no such ancestor, the initial //! containing block. //! //! Rules 3 and 4 are the pair that decides geometry in this engine: `crate::fragment` //! takes a box out of normal flow exactly when its `position` is `absolute` or `fixed` //! (`dom::computed::Position::is_out_of_flow`), and asks this seam which rect to solve //! §10.3.7/§10.6.4 against. Rules 1 and 2 are the rest of the function's domain -- an //! in-flow box's containing block, which normal flow already produces by construction -- //! and a candidate has to get them right for the corpus's own static and root cases, not //! because `fragment.rs` calls this seam to lay an in-flow box out. //! //! Every rect here is in viewport coordinates: the x/y of the root element's border box //! is the canvas origin, and `fragment.rs` builds the ancestor chain by accumulating each //! box's placement on the way up. The rect [`resolve`] returns is read in the same frame. //! //! Scope. In: `position: fixed` on block-level, non-replaced boxes with no text; the four //! §10.1 rules above; a fixed box escaping positioned ancestors, `relative` and //! `absolute` alike. Out: scrolling and scroll position (the oracle never scrolls, so a //! fixed box and a box against the initial containing block land in the same place at //! scroll 0); `transform`/`filter`/`contain` making an ancestor the containing block for //! its fixed descendants, which is CSS Transforms rather than CSS 2.1; //! `position: sticky`, which `dom::computed::position` still maps to `Static`; paged //! media, where §10.1 rule 3 names the page area instead of the viewport. //! //! [`resolve_baseline`] is the seam's deliberately wrong starting candidate: a `fixed` //! box is treated as a `static` one, sent to rule 2's nearest block container instead of //! the viewport -- the engine's behaviour before this seam existed, when //! `dom::computed::position` mapped `fixed` to `Static` and a fixed box was laid out in //! normal flow. It also answers rule 4's no-positioned-ancestor fallback with the root //! element's own padding box rather than the initial containing block, which is what //! `fragment.rs` did for an absolutely positioned box that reached the tree root. use dom::computed::Position; /// One box's rect, in viewport coordinates (this module's own doc comment). #[derive(Debug, Clone, Copy, PartialEq)] pub struct Rect { pub x: f64, pub y: f64, pub width: f64, pub height: f64, } /// One ancestor of the box whose containing block is being resolved, as CSS 2.1 §10.1 /// needs to see it: what kind of positioning scheme it uses, whether it is the kind of /// box rule 2 can land on, and the two edges rules 2 and 4 name. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AncestorBox { pub position: Position, /// Whether this ancestor is a block container -- the only kind of box CSS 2.1 §10.1 /// rule 2 lets a `static` or `relative` box's containing block be. Every box this /// engine builds is one today (there is no inline or table layout yet), so a /// candidate that reads it is right about the rule rather than about this corpus. pub is_block_container: bool, /// Whether this ancestor is the root element (``) -- the box CSS 2.1 §10.1 /// rule 1 gives the initial containing block to, and the last entry in any complete /// ancestor chain. pub is_root: bool, pub content_box: Rect, pub padding_box: Rect, } /// Plain-data inputs to one box's containing-block choice. #[derive(Debug, Clone, PartialEq)] pub struct ContainingBlockInputs { /// The box's own positioning scheme -- what CSS 2.1 §10.1 branches on first. pub position: Position, /// Every ancestor of the box, nearest first, ending at the root element. Empty for /// the root element itself, which has no ancestor box and takes rule 1. pub ancestors: Vec, /// The initial containing block: a viewport-sized rectangle at the canvas origin. pub viewport: Rect, } /// Which CSS 2.1 §10.1 rule chose the rect, carried for reporting rather than for /// geometry -- `fragment.rs` reads [`ContainingBlockOutputs::rect`] and /// [`ContainingBlockOutputs::ancestor`], never this. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContainingBlockRule { /// Rule 1: the box is the root element, so its containing block is the initial /// containing block. RootElement, /// Rule 2: a `static` or `relative` box takes the content edge of its nearest /// block-container ancestor. NearestBlockContainerContentEdge, /// Rule 3: a `fixed` box takes the viewport. Viewport, /// Rule 4: an `absolute` box takes the padding edge of the nearest ancestor whose /// `position` is not `static`. NearestPositionedAncestorPaddingEdge, /// Rule 4's fallback: an `absolute` box with no such ancestor takes the initial /// containing block. InitialContainingBlock, } /// The chosen containing block. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ContainingBlockOutputs { /// The rect the containing block covers, in viewport coordinates. pub rect: Rect, pub rule: ContainingBlockRule, /// Which ancestor `rect` belongs to, as an index into /// [`ContainingBlockInputs::ancestors`], or `None` when the rect is the initial /// containing block and belongs to no box. Reported, not consumed: `fragment.rs` reads /// `rect` alone and always hangs the resolved box's fragment from its DOM parent, /// since that is where a browser's own `getClientRects` measures it from. pub ancestor: Option, } /// 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 ContainingBlockResolver = fn(&ContainingBlockInputs) -> ContainingBlockOutputs; /// The seam's deliberately wrong starting candidate (this module's own doc comment): a /// `fixed` box goes to rule 2's nearest block container as if it were `static`, and rule /// 4's fallback lands on the root element's padding box rather than the initial /// containing block. pub fn resolve_baseline(inputs: &ContainingBlockInputs) -> ContainingBlockOutputs { let Some(last) = inputs.ancestors.len().checked_sub(1) else { return ContainingBlockOutputs { rect: inputs.viewport, rule: ContainingBlockRule::RootElement, ancestor: None, }; }; match inputs.position { Position::Absolute => { let found = inputs .ancestors .iter() .position(|a| a.position != Position::Static) .unwrap_or(last); ContainingBlockOutputs { rect: inputs.ancestors[found].padding_box, rule: ContainingBlockRule::NearestPositionedAncestorPaddingEdge, ancestor: Some(found), } } Position::Static | Position::Relative | Position::Fixed => { let found = inputs.ancestors.iter().position(|a| a.is_block_container).unwrap_or(last); ContainingBlockOutputs { rect: inputs.ancestors[found].content_box, rule: ContainingBlockRule::NearestBlockContainerContentEdge, ancestor: Some(found), } } } } #[cfg(test)] mod tests { use super::*; const VIEWPORT: Rect = Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 }; fn ancestor(position: Position, is_root: bool, x: f64, y: f64) -> AncestorBox { AncestorBox { position, is_block_container: true, is_root, content_box: Rect { x: x + 10.0, y: y + 10.0, width: 200.0, height: 100.0 }, padding_box: Rect { x, y, width: 220.0, height: 120.0 }, } } fn inputs(position: Position, ancestors: Vec) -> ContainingBlockInputs { ContainingBlockInputs { position, ancestors, viewport: VIEWPORT } } #[test] fn the_root_element_takes_the_initial_containing_block() { let out = resolve_baseline(&inputs(Position::Static, Vec::new())); assert_eq!(out.rect, VIEWPORT); assert_eq!(out.rule, ContainingBlockRule::RootElement); assert_eq!(out.ancestor, None); } #[test] fn baseline_sends_an_absolute_box_to_the_nearest_positioned_ancestors_padding_edge() { let chain = vec![ ancestor(Position::Static, false, 5.0, 5.0), ancestor(Position::Relative, false, 40.0, 60.0), ancestor(Position::Static, true, 0.0, 0.0), ]; let out = resolve_baseline(&inputs(Position::Absolute, chain.clone())); assert_eq!(out.ancestor, Some(1)); assert_eq!(out.rect, chain[1].padding_box); } #[test] fn baseline_falls_back_to_the_root_boxs_padding_edge_not_the_viewport() { let chain = vec![ ancestor(Position::Static, false, 5.0, 5.0), ancestor(Position::Static, true, 0.0, 0.0), ]; let out = resolve_baseline(&inputs(Position::Absolute, chain.clone())); assert_eq!(out.ancestor, Some(1), "the root box, which is what fragment.rs used to do"); assert_eq!(out.rect, chain[1].padding_box); assert_ne!(out.rect, VIEWPORT); } #[test] fn baseline_treats_a_fixed_box_as_a_static_one() { let chain = vec![ ancestor(Position::Relative, false, 40.0, 60.0), ancestor(Position::Static, true, 0.0, 0.0), ]; let out = resolve_baseline(&inputs(Position::Fixed, chain.clone())); assert_eq!(out.rect, chain[0].content_box, "the nearest block container's content edge"); assert_ne!(out.rect, VIEWPORT, "not the viewport, which is what rule 3 asks for"); assert_eq!(out.rule, ContainingBlockRule::NearestBlockContainerContentEdge); } }