diff --git a/FIDELITY.md b/FIDELITY.md
index 72f39af..b15c641 100644
--- a/FIDELITY.md
+++ b/FIDELITY.md
@@ -18,6 +18,62 @@ The committed PNGs under `testdata/renders/` back every claim here. Reproduce
them with the commands at the bottom. The measured-vs-Chrome numbers live in
[`bench/REPORT.md`](bench/REPORT.md).
+## 2026-09-05 (round 44) — a plain inline element's own `margin-left`/`margin-right` had NO EFFECT at all — `InlineItem` carries no margin field, so a browser-rendered gap between two adjacent inline elements with no source whitespace between them silently vanished (engine#124)
+
+news.ycombinator.com re-investigated fresh with a full top-to-bottom
+comparison (last touched round 33/38, via the general BlockBreak mechanism
+rather than a dedicated look at this page's own remaining diffs). Live at
+1024px, the header read "**Hacker News**new | past | comments | ..." — the
+site title and the first nav link ran together with no gap, where Chrome
+shows a clear space.
+
+Root cause, confirmed against the real source: `Hacker Newsnew` — genuinely
+no whitespace at all between the two elements in the HTML. The margin-right
+IS the only source of the gap Chrome renders. `InlineItem` (the atom of
+inline layout) has no margin field of any kind, and nothing in inline
+collection ever reads `Style.Margin.Left`/`.Right` for a plain (non-replaced,
+non-atomic) inline element — its margin was simply never consulted.
+
+Fixed with a `pendingMargin` accumulator on the layouter: entering a plain
+inline element adds its `margin-left`, leaving one adds its `margin-right`,
+and whichever `InlineItem` is created next (in `appendWords`, or the
+img/svg and form-control atomic-item branches) takes the accumulated
+value into its `SpaceBefore` field via a new `takeMargin()` method — the
+same field collapsible whitespace already uses to represent "space before
+this item," reusing the existing line-layout machinery rather than adding a
+parallel code path. Adjacent inline elements' margins correctly ADD (not
+collapse, unlike block margins) since accumulation only resets at
+`takeMargin()`. Reset alongside the whitespace-collapsing state at a
+genuine break (a promoted block or forced `
`): a stale margin has
+nothing left on the same line to apply to once one interrupts.
+
+**A real, narrower limitation surfaced by the session's own OWN regression
+test, not shipped over**: an inline margin lands correctly whenever the
+margined element is not the very first item on its line — but
+`layoutInline`/`wrapOneLine`/`WrapItems` deliberately ignore `SpaceBefore`
+for a line's first item (so collapsible leading whitespace never creates a
+phantom indent), and a margin riding in that SAME field is ignored for the
+identical reason. A dedicated non-collapsible field would be needed to
+survive that case too, not attempted here absent any CONFIRMED live page
+needing it (this engine's real, confirmed use of inline margin-right is
+always between two things already sharing a line, as on this page) — the
+regression test suite covers the working case, documents the gap plainly in
+`pendingMargin`'s own doc comment, and does not claim more than what is
+actually fixed.
+
+Verified live: "Hacker News new | past | ..." now renders with the correct
+gap. **Bench is flat (SSIM 0.559→0.556, pixdiff ~16% either way)** — a
+single-word header spacing fix on a page whose overall diff is dominated by
+ordinary font-rasteriser variance across dozens of story-list rows, the
+same already-documented pattern as every other small, correctly-scoped
+fix this session that didn't move a shared-region score.
+
+Four new regression tests in a new `layout/inlinemargin_test.go`
+(margin-right, margin-left, adjacent-margins-add, and the block-break
+drops-pending-margin boundary case), all confirmed to fail via genuine
+revert-and-rerun before the fix.
+
## 2026-09-05 (round 43) — `filter` had NO EFFECT on any `
` element, block or inline, because an image is never represented as a layout.Box (the only thing the filter/opacity group-buffer pipeline knows how to wrap) (engine#123)
pkg.go.dev re-investigated fresh (last touched round 35, 8 rounds stale).
diff --git a/bench/REPORT.md b/bench/REPORT.md
index f9de886..3d31982 100644
--- a/bench/REPORT.md
+++ b/bench/REPORT.md
@@ -2,7 +2,7 @@
-**Generated:** 2026-09-05 14:59 UTC
+**Generated:** 2026-09-05 15:43 UTC
**Viewport:** 1024×768, device-scale 1
**Timing:** median of N=5 runs after 1 warmup, wall-clock incl. network fetch
**Chrome:** `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
@@ -11,16 +11,16 @@
| URL | SSIM | pixdiff % | webengine ms | chrome ms | speed× | region | status |
|-----|-----:|----------:|-------------:|----------:|-------:|:------:|:------:|
-| example.com/ | 0.954 | 1.5 | 124.7 | 1859.6 | 14.91 | 1024×768 | ok |
-| en.wikipedia.org/wiki/Go_(programming_language) | 0.429 | 22.3 | 1758.9 | 4404.9 | 2.50 | 1024×2500 | ok |
-| pkg.go.dev/net/http | 0.616 | 49.6 | 4194.3 | 4600.1 | 1.10 | 1024×2500 | ok |
-| go.dev/blog/ | 0.684 | 19.0 | 3610.8 | 2959.8 | 0.82 | 1024×1597 | ok |
-| react.dev/ | 0.615 | 46.3 | 2189.0 | 2789.3 | 1.27 | 1024×2500 | ok |
-| news.ycombinator.com/ | 0.558 | 15.9 | 1346.8 | 2595.7 | 1.93 | 1024×1217 | ok |
-| developer.mozilla.org/en-US/docs/Web/CSS | 0.629 | 15.6 | 528.9 | 2068.5 | 3.91 | 1009×2500 | ok |
-| github.com/golang/go | 0.550 | 30.2 | 2741.2 | 2394.9 | 0.87 | 1024×2500 | ok |
-| tailwindcss.com/ | 0.695 | 14.2 | 3841.9 | 2553.2 | 0.66 | 1024×2500 | ok |
-| caniuse.com/ | 0.645 | 22.0 | 4075.2 | 7588.1 | 1.86 | 1024×1522 | ok |
+| example.com/ | 0.954 | 1.5 | 107.2 | 1906.4 | 17.78 | 1024×768 | ok |
+| en.wikipedia.org/wiki/Go_(programming_language) | 0.431 | 21.9 | 1658.8 | 4391.8 | 2.65 | 1024×2500 | ok |
+| pkg.go.dev/net/http | 0.616 | 49.6 | 4193.6 | 4503.4 | 1.07 | 1024×2500 | ok |
+| go.dev/blog/ | 0.684 | 19.0 | 3682.5 | 3083.2 | 0.84 | 1024×1597 | ok |
+| react.dev/ | 0.615 | 46.3 | 2289.2 | 2795.2 | 1.22 | 1024×2500 | ok |
+| news.ycombinator.com/ | 0.556 | 16.0 | 1367.3 | 2627.4 | 1.92 | 1024×1217 | ok |
+| developer.mozilla.org/en-US/docs/Web/CSS | 0.636 | 13.5 | 525.2 | 2080.6 | 3.96 | 1009×2500 | ok |
+| github.com/golang/go | 0.550 | 30.2 | 2780.6 | 2405.0 | 0.86 | 1024×2500 | ok |
+| tailwindcss.com/ | 0.702 | 13.7 | 3728.6 | 2657.7 | 0.71 | 1024×2500 | ok |
+| caniuse.com/ | 0.644 | 22.0 | 4088.8 | 4116.3 | 1.01 | 1024×1522 | ok |
Speed× is `chrome_ms / webengine_ms`: >1 means webengine is faster.
@@ -69,6 +69,23 @@ Speed× is `chrome_ms / webengine_ms`: >1 means webengine is faster.
+## Honest analysis — 2026-09-05 (round 44): a plain inline element's own margin now applies; news.ycombinator.com flat (engine#124)
+
+**news.ycombinator.com: SSIM 0.559→0.556, pixdiff ~16% both before and
+after — flat**, despite a real, visually-confirmed fix (the "Hacker
+News"/"new" header text no longer runs together — see the before/after
+crops referenced in FIDELITY.md's round 44 entry). A single 5px gap in
+one line of an otherwise-correct header is a vanishingly small fraction
+of ink on a page whose comparison region is dominated by 30 story-list
+rows' worth of ordinary font-rasteriser variance — the same pattern this
+session has now documented on nearly every small, correctly-scoped fix
+that touches header/nav chrome rather than the page's dominant content
+block (github.com rounds 39/41, pkg.go.dev round 43).
+
+The other nine corpus pages move by sub-0.01 SSIM / ordinary timing
+noise, unrelated to a fix scoped to inline-element margins that none of
+them happens to depend on for a visible difference from Chrome.
+
## Honest analysis — 2026-09-05 (round 43): `filter` now applies to `
` elements; pkg.go.dev flat, MDN moves slightly the OTHER way despite both being real fixes (engine#123)
**pkg.go.dev/net/http: SSIM 0.617→0.616, pixdiff 49.6%→49.6% — flat**,
diff --git a/bench/out/caniuse.com.png b/bench/out/caniuse.com.png
index b556908..7009bd6 100644
Binary files a/bench/out/caniuse.com.png and b/bench/out/caniuse.com.png differ
diff --git a/bench/out/developer.mozilla.org_en-US_docs_Web_CSS.png b/bench/out/developer.mozilla.org_en-US_docs_Web_CSS.png
index 99a22f8..c5deb2f 100644
Binary files a/bench/out/developer.mozilla.org_en-US_docs_Web_CSS.png and b/bench/out/developer.mozilla.org_en-US_docs_Web_CSS.png differ
diff --git a/bench/out/en.wikipedia.org_wiki_Go_programming_language.png b/bench/out/en.wikipedia.org_wiki_Go_programming_language.png
index a58fc19..0c13a63 100644
Binary files a/bench/out/en.wikipedia.org_wiki_Go_programming_language.png and b/bench/out/en.wikipedia.org_wiki_Go_programming_language.png differ
diff --git a/bench/out/github.com_golang_go.png b/bench/out/github.com_golang_go.png
index 408d6b9..20fdcb1 100644
Binary files a/bench/out/github.com_golang_go.png and b/bench/out/github.com_golang_go.png differ
diff --git a/bench/out/news.ycombinator.com.png b/bench/out/news.ycombinator.com.png
index b1859dd..3d8a21b 100644
Binary files a/bench/out/news.ycombinator.com.png and b/bench/out/news.ycombinator.com.png differ
diff --git a/bench/out/pkg.go.dev_net_http.png b/bench/out/pkg.go.dev_net_http.png
index d324e31..9097f6f 100644
Binary files a/bench/out/pkg.go.dev_net_http.png and b/bench/out/pkg.go.dev_net_http.png differ
diff --git a/bench/out/tailwindcss.com.png b/bench/out/tailwindcss.com.png
index af02202..f13d0dd 100644
Binary files a/bench/out/tailwindcss.com.png and b/bench/out/tailwindcss.com.png differ
diff --git a/bench/results.json b/bench/results.json
index 7fd386b..220c9bd 100644
--- a/bench/results.json
+++ b/bench/results.json
@@ -1,9 +1,9 @@
[
{
"url": "https://example.com/",
- "webengine_ms": 124.715,
- "chrome_ms": 1859.569,
- "speed_ratio": 14.91054804955298,
+ "webengine_ms": 107.223,
+ "chrome_ms": 1906.379,
+ "speed_ratio": 17.7795715471494,
"ssim": 0.9542315003467446,
"pixdiff_pct": 1.5338897705078125,
"region_w": 1024,
@@ -14,11 +14,11 @@
},
{
"url": "https://en.wikipedia.org/wiki/Go_(programming_language)",
- "webengine_ms": 1758.868,
- "chrome_ms": 4404.95,
- "speed_ratio": 2.5044232995312896,
- "ssim": 0.4290406279886116,
- "pixdiff_pct": 22.289140625,
+ "webengine_ms": 1658.794,
+ "chrome_ms": 4391.798,
+ "speed_ratio": 2.647584932185672,
+ "ssim": 0.4314415240770195,
+ "pixdiff_pct": 21.912499999999998,
"region_w": 1024,
"region_h": 2500,
"montage": "out/en.wikipedia.org_wiki_Go_programming_language.png",
@@ -27,11 +27,11 @@
},
{
"url": "https://pkg.go.dev/net/http",
- "webengine_ms": 4194.286,
- "chrome_ms": 4600.147,
- "speed_ratio": 1.0967652182040042,
- "ssim": 0.6159638197337821,
- "pixdiff_pct": 49.6362109375,
+ "webengine_ms": 4193.616,
+ "chrome_ms": 4503.437,
+ "speed_ratio": 1.0738792011476492,
+ "ssim": 0.6158435236491993,
+ "pixdiff_pct": 49.638984375,
"region_w": 1024,
"region_h": 2500,
"montage": "out/pkg.go.dev_net_http.png",
@@ -40,9 +40,9 @@
},
{
"url": "https://go.dev/blog/",
- "webengine_ms": 3610.751,
- "chrome_ms": 2959.778,
- "speed_ratio": 0.8197125750294051,
+ "webengine_ms": 3682.489,
+ "chrome_ms": 3083.207,
+ "speed_ratio": 0.8372616998991714,
"ssim": 0.6838510700295284,
"pixdiff_pct": 19.031594884940514,
"region_w": 1024,
@@ -53,9 +53,9 @@
},
{
"url": "https://react.dev/",
- "webengine_ms": 2189.023,
- "chrome_ms": 2789.28,
- "speed_ratio": 1.274212285572148,
+ "webengine_ms": 2289.198,
+ "chrome_ms": 2795.151,
+ "speed_ratio": 1.2210175790822813,
"ssim": 0.6145175634133307,
"pixdiff_pct": 46.2719140625,
"region_w": 1024,
@@ -66,11 +66,11 @@
},
{
"url": "https://news.ycombinator.com/",
- "webengine_ms": 1346.808,
- "chrome_ms": 2595.687,
- "speed_ratio": 1.927288076696901,
- "ssim": 0.558431492294827,
- "pixdiff_pct": 15.947016870377977,
+ "webengine_ms": 1367.262,
+ "chrome_ms": 2627.422,
+ "speed_ratio": 1.921666805630523,
+ "ssim": 0.5558571737864567,
+ "pixdiff_pct": 16.045234824363188,
"region_w": 1024,
"region_h": 1217,
"montage": "out/news.ycombinator.com.png",
@@ -79,11 +79,11 @@
},
{
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS",
- "webengine_ms": 528.852,
- "chrome_ms": 2068.479,
- "speed_ratio": 3.9112625082253634,
- "ssim": 0.6286406848741246,
- "pixdiff_pct": 15.597026759167493,
+ "webengine_ms": 525.244,
+ "chrome_ms": 2080.631,
+ "speed_ratio": 3.9612656213112376,
+ "ssim": 0.6357537218685836,
+ "pixdiff_pct": 13.493002973240834,
"region_w": 1009,
"region_h": 2500,
"montage": "out/developer.mozilla.org_en-US_docs_Web_CSS.png",
@@ -92,11 +92,11 @@
},
{
"url": "https://github.com/golang/go",
- "webengine_ms": 2741.196,
- "chrome_ms": 2394.866,
- "speed_ratio": 0.8736573378919275,
- "ssim": 0.5503981297417342,
- "pixdiff_pct": 30.208476562499996,
+ "webengine_ms": 2780.603,
+ "chrome_ms": 2405.04,
+ "speed_ratio": 0.8649346922232336,
+ "ssim": 0.5502544091087782,
+ "pixdiff_pct": 30.2062890625,
"region_w": 1024,
"region_h": 2500,
"montage": "out/github.com_golang_go.png",
@@ -105,11 +105,11 @@
},
{
"url": "https://tailwindcss.com/",
- "webengine_ms": 3841.902,
- "chrome_ms": 2553.221,
- "speed_ratio": 0.6645721311995986,
- "ssim": 0.6953557742919156,
- "pixdiff_pct": 14.155507812500002,
+ "webengine_ms": 3728.558,
+ "chrome_ms": 2657.735,
+ "speed_ratio": 0.7128050576120849,
+ "ssim": 0.7022401926884426,
+ "pixdiff_pct": 13.698046875,
"region_w": 1024,
"region_h": 2500,
"montage": "out/tailwindcss.com.png",
@@ -118,11 +118,11 @@
},
{
"url": "https://caniuse.com/",
- "webengine_ms": 4075.15,
- "chrome_ms": 7588.091,
- "speed_ratio": 1.8620396795209992,
- "ssim": 0.6449877966594669,
- "pixdiff_pct": 21.950776630256243,
+ "webengine_ms": 4088.794,
+ "chrome_ms": 4116.255,
+ "speed_ratio": 1.0067161612935258,
+ "ssim": 0.6440262660584839,
+ "pixdiff_pct": 22.03951420827858,
"region_w": 1024,
"region_h": 1522,
"montage": "out/caniuse.com.png",
diff --git a/layout/inlinemargin_test.go b/layout/inlinemargin_test.go
new file mode 100644
index 0000000..823d7de
--- /dev/null
+++ b/layout/inlinemargin_test.go
@@ -0,0 +1,90 @@
+// Copyright (c) the go-webengine/engine authors.
+// SPDX-License-Identifier: BSD-3-Clause
+
+package layout
+
+import "testing"
+
+// TestInlineElementMarginRightAddsGap covers news.ycombinator.com's own real
+// markup: `Hacker Newsnew` — no source whitespace between the two elements, so
+// the only gap between "Hacker News" and "new" is the 's own margin-right.
+// InlineItem carries no margin field of its own; before this fix that margin
+// was silently dropped, running the words together as "Hacker Newsnew".
+func TestInlineElementMarginRightAddsGap(t *testing.T) {
+ src := `` +
+ `ABCD` +
+ ``
+ items := firstLineItems(findBox(layoutHTML(t, src, 300), "body"))
+ if len(items) != 2 {
+ t.Fatalf("items = %d, want 2: %v", len(items), items)
+ }
+ assertF(t, "AB.X", items[0].X, 0)
+ assertF(t, "CD.SpaceBefore", items[1].SpaceBefore, 5)
+ // "AB" is 2 runes * 10px (fakeMeasurer) = 20px wide; "CD" starts 5px after.
+ assertF(t, "CD.X", items[1].X, 25)
+}
+
+// TestInlineElementMarginLeftAddsGap covers margin-left applying as leading
+// space before an inline element's own content, mirroring the margin-right
+// case above. Deliberately NOT the very first item of its line: pendingMargin
+// is carried in SpaceBefore, the same field collapsible whitespace uses, and
+// layoutInline/wrapOneLine's line-breaking deliberately ignore SpaceBefore
+// for a line's first item (so collapsible whitespace never creates a phantom
+// indent) — a real margin on the line-INITIAL element would need a separate
+// field to survive that, unconfirmed as a live bug on any of this project's
+// corpus pages and NOT fixed here; this test covers the confirmed, common
+// case (an inline margin between two things already on the same line).
+func TestInlineElementMarginLeftAddsGap(t *testing.T) {
+ src := `` +
+ `ABCD` +
+ ``
+ items := firstLineItems(findBox(layoutHTML(t, src, 300), "body"))
+ if len(items) != 2 {
+ t.Fatalf("items = %d, want 2: %v", len(items), items)
+ }
+ assertF(t, "CD.SpaceBefore", items[1].SpaceBefore, 5)
+ assertF(t, "CD.X", items[1].X, 25)
+}
+
+// TestAdjacentInlineMarginsAdd covers two adjacent inline elements' margins
+// stacking rather than collapsing — unlike BLOCK margins, adjacent inline
+// margins are independent horizontal space that simply adds up.
+func TestAdjacentInlineMarginsAdd(t *testing.T) {
+ src := `` +
+ `ABCD` +
+ ``
+ items := firstLineItems(findBox(layoutHTML(t, src, 300), "body"))
+ if len(items) != 2 {
+ t.Fatalf("items = %d, want 2: %v", len(items), items)
+ }
+ assertF(t, "CD.SpaceBefore", items[1].SpaceBefore, 8)
+ assertF(t, "CD.X", items[1].X, 28)
+}
+
+// TestBlockBreakDropsPendingMargin covers the boundary case explicitly: a
+// pending margin-right with nothing left on the same line to apply to (a
+// promoted block, see BlockBreak, immediately follows) is dropped rather than
+// leaking into whatever comes after the block in the SAME parent scope.
+func TestBlockBreakDropsPendingMargin(t *testing.T) {
+ src := `` +
+ `AB` +
+ `block
CD` +
+ ``
+ body := findBox(layoutHTML(t, src, 300), "body")
+ if body == nil || len(body.Children) < 3 {
+ n := -1
+ if body != nil {
+ n = len(body.Children)
+ }
+ t.Fatalf("body.Children = %d, want >= 3 (anon run, div, anon run)", n)
+ }
+ lastAnon := body.Children[len(body.Children)-1]
+ items := firstLineItems(lastAnon)
+ if len(items) != 1 || items[0].Text != "CD" {
+ t.Fatalf("items after the block break = %v, want just \"CD\"", items)
+ }
+ if items[0].SpaceBefore != 0 {
+ t.Errorf("CD.SpaceBefore = %v, want 0 (pending margin dropped at the block break)", items[0].SpaceBefore)
+ }
+}
diff --git a/layout/layout.go b/layout/layout.go
index 975f3e2..942d0f3 100644
--- a/layout/layout.go
+++ b/layout/layout.go
@@ -33,11 +33,52 @@ type layouter struct {
// state threaded through the recursive collection rather than per text node.
wsPending bool
wsEmitted bool
+
+ // pendingMargin accumulates the margin-left of a plain inline element about
+ // to be entered, plus the margin-right of one just left, until the next
+ // InlineItem is created — margin-left applies as space before an inline
+ // element's own content starts, margin-right as space after it ends.
+ // Confirmed live on news.ycombinator.com: `Hacker News` immediately followed by
+ // `new` with no source whitespace between them — the engine's own
+ // InlineItem carries no margin field, so the gap a real browser renders
+ // from margin-right alone was silently dropped, running the two together
+ // as "Hacker Newsnew". Reset alongside the whitespace state at a genuine
+ // break (a promoted block or forced
): unlike whitespace, which can
+ // legitimately carry a value AT that point (deferred to whatever comes
+ // after), a stale margin from before a hard break has nothing left on the
+ // same line to apply to.
+ //
+ // Consumed into the SAME SpaceBefore field collapsible whitespace uses
+ // (see takeMargin), not a dedicated one — a real, narrow consequence:
+ // layoutInline/wrapOneLine/WrapItems deliberately ignore SpaceBefore for
+ // a line's very FIRST item (so collapsible leading whitespace never
+ // creates a phantom indent), so a margin lands on an element that
+ // happens to start a line the SAME way — dropped, not applied. This
+ // engine's real, confirmed use of inline margin-right is always between
+ // two things already sharing a line (see the test above); a margin on a
+ // genuinely line-initial element would need its own field to survive
+ // line-breaking, not attempted here absent a confirmed live case.
+ pendingMargin float64
}
-// beginInlineContext resets the whitespace-collapsing state at the top of an
-// inline formatting context.
-func (l *layouter) beginInlineContext() { l.wsPending, l.wsEmitted = false, false }
+// beginInlineContext resets the whitespace-collapsing and pending-margin state
+// at the top of an inline formatting context.
+func (l *layouter) beginInlineContext() { l.wsPending, l.wsEmitted, l.pendingMargin = false, false, 0 }
+
+// takeMargin returns the accumulated pending inline margin and resets it —
+// called exactly once, when the next InlineItem after it is created. Style's
+// own Margin.Left/Right are already plain resolved pixel values by this
+// point (a percentage margin collapses to 0 at cascade time — see
+// applyMarginShorthand — and an auto one leaves Margin.Left/Right at its zero
+// value too, tracked instead via the separate MarginLeftAuto/MarginRightAuto
+// bools block layout consults), so callers add them into pendingMargin
+// directly with no further resolution needed here.
+func (l *layouter) takeMargin() float64 {
+ m := l.pendingMargin
+ l.pendingMargin = 0
+ return m
+}
// outOfFlowItem is a queued out-of-flow box plus the approximate static position
// (the normal-flow cursor at the point it was skipped) used to resolve the box's
@@ -605,8 +646,10 @@ func (l *layouter) appendElementInline(el *dom.Node, cs *css.Style, items *[]*In
switch el.Tag {
case "br":
*items = append(*items, &InlineItem{LineBreak: true, Style: cs, Node: el})
- // A forced break starts a new line: leading whitespace after it collapses.
- l.wsPending, l.wsEmitted = false, false
+ // A forced break starts a new line: leading whitespace after it
+ // collapses, and any pending margin has nothing left on this line to
+ // apply to (same reasoning as the BlockBreak case below).
+ l.wsPending, l.wsEmitted, l.pendingMargin = false, false, 0
case "img", "svg":
w, h := l.imageSize(el)
if w > 0 && h > 0 {
@@ -614,12 +657,14 @@ func (l *layouter) appendElementInline(el *dom.Node, cs *css.Style, items *[]*In
if l.wsEmitted && l.wsPending {
sb = l.m.Measure(" ", cs.FontFamily, cs.FontSize, cs.FontWeight, cs.Italic)
}
+ sb += l.takeMargin() + cs.Margin.Left
*items = append(*items, &InlineItem{
Style: cs, Image: el, Node: el, ImgW: w, ImgH: h,
Width: w, Ascent: h, LineHeight: h,
SpaceBefore: sb,
})
l.wsEmitted, l.wsPending = true, false
+ l.pendingMargin += cs.Margin.Right
}
default:
// A genuinely block-level element (display:block/flex/grid/table, or
@@ -653,8 +698,11 @@ func (l *layouter) appendElementInline(el *dom.Node, cs *css.Style, items *[]*In
// placeInlineSegments), so any whitespace pending before it must
// not carry across as a phantom leading space on the FIRST word
// of the run that resumes after it — the same reset the "br"
- // case above already applies for the identical reason.
- l.wsPending, l.wsEmitted = false, false
+ // case above already applies for the identical reason. Any
+ // pending margin (from a preceding sibling's margin-right) is
+ // dropped the same way: nothing remains on this line to apply it
+ // to once a hard block break ends it.
+ l.wsPending, l.wsEmitted, l.pendingMargin = false, false, 0
return
}
// A form control defaults to display:inline (see css/ua.go) and so is
@@ -675,15 +723,27 @@ func (l *layouter) appendElementInline(el *dom.Node, cs *css.Style, items *[]*In
if l.wsEmitted && l.wsPending {
sb = l.m.Measure(" ", cs.FontFamily, cs.FontSize, cs.FontWeight, cs.Italic)
}
+ sb += l.takeMargin() + cs.Margin.Left
*items = append(*items, &InlineItem{
Style: cs, FormControl: el, Node: el,
Width: w, Ascent: h, LineHeight: h,
SpaceBefore: sb, Label: l.buttonLabel(el),
})
l.wsEmitted, l.wsPending = true, false
+ l.pendingMargin += cs.Margin.Right
return
}
+ // A plain inline element contributes no box of its own — its margin
+ // is instead space around wherever its content ends up: margin-left
+ // as leading space before its first descendant InlineItem,
+ // margin-right as trailing space before whatever InlineItem follows
+ // it (see pendingMargin's own doc comment). Adjacent inline margins
+ // simply add up rather than collapsing, matching real CSS and
+ // falling out naturally here since pendingMargin only ever
+ // accumulates between takeMargin() calls.
+ l.pendingMargin += cs.Margin.Left
l.appendInline(el, cs, items, pre || cs.WhiteSpace == css.WSPre)
+ l.pendingMargin += cs.Margin.Right
}
}
@@ -698,12 +758,13 @@ func (l *layouter) appendWords(text string, st *css.Style, items *[]*InlineItem,
continue
}
*items = append(*items, &InlineItem{
- Text: seg,
- Style: st,
- Node: origin,
- Width: l.m.Measure(seg, st.FontFamily, st.FontSize, st.FontWeight, st.Italic),
- Ascent: asc,
- LineHeight: lh,
+ Text: seg,
+ Style: st,
+ Node: origin,
+ Width: l.m.Measure(seg, st.FontFamily, st.FontSize, st.FontWeight, st.Italic),
+ SpaceBefore: l.takeMargin(),
+ Ascent: asc,
+ LineHeight: lh,
})
}
return
@@ -730,6 +791,9 @@ func (l *layouter) appendWords(text string, st *css.Style, items *[]*InlineItem,
} else if l.wsEmitted && l.wsPending {
sb = space // a boundary space — but never a leading indent on the first item
}
+ if i == 0 {
+ sb += l.takeMargin()
+ }
*items = append(*items, &InlineItem{
Text: w,
Style: st,