//! CSS 2.1 10.6 block height resolution, isolated behind one function with plain-data //! inputs and outputs (`roadmap.md` -> "Phase 1 — Engine bootstrap" step 4, "Generated //! algorithms, one category at a time"). A generated candidate implements //! [`HeightResolver`] against the types in this module alone -- no `dom` beyond //! `LengthPercentageOrAuto` and the workspace fixed-point type, no Stylo -- which is what //! lets a candidate crate compile independently of the DOM/style layer and lets the runner //! swap resolvers without touching anything else in `fragment.rs`. //! //! [`resolve_baseline`] is the hand rule `fragment.rs` used before this extraction, //! unchanged: a box whose own `height` is a definite length uses it (10.5/10.6.4, the //! [`definite_content_height`] test), and everything else -- `auto`, a percentage against //! an indefinite basis, an unresolved `calc()` -- falls back to 10.6.3's rule, the distance //! from the content top edge to the bottom margin edge of the last in-flow child, extended //! for a box that establishes a block formatting context to the bottom margin edge of its //! lowest float (10.6.7). It is this seam's baseline candidate, and unlike the other seams' //! baselines it is already correct for every shape the corpus reaches rather than a //! deliberately-wrong placeholder. //! //! Two boundaries a candidate has to know about. //! //! `min-height`/`max-height` clamping (10.7) is **not** applied anywhere in this path and //! never was: the pre-extraction code in `fragment.rs` read neither property, and this //! extraction preserves that rather than adding clamping silently. It is an out-of-scope //! gap of this seam, not a rule a candidate is being asked to reproduce //! (`specs/block-height-equation.md` -> "Scope"), and no corpus case can reward one for //! implementing it, since nothing here hands it a `min-height` to clamp against. //! //! The 1/64px grid (`specs/device-pixel-rounding.md`) is applied on the definite branch //! only, inside [`definite_content_height`], exactly where it was before this extraction -- //! that value is a basis a child's percentage `height` resolves against, so it has to be //! on-grid before it is carried down. The 10.6.3 fallback branch is returned unquantized, //! because its two callers need it at different stages: `fragment.rs`'s `layout_box` //! quantizes the result at the point it becomes that box's used content height, while //! `layout_absolute_box` hands it on as an intermediate to the absolute seam's resolver //! (`crate::absolute::AbsoluteInputs::content_height`), which quantizes downstream of its //! own equation. Quantizing here would move the second caller's value off what CSS 2.1 //! 10.6.4 computed from. use dom::computed::LengthPercentageOrAuto; use dom::px64::{from_px64, to_px64_truncating}; /// Plain-data inputs to the height equation, narrowed to what CSS 2.1 10.6 reads: the /// box's own `height` already resolved past percentages by the percentage seam, its /// `box-sizing` and top/bottom edges (needed to convert a `border-box`-sized `height` back /// to a content height), whether it establishes a block formatting context, the content /// height its stacked in-flow children reached, and the bottom margin edge of the lowest /// float in the context it owns. #[derive(Debug, Clone, Copy, PartialEq)] pub struct HeightInputs { /// This box's own computed `height`, already run through the percentage seam. A /// `Percentage` still standing here is one that found no definite basis to resolve /// against, which 10.5 sends to the same `auto` fallback as `Auto` itself. pub resolved_height: LengthPercentageOrAuto, /// Whether this box establishes a block formatting context, the condition 10.6.7's /// float extension is gated on. A float in an inner context never reaches this box. pub establishes_bfc: bool, /// The distance from this box's content top edge to the bottom margin edge of its last /// in-flow child, as the stacking walk computed it (10.6.3). pub content_height_from_children: f64, /// The bottom margin edge of the lowest float in the block formatting context this box /// owns, measured from its own content top edge, or `None` when it establishes no /// context of its own. A box that establishes one but holds no float carries /// `Some(0.0)`. pub lowest_float_bottom: Option, /// Whether `height` is measured against the border box rather than the content box. pub border_box_sizing: bool, /// `padding-top + padding-bottom + border-top-width + border-bottom-width`, the amount /// a `border-box`-sized `height` has to give back to become a content height. pub edge_tb: f64, } /// The used content height CSS 2.1 10.6 resolves (content-box, independent of /// `box-sizing` -- CSS 2.1's box model predates it). #[derive(Debug, Clone, Copy, PartialEq)] pub struct HeightOutputs { pub content_height: 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 HeightResolver = fn(&HeightInputs) -> HeightOutputs; /// The hand rule `fragment.rs` shipped before this extraction, unchanged: a definite own /// `height` is the used content height, and everything else falls back to 10.6.3's /// last-in-flow-child distance, extended by 10.6.7's lowest float for a block-formatting- /// context root. This is the seam's baseline candidate, and it is already correct for /// every shape the corpus reaches (this module's own doc comment). pub fn resolve_baseline(inputs: &HeightInputs) -> HeightOutputs { let float_bottom = inputs.lowest_float_bottom.unwrap_or(0.0); let content_height = definite_content_height(inputs.resolved_height, inputs.border_box_sizing, inputs.edge_tb) .unwrap_or(inputs.content_height_from_children.max(float_bottom)); HeightOutputs { content_height } } /// Whether this box's own `height` gives it a definite content height, and what that /// height is (CSS 2.1 10.5/10.6.4). `None` is the answer for `auto`, for a percentage the /// percentage seam found no definite basis for, and for anything else unresolved /// (`calc()`, an intrinsic-sizing keyword) -- every shape 10.6.3's fallback is what /// [`resolve_baseline`] reaches for. /// /// `fragment.rs` calls this directly too, before recursing into a box's children, because /// it is also the basis a child's own percentage `height` resolves against and that has to /// be known before the children are laid out -- 10.5 branches on whether this box's own /// height is *specified* as a definite length, which never depends on its children. /// /// Quantized onto the 1/64px grid at the point the basis is chosen, for the same reason the /// width chain is (`fragment.rs`'s `quantize_px` doc comment): this is what a child's /// percentage `height` resolves against, so leaving it off-grid would let the block axis /// compound the way the inline axis did. pub fn definite_content_height( resolved_height: LengthPercentageOrAuto, border_box_sizing: bool, edge_tb: f64, ) -> Option { match resolved_height { LengthPercentageOrAuto::Length(h) if border_box_sizing => Some(quantize_px((h - edge_tb).max(0.0))), LengthPercentageOrAuto::Length(h) => Some(quantize_px(h)), LengthPercentageOrAuto::Auto | LengthPercentageOrAuto::Percentage(_) | LengthPercentageOrAuto::Unresolved => None, } } /// Quantizes one used value onto the 1/64px grid, through the workspace's single /// fixed-point type (`dom::px64`) rather than a second copy of the same arithmetic -- /// the seam's own copy of `fragment.rs`'s `quantize_px`, kept here so a candidate crate /// compiling against this module alone reaches the same grid. fn quantize_px(v: f64) -> f64 { from_px64(to_px64_truncating(v)) } #[cfg(test)] mod tests { use super::*; fn inputs(resolved_height: LengthPercentageOrAuto, content_height_from_children: f64) -> HeightInputs { HeightInputs { resolved_height, establishes_bfc: false, content_height_from_children, lowest_float_bottom: None, border_box_sizing: false, edge_tb: 0.0, } } #[test] fn baseline_falls_back_to_the_last_in_flow_child_when_auto() { let out = resolve_baseline(&inputs(LengthPercentageOrAuto::Auto, 140.0)); assert_eq!(out.content_height, 140.0); } #[test] fn baseline_passes_an_explicit_content_box_height_through() { let out = resolve_baseline(&inputs(LengthPercentageOrAuto::Length(220.0), 140.0)); assert_eq!(out.content_height, 220.0); } #[test] fn baseline_subtracts_the_vertical_edges_under_border_box_sizing() { let mut i = inputs(LengthPercentageOrAuto::Length(220.0), 0.0); i.border_box_sizing = true; i.edge_tb = 20.0; assert_eq!(resolve_baseline(&i).content_height, 200.0); } #[test] fn baseline_extends_an_auto_height_to_the_lowest_float() { let mut i = inputs(LengthPercentageOrAuto::Auto, 40.0); i.establishes_bfc = true; i.lowest_float_bottom = Some(90.0); assert_eq!(resolve_baseline(&i).content_height, 90.0); } #[test] fn baseline_ignores_a_float_shorter_than_the_stacked_children() { let mut i = inputs(LengthPercentageOrAuto::Auto, 140.0); i.establishes_bfc = true; i.lowest_float_bottom = Some(90.0); assert_eq!(resolve_baseline(&i).content_height, 140.0); } #[test] fn baseline_ignores_a_float_when_the_height_is_definite() { let mut i = inputs(LengthPercentageOrAuto::Length(50.0), 140.0); i.establishes_bfc = true; i.lowest_float_bottom = Some(90.0); assert_eq!(resolve_baseline(&i).content_height, 50.0); } /// An unresolved percentage height reaches the same 10.6.3 fallback `auto` does -- /// the fallthrough `specs/block-height-equation.md` puts in scope precisely because /// it already existed rather than because it is new. #[test] fn baseline_falls_back_for_a_percentage_with_no_definite_basis() { let out = resolve_baseline(&inputs(LengthPercentageOrAuto::Percentage(0.5), 140.0)); assert_eq!(out.content_height, 140.0); } #[test] fn definite_content_height_is_none_for_auto() { assert_eq!(definite_content_height(LengthPercentageOrAuto::Auto, false, 0.0), None); } #[test] fn definite_content_height_never_goes_negative_under_border_box_sizing() { assert_eq!(definite_content_height(LengthPercentageOrAuto::Length(10.0), true, 30.0), Some(0.0)); } /// The definite branch lands on the 1/64px grid; the fallback branch is left for the /// caller to quantize (this module's own doc comment). #[test] fn definite_content_height_quantizes_onto_the_grid() { let h = definite_content_height(LengthPercentageOrAuto::Length(10.3), false, 0.0).unwrap(); assert_eq!(h, 659.0 / 64.0); } }