Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282//! 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 | <length> |//! <percentage>`; `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<AtomicInline>,}
/// 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<InlinePlacement>, /// 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<LineBox>, /// 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<f64>,}
/// 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/// `<length>` and `<percentage>`. 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<f64> { 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<AtomicInline>) -> 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); }}