Skip to content

v1.9.0: fix flex-wrap:wrap ParentDataWidget crash (#15) with a real RenderFlexWrap - #16

Merged
vietnguyentuan2019 merged 4 commits into
mainfrom
fix/issue-15-flex-wrap-flexible
Sep 7, 2026
Merged

vietnguyentuan2019 merged 4 commits into
mainfrom
fix/issue-15-flex-wrap-flexible

Conversation

@vietnguyentuan2019

Copy link
Copy Markdown
Contributor

Fixes the crash reported in #15, plus a second crash the first fix introduced in a neighbouring shape, plus the CSS flex sizing that turned out to be parsed but never applied.

The reported bug

display:flex; flex-wrap:wrap mapped the container to Flutter's Wrap, but its items were still wrapped in FlexItemWidget, which emits Expanded/Flexible. Wrap provides WrapParentData, so Flutter threw "Incorrect use of ParentDataWidget" and cascaded into RenderBox was not laid out.

Worth noting for triage: the reporter suspected this was debug-only. It is not — Flexible.applyParentData asserts on line 6065 of basic.dart but casts on 6066, and the cast survives a release build.

The second crash, found while reviewing the first fix

The first commit laid wrapping flex out as a LayoutBuilder-driven Column of Rows. LayoutBuilder cannot answer intrinsic or dry-layout queries — and CSS's align-items default is stretch, which puts an IntrinsicHeight above every nested flex container. So any wrapping flex nested in any flex container died with LayoutBuilder does not support returning intrinsic dimensions plus ~34 cascading errors.

Those shapes were already broken before this PR (1–3 ParentDataWidget errors), so it was not a regression from working — but it swapped a small failure for a much larger one, in a shape users reach without writing any special CSS.

Shape Before #15 fix After 1st commit This PR
Nested in align-items:stretch 3 errors 34 errors 0
Parent has align-self:stretch 1 error 34 errors 0
wrap inside wrap 3 errors 35 errors 0
IntrinsicHeight count, flat case — 2 0

The fix

Horizontal wrapping flex is now RenderFlexWrap, a real MultiChildRenderObject (packages/hyper_render_core/lib/src/widgets/render_flex_wrap.dart). Neither Flutter built-in can express CSS here: Wrap has no flex-grow and rejects flex parent data; a LayoutBuilder cannot answer intrinsics. RenderFlexWrap packs lines, distributes each line's free space in proportion to flex-grow, clamps to min-width/max-width, and implements computeMin/MaxIntrinsicWidth, computeMin/MaxIntrinsicHeight, computeDryLayout, paint and hitTestChildren. It emits no IntrinsicHeight — cross-axis stretch is a second layout pass over only the children that stretch.

flex-direction: column + wrap keeps Flutter's Wrap (its main axis is height, which is unbounded here), with all flex parent data stripped.

Resolving widths arithmetically is also more correct than Expanded would have been: CSS distributes free space on top of each item's basis, whereas Expanded divides the whole line.

Sizing that was parsed but never read

Measured in a 500px container:

CSS Before After
flex: 0 0 50% 32.5px 250px
min-width: 300px (no flex) 32.5px 300px

min-width accepts % for the first time (new minWidthPercent, following the existing maxWidthPercent pattern), flex-basis accepts % and auto, and the flex: 1 / flex: 1 1 shorthands now imply CSS's 0% basis instead of leaving it unset.

Also fixed

  • align-items: baseline asserted on every flex container — CrossAxisAlignment.baseline was passed with no textBaseline. Pre-existing on the nowrap path too.
  • An  -only flex item disappeared — _buildFlexChild used String.trim().isEmpty, but Dart follows Unicode (U+00A0 is whitespace) while CSS Text Level 3 excludes it. Now uses isCssWhitespaceOnly.
  • align-content was marked ✅ for both flex and grid but is read by nothing in the render path — corrected to ❌ in the CSS matrix and added to docs_matrix_sync_test.dart so it cannot regress.

Tests

test/style/flex_wrap_test.dart, 15 → 33 tests.

The width helper previously matched SizedBox, and find.ancestor(...).first returns the furthest ancestor — so whenever an item was not sized, it silently returned the test harness's own SizedBox(width: 500) and Card 3 == 500 passed for entirely the wrong reason. It now anchors on FlexWrapItem, with a negative-control test under test-helper integrity pinning that down. When the render object replaced the LayoutBuilder, exactly 5 old tests failed — all 5 were this helper bug, returning harness widths of 800/400/500.

New coverage: the three nested shapes that crashed, an align-items:flex-start falsification control (that shape survived even the broken build, so a fix special-casing only stretch would not pass), an assertion that no IntrinsicHeight exists at all, percentage basis/min-width, max-width capping, the   item, the auto-basis path (the only route into getMaxIntrinsicWidth from inside computeDryLayout), and nowrap flex: 1 geometry — the resolver's new implied 0% basis had no nowrap assertion before, so the suite could not tell "unchanged" from "untested".

2374 root+core · 28 golden · 754 core standalone · 73 html · 48 epub · 44 example. flutter analyze and dart format clean.

One caveat, reported rather than swept: 1 of 5 full-suite runs failed. That run took 01:48 against a ~45s norm, and three subsequent runs were green. I could not identify or reproduce the failing test. The suite has 10 files using real-time waits, including performance_regression_test.dart with wall-clock budgets, which fits a load-induced flake — but I could not prove it.

Versioning

Root and hyper_render_core go to 1.9.0. Minor rather than patch because core gains public API (RenderFlexWrap, FlexWrapLayout, FlexWrapItem, FlexWrapParentData, FlexItemWidget.buildUnflexed(), ComputedStyle.minWidthPercent/flexBasisPercent), all backwards-compatible.

Root's hyper_render_core constraint moves to ^1.9.0, and that part is required, not cosmetic: lib/hyper_render.dart now re-exports types that do not exist in core 1.8.0, so at ^1.8.0 a flutter pub downgrade would resolve a core this package cannot compile against — the lower-bound defect v1.7.1 fixed to reach 160/160 on pub.dev.

The other six sub-packages are untouched; their ^1.7.0 constraints already admit 1.9.0.

Not touched: pubspec_publish_ready.yaml, still declaring 1.7.1 / core ^1.7.0. No script references it and the 1.8.0 release skipped it too, so it looks vestigial — flagged rather than silently updated.

Closes #15

…rap (#15)

A `display:flex; flex-wrap:wrap` container was mapped to Flutter's `Wrap`,
but its children were still wrapped in `FlexItemWidget`, which builds
`Expanded` (flex-grow > 0) or `Flexible` (shrinkable). `Wrap` provides
`WrapParentData`, so Flutter asserted

    Incorrect use of ParentDataWidget … wants to apply ParentData of type
    FlexParentData to a RenderObject … set up to accept WrapParentData

and the broken parent data cascaded into `RenderFlex children have non-zero
flex but incoming width constraints are unbounded`, `RenderBox was not laid
out` and `child.hasSize is not true` for the rest of the document. The
trigger is the ordinary responsive-card pattern `flex: 1 1 220px` +
`min-width: 220px`.

Wrapping flex is now laid out arithmetically in `FlexContainerWidget`:
items are packed into lines by their base size (`flex-basis` → `width` →
`min-width`, 0 for a growable item), each line's free space is distributed
in proportion to `flex-grow`, widths are clamped to `min-width`/`max-width`,
and the result is emitted as a `Column` of `Row`s. Because widths are
resolved numerically, no `Expanded`/`Flexible` is needed at all — `Expanded`
would also have been wrong, since CSS distributes *free* space on top of
each item's basis rather than dividing the whole line.

Shapes that cannot be sized at build time keep the `Wrap` path:
`flex-direction: column*` (unbounded cross axis), `row-reverse` /
`wrap-reverse`, an unbounded main axis, and item sets containing a
non-flex child or one with no knowable base. That path is now safe too —
`_buildStrippedWrapChildren` replaces every `FlexItemWidget` with its
unflexed child plus explicit `flex-basis`/`min-width`/`max-width` sizing,
so flex parent data can no longer reach a `Wrap` by any route.

Also fixes a pre-existing crash the new path would otherwise have inherited:
CSS `align-items: baseline` produced `CrossAxisAlignment.baseline` with no
`textBaseline`, which asserts in both `Row` and `Column`. Both the nowrap
path and the new wrapping path now pass `TextBaseline.alphabetic`.

The nowrap sizing behaviour is otherwise untouched.

Side effect: `flex-basis`, `min-width` and `max-width` were parsed into
`ComputedStyle` but read by nothing on the flex path; they now participate
in wrapping-flex sizing. `CSS_PROPERTIES_MATRIX.md` downgrades `flex-basis`
to ⚠️ accordingly — it executes only for wrapping containers, and `%`/`auto`
bases are still unparsed.

test/style/flex_wrap_test.dart adds 15 regression tests asserting geometry,
not just the absence of an exception: card widths and row membership at 500px
and 800px, `min-width` winning over a smaller basis, bare `flex: 1` splitting
evenly, plus explicit `findsNothing` checks for `Expanded`/`Flexible` under
`Wrap`, the `align-items`/`align-self` shapes under an unbounded height, and
fallback coverage for the column/wrap-reverse/mixed-children cases. The
original 11 all fail without this change. Suite: 2355 root+core, 28 golden.

Closes #15
… object

Follow-up to the issue #15 fix in the parent commit, which closed the
reported crash but opened a second one in a neighbouring shape.

The parent commit laid wrapping flex out as a LayoutBuilder-driven Column
of Rows. LayoutBuilder cannot answer intrinsic or dry-layout queries, and
CSS's `align-items` default is `stretch` (computed_style.dart) — so every
wrapping container got an IntrinsicHeight, and any wrapping flex nested
inside any flex container queried intrinsics across a LayoutBuilder and
died with

    LayoutBuilder does not support returning intrinsic dimensions

plus ~34 cascading `RenderBox was not laid out` errors. Those shapes were
already broken before (1-3 ParentDataWidget errors), so this was not a
regression from working — but it substituted a failure mode with a much
larger blast radius, and nested card grids are a common shape.

Horizontal wrapping flex is now `RenderFlexWrap`, a MultiChildRenderObject
that resolves CSS sizing itself: it packs items into lines by base size,
distributes each line's free space in proportion to `flex-grow`, clamps to
`min-width`/`max-width`, and implements computeMinIntrinsicWidth /
computeMaxIntrinsicWidth / computeMin|MaxIntrinsicHeight / computeDryLayout
/ paint / hitTestChildren. It emits no IntrinsicHeight at all (cross-axis
stretch is a second layout pass on only the children that stretch), and no
LayoutBuilder, so it composes with any ancestor. `flex-direction: column`
+ wrap keeps Flutter's `Wrap` — its main axis is height, which is unbounded
here — with all flex parent data stripped.

Sizing properties that were parsed but read by nothing now work:

- `flex-basis` including `%` and `auto`; the `flex: 1` / `flex: 1 1`
  shorthands now imply CSS's `0%` basis instead of leaving it unset
- `min-width`, including `%` for the first time (new `minWidthPercent`,
  following the existing `maxWidthPercent` pattern)
- `max-width` as a cap on a grown item

Measured before/after in a 500px container: `flex: 0 0 50%` gave 32.5px,
now 250px; a bare `min-width: 300px` gave 32.5px, now 300px.

Also fixed, both found while auditing the above:

- An ` `-only flex item vanished. `_buildFlexChild` used
  `String.trim().isEmpty`, but Dart follows Unicode (U+00A0 is whitespace)
  while CSS Text Level 3 excludes it. Now uses `isCssWhitespaceOnly`, and
  trims only CSS whitespace so an edge ` ` survives.
- `align-content` was marked supported for both flex and grid but is read
  by nothing in the render path; corrected to ❌ in the CSS matrix and added
  to docs_matrix_sync_test's must-not-be-full list so it cannot regress.

Flex children now always carry their ComputedStyle via FlexItemWidget so
the renderer can see `min-width` on items that declare no `flex-*`. For the
nowrap Row/Column path this is behaviour-preserving, and the full suite
confirms it: no nowrap test moved.

test/style/flex_wrap_test.dart is rewritten, 15 tests -> 33. Its width
helper previously matched `SizedBox`, which silently returned the test
harness's own box whenever an item was not sized — so `Card 3 == 500` could
pass for entirely the wrong reason. It now anchors on FlexWrapItem, and a
negative-control test pins that down. New coverage: the three nested shapes
that crashed, an align-items:flex-start falsification control, an assertion
that no IntrinsicHeight exists, percentage basis/min-width, max-width
capping, and the ` ` item. Also covered: the auto-basis path (no
`flex-basis`, no `width`), which is the only route into
`getMaxIntrinsicWidth` from inside computeDryLayout, and nowrap `flex: 1`
geometry — the resolver's new implied `0%` basis had no nowrap assertion
before, so the suite could not tell "unchanged" from "untested".

Suite: 2374 root+core (was 2355), 28 golden, 73 html, 48 epub, 44 example.
Minor, not patch: the wrapping-flex work adds public API to core —
RenderFlexWrap / FlexWrapLayout / FlexWrapItem / FlexWrapParentData,
FlexItemWidget.buildUnflexed(), and ComputedStyle.minWidthPercent /
flexBasisPercent — all backwards-compatible.

Root's hyper_render_core constraint moves to ^1.9.0 as well, and that part
is required rather than cosmetic: lib/hyper_render.dart now re-exports
FlexWrapItem, FlexWrapLayout and RenderFlexWrap, which do not exist in core
1.8.0. Left at ^1.8.0, `flutter pub downgrade` would resolve a core this
package cannot compile against — the same lower-bound defect v1.7.1 fixed
to reach 160/160 on pub.dev.

Only root and core are versioned here. The other six sub-packages are
untouched by this work and their ^1.7.0 core constraints already admit
1.9.0.

Not touched: pubspec_publish_ready.yaml, still declaring 1.7.1 / core
^1.7.0. No script references it and the 1.8.0 release skipped it too, so it
appears vestigial — flagged rather than silently updated.

Suite after the bump: 2374 root+core, 28 golden, 754 core-standalone.
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Layout Regression — All fixtures within 60 FPS budget

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 20 35
❌ mixed_inline 10 17 17
❌ float_layout 12 17 31
❌ table_20_rows 14 41 62
✅ cjk_ruby 14 12 14
❌ large_article 16 41 64

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

No action required.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

❌ Visual Regression Detected

27 golden test(s) failed on Flutter 3.41.5 / ubuntu-22.04.

The rendered output no longer matches the reference images.

If the change is intentional, regenerate the goldens on the
same platform via the Update Goldens workflow dispatch, or run
locally in Docker:

docker run --rm \
  -v $(pwd):/workspace -w /workspace \
  ghcr.io/cirruslabs/flutter:3.41.5 \
  bash -c "apt-get update -qq && \
    apt-get install -y fonts-noto fonts-noto-cjk fonts-roboto && \
    flutter pub get && \
    flutter test test/golden/ --update-goldens"
git add test/golden/goldens/
git commit -m "chore: update golden references (Flutter 3.41.5)"

⚠️ Always regenerate goldens on ubuntu-22.04 with Flutter
3.41.5
to keep references pixel-stable across machines.

…ll snippets

The feature matrix claimed "✅ Full" for Flexbox / Grid. It is not full, and
issue #15's reporter cited that exact row as the reason the crash mattered.
Replaced with what actually executes — wrapping flex on a custom RenderObject
— plus a footnote naming the real gaps: `align-content` is parsed and read by
nothing, `flex-basis` does not drive the `nowrap` path, and
`flex-direction: column` + wrap packs lines without growth. The footnote links
to CSS_PROPERTIES_MATRIX.md for per-property status.

Two test counts were stale and contradicted each other: the header badge said
"2 460+ tests" while the Architecture section said "1 646 passing tests" and
"fuzz (43 cases)". Actual, measured: 2 495 passing (2374 root+core, 73 html,
48 epub) plus 28 golden, and the fuzz suite runs 339 cases.

Install snippets go to ^1.9.0 in both published READMEs — the root one and
hyper_render_core's, which is its own pub.dev landing page.

Not touched: the Benchmarks table still carries competitor timings and a
"Scroll FPS 60" claim I have no measurements to confirm or replace. Flagged
rather than rewritten, since inventing replacement numbers would repeat the
problem this commit is fixing.
@vietnguyentuan2019
vietnguyentuan2019 merged commit a925fff into main Sep 7, 2026
4 of 6 checks passed
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Layout Regression — All fixtures within 60 FPS budget

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 13 22
❌ mixed_inline 10 12 13
✅ float_layout 12 11 18
❌ table_20_rows 14 28 44
✅ cjk_ruby 14 7 9
❌ large_article 16 22 44

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

No action required.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

❌ Visual Regression Detected

27 golden test(s) failed on Flutter 3.41.5 / ubuntu-22.04.

The rendered output no longer matches the reference images.

If the change is intentional, regenerate the goldens on the
same platform via the Update Goldens workflow dispatch, or run
locally in Docker:

docker run --rm \
  -v $(pwd):/workspace -w /workspace \
  ghcr.io/cirruslabs/flutter:3.41.5 \
  bash -c "apt-get update -qq && \
    apt-get install -y fonts-noto fonts-noto-cjk fonts-roboto && \
    flutter pub get && \
    flutter test test/golden/ --update-goldens"
git add test/golden/goldens/
git commit -m "chore: update golden references (Flutter 3.41.5)"

⚠️ Always regenerate goldens on ubuntu-22.04 with Flutter
3.41.5
to keep references pixel-stable across machines.

@vietnguyentuan2019
vietnguyentuan2019 deleted the fix/issue-15-flex-wrap-flexible branch September 7, 2026 08:39
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.

display:flex; flex-wrap:wrap generates Flexible inside Wrap and causes Flutter ParentDataWidget assertion

1 participant