v1.9.0: fix flex-wrap:wrap ParentDataWidget crash (#15) with a real RenderFlexWrap - #16
Conversation
…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.
✅ Layout Regression — All fixtures within 60 FPS budget
One or more fixtures exceeded the 16 ms budget.
No action required. |
❌ Visual Regression Detected27 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 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)"
|
…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.
✅ Layout Regression — All fixtures within 60 FPS budget
One or more fixtures exceeded the 16 ms budget.
No action required. |
❌ Visual Regression Detected27 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 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)"
|
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:wrapmapped the container to Flutter'sWrap, but its items were still wrapped inFlexItemWidget, which emitsExpanded/Flexible.WrapprovidesWrapParentData, so Flutter threw "Incorrect use of ParentDataWidget" and cascaded intoRenderBox was not laid out.Worth noting for triage: the reporter suspected this was debug-only. It is not —
Flexible.applyParentDataasserts on line 6065 ofbasic.dartbut 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-drivenColumnofRows.LayoutBuildercannot answer intrinsic or dry-layout queries — and CSS'salign-itemsdefault isstretch, which puts anIntrinsicHeightabove every nested flex container. So any wrapping flex nested in any flex container died withLayoutBuilder does not support returning intrinsic dimensionsplus ~34 cascading errors.Those shapes were already broken before this PR (1–3
ParentDataWidgeterrors), 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.align-items:stretchalign-self:stretchIntrinsicHeightcount, flat caseThe fix
Horizontal wrapping flex is now
RenderFlexWrap, a realMultiChildRenderObject(packages/hyper_render_core/lib/src/widgets/render_flex_wrap.dart). Neither Flutter built-in can express CSS here:Wraphas noflex-growand rejects flex parent data; aLayoutBuildercannot answer intrinsics.RenderFlexWrappacks lines, distributes each line's free space in proportion toflex-grow, clamps tomin-width/max-width, and implementscomputeMin/MaxIntrinsicWidth,computeMin/MaxIntrinsicHeight,computeDryLayout,paintandhitTestChildren. It emits noIntrinsicHeight— cross-axis stretch is a second layout pass over only the children that stretch.flex-direction: column+ wrap keeps Flutter'sWrap(its main axis is height, which is unbounded here), with all flex parent data stripped.Resolving widths arithmetically is also more correct than
Expandedwould have been: CSS distributes free space on top of each item's basis, whereasExpandeddivides the whole line.Sizing that was parsed but never read
Measured in a 500px container:
flex: 0 0 50%min-width: 300px(noflex)min-widthaccepts%for the first time (newminWidthPercent, following the existingmaxWidthPercentpattern),flex-basisaccepts%andauto, and theflex: 1/flex: 1 1shorthands now imply CSS's0%basis instead of leaving it unset.Also fixed
align-items: baselineasserted on every flex container —CrossAxisAlignment.baselinewas passed with notextBaseline. Pre-existing on the nowrap path too. -only flex item disappeared —_buildFlexChildusedString.trim().isEmpty, but Dart follows Unicode (U+00A0 is whitespace) while CSS Text Level 3 excludes it. Now usesisCssWhitespaceOnly.align-contentwas marked ✅ for both flex and grid but is read by nothing in the render path — corrected to ❌ in the CSS matrix and added todocs_matrix_sync_test.dartso it cannot regress.Tests
test/style/flex_wrap_test.dart, 15 → 33 tests.The width helper previously matched
SizedBox, andfind.ancestor(...).firstreturns the furthest ancestor — so whenever an item was not sized, it silently returned the test harness's ownSizedBox(width: 500)andCard 3 == 500passed for entirely the wrong reason. It now anchors onFlexWrapItem, with a negative-control test undertest-helper integritypinning that down. When the render object replaced theLayoutBuilder, 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-startfalsification control (that shape survived even the broken build, so a fix special-casing onlystretchwould not pass), an assertion that noIntrinsicHeightexists at all, percentage basis/min-width,max-widthcapping, the item, the auto-basis path (the only route intogetMaxIntrinsicWidthfrom insidecomputeDryLayout), and nowrapflex: 1geometry — the resolver's new implied0%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 analyzeanddart formatclean.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.dartwith wall-clock budgets, which fits a load-induced flake — but I could not prove it.Versioning
Root and
hyper_render_corego 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_coreconstraint moves to^1.9.0, and that part is required, not cosmetic:lib/hyper_render.dartnow re-exports types that do not exist in core 1.8.0, so at^1.8.0aflutter pub downgradewould 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.0constraints 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