Skip to content

fix(layout): honour box-sizing: content-box - #26

Open
leshchenko1979 wants to merge 1 commit into
Macawls:mainfrom
leshchenko1979:fix/box-sizing-content-box
Open

leshchenko1979 wants to merge 1 commit into
Macawls:mainfrom
leshchenko1979:fix/box-sizing-content-box

Conversation

@leshchenko1979

Copy link
Copy Markdown
Contributor

Problem

box-sizing: content-box is parsed and stored on ComputedStyle, but the layout package never reads it: layout.Style has no field for it and computeNode always subtracts padding and border from the declared size. The engine is unconditionally border-box.

Reproduced on main (32f3fad):

<div style="width:200px;padding:0 50px;box-sizing:content-box">
Ogre     -> <rect width="200">
Chromium -> 300

Cause

style.BoxSizing exists and is populated, but grep -r BoxSizing layout/ returns nothing — there is no consumer. layout.Style has no corresponding field, so mapStyle had nothing to map it to, and computeNode (layout/flex.go:71) always did:

contentWidth := containerWidth - bpW

Fix

  • layout.Style gains ContentBox bool, filled from cs.BoxSizing == style.BoxSizingContentBox in mapStyle.
  • computeNode adds padding and border back on top of the declared width and height when ContentBox is set. The engine keeps border-box as its internal model and the conversion happens exactly once, at the point where the declared size enters layout.

ContentBox is a bool, not an enum, on purpose: the zero value must mean the engine default (border-box, Satori-compatible), and an enum would silently make content-box the implicit default for every Style{} literal in the codebase and for downstream users of the package.

Verification

Against Chromium, same markup, 1280×720:

case before after Chromium
border-box, width:200, padding:0 50px 200 200 200
content-box, width:200, padding:0 50px 200 300 300
content-box, height:100, padding:25px 0 100 150 150

Pixel diff against Chromium for the content-box case: 1.09% → 0.00%.

The default case (box-sizing not declared) is deliberately left as border-box — that is the documented Satori-compatible behaviour and changing it would break the existing golden tests.

Tests added:

  • layout/layout_test.go — content-box adds padding, adds border, works without padding; border-box keeps the declared width.
  • ogre_box_sizing_e2e_test.go — through the public Render: horizontal and vertical conversion.

go build ./..., go vet ./... and go test ./... pass.

Fixes #23

@Macawls Macawls left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks, the bug is real and the e2e tests reproduce it on main. The fix is incomplete though: the conversion only happens in computeNode, but the parent's flex pass still sizes items from Style.Width / Style.Height directly (flex basis and cross size in layout/flex.go, around lines 424, 459, 702 and 765). A content-box element works as a root or a block child, but not as a flex item. Most elements here are flex items.

Repro on this branch:

<div style="display:flex;flex-direction:row">
  <div style="width:200px;padding:0 50px;box-sizing:content-box;background:red;height:40px"></div>
  <div style="width:50px;height:40px;background:blue"></div>
</div>
<rect x="0"   y="0" width="300" height="40" fill="#ff0000"/>
<rect x="200" y="0" width="50"  height="40" fill="#0000ff"/>

The red box draws 300px wide, but the parent allocated 200px, so the blue sibling overlaps it. The same applies on the main axis of a column container with vertical padding. Could you apply the content-box conversion wherever an item's declared size enters the flex algorithm (or resolve it once into the style before layout), and add a test with a content-box item next to a sibling in a row?

box-sizing was parsed and stored on ComputedStyle, but nothing in the layout
package ever read it: layout.Style had no field for it and computeNode always
subtracted padding and border from the declared size, i.e. the engine was
unconditionally border-box.

  <div style="width:200px;padding:0 50px;box-sizing:content-box">
  Ogre     -> 200
  Chromium -> 300

layout.Style gains ContentBox, filled from cs.BoxSizing, and borderBoxSize()
does the conversion once: the declared content size plus the padding and
border it excludes, which is the border-box model the engine uses internally.

It has to happen at every sizing site, not only in computeNode — the flex
pass reads Style.Width/Height directly for the base size, the cross size and
the fallback estimate, and computeAbsolute and both aspect-ratio paths
resolve the declared size themselves. Without that, a padded content-box item
measured 300px but occupied a 200px slot and its sibling was laid on top of
it. Under content-box the ratio describes the content box, so both ratio
paths strip the padding and border before applying it and add them back.

ContentBox is a bool rather than an enum on purpose: the zero value has to
mean the engine default (border-box, which matches Satori), and an enum would
silently make content-box the implicit default for every Style{} literal in
the codebase and in downstream users.

Verified against Chromium 151, same markup:

  border-box   200 + 0 50 padding        -> 200    (unchanged)
  content-box  200 + 0 50 padding        -> 300    (was 200)
  content-box  100 + 25 0 padding        -> 150    (was 100)
  flex sibling after a padded item        -> x 300  (was 200, overlapping)
  right-anchored absolute content-box     -> x 280  (was 300)
  aspect-ratio 2, width 300               -> h 100  (was 150)

