Skip to content

feat(core): apply box-sizing and position: relative - #36

Merged
vietnguyentuan2019 merged 7 commits into
mainfrom
feat/box-sizing-position-relative
Nov 5, 2026
Merged

vietnguyentuan2019 merged 7 commits into
mainfrom
feat/box-sizing-position-relative

Conversation

@vietnguyentuan2019

@vietnguyentuan2019 vietnguyentuan2019 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Closes #33, closes #34.

What changes

  • box-sizing: border-box applies to width / min-width / max-width, including %, on in-flow blocks.
  • position: relative: top / right / bottom / left (lengths) move blocks, inline spans and flex/grid items. Sibling layout and box height stay as they were, and hit-testing and selection follow the moved text.

Rendering changes to review

These affect existing documents:

  • A block's background and border now follow its width. They used to span the whole container.
  • min-width: % now applies on blocks.

Bugs fixed along the way (first commit):

  • A first block styled only with a background or border painted nothing.

  • Gradients on canvas blocks and inline spans painted nothing: the fill paint's alpha was 0.

  • _blockDecorations wasn't cleared when line layout re-ran, so a <details> toggle left the old rect painted.

  • HyperViewer(textDirection:) and an ambient RTL Directionality now apply in sync mode. They were dropped there, while virtualized and paged modes honoured them.

Decisions

  • Clipping: moved content, including flex/grid items and moved backgrounds, paints in its own pass clipped to the render box, because the sanitizer doesn't filter CSS properties. The cost is that a shift past the box edge gets cut, even a small one on the first or last line. Other content stays unclipped.
  • Stacking: that pass runs last, so moved content paints above normal content, as in CSS. There's no finer order (no z-index).
  • Borders still don't reserve layout space, so text can overlap a border. Fixing that moves every bordered block, so it's a separate change.
  • Docs say ⚠️, not ✅, with the limits in doc/LIMITATIONS.md.
  • Versions: root and core go to 1.14.0, because the root barrel re-exports the new HyperBoxSizing. Nothing is published yet.

Testing

  • New: test/box_sizing_test.dart, test/position_relative_test.dart, test/first_block_decoration_test.dart. They measure fragment geometry, raw pixels and, through a recording canvas, how many times text is drawn.
  • Tests failed before each fix. Reverting each main fix one at a time makes the matching tests fail.
  • Root + core: 2710 passing. Sub-packages and the example app pass; the devtools_ui Chrome-only test wasn't run.
  • Goldens: 27 of 28 already fail locally on main (platform differences), so I compared against a baseline generated locally from main: no differences. The golden.yml run on this PR is the authoritative check.

Not proven by a test: per-line paint culling doesn't take effect in a widget test, because RenderHyperBox is a repaint boundary and its canvas clip is unbounded. The separate paint pass for moved text is tested; its culling isn't.

…stale block rects

- A first block styled only with a background or border lost its
  block-start fragment, which carries the decoration, and painted nothing.
- Canvas gradients painted nothing anywhere: the fill paint's alpha was 0
  and a shader is multiplied by it.
- _blockDecorations was not cleared when line layout re-ran, so a details
  toggle kept painting the block's previous rect.
…33)

box-sizing was never parsed. Add ComputedStyle.boxSizing (not inherited) and
apply border-box to width / min-width / max-width, including %, on in-flow
canvas blocks. min-width:% was also unread on this path and now applies.

The painted background ignored width and always spanned the containing block,
so a correct line width alone would not have changed what the box looks like.
The rect now follows the resolved width. Borders still do not reserve layout
space; they only widen the painted box.
position and its offsets were never parsed. A post-layout pass moves each
relative element's fragments and block decoration, including those of relative
ancestors, leaving sibling layout and box height as they were.

Moved fragments stay on their original line, so hit-testing, character lookup,
getBoxesForCharRange and text painting now check them separately. Flex and grid
items use Transform.translate.

While any content is moved the box clips its painting to its own bounds. The
sanitizer doesn't filter CSS properties, so without this untrusted HTML could
draw over the host app. Offsets take lengths only; % is not supported.
The docs called both "parsed, not applied", but neither was parsed. Record
what now applies and the limits, and pin both in the CSS execution guard.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ Layout Regression — Budget exceeded (informational)

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 15 22
❌ mixed_inline 10 13 15
❌ float_layout 12 17 21
❌ table_20_rows 14 36 62
✅ cjk_ruby 14 9 10
❌ large_article 16 44 76

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

Informational, does not block merge: CI runners use software
rendering and are 2-3x slower than the devices the budgets are
calibrated for. Compare against the base branch; if a fixture
regressed relative to it, profile with:

flutter test benchmark/layout_regression.dart --reporter expanded

and check _performLineLayout / _buildCharacterMapping for
any new O(N²) or O(N log N) paths introduced in this PR.

…gins

Block margins now show more, since the background follows width.
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

⚠️ Layout Regression — Budget exceeded (informational)

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 22 27
❌ mixed_inline 10 18 20
❌ float_layout 12 21 27
❌ table_20_rows 14 49 78
✅ cjk_ruby 14 14 20
❌ large_article 16 53 84

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

Informational, does not block merge: CI runners use software
rendering and are 2-3x slower than the devices the budgets are
calibrated for. Compare against the base branch; if a fixture
regressed relative to it, profile with:

flutter test benchmark/layout_regression.dart --reporter expanded

and check _performLineLayout / _buildCharacterMapping for
any new O(N²) or O(N log N) paths introduced in this PR.

- Clip escapes: flex/grid items and moved block backgrounds (empty blocks, or a
  parent whose child cancels its offset) could paint over the host app. The
  clip only looked at moved fragments.
- Moved content now paints in its own clipped pass, above normal content, so one
  moved span no longer crops unrelated overflow such as a box-shadow.
- Double drawing: fragments the line breaker replaced were moved and drawn too,
  and list markers were drawn twice. Only line fragments are drawn now.
- Drift: a fragment line layout doesn't re-place gained the shift on every
  relayout. Shifts are undone before line layout.
- em offsets used the root font size, not the element's.
- Shifts are collected walking down the tree. Building a nested box re-parents
  shared nodes, so a walk up missed relative ancestors of a float's text.
- An outer inline background followed a relative child off its line.
- Flex and grid items ignored the writing direction, and HyperViewer dropped
  textDirection in sync mode.
The root barrel re-exports the new HyperBoxSizing, so root must require a core
that has it.
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

⚠️ Layout Regression — Budget exceeded (informational)

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 17 26
❌ mixed_inline 10 15 16
❌ float_layout 12 16 21
❌ table_20_rows 14 34 45
✅ cjk_ruby 14 9 10
❌ large_article 16 40 79

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

Informational, does not block merge: CI runners use software
rendering and are 2-3x slower than the devices the budgets are
calibrated for. Compare against the base branch; if a fixture
regressed relative to it, profile with:

flutter test benchmark/layout_regression.dart --reporter expanded

and check _performLineLayout / _buildCharacterMapping for
any new O(N²) or O(N log N) paths introduced in this PR.

@vietnguyentuan2019
vietnguyentuan2019 merged commit 84e9d63 into main Nov 5, 2026
9 checks passed
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.

CSS: apply position: relative with top / left / right / bottom offsets CSS: apply box-sizing (border-box) to width / max-width / min-width

1 participant