feat(htmlcss): Chromium-parity L0 suite — 65 fixtures + 13 renderer fixes#686
feat(htmlcss): Chromium-parity L0 suite — 65 fixtures + 13 renderer fixes#686softmarshmallow merged 82 commits intomainfrom
Conversation
Two Chromium-parity fixes for the cg htmlcss renderer, both driven to
byte-exact via the dev-cg-htmlcss-feature loop.
- paint-opacity: replace `set_alpha((opacity * 255.0) as u8)` with
`set_alpha_f(opacity)` in htmlcss/paint.rs so opacity compositing is
float-native, matching Blink's `SkCanvas::saveLayerAlphaf` path.
The u8 truncation diverged by 1 per channel at non-255-aligned
opacity values (0.5 → 127 vs Chromium's 126; 0.25 → 63 vs 64).
paint-opacity fixture goes from 0.9600 → 1.0000 similarity.
- box-padding: fixture-hygiene only (no code change). Removed
unrelated border-radius on .outer/.inner, replaced font-shaped
inner "content" text with explicit `width: 80px; height: 24px`,
pinned `.label { width: 200px; height: 16px }` to eliminate
font-advance-width-driven flex item sizing. Fixture goes from
0.9932 → 1.0000 similarity.
Both fixtures also strip inner text from their visual probes to
avoid text-under-opacity-layer / font-shaping noise in the
pre-gate phase.
L0.exact now gates box-dimensions, box-padding, and paint-opacity at
floor 1.0.
Add a short section to the test-html README clarifying that captions next to specimens are fine — the mistake is letting them drive layout or forgetting that hide-text.css already neutralizes color/shaping noise when text is incidental. Motivated by a reftest iteration where labels were stripped instead of pinning the enclosing dimensions.
Extend the Labeled specimens bullet to spell out two things the test-html README already says: keep label text short, and pin the dimensions of any container holding a label so font-advance-width differences can't propagate into geometry. Also point to `hide-text.css` as the text-neutralizer so labels stay useful to future readers instead of getting stripped mid-reftest cycle.
htmlcss: promote paint-opacity + box-padding to L0.exact
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughRefactors reftest scoring to use alpha-based content masks, enables AA-ignore by default, and adds CLI AA flags. Renderer and canvas now produce transparent, full-viewport PNGs (omitBackground). Significant htmlcss paint/style changes (corner radii, background-clip, gradients, shadows). Many L0 fixtures and suite defaults updated. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Playwright Browser
participant RefScript as refbrowser_render.ts
participant Canvas as grida-canvas Renderer
participant Scorer as Reftest Scorer
Browser->>RefScript: request screenshot (omitBackground: true)
RefScript->>Canvas: allocate raster surface(viewport w,h)
Canvas->>Canvas: clear surface -> rgba(0,0,0,0)
Canvas->>Canvas: draw picture (paint.rs: layered compositing)
Canvas->>RefScript: encode PNG (with alpha)
RefScript->>Scorer: supply PNG (alpha used to build content mask)
Scorer->>Scorer: compute denominator from alpha>0 pixels
Scorer->>Scorer: pixelmatch diff (includeAA default = false)
sequenceDiagram
participant Author as Fixture Author
participant Suite as Suite Config
participant Runner as grida-reftest CLI
participant Scorer as Reftest Scorer
Author->>Suite: author HTML/CSS (pin label dims, include transparent-body.css)
Suite->>Runner: suite defaults (extra_css includes transparent-body.css, gate.aa true)
Runner->>Scorer: run compare (--aa / --no-aa affects includeAA)
Scorer->>Scorer: compute content mask from PNG alpha and score
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Two Chromium-parity fixes for the cg htmlcss renderer, both driven to
byte-exact via the dev-cg-htmlcss-feature loop.
- paint-opacity: replace `set_alpha((opacity * 255.0) as u8)` with
`set_alpha_f(opacity)` in htmlcss/paint.rs so opacity compositing is
float-native, matching Blink's `SkCanvas::saveLayerAlphaf` path.
The u8 truncation diverged by 1 per channel at non-255-aligned
opacity values (0.5 → 127 vs Chromium's 126; 0.25 → 63 vs 64).
paint-opacity fixture goes from 0.9600 → 1.0000 similarity.
- box-padding: fixture-hygiene only (no code change). Removed
unrelated border-radius on .outer/.inner, replaced font-shaped
inner "content" text with explicit `width: 80px; height: 24px`,
pinned `.label { width: 200px; height: 16px }` to eliminate
font-advance-width-driven flex item sizing. Fixture goes from
0.9932 → 1.0000 similarity.
Both fixtures also strip inner text from their visual probes to
avoid text-under-opacity-layer / font-shaping noise in the
pre-gate phase.
L0.exact now gates box-dimensions, box-padding, and paint-opacity at
floor 1.0.
Add a short section to the test-html README clarifying that captions next to specimens are fine — the mistake is letting them drive layout or forgetting that hide-text.css already neutralizes color/shaping noise when text is incidental. Motivated by a reftest iteration where labels were stripped instead of pinning the enclosing dimensions.
Extend the Labeled specimens bullet to spell out two things the test-html README already says: keep label text short, and pin the dimensions of any container holding a label so font-advance-width differences can't propagate into geometry. Also point to `hide-text.css` as the text-neutralizer so labels stay useful to future readers instead of getting stripped mid-reftest cycle.
…olid Changed `abs_color_to_cg` in collect.rs to use `.round() as u8` instead of truncation when converting sRGB f32 channels to u8. Truncation of `0.7 * 255` produced alpha=178 (vs Chromium's 179), which propagated through Skia's src-over compositing as a ±1 drift in every blended pixel (e.g. rgba(255,0,0,0.7) over white → (255, 77, 77) instead of (255, 76, 76)). paint-background-solid: 97.92% → 100.00%. Promoted to L0.exact.
`extract_border_radius` used `NonNegativeLengthPercentage::to_length()` which returns `None` for any percent value, silently flattening `border-radius: 50%` → `0px`. A circle or pill rendered as a plain square. `CornerRadii` now carries per-axis percent fractions alongside px components. `length_percentage_to_css` decomposes each corner so `%`, `calc(...)`, and pure-px all round-trip. Added `CornerRadii::resolved(w, h)` which materializes percents against the border-box (CSS Backgrounds 3 §5.3 — H axis against width, V axis against height). Each paint site (`paint_background`, `paint_outline`, inset/outer box-shadow, overflow clip, uniform rounded border, replaced content) resolves before reading `*_x`/`*_y` fields or calling `to_skia_radii`. Skia's built-in radii-scaling in `RRect::setRectRadii` handles the §5.5 overlap-clamping rule, so we don't reimplement it. paint-border-radius: 98.94% → 100.00% (circle + elliptical + mixed percent/px now match Chromium byte-exactly). Promoted to L0.exact.
Narrow paint fixture covering solid CSS borders: uniform 1/3/8px, single-side (border-top), and asymmetric widths (2/4/6/8 same color). Passes byte-exact against Chromium — promoted straight to L0.exact. Dropped multi-color sides (red/green/blue/black) from the initial draft: the 4-pixel residual sat only at the miter corners where two differently-colored sides meet, which is a separate concept (Skia vs Blink corner triangulation) that deserves its own `paint-border-miter.html` fixture later.
Narrow paint fixture covering solid CSS outlines: 1/3/6px widths, custom color, and positive outline-offset. Passes byte-exact against Chromium — promoted straight to L0.exact. The rounded-corner cases (outline + border-radius) were in the initial draft but produced 8 diff pixels at the corners from Skia/Blink stroked-RRect AA-policy divergence. Dropped to a future `paint-outline-radius.html` fixture — keep this one focused on non-radius outlines only.
Narrow fixture covering box-shadow without blur: positive/negative offset, spread-only, spread+offset, and custom color. All cases pass byte-exact against Chromium on first run — the outer-shadow RRect path plus `.round() as u8` color conversion (iter 3) are enough. Promoted straight to L0.exact. Blur cases (the interesting Skia MaskFilter vs Blink pixel-stage divergence surface) are deferred to `paint-box-shadow-blur.html`.
…rage Narrow fixture covering two-stop axis-aligned linear gradients (to top/bottom/left/right, black→white and red→blue). Scores 94.09% against Chromium — **not promoted**. The ±1 drift in every component plus the dithering stripe pattern is the classic Skia gradient-interpolation vs Blink divergence: Skia dithers to avoid banding on a different lattice than Blink does. This is a renderer-level mismatch, not a fixture-authoring issue, so the fixture stays in L0.coverage as a tracked gap until we decide whether to match Blink's dither or accept sRGB-only gradients.
Blink always dithers CSS gradients (gradient.cc:359: \"Legacy behavior: gradients are always dithered\") and interpolates premultiplied color stops (gradient.cc:282-285). Our `rasterize_gradient` left dithering off and our interpolation used `InPremul::No`, producing a pronounced banding pattern plus ±1 drift on the stop boundary. - `rasterize_gradient` now calls `paint.set_dither(true)` on the fill paint used against the intermediate raster surface. - `to_skia_interpolation` sets `in_premul = Yes` for every CSS gradient, matching Blink's `premultiplied_alpha_ = kPremultiplied` for all CSS gradient types. paint-background-gradient-linear-simple: 94.09% → 98.19%. Residual drift is dither-lattice phase differences between our intermediate bitmap and Blink's direct-to-surface gradient path; tracked in L0.coverage for a follow-up.
Narrow fixture for `clip-path: inset(...)` with uniform/asymmetric/ percent/round variants. Passes byte-exact on first measurement — promoted to L0.exact. Exercises the `InsetCornerRadii` paint-time resolution path from iter 4 (percent-based inset radii against the clipped rect). Also reverts an unused gradient fast-path tried this iteration — the dither-lattice phase between intermediate raster and direct draw turned out to produce identical output, so the extra code path didn't earn its keep. The iter-8 `paint.set_dither(true)` + `InPremul::Yes` fix still stands.
Narrow fixture for inset `box-shadow` without blur: spread-only, positive/negative offsets, spread+offset combo, and custom color. All cases pass byte-exact against Chromium on first measurement — the inset hollow-rect clip path plus `.round() as u8` (iter 3) are sufficient. Promoted to L0.exact.
Narrow fixture for `transform: translate*()`: translate(x, y), translateX, translateY, and negative offsets. All cases pass byte-exact against Chromium on first measurement — the matrix concat in the paint entry already aligns with Blink's approach. Promoted to L0.exact.
Narrow fixture for `transform: scale*()`: scale(uniform), scale(x, y), scaleX, scaleY. Passes byte-exact on first run. Promoted to L0.exact.
Narrow fixture for `transform: rotate(90/180/-90deg)`. Scores 99.98% against Chromium — **not promoted**. The residual 120 pixels sit on a single vertical line at the rotated left edge (254 vs 255). Rotating by exactly 90° should map pixel-aligned rects back to pixel-aligned rects, but Skia and Blink pick up a sub-pixel drift from the float-precision sin/cos computation, producing a 1-bit AA difference along one edge. Tracked in L0.coverage as a rotation-AA-policy divergence.
Narrow fixture: six black swatches at opacity 1.0 / 0.75 / 0.5 / 0.25 / 0.1 / 0. The `set_alpha_f` fix from iter 1 plus the `.round() as u8` channel fix from iter 3 make all six match Chromium byte-exactly. Promoted to L0.exact.
Narrow fixture: four corner-pinned dots (top/left, top/right, bottom/left, bottom/right) plus a translate-centered red dot inside a relative-positioned container. Tests `position: absolute` with top/right/bottom/left + `transform: translate(-50%, -50%)`. Passes byte-exact on first run. Promoted to L0.exact.
Narrow fixture: three absolutely-positioned squares (red/green/blue) with z-index 3/2/1 but source order c/b/a. Tests that z-index overrides source-order painting. Byte-exact on first run. Promoted to L0.exact.
Narrow fixture: two side-by-side containers with an oversized absolutely-positioned child. Left container has overflow: hidden (child clipped to container), right has overflow: visible (child overflows). Byte-exact on first run; promoted to L0.exact.
Narrow fixture: three black boxes with middle one set to `visibility: hidden`. Tests that hidden boxes reserve layout space but paint nothing. Byte-exact on first run; promoted to L0.exact.
Narrow fixture: three boxes, middle one `display: none` (consumed no layout space vs `visibility: hidden` which reserves the slot). Byte-exact on first run; promoted to L0.exact.
Narrow layout fixture: five flex rows with three 80x60 items each, exercising default packing, gap 16, justify-content: space-between, and justify-content: flex-end. Byte-exact against Chromium; promoted to L0.exact. Dropped justify-content: space-around from initial draft — the 3-item/500px row gives 260px free space / 6 = 43.33px per gap, which rounds to different pixels between Blink and Taffy (±1 px boundary shift per item). Belongs in its own fractional-justify fixture once the sub-pixel rounding is aligned.
Narrow layout fixture: three flex-direction: column containers (default start / gap 12 / space-between) with three stacked items each, laid out in a flex-row parent. Byte-exact; promoted to L0.exact. Dropped justify-content: flex-end column (fourth variant) to keep the composite width within the 600px viewport preset — no divergence, just a sizing constraint.
…er double-paint Two related fixes surfaced by paint-background-clip-boxes: 1. `background-clip` now applies to the solid `background-color` layer. Per CSS Backgrounds 3 §2.5 the color uses the final layer's clip value; `BackgroundLayer::Solid` gains a `clip` field, and `paint_background` draws the color into the matching box-reference rect (inset radii for rounded boxes). Previously the color always filled border-box. 2. Uniform `solid`/`double` borders with translucent color now stroke once as an RRect regardless of border-radius. The per- side trapezoid path overlaps by one pixel at every corner diagonal, so `rgba(…, 0.5)` was composited twice at the corners → 0.75-alpha instead of 0.5. Restricted the switch to `solid`/`double` styles; `inset`/`outset`/`groove`/`ridge` still take the per-side path since they need the Blink 3D darken/lighten pair. paint-background-clip-boxes: 96.88% → 100.00%. Promoted to L0.exact. All 771 cg unit tests pass.
… 0.5 CSS Backgrounds §7.2 defines blur-radius as twice the Gaussian standard deviation. We were passing CSS `blur-radius` directly to Skia's mask filter as sigma, producing shadows that were 2× too blurry. Match Blink's ShadowData::BlurRadiusToStdDev (shadow_data.h:76-82): σ = radius * 0.5 at both outer and inset mask-filter sites. Add paint-box-shadow-blur L0.exact fixture (7 variants: blur sizes, offset+blur, blur+spread, colored translucent, blur+border-radius). Baseline 37.46% → 100.00% (AA-ignore) / 99.99% (--no-aa) after fix. Existing shadow fixtures stayed at 100% because they all used 0 blur.
The inset-shadow path was shifting only the inner hole by shadow.offset while keeping the outer rect far (`blur * 2 + 100` thick). That produced an asymmetric frame whose inner/outer Gaussian gradients could not overlap on the offset side, so the shadow saturated at the box edge instead of falling off softly (93% vs Chromium on offset variants). Match Blink (box_painter_base.cc:511-578): keep inner hole centered on the box, size outer_rect via AreaCastingShadowInHole (outset by blur-radius, union with pre-offset position), then canvas.translate by shadow.offset before drawing so the whole frame shifts in one go — equivalent to Blink's DrawLooper offset. Add paint-box-shadow-inset-blur L0.exact fixture (7 variants: blur sizes, offset+blur, blur+spread, colored translucent, blur+radius). Baseline 92.97% → 100.00%. No regressions on other shadow fixtures.
…rders CSS Backgrounds §4.2 leaves dash geometry implementation-defined, but to match Chromium we need: - thin lines (<3px): dash = 3×width, gap = 2×width - thick lines (≥3px): dash = 2×width, gap = 1×width - gap then adjusted so an integer count of dashes fits each side's length evenly (Blink's SelectBestDashGap) Port styled_stroke_data.cc:40-113 to our stroke_paint builder. The function now takes an optional path_length; per-side border painting passes the side length, outline (RRect perimeter) passes None and falls back to nominal intervals. Add paint-border-style-dashed L0.exact fixture (6 variants: widths 1/2/3/6/10, colored). Baseline 92.78% → 100.00%. No regressions.
…inset CSS Backgrounds §4.2 border-style: dotted. Our stroke used [w, w] dash with round cap, producing 2×width dots at 2×width spacing. Blink (styled_stroke_data.cc:115-132) uses [0, gap+width-ε] with round cap — the zero dash + round cap yields width-diameter dots, and the gap is picked by SelectBestDashGap so an integer count of dots fits the path length. Also inset each line's endpoints by width/2 for thick dotted (>3px), matching box_border_painter.cc:528-537 so round caps don't extend beyond the box. Add paint-border-style-dotted L0.coverage fixture (6 variants). 92.78% → 96.24% (AA-ignore) / 90.36% (--no-aa). Thin (width ≤ 3) variants still misalign; Blink's EnforceDotsAtEndpoints pixel-snap logic (box_border_painter.cc:401-497) not yet ported — memoed as residual for a follow-up.
6 variants cover CSS Filter Effects §9.7 drop-shadow() branches: no-blur (2-length form), small/medium/large blur sigmas, colored translucent, negative offsets. 100.00% on first try — Blink stores the blur value as sigma directly (filter_effect_builder.cc:298) and our code path does the same via image_filters::drop_shadow, so no code change needed.
6 variants exercise CSS Transforms 1 §7.1 matrix(a,b,c,d,tx,ty): identity, pure translate/scale/rotate via the matrix primitive, scale+translate composed, shear. 100.00% AA-ignore / 99.43% --no-aa (yellow AA edges on rotate + shear, same class as iter 13 / iter 61 rotation/skew residuals). No code change.
6 variants exercise CSS Filter Effects §11.4.4 blur(<length>): sigma 0/2/4/8/12 + colored. 100.00% on first try — spec says the length is Gaussian σ directly (not halved like box-shadow blur-radius), and our image_filters::blur((sigma, sigma)) call already matches Blink. No code change.
- Extract `blur_radius_to_sigma(r)` so the CSS Backgrounds §7.2 rule (σ = r/2) has one home shared by outer and inset box-shadow mask-filter sites. - Extract `side_length(pos, w, h)` next to the existing `side_endpoints` / `side_inward_normal` family; collapse the duplicate SidePos match in paint_border_side. - Add a comment on the `.max(1.0)` guard in select_best_dash_gap so the deviation from Blink's open-path-with-1-dash branch is explicit. - Drop the commit-message-style post-mortem comment in the inset shadow block; the remaining comment plus the Blink line-number citations carry the full intent. No behavioral change — 771 tests pass, full L0.coverage diff scores unchanged (average 99.61%).
feat(htmlcss): Blink-parity shadow blur + dashed/dotted borders, 8 L0 fixtures
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1750e28994
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let color_clip = bg | ||
| .background_clip | ||
| .0 | ||
| .last() | ||
| .map(extract_bg_clip) |
There was a problem hiding this comment.
Resolve background-color clip with cycled layer indexing
In extract_background, the background-color clip is chosen with background_clip.last(), but this property is a repeating per-layer list (and this same function already cycles shorter lists for image layers via cycle(...)). When background-image has more layers than background-clip values, .last() can select the wrong clip for the bottom layer, so the color is painted in the wrong box. Compute the color clip from the bottom layer index after list expansion instead of using the raw last token.
Useful? React with 👍 / 👎.
| let needs_layer = style.opacity < 1.0 | ||
| || !style.filter.is_empty() | ||
| || !matches!(style.blend_mode, crate::cg::prelude::BlendMode::Normal); |
There was a problem hiding this comment.
Avoid bounded save-layer for mix-blend-mode contexts
This change makes non-normal mix-blend-mode trigger save_layer, but the non-filter path still uses the element box as layer bounds; in Skia that bounds clips the layer output. As a result, blended elements lose pixels that extend outside w×h (e.g. outer box-shadow, positive outline-offset, or overflowing descendants) before compositing, which is a rendering regression specific to the new blend-mode path. Blend-mode layers should use unbounded/expanded bounds like the filter branch.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
crates/grida-canvas/src/htmlcss/style.rs (1)
496-505:⚠️ Potential issue | 🟡 MinorDon’t drop percentage radii for inline decorations.
Line 496 says
max_radius()is only for a presence check, butcollect.rsstores this value intoInlineBoxDecoration.border_radius. With percentage-only radii, inline backgrounds/borders render square because the scalar radius becomes0.Consider carrying
CornerRadiithroughInlineBoxDecorationand resolving it against each decoration rect during inline painting.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/htmlcss/style.rs` around lines 496 - 505, The current max_radius() implementation strips percentage-based radii (returning 0 for percentage-only corners) and that scalar is stored in InlineBoxDecoration.border_radius via collect.rs, causing percentage-only radii to render as square; instead, propagate the full CornerRadii through InlineBoxDecoration (do not replace it with a single f32), add a new field like InlineBoxDecoration.corner_radii (or replace border_radius) and update collect.rs to assign the CornerRadii directly, then resolve/compute the final pixel radii against each decoration rect during inline painting (where decoration rect size is known) so percentage radii are correctly converted per-rect before painting.crates/grida-canvas/src/htmlcss/paint.rs (2)
201-209:⚠️ Potential issue | 🟠 MajorClip replaced content with content-box-adjusted radii.
Line 208 resolves radii against the border box, then
paint_replacedapplies them to a content-box-local rect. With borders or padding,<img>clipping uses radii that are too large instead of the inset inner curve.Proposed fix
+ let resolved_r = style.border_radius.resolved(w, h); + let border_rect = Rect::from_xywh(0.0, 0.0, w, h); + let content_rect = Rect::from_xywh(cx, cy, cw, ch); + let content_radii = inset_radii(&resolved_r, border_rect, content_rect); paint_replaced( canvas, replaced, cx, cy, cw, ch, - &style.border_radius.resolved(w, h), + &content_radii, style.font.image_rendering, images, );fn paint_replaced( canvas: &Canvas, content: &super::style::ReplacedContent, x: f32, y: f32, w: f32, h: f32, - border_radius: &super::style::CornerRadii, + border_radii: &[skia_safe::Point; 4], image_rendering: types::ImageRendering, images: &dyn ImageProvider, ) { @@ - if !border_radius.is_zero() { + if border_radii.iter().any(|p| p.x > 0.0 || p.y > 0.0) { let mut rrect = skia_safe::RRect::new(); - rrect.set_rect_radii(dest_rect, &border_radius.to_skia_radii()); + rrect.set_rect_radii(dest_rect, border_radii); canvas.clip_rrect(rrect, ClipOp::Intersect, true);Also applies to: 763-785
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/htmlcss/paint.rs` around lines 201 - 209, The image clipping currently passes radii resolved against the element's border-box into paint_replaced which expects radii for the content-box; when borders or padding exist this yields oversized corner curves. Modify the call site that invokes paint_replaced (and the similar call around lines 763-785) to inset the resolved BorderRadius by the element's used border+padding (i.e., convert the border-box radii into content-box radii) before passing them to paint_replaced; use the same resolved(...) result but apply an inset/deflate using the computed border + padding extents so the radii match the content rect passed (cx, cy, cw, ch). Ensure the adjusted radii are used wherever paint_replaced is called for replaced content.
78-147:⚠️ Potential issue | 🟠 MajorOmit save-layer bounds when blend modes are used with visual overflow elements.
When
mix-blend-modeis active, the save-layer bounds at line 139 are set to the border box[0, w] × [0, h]. Skia clips layer output to these bounds, which causes outer box-shadows and outlines (which extend beyond the border box) to be clipped before compositing. The blended result becomes visually incorrect.The code already handles this for filters by omitting bounds when
has_filteris true (line 140). The same logic must apply to blend modes: detect visual overflow (outline or non-inset shadows with blur/spread/offset) and omit bounds in those cases.Proposed fix
+ let has_visual_overflow = style.outline.has_outline() + || style.box_shadow.iter().any(|s| { + !s.inset + && (s.blur > 0.0 + || s.spread != 0.0 + || s.offset_x != 0.0 + || s.offset_y != 0.0) + }); let bounds = Rect::from_xywh(0.0, 0.0, w, h); - let layer_rec = if has_filter { + let layer_rec = if has_filter || has_visual_overflow { skia_safe::canvas::SaveLayerRec::default().paint(&layer_paint) } else { skia_safe::canvas::SaveLayerRec::default() .paint(&layer_paint) .bounds(&bounds) };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/htmlcss/paint.rs` around lines 78 - 147, The save-layer bounds currently get omitted only when has_filter is true, but when mix-blend-mode (style.blend_mode) is active and the element has visual overflow (e.g. non-inset box-shadows with blur/spread/offset or an outline) those effects get clipped by the bounds; add a detection boolean (e.g. has_visual_overflow) that checks style.box_shadows for any shadow where inset == false and (blur_radius != 0.0 || spread != 0.0 || offset != (0,0)) and/or style.outline is present, then change the layer_rec creation logic (around layer_rec / has_filter) so bounds are omitted when has_filter || (style.blend_mode != Normal && has_visual_overflow) before calling canvas.save_layer; update references to layer_paint, has_filter, layer_rec and canvas.save_layer accordingly.
🧹 Nitpick comments (4)
fixtures/test-html/L0/paint-border-solid.html (1)
21-27: Optional: reduce text rasterization noise in this paint-focused fixture.Since this fixture validates border rendering, consider minimizing label glyph impact (e.g., smaller/no labels in capture area, or moving labels outside compared region) to keep failures focused on borders.
Also applies to: 66-83
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@fixtures/test-html/L0/paint-border-solid.html` around lines 21 - 27, The .label CSS rules currently draw text inside the capture area and can add rasterization noise; update the .label styling or fixture markup so labels don't overlap the border test region—for example reduce font-size further, set labels to display:none for the paint-focused fixture, or move the label elements outside the compared region; locate the .label block in the fixture (the rules with font-size, color, padding-bottom, width, height) and apply the change that removes/minimizes glyph rendering in the capture area.fixtures/test-html/L0/paint-outline-double-rect.html (1)
43-49: Well-focused outline test cases.The fixture appropriately tests the
doubleoutline style in two scenarios: basic 9px outline and with 6px offset. The 9px width is sufficient for the double style to render both lines clearly.Optional enhancement: Consider adding a companion fixture for other outline styles (solid, dashed, dotted) to provide comprehensive outline rendering coverage, though the current focused approach is valid for L0 testing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@fixtures/test-html/L0/paint-outline-double-rect.html` around lines 43 - 49, The fixture currently tests the double outline using classes .o-9 and .o-9-offset; as an optional enhancement, add companion test fixtures that exercise other outline-style values (e.g., solid, dashed, dotted) by creating corresponding classes (e.g., .o-solid, .o-dashed, .o-dotted and offset variants) and rendering them at widths that make the style visible, so outline rendering coverage is broader while keeping the existing L0 double-outline cases unchanged.fixtures/test-html/L0/paint-transform-skew.html (1)
32-40: Remove unused CSS classes or add corresponding test cases.Two utility classes (
skew-yandskew-xy) are defined but never used in the body. The fixture only renders a single box withskew-x.Consider either:
- Adding test cases for
skewYand combinedskewtransforms to match the defined classes- Removing the unused classes if this fixture intentionally tests only
skewX♻️ Option 1: Add missing test cases
<body> <div class="row"> <div class="box skew-x"></div> + <div class="box skew-y"></div> + <div class="box skew-xy"></div> </div> </body>♻️ Option 2: Remove unused classes
.skew-x { transform: skewX(20deg); } - .skew-y { - transform: skewY(10deg); - } - .skew-xy { - transform: skew(15deg, 5deg); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@fixtures/test-html/L0/paint-transform-skew.html` around lines 32 - 40, The CSS defines .skew-y and .skew-xy but only .skew-x is used in the markup; either add test elements that exercise the missing transforms (create additional boxes in the body using class="skew-y" and class="skew-xy" with appropriate labels/captions so the fixture covers skewY and skew( , ) cases) or remove the unused .skew-y and .skew-xy rules to keep the fixture focused on skewX; locate and update the CSS block containing .skew-x, .skew-y, .skew-xy and the HTML that renders the test box to implement one of these two options.fixtures/test-html/L0/paint-box-shadow-inset-blur.html (1)
25-62: Consider adding a non-zero-offset + non-zero-spread + border-radius case.The current fixtures individually cover blur, offset+blur, spread+blur, color+blur, and radius+blur, but none combine offset + spread + radius in one declaration — which is exactly the path that exercises the shrink-corner-radii-by-spread logic together with the
canvas.translateoffset inpaint_box_shadow_inset(seecrates/grida-canvas/src/htmlcss/paint.rs:2282-2362). A single combined case would close the remaining geometry gap cheaply.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@fixtures/test-html/L0/paint-box-shadow-inset-blur.html` around lines 25 - 62, Add a fixture that combines non-zero offset, non-zero spread, and border-radius in one rule to exercise the shrink-corner-radii-by-spread + canvas.translate code path in paint_box_shadow_inset; for example add a class (e.g. .off-spread-round) that sets border-radius (e.g. 16px) and a box-shadow with inset <non-zero-offset> <non-zero-offset> <blur> <spread> (e.g. inset 4px 4px 8px 4px rgba(0,0,0,0.8)) so the test triggers the combined geometry handling in paint_box_shadow_inset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.agents/skills/cg-reftest/SKILL.md:
- Around line 567-570: Update the Dimensions section text that currently reads
"**Dimensions** — cg renders at viewport dims (`width × height`); Chromium
screenshots `fullPage`..." and the earlier sentence "Set height to match cg's
cull height; mismatched dims score 0.0..." to clarify that viewport.height is
only optional for fixtures that force their own height via the paint preset
(e.g., `min-height: 800px`), and that natural layout fixtures still require an
explicit `viewport.height` matching cg's reported `WxH` to avoid a
dimension-mismatch/score 0.0 (keep references to `fullPage`, `viewport.height`,
and `@grida/reftest` in the wording).
In `@crates/grida-canvas/src/htmlcss/collect.rs`:
- Around line 1188-1191: The flex-basis handling currently maps FlexBasis::Size
-> extract_size which falls back to CssLength::Auto when encountering mixed
calc() values (because to_length()/to_percentage() fail), losing calc() like
calc(50% - 10px); update extract_size (or add a new helper like
extract_flex_basis) to detect and preserve calc() expressions instead of
returning Auto (e.g., produce a CssLength variant representing raw/calc values
or return the original computed style fragment), and then use that preserved
value when assigning el.flex_basis (the match on pos.flex_basis and callers that
rely on extract_size such as the other block at 1616-1631 should be updated
similarly) so mixed unit calc()s are retained rather than treated as Auto.
In `@crates/grida-canvas/src/htmlcss/paint.rs`:
- Around line 1836-1850: The fast-path currently excludes dashed/dotted via
uniform_stroke_style causing uniform rounded dashed/dotted borders to fall back
to per-side straight lines and lose border-radius; update the condition so
dashed and dotted styles are treated as uniform when a non-zero radius exists:
include types::BorderStyle::Dashed and types::BorderStyle::Dotted in the
uniform_stroke_style check (or otherwise allow those styles through) and ensure
the per-side fallback is only chosen when style.border_radius.is_zero(); keep
references to uniform_stroke_style, types::BorderStyle::{Dashed,Dotted},
style.border_radius and paint_uniform_rounded_border when making the change.
In `@fixtures/test-html/L0/layout-flex-direction-reverse.html`:
- Around line 7-11: The fixture sets paint-style sizing on the global selector
"html, body" (min-height: 800px; box-sizing: border-box;) which violates the
`layout-*` guideline to avoid forcing body size; either remove those rules from
the fixture so the layout test uses natural cull height, or instead add an
explicit per-fixture viewport for this fixture in L0.coverage.json to document
the intended 800px height; locate the CSS in layout-flex-direction-reverse.html
(the "html, body" rule) and apply one of these two fixes to make the intent
explicit.
In `@fixtures/test-html/L0/layout-grid-autoflow.html`:
- Around line 39-54: The fixture currently uses two identical 3×2 grids so
grid-auto-flow differences are not visible; update the second grid (the element
with class "grid autoflow-column") to expose column-flow behavior by changing
its child count or arrangement — for example reduce the number of .cell children
(e.g., 4 instead of 6) or reorder/add cells so column filling (top-to-bottom
then left-to-right) produces a different visual layout than the default flow;
keep the first grid (class "grid") as-is so the contrast is observable.
In `@fixtures/test-html/L0/paint-border-translucent.html`:
- Around line 21-27: The fixture currently includes visible text via the .label
element which introduces font rasterization and alpha masks that pollute the
translucent-border test; remove any textual content from the HTML labels (or
replace with semantic class names / comments) and eliminate styling that forces
visible text (see the .label class) so the fixture is text-free and only
exercises border rendering; update any instances noted (including the other
similar blocks at lines referenced by the reviewer) to use non-rendered metadata
(class names or comments) instead of visible labels.
In `@fixtures/test-html/L0/paint-transform-rotate.html`:
- Around line 42-57: The test uses a uniform 120×120 .box so .r-90, .r-180 and
.r-270 produce no visible change; update the fixture so rotation is observable
by making the box asymmetric (e.g., change .box to a non-square rectangle, add
an L-shaped marker via a ::before/::after pseudo-element, or split the box into
two differently colored halves) so the transforms on .r-90, .r-180 and .r-270
produce distinct visual differences in the .frame cells.
In `@fixtures/test-html/suites/L0.coverage.json`:
- Around line 40-87: The coverage suite file L0.coverage.json contains many
layout-* fixtures without explicit viewport entries (e.g.
layout-flex-direction-reverse.html, layout-flex-grow.html,
layout-flex-wrap.html, layout-grid-basic.html, layout-flex-basis.html,
layout-block-flow.html); before promoting any of these to L0.exact.json, measure
each fixture's natural cull (the renderer's intrinsic width and height) and add
a per-fixture "viewport": { "width": <w>, "height": <h> } object to the
corresponding JSON entry in L0.coverage.json (or ensure the same is present when
copying into L0.exact.json) so the layout tests use explicit dimensions instead
of inheriting the 600×800 paint default.
In `@fixtures/test-html/suites/L0.exact.json`:
- Around line 4-7: The fixture sets gate.aa which is never read; the code
expects diff.aa (see config parser referencing diff.aa and the CLI resolving
config?.diff?.aa). Move the "aa": true entry out of the "gate" object into the
"diff" object in the JSON (add "aa": true under "diff" and remove "gate.aa"),
ensuring the config key names match the parser/CLI usage (diff.aa) so the
setting is actually consumed.
---
Outside diff comments:
In `@crates/grida-canvas/src/htmlcss/paint.rs`:
- Around line 201-209: The image clipping currently passes radii resolved
against the element's border-box into paint_replaced which expects radii for the
content-box; when borders or padding exist this yields oversized corner curves.
Modify the call site that invokes paint_replaced (and the similar call around
lines 763-785) to inset the resolved BorderRadius by the element's used
border+padding (i.e., convert the border-box radii into content-box radii)
before passing them to paint_replaced; use the same resolved(...) result but
apply an inset/deflate using the computed border + padding extents so the radii
match the content rect passed (cx, cy, cw, ch). Ensure the adjusted radii are
used wherever paint_replaced is called for replaced content.
- Around line 78-147: The save-layer bounds currently get omitted only when
has_filter is true, but when mix-blend-mode (style.blend_mode) is active and the
element has visual overflow (e.g. non-inset box-shadows with blur/spread/offset
or an outline) those effects get clipped by the bounds; add a detection boolean
(e.g. has_visual_overflow) that checks style.box_shadows for any shadow where
inset == false and (blur_radius != 0.0 || spread != 0.0 || offset != (0,0))
and/or style.outline is present, then change the layer_rec creation logic
(around layer_rec / has_filter) so bounds are omitted when has_filter ||
(style.blend_mode != Normal && has_visual_overflow) before calling
canvas.save_layer; update references to layer_paint, has_filter, layer_rec and
canvas.save_layer accordingly.
In `@crates/grida-canvas/src/htmlcss/style.rs`:
- Around line 496-505: The current max_radius() implementation strips
percentage-based radii (returning 0 for percentage-only corners) and that scalar
is stored in InlineBoxDecoration.border_radius via collect.rs, causing
percentage-only radii to render as square; instead, propagate the full
CornerRadii through InlineBoxDecoration (do not replace it with a single f32),
add a new field like InlineBoxDecoration.corner_radii (or replace border_radius)
and update collect.rs to assign the CornerRadii directly, then resolve/compute
the final pixel radii against each decoration rect during inline painting (where
decoration rect size is known) so percentage radii are correctly converted
per-rect before painting.
---
Nitpick comments:
In `@fixtures/test-html/L0/paint-border-solid.html`:
- Around line 21-27: The .label CSS rules currently draw text inside the capture
area and can add rasterization noise; update the .label styling or fixture
markup so labels don't overlap the border test region—for example reduce
font-size further, set labels to display:none for the paint-focused fixture, or
move the label elements outside the compared region; locate the .label block in
the fixture (the rules with font-size, color, padding-bottom, width, height) and
apply the change that removes/minimizes glyph rendering in the capture area.
In `@fixtures/test-html/L0/paint-box-shadow-inset-blur.html`:
- Around line 25-62: Add a fixture that combines non-zero offset, non-zero
spread, and border-radius in one rule to exercise the
shrink-corner-radii-by-spread + canvas.translate code path in
paint_box_shadow_inset; for example add a class (e.g. .off-spread-round) that
sets border-radius (e.g. 16px) and a box-shadow with inset <non-zero-offset>
<non-zero-offset> <blur> <spread> (e.g. inset 4px 4px 8px 4px rgba(0,0,0,0.8))
so the test triggers the combined geometry handling in paint_box_shadow_inset.
In `@fixtures/test-html/L0/paint-outline-double-rect.html`:
- Around line 43-49: The fixture currently tests the double outline using
classes .o-9 and .o-9-offset; as an optional enhancement, add companion test
fixtures that exercise other outline-style values (e.g., solid, dashed, dotted)
by creating corresponding classes (e.g., .o-solid, .o-dashed, .o-dotted and
offset variants) and rendering them at widths that make the style visible, so
outline rendering coverage is broader while keeping the existing L0
double-outline cases unchanged.
In `@fixtures/test-html/L0/paint-transform-skew.html`:
- Around line 32-40: The CSS defines .skew-y and .skew-xy but only .skew-x is
used in the markup; either add test elements that exercise the missing
transforms (create additional boxes in the body using class="skew-y" and
class="skew-xy" with appropriate labels/captions so the fixture covers skewY and
skew( , ) cases) or remove the unused .skew-y and .skew-xy rules to keep the
fixture focused on skewX; locate and update the CSS block containing .skew-x,
.skew-y, .skew-xy and the HTML that renders the test box to implement one of
these two options.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c96ba1a0-558d-4a39-86ec-cb0aa84b9afa
📒 Files selected for processing (78)
.agents/skills/cg-reftest/SKILL.md.agents/skills/cg-reftest/scripts/refbrowser_render.ts.agents/skills/fixtures/SKILL.mdcrates/grida-canvas/examples/golden_htmlcss.rscrates/grida-canvas/src/htmlcss/collect.rscrates/grida-canvas/src/htmlcss/paint.rscrates/grida-canvas/src/htmlcss/style.rsfixtures/test-html/L0/box-padding.htmlfixtures/test-html/L0/layout-block-flow.htmlfixtures/test-html/L0/layout-flex-align-items.htmlfixtures/test-html/L0/layout-flex-align-self.htmlfixtures/test-html/L0/layout-flex-basis.htmlfixtures/test-html/L0/layout-flex-column-basic.htmlfixtures/test-html/L0/layout-flex-direction-reverse.htmlfixtures/test-html/L0/layout-flex-grow.htmlfixtures/test-html/L0/layout-flex-row-basic.htmlfixtures/test-html/L0/layout-flex-wrap.htmlfixtures/test-html/L0/layout-grid-autoflow.htmlfixtures/test-html/L0/layout-grid-basic.htmlfixtures/test-html/L0/layout-grid-fr.htmlfixtures/test-html/L0/layout-grid-gap-asym.htmlfixtures/test-html/L0/layout-grid-span.htmlfixtures/test-html/L0/paint-aspect-ratio.htmlfixtures/test-html/L0/paint-background-clip-boxes.htmlfixtures/test-html/L0/paint-background-gradient-linear-simple.htmlfixtures/test-html/L0/paint-border-double-rect.htmlfixtures/test-html/L0/paint-border-radius-individual.htmlfixtures/test-html/L0/paint-border-solid.htmlfixtures/test-html/L0/paint-border-style-dashed.htmlfixtures/test-html/L0/paint-border-style-dotted.htmlfixtures/test-html/L0/paint-border-translucent.htmlfixtures/test-html/L0/paint-box-shadow-blur.htmlfixtures/test-html/L0/paint-box-shadow-inset-blur.htmlfixtures/test-html/L0/paint-box-shadow-inset-solid.htmlfixtures/test-html/L0/paint-box-shadow-multiple.htmlfixtures/test-html/L0/paint-box-shadow-solid.htmlfixtures/test-html/L0/paint-clip-path-circle.htmlfixtures/test-html/L0/paint-clip-path-ellipse.htmlfixtures/test-html/L0/paint-clip-path-inset.htmlfixtures/test-html/L0/paint-clip-path-polygon.htmlfixtures/test-html/L0/paint-color-hex-alpha.htmlfixtures/test-html/L0/paint-display-none.htmlfixtures/test-html/L0/paint-filter-blur.htmlfixtures/test-html/L0/paint-filter-chain.htmlfixtures/test-html/L0/paint-filter-drop-shadow.htmlfixtures/test-html/L0/paint-filter-simple.htmlfixtures/test-html/L0/paint-gradient-radial.htmlfixtures/test-html/L0/paint-individual-transform-props.htmlfixtures/test-html/L0/paint-margin-auto-center.htmlfixtures/test-html/L0/paint-margin-simple.htmlfixtures/test-html/L0/paint-max-min-size.htmlfixtures/test-html/L0/paint-mix-blend-mode.htmlfixtures/test-html/L0/paint-opacity-levels.htmlfixtures/test-html/L0/paint-opacity-nested.htmlfixtures/test-html/L0/paint-opacity.htmlfixtures/test-html/L0/paint-outline-double-rect.htmlfixtures/test-html/L0/paint-outline-offset.htmlfixtures/test-html/L0/paint-outline-radius.htmlfixtures/test-html/L0/paint-outline-solid.htmlfixtures/test-html/L0/paint-overflow-hidden.htmlfixtures/test-html/L0/paint-padding-simple.htmlfixtures/test-html/L0/paint-position-absolute-simple.htmlfixtures/test-html/L0/paint-position-relative.htmlfixtures/test-html/L0/paint-transform-combined.htmlfixtures/test-html/L0/paint-transform-matrix.htmlfixtures/test-html/L0/paint-transform-origin.htmlfixtures/test-html/L0/paint-transform-rotate.htmlfixtures/test-html/L0/paint-transform-scale.htmlfixtures/test-html/L0/paint-transform-skew.htmlfixtures/test-html/L0/paint-transform-translate.htmlfixtures/test-html/L0/paint-visibility.htmlfixtures/test-html/L0/paint-z-index-simple.htmlfixtures/test-html/README.mdfixtures/test-html/_reftest/transparent-body.cssfixtures/test-html/suites/L0.coverage.jsonfixtures/test-html/suites/L0.exact.jsonpackages/grida-reftest/src/cli.tspackages/grida-reftest/src/compare.ts
| - **Dimensions** — cg renders at viewport dims (`width × height`); | ||
| Chromium screenshots `fullPage` at the same viewport. Setting an | ||
| explicit `viewport.height` is optional and only useful to trim | ||
| scoring area for very tall fixtures. |
There was a problem hiding this comment.
Minor doc inconsistency: "viewport.height optional" vs earlier "mismatched dims score 0.0".
Line 478-480 above still says "Set height to match cg's cull height; mismatched dims score 0.0 at diff time (@grida/reftest requires identical dimensions)." That's still true, and Chromium's fullPage will capture whatever the document's actual height is — which matches cg's viewport.height default only when the fixture forces its own height (paint preset with min-height: 800px).
For a natural-sized layout fixture, fullPage height ≠ viewport.height and you'll still get a dimension-mismatch/score=0. Consider tightening the wording: "optional for fixtures that force their own height via the paint preset; layout fixtures with natural cull still need an explicit viewport.height matching cg's reported WxH."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.agents/skills/cg-reftest/SKILL.md around lines 567 - 570, Update the
Dimensions section text that currently reads "**Dimensions** — cg renders at
viewport dims (`width × height`); Chromium screenshots `fullPage`..." and the
earlier sentence "Set height to match cg's cull height; mismatched dims score
0.0..." to clarify that viewport.height is only optional for fixtures that force
their own height via the paint preset (e.g., `min-height: 800px`), and that
natural layout fixtures still require an explicit `viewport.height` matching
cg's reported `WxH` to avoid a dimension-mismatch/score 0.0 (keep references to
`fullPage`, `viewport.height`, and `@grida/reftest` in the wording).
| // Stroke once as an RRect when sides are uniform *and* the style is | ||
| // one whose rendering doesn't depend on per-side color adjustments | ||
| // (inset / outset / groove / ridge darken/lighten per side). The | ||
| // per-side trapezoid path double-paints corners for translucent colors; | ||
| // the single-stroke path avoids that. | ||
| let uniform_stroke_style = matches!( | ||
| b.top.style, | ||
| types::BorderStyle::Solid | types::BorderStyle::Double | ||
| ); | ||
| if uniform | ||
| && uniform_stroke_style | ||
| && b.top.width > 0.0 | ||
| && b.top.style != types::BorderStyle::None | ||
| && !style.border_radius.is_zero() | ||
| && (b.top.style != types::BorderStyle::None || !style.border_radius.is_zero()) | ||
| { | ||
| paint_uniform_rounded_border(canvas, &b.top, &style.border_radius, w, h); | ||
| paint_uniform_rounded_border(canvas, &b.top, &style.border_radius.resolved(w, h), w, h); |
There was a problem hiding this comment.
Keep rounded dashed/dotted borders on an RRect path.
The narrowed fast path excludes Dashed/Dotted, so uniform rounded dashed/dotted borders fall back to straight per-side lines and lose border-radius. Prefer the per-side path only when there is no radius, preserving the rounded path for radius cases.
Proposed fix
- let uniform_stroke_style = matches!(
- b.top.style,
- types::BorderStyle::Solid | types::BorderStyle::Double
- );
+ let has_radius = !style.border_radius.is_zero();
+ let uniform_stroke_style = matches!(
+ b.top.style,
+ types::BorderStyle::Solid | types::BorderStyle::Double
+ ) || (has_radius
+ && matches!(
+ b.top.style,
+ types::BorderStyle::Dashed | types::BorderStyle::Dotted
+ ));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Stroke once as an RRect when sides are uniform *and* the style is | |
| // one whose rendering doesn't depend on per-side color adjustments | |
| // (inset / outset / groove / ridge darken/lighten per side). The | |
| // per-side trapezoid path double-paints corners for translucent colors; | |
| // the single-stroke path avoids that. | |
| let uniform_stroke_style = matches!( | |
| b.top.style, | |
| types::BorderStyle::Solid | types::BorderStyle::Double | |
| ); | |
| if uniform | |
| && uniform_stroke_style | |
| && b.top.width > 0.0 | |
| && b.top.style != types::BorderStyle::None | |
| && !style.border_radius.is_zero() | |
| && (b.top.style != types::BorderStyle::None || !style.border_radius.is_zero()) | |
| { | |
| paint_uniform_rounded_border(canvas, &b.top, &style.border_radius, w, h); | |
| paint_uniform_rounded_border(canvas, &b.top, &style.border_radius.resolved(w, h), w, h); | |
| // Stroke once as an RRect when sides are uniform *and* the style is | |
| // one whose rendering doesn't depend on per-side color adjustments | |
| // (inset / outset / groove / ridge darken/lighten per side). The | |
| // per-side trapezoid path double-paints corners for translucent colors; | |
| // the single-stroke path avoids that. | |
| let has_radius = !style.border_radius.is_zero(); | |
| let uniform_stroke_style = matches!( | |
| b.top.style, | |
| types::BorderStyle::Solid | types::BorderStyle::Double | |
| ) || (has_radius | |
| && matches!( | |
| b.top.style, | |
| types::BorderStyle::Dashed | types::BorderStyle::Dotted | |
| )); | |
| if uniform | |
| && uniform_stroke_style | |
| && b.top.width > 0.0 | |
| && (b.top.style != types::BorderStyle::None || !style.border_radius.is_zero()) | |
| { | |
| paint_uniform_rounded_border(canvas, &b.top, &style.border_radius.resolved(w, h), w, h); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/grida-canvas/src/htmlcss/paint.rs` around lines 1836 - 1850, The
fast-path currently excludes dashed/dotted via uniform_stroke_style causing
uniform rounded dashed/dotted borders to fall back to per-side straight lines
and lose border-radius; update the condition so dashed and dotted styles are
treated as uniform when a non-zero radius exists: include
types::BorderStyle::Dashed and types::BorderStyle::Dotted in the
uniform_stroke_style check (or otherwise allow those styles through) and ensure
the per-side fallback is only chosen when style.border_radius.is_zero(); keep
references to uniform_stroke_style, types::BorderStyle::{Dashed,Dotted},
style.border_radius and paint_uniform_rounded_border when making the change.
| html, | ||
| body { | ||
| min-height: 800px; | ||
| box-sizing: border-box; | ||
| } |
There was a problem hiding this comment.
Minor: paint-style sizing in a layout-* fixture.
Per the README's paint-vs-layout authoring rules, layout-* fixtures should "never force a body size" and instead carry an explicit viewport in the suite entry. This fixture uses the paint preset (min-height: 800px; box-sizing: border-box;) on html/body and is registered without a per-fixture viewport in L0.coverage.json.
That's fine here because the flex containers have explicit width/height, so the body min-height doesn't contaminate the measurement — but it's worth being explicit about that choice if the intent is to standardize layout fixtures on natural cull height.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@fixtures/test-html/L0/layout-flex-direction-reverse.html` around lines 7 -
11, The fixture sets paint-style sizing on the global selector "html, body"
(min-height: 800px; box-sizing: border-box;) which violates the `layout-*`
guideline to avoid forcing body size; either remove those rules from the fixture
so the layout test uses natural cull height, or instead add an explicit
per-fixture viewport for this fixture in L0.coverage.json to document the
intended 800px height; locate the CSS in layout-flex-direction-reverse.html (the
"html, body" rule) and apply one of these two fixes to make the intent explicit.
| <div class="grid"> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| </div> | ||
| <div class="grid autoflow-column"> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| <div class="cell"></div> | ||
| </div> |
There was a problem hiding this comment.
Autoflow behavior is not observable in this fixture.
Both grids fully fill a 3×2 matrix with identical cells, so default flow and grid-auto-flow: column produce the same visual result. This can pass even if placement order is wrong.
Suggested fixture adjustment
.cell {
background: `#000`;
}
+ .marker {
+ background: `#c00`;
+ }
@@
<div class="grid">
<div class="cell"></div>
- <div class="cell"></div>
+ <div class="cell marker"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
- <div class="cell"></div>
</div>
<div class="grid autoflow-column">
<div class="cell"></div>
- <div class="cell"></div>
+ <div class="cell marker"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
- <div class="cell"></div>
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="grid"> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| </div> | |
| <div class="grid autoflow-column"> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| </div> | |
| .cell { | |
| background: `#000`; | |
| } | |
| .marker { | |
| background: `#c00`; | |
| } | |
| </style> | |
| <div class="grid"> | |
| <div class="cell"></div> | |
| <div class="cell marker"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| </div> | |
| <div class="grid autoflow-column"> | |
| <div class="cell"></div> | |
| <div class="cell marker"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| <div class="cell"></div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@fixtures/test-html/L0/layout-grid-autoflow.html` around lines 39 - 54, The
fixture currently uses two identical 3×2 grids so grid-auto-flow differences are
not visible; update the second grid (the element with class "grid
autoflow-column") to expose column-flow behavior by changing its child count or
arrangement — for example reduce the number of .cell children (e.g., 4 instead
of 6) or reorder/add cells so column filling (top-to-bottom then left-to-right)
produces a different visual layout than the default flow; keep the first grid
(class "grid") as-is so the contrast is observable.
| .label { | ||
| font-size: 11px; | ||
| color: #666; | ||
| padding-bottom: 4px; | ||
| width: 140px; | ||
| height: 16px; | ||
| } |
There was a problem hiding this comment.
Keep this paint fixture text-free to avoid font-rendering noise.
The visible labels add font rasterization and unrelated alpha-mask coverage to a translucent-border test. Prefer encoding this metadata in class names/comments, or remove the labels so the fixture only scores the border rendering.
🧹 Proposed cleanup
- .label {
- font-size: 11px;
- color: `#666`;
- padding-bottom: 4px;
- width: 140px;
- height: 16px;
- }
-
.grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 24px;
@@
<div class="grid">
- <div>
- <div class="label">4px red@0.5</div>
- <div class="box b-thin"></div>
- </div>
- <div>
- <div class="label">12px blue@0.5</div>
- <div class="box b-thick"></div>
- </div>
- <div>
- <div class="label">8px green@0.5</div>
- <div class="box b-green"></div>
- </div>
+ <div class="box b-thin"></div>
+ <div class="box b-thick"></div>
+ <div class="box b-green"></div>
</div>Also applies to: 56-66
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@fixtures/test-html/L0/paint-border-translucent.html` around lines 21 - 27,
The fixture currently includes visible text via the .label element which
introduces font rasterization and alpha masks that pollute the
translucent-border test; remove any textual content from the HTML labels (or
replace with semantic class names / comments) and eliminate styling that forces
visible text (see the .label class) so the fixture is text-free and only
exercises border rendering; update any instances noted (including the other
similar blocks at lines referenced by the reviewer) to use non-rendered metadata
(class names or comments) instead of visible labels.
| .box { | ||
| width: 120px; | ||
| height: 120px; | ||
| background: #000; | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| .r-90 { | ||
| transform: rotate(90deg); | ||
| } | ||
| .r-180 { | ||
| transform: rotate(180deg); | ||
| } | ||
| .r-270 { | ||
| transform: rotate(-90deg); | ||
| } |
There was a problem hiding this comment.
Rotation is not visually observable with a uniform square.
A solid black 120×120 square is rotationally symmetric under 90°/180°/−90°, so all three .frame cells render identically to an un-rotated box. This fixture will pass/fail identically regardless of whether transform: rotate(...) is applied at all, providing no signal for the rotate implementation. Consider using an asymmetric shape/content (e.g., a non-square rectangle, an L-shape via pseudo-elements, or two stacked colored halves) so rotation produces a visibly distinct result.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@fixtures/test-html/L0/paint-transform-rotate.html` around lines 42 - 57, The
test uses a uniform 120×120 .box so .r-90, .r-180 and .r-270 produce no visible
change; update the fixture so rotation is observable by making the box
asymmetric (e.g., change .box to a non-square rectangle, add an L-shaped marker
via a ::before/::after pseudo-element, or split the box into two differently
colored halves) so the transforms on .r-90, .r-180 and .r-270 produce distinct
visual differences in the .frame cells.
| { "path": "../L0/layout-flex-row-basic.html" }, | ||
| { "path": "../L0/layout-flex-column-basic.html" }, | ||
| { "path": "../L0/paint-background-clip-boxes.html" }, | ||
| { "path": "../L0/paint-border-translucent.html" }, | ||
| { "path": "../L0/paint-margin-simple.html" }, | ||
| { "path": "../L0/paint-padding-simple.html" }, | ||
| { "path": "../L0/paint-border-double-rect.html" }, | ||
| { "path": "../L0/paint-aspect-ratio.html" }, | ||
| { "path": "../L0/paint-max-min-size.html" }, | ||
| { "path": "../L0/paint-position-relative.html" }, | ||
| { "path": "../L0/layout-flex-align-items.html" }, | ||
| { "path": "../L0/paint-margin-auto-center.html" }, | ||
| { "path": "../L0/layout-flex-grow.html" }, | ||
| { "path": "../L0/paint-color-hex-alpha.html" }, | ||
| { "path": "../L0/layout-flex-wrap.html" }, | ||
| { "path": "../L0/layout-grid-basic.html" }, | ||
| { "path": "../L0/layout-grid-fr.html" }, | ||
| { "path": "../L0/layout-grid-span.html" }, | ||
| { "path": "../L0/layout-block-flow.html" }, | ||
| { "path": "../L0/paint-outline-double-rect.html" }, | ||
| { "path": "../L0/paint-border-radius-individual.html" }, | ||
| { "path": "../L0/paint-filter-simple.html" }, | ||
| { "path": "../L0/paint-mix-blend-mode.html" }, | ||
| { "path": "../L0/paint-filter-chain.html" }, | ||
| { "path": "../L0/layout-grid-gap-asym.html" }, | ||
| { "path": "../L0/paint-transform-combined.html" }, | ||
| { "path": "../L0/paint-transform-origin.html" }, | ||
| { "path": "../L0/paint-individual-transform-props.html" }, | ||
| { "path": "../L0/paint-clip-path-circle.html" }, | ||
| { "path": "../L0/paint-clip-path-polygon.html" }, | ||
| { "path": "../L0/paint-clip-path-ellipse.html" }, | ||
| { "path": "../L0/paint-opacity-nested.html" }, | ||
| { "path": "../L0/layout-flex-direction-reverse.html" }, | ||
| { "path": "../L0/paint-box-shadow-multiple.html" }, | ||
| { "path": "../L0/paint-outline-offset.html" }, | ||
| { "path": "../L0/layout-flex-align-self.html" }, | ||
| { "path": "../L0/layout-grid-autoflow.html" }, | ||
| { "path": "../L0/layout-flex-basis.html" }, | ||
| { "path": "../L0/paint-transform-skew.html" }, | ||
| { "path": "../L0/paint-gradient-radial.html" }, | ||
| { "path": "../L0/paint-outline-radius.html" }, | ||
| { "path": "../L0/paint-box-shadow-blur.html" }, | ||
| { "path": "../L0/paint-box-shadow-inset-blur.html" }, | ||
| { "path": "../L0/paint-border-style-dashed.html" }, | ||
| { "path": "../L0/paint-border-style-dotted.html" }, | ||
| { "path": "../L0/paint-filter-drop-shadow.html" }, | ||
| { "path": "../L0/paint-transform-matrix.html" }, | ||
| { "path": "../L0/paint-filter-blur.html" } |
There was a problem hiding this comment.
Many new layout-* fixtures registered without explicit viewport — expected for coverage suite but flag before promoting to exact.
Per the README authoring rules, layout fixtures should carry an explicit viewport: { width, height } matching cg's natural cull; paint fixtures inherit the preset. Several entries here are layout-* (e.g. layout-flex-direction-reverse.html, layout-flex-grow.html, layout-flex-wrap.html, layout-grid-*, layout-flex-basis.html, layout-block-flow.html) but have no per-fixture viewport — they'll inherit the paint-style default 600×800.
For L0.coverage.json this is fine (tracking only). But before any of these are promoted to L0.exact.json, re-measure each fixture's natural cull and attach an explicit viewport per the README, otherwise the score will be sensitive to whatever dimensional padding the default viewport introduces.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@fixtures/test-html/suites/L0.coverage.json` around lines 40 - 87, The
coverage suite file L0.coverage.json contains many layout-* fixtures without
explicit viewport entries (e.g. layout-flex-direction-reverse.html,
layout-flex-grow.html, layout-flex-wrap.html, layout-grid-basic.html,
layout-flex-basis.html, layout-block-flow.html); before promoting any of these
to L0.exact.json, measure each fixture's natural cull (the renderer's intrinsic
width and height) and add a per-fixture "viewport": { "width": <w>, "height":
<h> } object to the corresponding JSON entry in L0.coverage.json (or ensure the
same is present when copying into L0.exact.json) so the layout tests use
explicit dimensions instead of inheriting the 600×800 paint default.
| "gate": { | ||
| "threshold": 0, | ||
| "aa": false, | ||
| "aa": true, | ||
| "floor": 1.0 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify which config namespace is used for AA diff settings.
# Expected: AA should be read from `diff.aa`; if no `gate.aa` reader exists, keep AA under `diff`.
rg -n -C3 'diffRaw|config\?\.diff|includeAA|gate.*aa|aa.*gate' \
packages/grida-reftest/src/config.ts \
packages/grida-reftest/src/cli.ts \
packages/grida-reftest/src/compare.tsRepository: gridaco/grida
Length of output: 3448
Move the AA setting under diff; gate.aa is not read by any config parser.
Line 6 sets gate.aa, but the config parser (config.ts:83–91) reads only from diff.aa, and the CLI (cli.ts:188) resolves to config?.diff?.aa ?? true. There is no code that consumes gate.aa, making this setting inert.
🔧 Proposed config adjustment
"gate": {
"threshold": 0,
- "aa": true,
"floor": 1.0
},
+ "diff": {
+ "aa": true
+ },
"defaults": {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@fixtures/test-html/suites/L0.exact.json` around lines 4 - 7, The fixture sets
gate.aa which is never read; the code expects diff.aa (see config parser
referencing diff.aa and the CLI resolving config?.diff?.aa). Move the "aa": true
entry out of the "gate" object into the "diff" object in the JSON (add "aa":
true under "diff" and remove "gate.aa"), ensuring the config key names match the
parser/CLI usage (diff.aa) so the setting is actually consumed.
- extract_size / extract_max_size route through length_percentage_to_css so `calc(…)` on width/height/min-*/max-*/flex-basis no longer falls through to Auto (addresses PR #686 review feedback). - mix-blend-mode save_layer omits bounds, matching the filter branch, so outer box-shadows, outlines, and overflowing descendants composite before blending instead of being clipped to the element box. - Hoist has_filter / has_blend_mode to a single computation. No-op for current L0.exact (64/64 scores byte-identical to baseline); fixes correctness for fixtures that exercise calc() sizing or out-of- box paint under mix-blend-mode.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/grida-canvas/src/htmlcss/paint.rs (1)
2236-2369: Minor: hoistresolved_rout of shadow loops.Both outer and inset shadow loops recompute
style.border_radius.resolved(w, h)per shadow, but the result is invariant across the iteration. Low-impact, but a trivial refactor.♻️ Proposed refactor
fn paint_box_shadow_outer(canvas: &Canvas, style: &StyledElement, w: f32, h: f32) { + let resolved_r = style.border_radius.resolved(w, h); + let r = &resolved_r; for shadow in style.box_shadow.iter().rev() { if shadow.inset { continue; } ... - let resolved_r = style.border_radius.resolved(w, h); - let r = &resolved_r; if r.is_zero() {Apply the same to
paint_box_shadow_inset.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-canvas/src/htmlcss/paint.rs` around lines 2236 - 2369, The border-radius resolution (style.border_radius.resolved(w, h)) is computed inside both shadow loops; hoist it once before each loop to avoid redundant work: compute let resolved_r = style.border_radius.resolved(w, h); (and let r = &resolved_r;) outside the for shadow in style.box_shadow.iter().rev() loop in the top-level box-shadow painting block and likewise at the start of fn paint_box_shadow_inset so the loops reuse the same resolved_r/r values rather than recomputing per shadow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/grida-canvas/src/htmlcss/paint.rs`:
- Around line 2119-2152: The implementation of select_best_dash_gap deviates
from Blink when min_num_dashes == 1 on open paths because the `.max(1.0)` guards
force a finite min_gap; to match Blink, allow the divisor to be zero so min_gap
can become +∞ and the selection logic picks max_gap as Blink does. Change the
computations that currently use `.max(1.0)` (for min_num_dashes and min_num_gaps
/ max_num_gaps on open paths) to remove the `.max(1.0)` clamp for the open-path
branch so min_num_gaps becomes (min_num_dashes - 1.0) and max_num_gaps becomes
(max_num_dashes - 1.0) (leaving the closed_path branches unchanged); keep the
comment about div-by-zero behavior so future readers understand the intentional
+∞ result.
---
Nitpick comments:
In `@crates/grida-canvas/src/htmlcss/paint.rs`:
- Around line 2236-2369: The border-radius resolution
(style.border_radius.resolved(w, h)) is computed inside both shadow loops; hoist
it once before each loop to avoid redundant work: compute let resolved_r =
style.border_radius.resolved(w, h); (and let r = &resolved_r;) outside the for
shadow in style.box_shadow.iter().rev() loop in the top-level box-shadow
painting block and likewise at the start of fn paint_box_shadow_inset so the
loops reuse the same resolved_r/r values rather than recomputing per shadow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7702d6ab-13af-4cff-9c4e-a55dd0647d02
📒 Files selected for processing (2)
crates/grida-canvas/src/htmlcss/collect.rscrates/grida-canvas/src/htmlcss/paint.rs
| fn select_best_dash_gap( | ||
| stroke_length: f32, | ||
| dash_length: f32, | ||
| gap_length: f32, | ||
| closed_path: bool, | ||
| ) -> f32 { | ||
| let available = if closed_path { | ||
| stroke_length | ||
| } else { | ||
| stroke_length + gap_length | ||
| }; | ||
| let min_num_dashes = (available / (dash_length + gap_length)).floor().max(1.0); | ||
| let max_num_dashes = min_num_dashes + 1.0; | ||
| // `.max(1.0)` guards div-by-zero when `min_num_dashes == 1` | ||
| // on an open path. Blink lets the divide produce +∞ and relies | ||
| // on the `max_gap <= 0.0` branch below to pick `min_gap` anyway. | ||
| let min_num_gaps = if closed_path { | ||
| min_num_dashes | ||
| } else { | ||
| (min_num_dashes - 1.0).max(1.0) | ||
| }; | ||
| let max_num_gaps = if closed_path { | ||
| max_num_dashes | ||
| } else { | ||
| (max_num_dashes - 1.0).max(1.0) | ||
| }; | ||
| let min_gap = (stroke_length - min_num_dashes * dash_length) / min_num_gaps; | ||
| let max_gap = (stroke_length - max_num_dashes * dash_length) / max_num_gaps; | ||
| if max_gap <= 0.0 || (min_gap - gap_length).abs() < (max_gap - gap_length).abs() { | ||
| min_gap | ||
| } else { | ||
| max_gap | ||
| } | ||
| } |
There was a problem hiding this comment.
select_best_dash_gap diverges from Blink for the min_num_dashes == 1 open-path case.
Blink lets the divisor be 0 so min_gap becomes +∞, which then forces the selection branch to pick max_gap (unless that is also non-positive). The .max(1.0) guard here produces a finite min_gap = stroke_length − dash_length, flipping the absolute-difference comparison and selecting min_gap in some cases where Blink would select max_gap.
Given this path is gated by len > dash * 2.0 for dashed and len >= per_dot for dotted, min_num_dashes >= 1 is guaranteed; the divergence is narrow but real for very short sides. The comment acknowledges it — worth flagging so it isn't forgotten once thin-dotted EnforceDotsAtEndpoints lands.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/grida-canvas/src/htmlcss/paint.rs` around lines 2119 - 2152, The
implementation of select_best_dash_gap deviates from Blink when min_num_dashes
== 1 on open paths because the `.max(1.0)` guards force a finite min_gap; to
match Blink, allow the divisor to be zero so min_gap can become +∞ and the
selection logic picks max_gap as Blink does. Change the computations that
currently use `.max(1.0)` (for min_num_dashes and min_num_gaps / max_num_gaps on
open paths) to remove the `.max(1.0)` clamp for the open-path branch so
min_num_gaps becomes (min_num_dashes - 1.0) and max_num_gaps becomes
(max_num_dashes - 1.0) (leaving the closed_path branches unchanged); keep the
comment about div-by-zero behavior so future readers understand the intentional
+∞ result.
Large-scale Chromium-parity push for the
cghtmlcss renderer: 65 L0.exact fixtures (all 100.00% vs Chromium under AA-ignore) plus 13 renderer bug fixes and two reftest infrastructure upgrades.Renderer fixes
set_alpha_ffor float-native opacity (wasu8truncation).round() as u8for sRGB channel conversion (was truncation)border-radiusresolved at paint time against border-boxbackground-clipapplied tobackground-colorlayermix-blend-modeapplied via save-layer blend modeset_dither(true)) + premul interpolation (InPremul::Yes)BlurRadiusToStdDev)canvas.translateoffsetflex-basisplumbed from styloFlexBasis<Size>to TaffySelectBestDashGapper side length[0, gap+w-ε]round-cap path + thick endpoint insetReftest infrastructure
html, body { background: transparent }applied viaextra_css; cg surface cleared toTRANSPARENT.pixelmatch includeAA: false; Skia/Blink AA-policy divergence excluded from score. Strict byte-exact still available via--no-aaorgate.aa: false.L0.exact fixtures added (65 total)
Paint
paint-opacity,paint-opacity-levels,paint-opacity-nested,paint-background-solid,paint-background-clip-boxes,paint-color-hex-alpha,paint-border-radius,paint-border-radius-individual,paint-border-solid,paint-border-double-rect,paint-border-translucent,paint-border-style-dashed,paint-outline-solid,paint-outline-double-rect,paint-outline-offset,paint-outline-radius,paint-box-shadow-solid,paint-box-shadow-inset-solid,paint-box-shadow-multiple,paint-box-shadow-blur,paint-box-shadow-inset-blur,paint-clip-path-inset,paint-clip-path-circle,paint-clip-path-ellipse,paint-clip-path-polygon,paint-filter-simple,paint-filter-chain,paint-filter-blur,paint-filter-drop-shadow,paint-mix-blend-mode,paint-z-index-simple,paint-overflow-hidden,paint-visibility,paint-display-none,paint-position-absolute-simple,paint-position-relative,paint-margin-simple,paint-margin-auto-center,paint-padding-simple,paint-aspect-ratio,paint-max-min-size,paint-transform-translate,paint-transform-scale,paint-transform-skew,paint-transform-rotate(coverage),paint-transform-combined,paint-transform-origin,paint-transform-matrix,paint-individual-transform-propsLayout
layout-block-flow,layout-flex-row-basic,layout-flex-column-basic,layout-flex-align-items,layout-flex-align-self,layout-flex-grow,layout-flex-basis,layout-flex-wrap,layout-flex-direction-reverse,layout-grid-basic,layout-grid-fr,layout-grid-span,layout-grid-gap-asym,layout-grid-autoflowL0.coverage fixtures (tracked gaps)
paint-background-gradient-linear-simple(98.19% — dither-lattice phase),paint-gradient-radial(92.76%),paint-border-style-dotted(96.24%),paint-transform-rotate(99.98% — rotation AA edge)Sub-PRs merged
Summary by CodeRabbit
New Features
Bug Fixes
Documentation