enabled: true touches: [crates/layout/Cargo.toml, crates/layout/src/bin/paint.rs, crates/dom/src/computed.rs, roadmap.md] depends-on: [] #
Visual proof: rasterize the fragment tree to a PNG #
roadmap.md → "Visual proof": "nothing in the engine has ever drawn a pixel" — every
measurement so far is a JSON diff of geometry (getBoundingClientRect) and computed style
against the three-browser oracle, never against pixels. The goal it states: "take the
fragment tree layout() already produces and turn it into a picture on screen (or, at
minimum, in a saved image) — a blank page first, then a box with a background color and a
border... This is demo-ware, explicitly not production: no text, no images, no
interactivity, no compositing beyond flat rectangles, no accessibility tree, no input
handling. One box rendered correctly is success." The same section notes "A software
rasterizer writing a PNG (e.g. via the image crate, walking border-box rects to filled
rectangles) is very likely simpler and faster to demo than wiring up wgpu/vello plus a
window — but the exact mechanism is an implementation decision for the track that builds
this, not fixed here." This spec makes that decision: a software rasterizer via the image
crate, no window.
Scope. In: two deliverables, each a PNG file at a fixed path under target/visual-proof/
(target/ is already gitignored build output, not a committed asset). "Blank page" is an
800×600 canvas (fragment_check.rs's own VIEWPORT_WIDTH and the dump format's
Viewport { width: 800, height: 600 }) filled with one background color and nothing else,
produced by running the real pipeline (Document::parse → BoxTree::build → layout())
on an empty document and confirming there is nothing to paint, not by skipping the pipeline
and writing a blank buffer directly. "A box" is a single <div> with an explicit
background-color and a border, laid out and painted at its actual fragment rect: the
properties painted are exactly background-color and the four border-*-color/
border-*-width values, nothing else. Out: this is not a generated seam. No model batch,
no candidate-admission loop, no corpus of its own, no provenance record — hand-written
engineering directly against the existing substrate, the same shape as
tasks/device-pixel-rounding.md's disclaimer for fragment.rs; a future reader must not
assume the batch-loop machinery this repo uses for queued CSS seams is in play here. Also
out: an on-screen window — the roadmap's own phrasing ("on screen (or, at minimum, in a
saved image)") makes a saved image sufficient, and a window pulls in an entire second
category of complexity (winit, an event loop) the demo-ware framing does not require; a
saved PNG is the actual deliverable this spec builds, and no future reader should treat a
window as still owed. Also out: text metrics, images, border-radius, shadows, transforms,
any corner rounding or overlap logic beyond two nested flat rectangles, and reusing
crates/harness/src/cases.rs's recipe corpus for color — that corpus carries no color
today (confirmed: no recipe sets background/border-color, and its own RESET rule
zeroes border: 0), and coloring it is its own scope creep this track does not take on.
Build:
-
crates/layout/Cargo.toml— addimage(latest 0.25-series) as a dependency, the workspace's first. No other crate references a raster or PNG-writing crate today. -
crates/dom/src/computed.rs— four new accessors,background_colorandborder_{top,right,bottom,left}_color, mirroringborder_top_width's existing pattern (style.clone_border_top_width()gated onstyle.clone_border_top_style() .none_or_hidden(), lines 381–401): readstyle.clone_background_color()/style.clone_border_*_color(), resolve to an RGBA quad, and zero out (fully transparent) a border side whose style isnone/hidden, matching the CSSOM rulecomparable_style's existing border-width entries already apply.computed.rs's own doc comment says color accessors are net-new to this module (there are currently zero); this is the one place the exact Stylo 0.21.0 API name for the computed color type needs confirming against the crate directly, since no vendored source was available to check here — the shape (aclone_*accessor returning a Stylo computed-color type with an RGBA resolution path) is not in doubt, only the exact type/method name. -
crates/layout/src/bin/paint.rs— new binary, mirroringfragment_check.rs's/style_check.rs's shape (amain()with--case/--outargs, no shared library module needed given the scope). Pipeline:Document::parse(html)→BoxTree::build(&document)→fragment::layout(&document, &tree, 800.0), exactlyfragment_check.rs'sengine_dump()'s first four lines, reused verbatim rather than inventing a second entry point. Two literal HTML documents, hand-built in this file (notcases.rs):blank:<!doctype html><html><body></body></html>.box: a single reset rule (* { margin:0; padding:0; box-sizing:border-box; }) plus one<div style="width:200px;height:100px;background-color:red;border:10px solid blue;"></div>.box-sizing:border-boxmakes the fragment's border-box exactly 200×100, so the expected paint rect is the literal declared size, not something that needs computing from content-box + border independently.
Rasterization: an
image::RgbImage(orRgbaImage) 800×600, filled with a fixed background color (white), thenFragment::walk_absolute(0.0, 0.0, f)walks every fragment's absolute rect in the same order the tree was built (parent before children — correct paint order for non-overlapping boxes, sufficient given "no compositing beyond flat rectangles"). For each(node, x, y, w, h): look upbackground_color/border_*_color/border_*_widthvia the newcomputed.rsaccessors on thatnode; if any border side has nonzero width and non-transparent color, fill the full border-box rect(x, y, w, h)with that side's color first (uniform-border case: one rect for all four sides, since the milestone's test document sets one shorthand and all four sides match); then fill the inner rect —(x + border_left, y + border_top, w - border_left - border_right, h - border_top - border_bottom)— withbackground-color, on top. A fully transparentbackground-color(CSS initial value) leaves the canvas's existing pixels untouched rather than painting white over what a parent already painted. Write the canvas totarget/visual-proof/{blank,box}.pngviaimage's PNG encoder.
Gates:
cargo test --workspacepasses.cargo run --release -p layout --bin paint -- --case blankexits 0 and producestarget/visual-proof/blank.png; a test reads it back withimage::open, asserting 800×600 dimensions and that every pixel equals the fixed background color — this is what makes "produces a valid image" checkable rather than eyeballed.cargo run --release -p layout --bin paint -- --case boxexits 0 and producestarget/visual-proof/box.png; a test reads it back and asserts, at pixel coordinates derived from the same fragment rect the binary painted from (not a hand-computed or independently-guessed coordinate): the interior point (rect center) equals red(255, 0, 0), and a point inside the border band (e.g. rect origin + 2px in both axes, inside the 10px border) equals blue(0, 0, 255).- No requirement to move or re-check
fragment_check --random's 216/227 floor: this track readsFragment's existingnode/offset_x/offset_y/width/heightfields throughwalk_absolute, which already exists, and adds no field and no changed computation tofragment.rsitself — paint properties are read independently through newcomputed.rsaccessors, not through the fragment tree. The floor is unaffected and this spec does not re-runfragment_checkas a gate.
Afterwards, update roadmap.md → "Visual proof" with what landed: a software rasterizer
via the image crate (crates/layout/src/bin/paint.rs), producing saved PNGs rather than
an on-screen window, and the blank-page and one-box results, the way
tasks/floats-bfc.md's own closing paragraph updates its roadmap section.