//! CSS 2.1 inline formatting contexts over atomic inlines with no text -- §9.4.2 (line //! boxes and how inline-level boxes are distributed among them), §10.8 and §10.8.1 //! (`line-height`, the strut, and `vertical-align`), and the §10.6.3 auto height a stack //! of line boxes gives its container -- isolated behind one function with plain-data //! inputs and outputs (`roadmap.md` -> "Phase 1 — Engine bootstrap" step 4). A generated //! candidate implements [`InlineAtomicResolver`] against the types in this module alone: //! //! - [`resolve`-shaped](InlineAtomicResolver): given the container's content width, its //! strut `line-height` and `text-align`, and, per atomic inline in document order, its //! margin-box width and height, its own baseline measured from its margin-box top and //! its `vertical-align`, returns where each atomic inline's margin box goes in the //! container's content box, the top, height and baseline of each line box, the total //! height of the stack, and the last line box's baseline for an enclosing //! inline-block. //! //! The container is a block container whose in-flow children are all `display: //! inline-block` with explicit `width` and `height`, which is the one kind of inline //! content that needs no font (`specs/inline-atomic.md`). `crates/layout/src/boxtree.rs` //! decides which containers those are ([`boxtree::Box::inline_level`](crate::boxtree::Box::inline_level)), //! and `crates/layout/src/fragment.rs` lays each atomic inline out as a block formatting //! context root at its explicit size before handing the measurements here -- a candidate //! never lays anything out itself, it only places boxes whose sizes are already known. //! //! Two things a candidate is not asked to do, because `fragment.rs` has already done //! them. A `vertical-align` percentage is resolved against the box's own `line-height` //! before it arrives, so [`VerticalAlign::Length`] here always carries //! `LengthPercentage::Length` (the same already-resolved discipline //! `crate::absolute::AbsoluteInputs` keeps). And an inline-block's own baseline is //! measured for it: §10.8.1 puts that baseline at its last in-flow line box's, or at its //! bottom margin edge when it has no line box or its `overflow` computes to something //! other than `visible`, and that choice is `fragment.rs`'s, since only `fragment.rs` can //! see inside the box. //! //! **The strut, and why no case here depends on a font.** §10.8 starts every line box //! with a zero-width inline box carrying the container's own font and `line-height` -- the //! strut. Its ascent and descent place the baseline inside the line height, so in general //! the strut reads a font. Every container in this function's corpus sets `font-size: 0` //! with an explicit `line-height` in px //! (`crates/harness/src/inline_atomic_cases.rs`'s own module doc), which makes the //! strut's ascent and descent both zero: its content area is empty, the half-leading is //! split equally above and below it, and so the strut's baseline sits exactly at the //! middle of [`InlineAtomicInputs::line_height`]. `middle` then aligns on the baseline //! itself, since the parent's x-height is zero too. That is the whole of what //! [`line_height`](InlineAtomicInputs::line_height) means to a candidate: a minimum line //! box height whose baseline is at its midpoint. //! //! Scope. In: one line of atomic inlines and the line breaking that starts a new one when //! the next box does not fit; an atomic inline wider than the container placed alone on //! its own line; `vertical-align: baseline | top | bottom | middle | | //! `; `line-height` on the container as the strut; `text-align: left | right //! | center`; margins, padding and borders on the atomic inlines; an inline-block's own //! baseline, and so inline-blocks nested inside inline-blocks; and the container's auto //! height as the sum of its line boxes. //! //! Out, and not modelled anywhere in this module: text and every font-dependent value //! (`sub`, `super`, `text-top`, `text-bottom`, `line-height: normal` over a non-zero font //! size); `width: auto` inline-blocks, whose shrink-to-fit width belongs to //! `min-max-content`; a container mixing block-level and inline-level children, which //! CSS 2.1 §9.2.1.1 wraps in anonymous block boxes; floats and absolutely positioned //! boxes inside the inline formatting context; `text-align: justify`; bidi and vertical //! writing modes; `inline-table`, `inline-flex` and replaced elements. //! //! [`resolve_baseline`] is this function's deliberately wrong starting candidate: every //! atomic inline is stacked vertically at the container's left content edge, one per //! line, which is exactly what the engine did before this module existed -- `boxtree.rs` //! gave a `display: inline-block` element one block-level box and `fragment.rs` stacked //! it in normal flow like any other. use dom::computed::{LengthPercentage, TextAlign, VerticalAlign}; /// One atomic inline, measured. Sizes are margin-box sizes: §10.8 takes the height of an /// inline-block in a line box to be the height of its margin box, and the horizontal /// margins, borders and padding between two atomic inlines are respected the same way /// (§9.4.2), so a candidate never needs the box's separate edges. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AtomicInline { /// The box's margin-box width: content width plus its own padding, borders and /// horizontal margins. This is the width it takes up on a line. pub margin_box_width: f64, /// The box's margin-box height, which §10.8 uses as its height in the line box. pub margin_box_height: f64, /// This box's own baseline, as a distance down from its margin-box top. §10.8.1: for /// an inline-block it is the baseline of its last in-flow line box, or its bottom /// margin edge -- equal to /// [`margin_box_height`](AtomicInline::margin_box_height) -- when it has no in-flow /// line box or its `overflow` computes to something other than `visible`. /// `fragment.rs` has already made that choice (this module's own doc comment). pub baseline: f64, /// This box's computed `vertical-align`, with any percentage already resolved /// against its own `line-height` into [`VerticalAlign::Length`] (this module's own /// doc comment). [`VerticalAlign::Other`] names the font-dependent keywords this /// function does not model and never reaches a candidate. pub vertical_align: VerticalAlign, } /// Plain-data inputs to one inline formatting context, narrowed to what §9.4.2 and §10.8 /// read. #[derive(Debug, Clone, PartialEq)] pub struct InlineAtomicInputs { /// The container's own content width. Every line box is this wide -- no float can /// narrow one here, floats inside an inline formatting context being out of scope -- /// so it is both the space a line has to fill and the width `text-align` distributes /// leftover space within. pub content_width: f64, /// The container's computed `line-height` in CSS pixels: the strut's height, and so /// the minimum height of every line box, with the strut's baseline at its midpoint /// (this module's own doc comment). pub line_height: f64, /// The container's computed `text-align`, which §9.4.2 applies to the leftover space /// on a line. [`TextAlign::Other`] -- `justify` above all -- is out of scope and /// never reaches a candidate. pub text_align: TextAlign, /// Every in-flow atomic inline the container holds, in document order. pub inlines: Vec, } /// Where one atomic inline's margin box goes, as an offset from the container's /// content-box origin. `fragment.rs` adds the box's own top-left margin back out to reach /// the border box it actually places, the same split /// `crate::absolute::AbsoluteOutputs` uses between `left`/`top` and `margin_left`. #[derive(Debug, Clone, Copy, PartialEq)] pub struct InlinePlacement { pub x: f64, pub y: f64, } /// One line box. §9.4.2 stacks line boxes with no vertical separation and never overlaps /// them, so `top` is the previous line box's `top + height` for every line but the first, /// whose `top` is zero. #[derive(Debug, Clone, Copy, PartialEq)] pub struct LineBox { /// The line box's top edge, as a distance down from the container's content-box top. pub top: f64, /// The line box's height: §10.8's distance from the uppermost box top to the /// lowermost box bottom, never less than the strut's /// [`line_height`](InlineAtomicInputs::line_height). pub height: f64, /// This line box's baseline, as a distance down from its own /// [`top`](LineBox::top) -- the baseline every `vertical-align: baseline` box on the /// line, and the strut, align on. pub baseline: f64, } /// Where every atomic inline and every line box ended up, and what the container's own /// height and baseline become. #[derive(Debug, Clone, PartialEq)] pub struct InlineAtomicOutputs { /// One placement per entry of [`InlineAtomicInputs::inlines`], in the same order. pub placements: Vec, /// The line boxes, top to bottom. Empty when the container holds no atomic inline at /// all -- §9.4.2 generates no line box for content that is not there. pub lines: Vec, /// The height of the whole stack of line boxes, which §10.6.3 makes the container's /// content height when its own `height` is `auto`. The last line box's `top + /// height`, and zero when there is no line box. pub total_height: f64, /// The last line box's baseline, as a distance down from the container's content-box /// top -- what §10.8.1 makes the container's own baseline when the container is /// itself an inline-block. `None` when there is no line box, which sends §10.8.1 to /// its bottom-margin-edge fallback instead. pub last_baseline: Option, } /// One candidate's implementation: plain data in, plain data out. Every candidate module /// the generation loop compiles exports a function of this type named `resolve`. pub type InlineAtomicResolver = fn(&InlineAtomicInputs) -> InlineAtomicOutputs; /// This function's deliberately wrong starting candidate (this module's own doc comment): /// no line box ever holds more than one atomic inline, every box sits at the container's /// left content edge, and `vertical-align`, `text-align` and the strut are all ignored -- /// the vertical stacking the engine gave a `display: inline-block` box before this module /// existed. pub fn resolve_baseline(inputs: &InlineAtomicInputs) -> InlineAtomicOutputs { let mut placements = Vec::with_capacity(inputs.inlines.len()); let mut lines = Vec::with_capacity(inputs.inlines.len()); let mut y = 0.0; for inline in &inputs.inlines { placements.push(InlinePlacement { x: 0.0, y }); lines.push(LineBox { top: y, height: inline.margin_box_height, baseline: inline.baseline }); y += inline.margin_box_height; } let last_baseline = lines.last().map(|line| line.top + line.baseline); InlineAtomicOutputs { placements, lines, total_height: y, last_baseline } } /// A `vertical-align` whose percentage has already been resolved, as the plain shift in /// CSS pixels it stands for, or `None` for a keyword that is not a shift. Positive raises /// the box above the parent's baseline, which is the direction §10.8.1 gives both /// `` and ``. A helper rather than part of the function's own data, /// kept here so a candidate crate compiling against this module alone can reach it. pub fn baseline_shift(vertical_align: VerticalAlign) -> Option { match vertical_align { VerticalAlign::Length(LengthPercentage::Length(px)) => Some(px), VerticalAlign::Baseline => Some(0.0), VerticalAlign::Length(LengthPercentage::Percentage(_)) | VerticalAlign::Length(LengthPercentage::Unresolved) | VerticalAlign::Top | VerticalAlign::Bottom | VerticalAlign::Middle | VerticalAlign::Other => None, } } #[cfg(test)] mod tests { use super::*; fn inline(width: f64, height: f64) -> AtomicInline { AtomicInline { margin_box_width: width, margin_box_height: height, baseline: height, vertical_align: VerticalAlign::Baseline, } } fn inputs(inlines: Vec) -> InlineAtomicInputs { InlineAtomicInputs { content_width: 300.0, line_height: 20.0, text_align: TextAlign::Left, inlines, } } #[test] fn baseline_gives_every_box_its_own_line() { let out = resolve_baseline(&inputs(vec![inline(50.0, 10.0), inline(60.0, 30.0)])); assert_eq!(out.placements, vec![ InlinePlacement { x: 0.0, y: 0.0 }, InlinePlacement { x: 0.0, y: 10.0 }, ]); assert_eq!(out.lines.len(), 2, "two boxes that would fit side by side, stacked anyway"); assert_eq!(out.total_height, 40.0); } /// The wrongness that matters most: two boxes narrow enough to share a line never do. #[test] fn baseline_never_puts_two_boxes_on_one_line() { let out = resolve_baseline(&inputs(vec![inline(10.0, 10.0), inline(10.0, 10.0)])); assert_eq!(out.placements[1].y, 10.0, "below the first, not beside it"); assert_eq!(out.placements[1].x, 0.0); } /// The strut is ignored outright: a line box shorter than `line-height` stays short. #[test] fn baseline_ignores_the_strut() { let out = resolve_baseline(&inputs(vec![inline(10.0, 4.0)])); assert_eq!(out.lines[0].height, 4.0, "not the 20px line-height"); assert_eq!(out.total_height, 4.0); } #[test] fn baseline_ignores_text_align() { let mut i = inputs(vec![inline(10.0, 10.0)]); i.text_align = TextAlign::Right; assert_eq!(resolve_baseline(&i).placements[0].x, 0.0, "not flush right"); } #[test] fn no_inlines_is_no_line_box() { let out = resolve_baseline(&inputs(Vec::new())); assert!(out.lines.is_empty()); assert_eq!(out.total_height, 0.0); assert_eq!(out.last_baseline, None); } #[test] fn the_last_baseline_is_measured_from_the_content_top() { let out = resolve_baseline(&inputs(vec![inline(10.0, 10.0), inline(10.0, 30.0)])); assert_eq!(out.last_baseline, Some(40.0), "the second line's top plus its own baseline"); } #[test] fn baseline_shift_reads_a_resolved_length_and_nothing_else() { assert_eq!(baseline_shift(VerticalAlign::Length(LengthPercentage::Length(6.0))), Some(6.0)); assert_eq!(baseline_shift(VerticalAlign::Baseline), Some(0.0)); assert_eq!(baseline_shift(VerticalAlign::Middle), None); assert_eq!(baseline_shift(VerticalAlign::Length(LengthPercentage::Percentage(0.5))), None); } }