Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
072d24d
test(htmlcss): promote paint-opacity + box-padding to L0.exact
softmarshmallow Apr 22, 2026
a5d4b29
docs(fixtures/test-html): captions OK, keep short, pin dims around them
softmarshmallow Apr 22, 2026
ffd5c98
docs(skills/fixtures): note short labels + pin container dims
softmarshmallow Apr 22, 2026
1082111
Merge pull request #685 from gridaco/feature/nice-satoshi-568a9f
softmarshmallow Apr 22, 2026
09f4ec2
test(htmlcss): promote paint-opacity + box-padding to L0.exact
softmarshmallow Apr 22, 2026
b93bc62
docs(fixtures/test-html): captions OK, keep short, pin dims around them
softmarshmallow Apr 22, 2026
9224fe4
docs(skills/fixtures): note short labels + pin container dims
softmarshmallow Apr 22, 2026
1054ddc
fix(htmlcss): round sRGB components to u8, promote paint-background-s…
softmarshmallow Apr 22, 2026
d5a323c
feat(htmlcss): resolve percentage border-radius at paint time
softmarshmallow Apr 22, 2026
957c1f3
test(htmlcss): add paint-border-solid fixture at 100.00%
softmarshmallow Apr 22, 2026
584ea56
test(htmlcss): add paint-outline-solid fixture at 100.00%
softmarshmallow Apr 22, 2026
753e7e7
test(htmlcss): add paint-box-shadow-solid fixture at 100.00%
softmarshmallow Apr 22, 2026
75b5327
test(htmlcss): add paint-background-gradient-linear-simple to L0.cove…
softmarshmallow Apr 22, 2026
7f83721
fix(htmlcss): match Blink gradient dither + premul interpolation
softmarshmallow Apr 22, 2026
394e0cf
test(htmlcss): add paint-clip-path-inset fixture at 100.00%
softmarshmallow Apr 22, 2026
7818bda
test(htmlcss): add paint-box-shadow-inset-solid fixture at 100.00%
softmarshmallow Apr 22, 2026
ae93f20
test(htmlcss): add paint-transform-translate fixture at 100.00%
softmarshmallow Apr 22, 2026
64bdc6e
test(htmlcss): add paint-transform-scale fixture at 100.00%
softmarshmallow Apr 22, 2026
561e604
test(htmlcss): add paint-transform-rotate to L0.coverage
softmarshmallow Apr 22, 2026
ab3408e
test(htmlcss): add paint-opacity-levels fixture at 100.00%
softmarshmallow Apr 22, 2026
4ba80d5
test(htmlcss): add paint-position-absolute-simple at 100.00%
softmarshmallow Apr 22, 2026
7713293
test(htmlcss): add paint-z-index-simple at 100.00%
softmarshmallow Apr 22, 2026
7dd1576
test(htmlcss): add paint-overflow-hidden at 100.00%
softmarshmallow Apr 22, 2026
b89badb
test(htmlcss): add paint-visibility at 100.00%
softmarshmallow Apr 22, 2026
0413ed5
test(htmlcss): add paint-display-none at 100.00%
softmarshmallow Apr 22, 2026
b0af892
test(htmlcss): add layout-flex-row-basic at 100.00%
softmarshmallow Apr 22, 2026
f0bfb46
test(htmlcss): add layout-flex-column-basic at 100.00%
softmarshmallow Apr 22, 2026
9380ae7
feat(htmlcss): honor background-clip for color layer, fix border corn…
softmarshmallow Apr 22, 2026
d502d9d
test(htmlcss): add paint-border-translucent at 100.00%
softmarshmallow Apr 22, 2026
324fdae
test(htmlcss): add paint-margin-simple at 100.00%
softmarshmallow Apr 22, 2026
1b13b15
test(htmlcss): add paint-padding-simple at 100.00%
softmarshmallow Apr 22, 2026
608eaaa
test(htmlcss): add paint-border-double-rect at 100.00%
softmarshmallow Apr 22, 2026
e36ba1a
test(htmlcss): add paint-aspect-ratio at 100.00%
softmarshmallow Apr 22, 2026
dc7c21e
test(htmlcss): add paint-max-min-size at 100.00%
softmarshmallow Apr 22, 2026
55c37af
test(htmlcss): add paint-position-relative at 100.00%
softmarshmallow Apr 22, 2026
4511cd5
test(htmlcss): add layout-flex-align-items at 100.00%
softmarshmallow Apr 22, 2026
d0982ce
test(htmlcss): add paint-margin-auto-center at 100.00%
softmarshmallow Apr 22, 2026
f08bf89
test(htmlcss): add layout-flex-grow at 100.00%
softmarshmallow Apr 22, 2026
1650c3c
test(htmlcss): add paint-color-hex-alpha at 100.00%
softmarshmallow Apr 22, 2026
c902c0f
test(htmlcss): add layout-flex-wrap at 100.00%
softmarshmallow Apr 22, 2026
b8828a3
test(htmlcss): add layout-grid-basic at 100.00%
softmarshmallow Apr 22, 2026
1ec9f81
test(htmlcss): add layout-grid-fr at 100.00%
softmarshmallow Apr 22, 2026
20ad912
test(htmlcss): add layout-grid-span at 100.00%
softmarshmallow Apr 22, 2026
8d3f06c
test(htmlcss): add layout-block-flow at 100.00%
softmarshmallow Apr 22, 2026
90be00f
test(htmlcss): add paint-outline-double-rect at 100.00%
softmarshmallow Apr 22, 2026
6fbc483
test(htmlcss): add paint-border-radius-individual at 100.00%
softmarshmallow Apr 22, 2026
2d48548
test(htmlcss): add paint-filter-simple at 100.00%
softmarshmallow Apr 22, 2026
6a6701b
test(htmlcss): add paint-mix-blend-mode to L0.coverage
softmarshmallow Apr 22, 2026
cd0501d
feat(htmlcss): apply mix-blend-mode via save-layer blend mode
softmarshmallow Apr 22, 2026
62e6d88
test(htmlcss): add paint-filter-chain at 100.00%
softmarshmallow Apr 22, 2026
52774d1
test(htmlcss): add layout-grid-gap-asym at 100.00%
softmarshmallow Apr 22, 2026
b401dd0
test(htmlcss): add paint-transform-combined at 100.00%
softmarshmallow Apr 22, 2026
69a6e1e
test(htmlcss): add paint-transform-origin at 100.00%
softmarshmallow Apr 22, 2026
2b196a6
test(htmlcss): add paint-individual-transform-props at 100.00%
softmarshmallow Apr 22, 2026
19a5d41
test(htmlcss): add paint-clip-path-circle at 100.00%
softmarshmallow Apr 22, 2026
70323cd
test(htmlcss): add paint-clip-path-polygon at 100.00%
softmarshmallow Apr 22, 2026
c6ee13c
test(htmlcss): add paint-clip-path-ellipse at 100.00%
softmarshmallow Apr 23, 2026
776c6f8
test(htmlcss): add paint-opacity-nested at 100.00%
softmarshmallow Apr 23, 2026
9d17d93
test(htmlcss): add layout-flex-direction-reverse at 100.00%
softmarshmallow Apr 23, 2026
0eea64b
test(htmlcss): add paint-box-shadow-multiple at 100.00%
softmarshmallow Apr 23, 2026
8c98ff3
test(htmlcss): add paint-outline-offset at 100.00%
softmarshmallow Apr 23, 2026
a305876
test(htmlcss): add layout-flex-align-self at 100.00%
softmarshmallow Apr 23, 2026
30b3634
test(htmlcss): add layout-grid-autoflow at 100.00%
softmarshmallow Apr 23, 2026
5e1f9f1
test(htmlcss): add layout-flex-basis at 100.00%
softmarshmallow Apr 23, 2026
fede08a
feat(htmlcss): plumb flex-basis from stylo to el.flex_basis
softmarshmallow Apr 23, 2026
f293b7f
fix(htmlcss): reverse box-shadow iteration for CSS stacking order
softmarshmallow Apr 23, 2026
bcf235f
test(htmlcss): add paint-transform-skew L0 fixture
softmarshmallow Apr 23, 2026
7cabe0c
feat(reftest): transparent-canvas content mask + AA-ignore by default
softmarshmallow Apr 23, 2026
a376f1b
Merge remote-tracking branch 'origin/htmlcss' into feature/vibrant-me…
softmarshmallow Apr 23, 2026
157a32c
Merge pull request #687 from gridaco/feature/vibrant-meninsky-1d93d3
softmarshmallow Apr 23, 2026
bde2280
test(htmlcss): add paint-gradient-radial L0 coverage fixture + Blink-…
softmarshmallow Apr 23, 2026
2e8cc05
test(htmlcss): add paint-outline-radius L0.exact fixture
softmarshmallow Apr 23, 2026
81cd840
fix(htmlcss): convert box-shadow blur-radius to Gaussian σ = radius *…
softmarshmallow Apr 23, 2026
cfde7fd
fix(htmlcss): symmetric inset box-shadow frame via canvas translate
softmarshmallow Apr 23, 2026
8689c76
fix(htmlcss): Blink-parity dash ratios + gap adjustment for dashed bo…
softmarshmallow Apr 23, 2026
f39db8f
feat(htmlcss): port Blink dotted-border intervals and thick endpoint …
softmarshmallow Apr 23, 2026
7dad4f3
test(htmlcss): add paint-filter-drop-shadow L0.exact fixture
softmarshmallow Apr 23, 2026
42ff8ab
test(htmlcss): add paint-transform-matrix L0.exact fixture
softmarshmallow Apr 23, 2026
82df66e
test(htmlcss): add paint-filter-blur L0.exact fixture
softmarshmallow Apr 23, 2026
495030e
refactor(htmlcss): simplify shadow + border helpers
softmarshmallow Apr 23, 2026
1750e28
Merge pull request #689 from gridaco/feature/affectionate-kapitsa-f3ff4b
softmarshmallow Apr 23, 2026
33082ac
fix(htmlcss): preserve calc() in sizing + unbounded blend-mode layer
softmarshmallow Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 64 additions & 57 deletions .agents/skills/cg-reftest/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,34 +325,32 @@ against the current suite config, move its entry from `coverage` →
`exact`. Do **not** lower the exact suite's floor to fit new entries;
the bar exists so regressions are loud.

