//! CSS 2.1 absolute positioning for block-level, non-replaced boxes with no text -- //! §9.3.1 (positioning schemes), §9.3.2 (`top`/`right`/`bottom`/`left`), §10.1 (the //! containing block of an absolutely positioned box), §10.3.7 (width) and §10.6.4 //! (height) -- isolated beside `percentage.rs`/`width.rs`'s seams (`roadmap.md` -> //! "Proof of concept", claim 3: the first seam attempted by the model before any //! hand-written candidate exists). One plain-data function a generated candidate //! implements: //! //! - [`resolve`]-shaped ([`AbsoluteResolver`]): given the box's own (already //! percentage-resolved) offsets, size, margins, padding, border and box-sizing, plus //! its containing block's padding-box size and the static position a hypothetical //! in-flow box would have occupied, returns the used `left`/`top` (offsets from the //! containing block's padding-box origin to the box's margin edge -- `crate::width`'s //! `WidthOutputs::used_margin_left` is added on top by the caller the same way, not //! folded in here), the used content-box `width`/`height`, and the used margins (CSS //! 2.1 10.3.7's centering rule can resolve an `auto` margin to something other than //! zero, unlike the width equation's over-constraint rule, which only ever *discards* //! a margin). //! //! `crates/layout/src/fragment.rs` owns which box is the containing block for which //! absolutely positioned descendant, resolving every percentage in this seam's inputs //! against that containing block's own padding-box size before calling [`resolve`] -- //! this seam's candidate sees `Length` or `Auto` only, never `Percentage` (mirroring why //! `percentage.rs` exists as its own seam rather than being folded into this one: a //! candidate here is not asked to also resolve percentages). `content_height` is `None` //! unless the box's own height needs its children's content to be known first (CSS 2.1 //! 10.6.4's content-based auto-height rule, the absolute-positioning analogue of //! `crate::percentage`'s auto-containing-block-height case) -- `fragment.rs` lays the //! box's children out against the used width this same function returns from an earlier //! call with `content_height: None` before calling it again with `content_height: Some`, //! the same two-call shape `crate::percentage`'s own auto-height basis needs from its //! caller, just spread across two calls to this one function instead of one call each to //! two different resolvers. //! //! [`resolve_baseline`] is the seam's deliberately wrong starting candidate //! (`handoff.md`): every offset is ignored and the box is placed at its static position //! with whatever size its own `width`/`height` already give it in-flow -- exactly what //! `fragment.rs` did before this seam existed, since Phase 0's box tree already gives an //! absolutely positioned element a box (`crates/layout/src/boxtree.rs` is unchanged by //! this seam) but the structural half had nothing to place it against yet. Unlike every //! other seam in this codebase, no hand-written `correct` implementation is written //! before a model has had a chance at this one (`roadmap.md` -> "Proof of concept", //! claim 3). use dom::computed::LengthPercentageOrAuto; use crate::width::Direction; /// Plain-data inputs to one absolutely positioned box's placement. Every /// `LengthPercentageOrAuto` field here is already resolved past `Percentage` by /// `fragment.rs` (this module's own doc comment) -- a correct [`resolve`] never needs to /// see the containing block's size to interpret one of these fields, only to solve the /// CSS 2.1 10.3.7/10.6.4 equations once `Auto` is the shape in play. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AbsoluteInputs { /// The nearest positioned ancestor's padding-box width, or the initial containing /// block's (the viewport's) when there is none (CSS 2.1 10.1). pub containing_block_width: f64, pub containing_block_height: f64, /// The offset from the containing block's padding-box origin to the left/top margin /// edge of the hypothetical box this element would have generated had its `position` /// been `static` (CSS 2.1 10.3.7/10.6.4's static-position fallback, consulted only /// when both `left`/`right` -- respectively `top`/`bottom` -- are `Auto`). pub static_left: f64, pub static_top: f64, pub top: LengthPercentageOrAuto, pub right: LengthPercentageOrAuto, pub bottom: LengthPercentageOrAuto, pub left: LengthPercentageOrAuto, pub width: LengthPercentageOrAuto, pub height: LengthPercentageOrAuto, pub margin_left: LengthPercentageOrAuto, pub margin_right: LengthPercentageOrAuto, pub margin_top: LengthPercentageOrAuto, pub margin_bottom: LengthPercentageOrAuto, pub padding_left: f64, pub padding_right: f64, pub padding_top: f64, pub padding_bottom: f64, pub border_left: f64, pub border_right: f64, pub border_top: f64, pub border_bottom: f64, pub border_box_sizing: bool, /// The containing block's `direction`, the property CSS 2.1 10.3.7's over-constraint /// rule branches on (mirroring `crate::width::WidthInputs::direction`): in `Ltr`, /// `right` is the offset discarded when `left`/`width`/`right` are all given; in /// `Rtl`, `left` is. pub direction: Direction, /// The box's own used content height, once known -- `Some` only on a second call for /// a box whose height depends on its children's content (this module's own doc /// comment), `None` on every other call, including a box's first call and any box /// whose height never needs it. pub content_height: Option, } /// The used `left`/`top` (offsets from the containing block's padding-box origin to this /// box's margin edge), used content-box `width`/`height`, and used margins CSS 2.1 /// 10.3.7/10.6.4 resolve. `fragment.rs` adds `margin_left`/`margin_top` on top of /// `left`/`top` to place the box's border-box edge, the same split /// `crate::width::WidthOutputs` already uses for an in-flow box's own margin-left. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AbsoluteOutputs { pub left: f64, pub top: f64, pub used_width: f64, pub used_height: f64, pub margin_left: f64, pub margin_right: f64, pub margin_top: f64, pub margin_bottom: f64, } /// 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 AbsoluteResolver = fn(&AbsoluteInputs) -> AbsoluteOutputs; fn length_or_auto_zero(lp: LengthPercentageOrAuto) -> f64 { match lp { LengthPercentageOrAuto::Length(px) => px, LengthPercentageOrAuto::Auto | LengthPercentageOrAuto::Percentage(_) | LengthPercentageOrAuto::Unresolved => 0.0, } } /// The seam's deliberately wrong starting candidate (this module's own doc comment): /// every offset (`top`/`right`/`bottom`/`left`) is ignored outright, and the box is /// placed at its static position with whatever `width`/`height` it would have used /// in-flow -- an explicit length folds `border_box_sizing` back to a content size the /// same way `crate::width::resolve_block_width_baseline` does, and every other shape /// (`Auto`, an unresolved percentage) falls back to zero, since this baseline does not /// attempt the width/height equations at all. pub fn resolve_baseline(inputs: &AbsoluteInputs) -> AbsoluteOutputs { let edge_lr = inputs.padding_left + inputs.padding_right + inputs.border_left + inputs.border_right; let edge_tb = inputs.padding_top + inputs.padding_bottom + inputs.border_top + inputs.border_bottom; let used_width = match inputs.width { LengthPercentageOrAuto::Length(w) if inputs.border_box_sizing => (w - edge_lr).max(0.0), LengthPercentageOrAuto::Length(w) => w, LengthPercentageOrAuto::Auto | LengthPercentageOrAuto::Percentage(_) | LengthPercentageOrAuto::Unresolved => { 0.0 } }; let used_height = match inputs.height { LengthPercentageOrAuto::Length(h) if inputs.border_box_sizing => (h - edge_tb).max(0.0), LengthPercentageOrAuto::Length(h) => h, LengthPercentageOrAuto::Auto | LengthPercentageOrAuto::Percentage(_) | LengthPercentageOrAuto::Unresolved => { 0.0 } }; AbsoluteOutputs { left: inputs.static_left, top: inputs.static_top, used_width, used_height, margin_left: length_or_auto_zero(inputs.margin_left), margin_right: length_or_auto_zero(inputs.margin_right), margin_top: length_or_auto_zero(inputs.margin_top), margin_bottom: length_or_auto_zero(inputs.margin_bottom), } } #[cfg(test)] mod tests { use super::*; fn inputs() -> AbsoluteInputs { AbsoluteInputs { containing_block_width: 800.0, containing_block_height: 600.0, static_left: 10.0, static_top: 20.0, top: LengthPercentageOrAuto::Length(50.0), right: LengthPercentageOrAuto::Auto, bottom: LengthPercentageOrAuto::Auto, left: LengthPercentageOrAuto::Length(30.0), width: LengthPercentageOrAuto::Length(100.0), height: LengthPercentageOrAuto::Length(40.0), margin_left: LengthPercentageOrAuto::Length(0.0), margin_right: LengthPercentageOrAuto::Length(0.0), margin_top: LengthPercentageOrAuto::Length(0.0), margin_bottom: LengthPercentageOrAuto::Length(0.0), padding_left: 0.0, padding_right: 0.0, padding_top: 0.0, padding_bottom: 0.0, border_left: 0.0, border_right: 0.0, border_top: 0.0, border_bottom: 0.0, border_box_sizing: false, direction: Direction::Ltr, content_height: None, } } #[test] fn baseline_ignores_every_offset_and_uses_the_static_position() { let out = resolve_baseline(&inputs()); assert_eq!(out.left, 10.0, "the static position, not the explicit left:30px"); assert_eq!(out.top, 20.0, "the static position, not the explicit top:50px"); assert_eq!(out.used_width, 100.0); assert_eq!(out.used_height, 40.0); } #[test] fn baseline_folds_border_box_sizing_back_to_a_content_size() { let mut i = inputs(); i.border_box_sizing = true; i.padding_left = 5.0; i.padding_right = 5.0; i.border_top = 2.0; i.border_bottom = 2.0; let out = resolve_baseline(&i); assert_eq!(out.used_width, 90.0, "100 - 2*5 padding"); assert_eq!(out.used_height, 36.0, "40 - 2*2 border"); } }