Pixel diff against Chromium for the content-box case dropped from 1.09% to
0.00%.

Red tests, one per site, before the fix:

  sib:   got (200.0, ...), want (300.0, ...)
  sib:   got (0.0, 100.0, ...), want (0.0, 150.0, ...)
  abs:   got (300.0, ...), want (280.0, ...)
  child: got (..., 300.0, 150.0), want (..., 300.0, 100.0)

Fixes Macawls#23
Refs Macawls#26
@leshchenko1979
leshchenko1979 force-pushed the fix/box-sizing-content-box branch from 7dc7eb5 to efd1d5d Compare October 29, 2026 22:52
@leshchenko1979

Copy link
Copy Markdown
Contributor Author

You were right: the conversion was in computeNode only, while the flex pass reads Style.Width/Height directly. A padded content-box item measured 300px and occupied a 200px slot, so the sibling landed on top of it.

I audited every place that resolves a declared size and found five, not one:

site what read the raw declaration
determineFlexBaseSize main-axis base size (and flex-basis, which replaces width there)
determineCrossSize cross size
estimateSize the fallback for nodes without a measure func
computeAbsolute absolutely positioned children
aspect-ratio, both paths computeNode and determineCrossSize derived the other axis from the border box

borderBoxSize() now does the conversion once and each of them calls it. Under content-box the ratio describes the content box, so both ratio paths strip the padding and border before applying it and add them back after.

Verified against Chromium 151, same markup:

case Chromium before after
flex sibling after content-box;width:200px;padding:0 50px x 300 x 200 (overlapping) x 300
right-anchored absolute, same padding x 280 x 300 x 280
content-box;width:200px;padding:0 50px;aspect-ratio:2 300 × 100 300 × 150 300 × 100

Red tests, one per site, before the fix:

sib:   got (200.0, ...), want (300.0, ...)
sib:   got (0.0, 100.0, ...), want (0.0, 150.0, ...)
abs:   got (300.0, ...), want (280.0, ...)
child: got (..., 300.0, 150.0), want (..., 300.0, 100.0)

Two notes on scope, so you can judge them rather than discover them:

  • min-width/max-width are still clamped in border-box terms, so content-box combined with a min/max on the same axis is approximate. Not touched here; say the word if you want it in this PR.
  • ContentBox stays a bool: the zero value has to mean border-box, which is the Satori-compatible default this engine keeps.

Branch rebuilt on current main (0204fe0), squashed to one commit (efd1d5d), go test ./... green across all packages.

leshchenko1979 added a commit to leshchenko1979/ogre that referenced this pull request Oct 29, 2026
box-sizing was parsed and stored on ComputedStyle, but nothing in the layout
package ever read it: layout.Style had no field for it and computeNode always
subtracted padding and border from the declared size, i.e. the engine was
unconditionally border-box.

  <div style="width:200px;padding:0 50px;box-sizing:content-box">
  Ogre     -> 200
  Chromium -> 300

layout.Style gains ContentBox, filled from cs.BoxSizing, and borderBoxSize()
does the conversion once: the declared content size plus the padding and
border it excludes, which is the border-box model the engine uses internally.

It has to happen at every sizing site, not only in computeNode — the flex
pass reads Style.Width/Height directly for the base size, the cross size and
the fallback estimate, and computeAbsolute and both aspect-ratio paths
resolve the declared size themselves. Without that, a padded content-box item
measured 300px but occupied a 200px slot and its sibling was laid on top of
it. Under content-box the ratio describes the content box, so both ratio
paths strip the padding and border before applying it and add them back.

ContentBox is a bool rather than an enum on purpose: the zero value has to
mean the engine default (border-box, which matches Satori), and an enum would
silently make content-box the implicit default for every Style{} literal in
the codebase and in downstream users.

Verified against Chromium 151, same markup:

  border-box   200 + 0 50 padding        -> 200    (unchanged)
  content-box  200 + 0 50 padding        -> 300    (was 200)
  content-box  100 + 25 0 padding        -> 150    (was 100)
  flex sibling after a padded item        -> x 300  (was 200, overlapping)
  right-anchored absolute content-box     -> x 280  (was 300)
  aspect-ratio 2, width 300               -> h 100  (was 150)

Pixel diff against Chromium for the content-box case dropped from 1.09% to
0.00%.

Red tests, one per site, before the fix:

  sib:   got (200.0, ...), want (300.0, ...)
  sib:   got (0.0, 100.0, ...), want (0.0, 150.0, ...)
  abs:   got (300.0, ...), want (280.0, ...)
  child: got (..., 300.0, 150.0), want (..., 300.0, 100.0)

Fixes Macawls#23
Refs Macawls#26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

box-sizing: content-box is ignored by layout

2 participants