Per-fixture `.reftest.json` sidecars **do not exist** anymore. All
config lives in the suite file.
All per-fixture config lives in the suite file. There are no
per-fixture `.reftest.json` sidecars.

#### Suite JSON shape

```json
{
"name": "L0.exact",
"description": "Byte-exact fixtures; any drop = regression.",
"gate": { "threshold": 0, "aa": false, "floor": 1.0 },
"gate": { "threshold": 0, "aa": true, "floor": 1.0 },
"defaults": {
"wait_for": ["fonts", "networkidle"],
"extra_css": ["../_reftest/hide-text.css"],
"extra_css": [
"../_reftest/hide-text.css",
"../_reftest/transparent-body.css"
],
"full_page": true
},
"fixtures": [
{
"path": "../L0/box-dimensions.html",
"viewport": { "width": 600, "height": 522 }
}
]
"fixtures": [{ "path": "../L0/box-dimensions.html" }]
}
```

- `defaults` — applied to every fixture. Each fixture entry can override any field.
- `fixtures[].path` and every `extra_css[]` path resolve **relative to the suite file**.
- `viewport.height` must match cg's cull height for the diff to succeed; render cg once and read `WxH` to calibrate.
- `gate.threshold` / `gate.aa` are inputs to the pixelmatch diff; `gate.floor` is the aggregate pass bar on similarity.
- **`aa: true` (default)** → pixelmatch `includeAA: false`. Pixelmatch's AA detector fires and excludes anti-aliased edge pixels from the diff count, separating rasterizer edge noise (Skia vs. Blink) from real divergence. Set `aa: false` for strict byte-exact accounting (e.g. probing an AA-class regression).

#### The three-step pipeline

Expand Down Expand Up @@ -395,9 +393,13 @@ cp "${TMPDIR:-/tmp}/grida-htmlcss-goldens/"*.png target/refbrowser/L0.exact/actu
**3. Diff via `@grida/reftest`** — format-agnostic, same bucket layout
and `report.json` schema as the Rust and refig runners.

Default refbrowser diff: **`--threshold 0`** (pixelmatch strictest,
AA off). Pass each fixture's similarity against the suite's
`gate.floor` — for `L0.exact`, that's `1.0` (100.00% byte-exact).
Default refbrowser diff: **`--threshold 0`** (pixelmatch's tightest
color-delta) with **AA-ignore mode on by default** (`aa: true` →
`includeAA: false`; pixelmatch's Vysniauskas AA detector fires and
excludes edge AA pixels from the diff count). Pass `--no-aa` to flip
to strict byte-exact accounting. Pass each fixture's similarity
against the suite's `gate.floor` — for `L0.exact`, that's `1.0`
(100.00% similarity with AA detection active).

```sh
pnpm --filter @grida/reftest exec reftest \
Expand All @@ -419,43 +421,46 @@ Pass bar: the suite's `gate.floor`. For `L0.exact`, anything below
100.00% is a real divergence from Blink (rounding policy, layout
math, AA emission, etc.) — not noise. See "Reading the score" below.

### Reading the score — do not trust it naively

The similarity score is `1 - diff_pixels / scoring_pixels`, where
`scoring_pixels ≈ width × height` of the screenshot. **The denominator
is the whole canvas, not the subject under test.**

This has two consequences you must internalize before reading any
report:

1. **Background dominates the score.** A fixture that paints a
100×100 subject on a 600×800 canvas has 92% background. A renderer
that emits _nothing_ for the subject still scores ~92%. A
renderer that paints the subject at 50% accuracy scores ~96%.
Neither number means what it naively looks like.
2. **Small fixtures inflate. Full-bleed fixtures are honest.** A
card-in-corner composition will always look "good" on the score
even when broken; a composition that fills the viewport gives
numeric feedback proportional to real error.

**Fixture-authoring rule:** size the fixture so the subject under
test fills as much of the canvas as practical. Viewport height
tuned to the subject's bounding box (via the suite entry's
`viewport.height`) is the usual lever. Padding/margins around the
subject are scoring dead weight — use them only when the test is
_about_ spacing.

**Reviewing rule:** never report a similarity number without
eyeballing the diff PNG. A 96% score on a sparse fixture and a 96%
score on a full-bleed fixture are _orders of magnitude_ apart in
severity. The diff image is the source of truth; the score is a
coarse index.

For a true "fraction of the subject that matches," author a
probe-friendly fixture (see the probe test section) and assert on
specific pixels, or mask the background to transparent so
`mask: alpha` counts only subject pixels. Plain refbrowser scores
cannot give you that signal.
### Scoring model — content mask

The similarity score is `1 - diff_pixels / scoring_pixels`.
`scoring_pixels` is the count of pixels where either side has
`alpha > 0` — the content mask, not the full canvas. Three
coupled defaults wire this up:

- **Chromium** screenshots with `omitBackground: true` (in
`refbrowser_render.ts`). Root canvas default bg is dropped; PNG
alpha encodes "did the CSS cascade draw here?"
- **cg** clears its Skia surface with `Color::TRANSPARENT` and
renders at viewport dims (in `examples/golden_htmlcss.rs`).
- **Both sides** apply `_reftest/transparent-body.css` via
`extra_css`. `!important` forces `html, body { background:
transparent }`, so fixtures with `body { background: #fff }`
still produce a content mask without being edited.

Chromium and cg produce identical alpha masks on every L0.exact
fixture. Diffs that appear under `alpha > 0` are genuine pixel
differences.

**AA-ignore on by default** (`aa: true` → pixelmatch
`includeAA: false`). The Vysniauskas AA detector excludes
anti-aliased edge pixels from the diff count. Combined with the
content mask, this yields:

| pattern | strict (`--no-aa`) | default (`--aa`) | meaning |
| ------------------- | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------ |
| **pass** | 100% | 100% | identical — no action. |
| **AA noise** | 99.9+% | 100% | Skia/Blink rasterizer edge jitter on curves, radii, tilted geometry. Safe to ignore. |
| **real divergence** | <100% | <100% | renderer bug or non-AA rasterizer mismatch (dither lattice, multi-color miter wedges). Inspect the diff PNG. |

**Reviewing rule:** always eyeball the diff PNG. A fixture below
100% with `aa: true` has mismatched pixels that pixelmatch could
not explain as edge AA — treat as real.

**Fixture-authoring rule:** the content mask excludes blank bg, so
there's no need to shrink viewports for scoring density. Focus on
minimality (one concept per fixture). Probe tests remain the tool
for vision-free pixel assertions at known coordinates.

**Per-fixture fields inside a suite entry** — all optional,
defaults shown; any field set on an entry overrides `defaults`.
Expand Down Expand Up @@ -484,9 +489,10 @@ defaults shown; any field set on an entry overrides `defaults`.

**Pre-built helper stylesheets** under `fixtures/test-html/_reftest/`:

| File | Effect |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `hide-text.css` | `color: transparent` + `line-height: 1`. Zeros glyph coverage and pins line-box height. Use when a fixture isn't testing text. |
| File | Effect |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hide-text.css` | `color: transparent` + `line-height: 1`. Zeros glyph coverage and pins line-box height. Use when a fixture isn't testing text. |
| `transparent-body.css` | Forces `html, body { background: transparent !important }`. Enables the content mask (alpha>0 = drawn). Both L0 suites apply this by default; drop from `extra_css` for fixtures testing canvas bg. |

Add more helpers here as divergence patterns emerge. Keep each one
scoped to a single concern (hide text, normalize scrollbars, force
Expand Down Expand Up @@ -558,9 +564,10 @@ PR description; let the score carry the truth.
- **Scrollbar width** — default `full_page: true` captures document
height and sidesteps scrollbar chrome; flip only when testing
scrollbar geometry.
- **Dimension drift** — changing a fixture's layout invalidates its
`viewport.height` in the suite entry. Re-run `golden_htmlcss` with
`--suite`, update the entry's `viewport.height`, re-run refbrowser.
- **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.
Comment on lines +567 to +570
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).


**Oracle type summary:**

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/cg-reftest/scripts/refbrowser_render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ async function renderOne(
fullPage: config.full_page,
animations: "disabled",
caret: "hide",
// Alpha encodes "CSS drew here" — used as content mask by scoring.
omitBackground: true,
});

await page.close();
Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/fixtures/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ edge case** that the codebase supports or intends to support. This includes:
filename alone should tell you what's being tested.
- **Labeled specimens.** Within a fixture, label each test case with the
value being exercised so both humans and heuristics can identify regions.
Keep labels short, and pin the dimensions of any container holding a
label (flex item, grid cell, stretched block) so font-advance-width
differences between engines can't leak into box geometry. When a test
pipeline offers a text-neutralizing stylesheet (e.g.
`fixtures/test-html/_reftest/hide-text.css` for the htmlcss reftests),
prefer that over stripping the label — keeping the text helps the next
reader understand the fixture.
- **Match the fixture's subject to the viewport policy.** For refbrowser
fixtures under `fixtures/test-html/`, **paint / visual-property**
fixtures should size their root to a preset viewport (via `min-height`)
Expand Down
9 changes: 5 additions & 4 deletions crates/grida-canvas/examples/golden_htmlcss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,14 @@ fn render_to_png(
) {
let picture =
htmlcss::render(html, width, height, fonts, &htmlcss::NoImages).expect("render failed");
let cull = picture.cull_rect();
let w = cull.width().max(1.0) as i32;
let h = cull.height().max(1.0) as i32;
// Full-viewport dims match Chromium's fullPage footprint; transparent clear
// lets PNG alpha double as the reftest content mask.
let w = width.max(1.0) as i32;
let h = height.max(1.0) as i32;

let mut surface = surfaces::raster_n32_premul((w, h)).expect("surface");
let canvas = surface.canvas();
canvas.clear(Color::WHITE);
canvas.clear(Color::TRANSPARENT);
canvas.draw_picture(&picture, None, None);

let image = surface.image_snapshot();
Expand Down
92 changes: 57 additions & 35 deletions crates/grida-canvas/src/htmlcss/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ fn collect_inline_items(el: &StyledElement, items: &mut Vec<InlineRunItem>) {
/// Returns `None` if the element has no visual box decoration.
fn build_inline_decoration(el: &StyledElement) -> Option<InlineBoxDecoration> {
let bg = el.background.first().and_then(|l| match l {
BackgroundLayer::Solid(c) if c.a > 0 => Some(*c),
BackgroundLayer::Solid { color, .. } if color.a > 0 => Some(*color),
_ => None,
});

Expand Down Expand Up @@ -1185,6 +1185,10 @@ fn extract_style(tag: &str, style: &ComputedValues) -> StyledElement {
// Flex child
el.flex_grow = style.clone_flex_grow().0;
el.flex_shrink = style.clone_flex_shrink().0;
el.flex_basis = match &pos.flex_basis {
style::values::generics::flex::FlexBasis::Size(s) => extract_size(s),
style::values::generics::flex::FlexBasis::Content => CssLength::Auto,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Grid container
if el.display == types::Display::Grid {
Expand Down Expand Up @@ -1571,34 +1575,49 @@ fn extract_gradient_interpolation(

fn extract_border_radius(style: &ComputedValues) -> CornerRadii {
let b = style.get_border();
let lp = |lp: &style::values::computed::NonNegativeLengthPercentage| -> f32 {
lp.0.to_length().map(|l| l.px()).unwrap_or(0.0)
// Returns (px, percent_fraction). Percent is deferred to paint time,
// where the border-box w/h is known (CSS Backgrounds 3 §5.3).
let lp = |v: &style::values::computed::NonNegativeLengthPercentage| -> (f32, f32) {
match length_percentage_to_css(&v.0) {
CssLength::Px(p) => (p, 0.0),
CssLength::Percent(p) => (0.0, p),
CssLength::Calc { px, percent } => (px, percent),
CssLength::Auto => (0.0, 0.0),
}
};
let (tl_x, tl_x_pct) = lp(&b.border_top_left_radius.0.width);
let (tl_y, tl_y_pct) = lp(&b.border_top_left_radius.0.height);
let (tr_x, tr_x_pct) = lp(&b.border_top_right_radius.0.width);
let (tr_y, tr_y_pct) = lp(&b.border_top_right_radius.0.height);
let (br_x, br_x_pct) = lp(&b.border_bottom_right_radius.0.width);
let (br_y, br_y_pct) = lp(&b.border_bottom_right_radius.0.height);
let (bl_x, bl_x_pct) = lp(&b.border_bottom_left_radius.0.width);
let (bl_y, bl_y_pct) = lp(&b.border_bottom_left_radius.0.height);
CornerRadii {
tl_x: lp(&b.border_top_left_radius.0.width),
tl_y: lp(&b.border_top_left_radius.0.height),
tr_x: lp(&b.border_top_right_radius.0.width),
tr_y: lp(&b.border_top_right_radius.0.height),
br_x: lp(&b.border_bottom_right_radius.0.width),
br_y: lp(&b.border_bottom_right_radius.0.height),
bl_x: lp(&b.border_bottom_left_radius.0.width),
bl_y: lp(&b.border_bottom_left_radius.0.height),
tl_x,
tl_y,
tr_x,
tr_y,
br_x,
br_y,
bl_x,
bl_y,
tl_x_pct,
tl_y_pct,
tr_x_pct,
tr_y_pct,
br_x_pct,
br_y_pct,
bl_x_pct,
bl_y_pct,
}
}

fn extract_size(
size: &GenericSize<style::values::computed::NonNegativeLengthPercentage>,
) -> CssLength {
match size {
GenericSize::LengthPercentage(lp) => {
if let Some(len) = lp.0.to_length() {
CssLength::Px(len.px())
} else if let Some(pct) = lp.0.to_percentage() {
CssLength::Percent(pct.0)
} else {
CssLength::Auto
}
}
GenericSize::LengthPercentage(lp) => length_percentage_to_css(&lp.0),
_ => CssLength::Auto,
}
}
Expand All @@ -1610,15 +1629,7 @@ fn extract_max_size(
) -> CssLength {
use style::values::generics::length::GenericMaxSize;
match size {
GenericMaxSize::LengthPercentage(lp) => {
if let Some(len) = lp.0.to_length() {
CssLength::Px(len.px())
} else if let Some(pct) = lp.0.to_percentage() {
CssLength::Percent(pct.0)
} else {
CssLength::Auto
}
}
GenericMaxSize::LengthPercentage(lp) => length_percentage_to_css(&lp.0),
_ => CssLength::Auto, // None, MaxContent, MinContent, FitContent, etc.
}
}
Expand Down Expand Up @@ -1653,11 +1664,22 @@ fn extract_background(style: &ComputedValues, current_color: CGColor) -> Vec<Bac
let bg = style.get_background();
let mut layers: Vec<BackgroundLayer> = Vec::new();

// 1. Background color (bottom layer)
// 1. Background color (bottom layer). Per CSS Backgrounds 3 §2.5 the
// color uses the `background-clip` value from the *final* layer
// entry in the list.
if let Some(abs) = bg.background_color.as_absolute() {
let c = abs_color_to_cg(abs);
if c.a > 0 {
layers.push(BackgroundLayer::Solid(c));
let color_clip = bg
.background_clip
.0
.last()
.map(extract_bg_clip)
Comment on lines +1673 to +1677
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

.unwrap_or(BackgroundBox::BorderBox);
layers.push(BackgroundLayer::Solid {
color: c,
clip: color_clip,
});
}
}

Expand Down Expand Up @@ -2771,10 +2793,10 @@ fn map_overflow(ov: style::values::specified::box_::Overflow) -> types::Overflow
fn abs_color_to_cg(color: &AbsoluteColor) -> CGColor {
let srgb = color.to_color_space(ColorSpace::Srgb);
CGColor::from_rgba(
(srgb.components.0.clamp(0.0, 1.0) * 255.0) as u8,
(srgb.components.1.clamp(0.0, 1.0) * 255.0) as u8,
(srgb.components.2.clamp(0.0, 1.0) * 255.0) as u8,
(srgb.alpha.clamp(0.0, 1.0) * 255.0) as u8,
(srgb.components.0.clamp(0.0, 1.0) * 255.0).round() as u8,
(srgb.components.1.clamp(0.0, 1.0) * 255.0).round() as u8,
(srgb.components.2.clamp(0.0, 1.0) * 255.0).round() as u8,
(srgb.alpha.clamp(0.0, 1.0) * 255.0).round() as u8,
)
}

Expand Down
Loading
Loading