Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/mix/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@

### New features

- **`GridTrack.auto()`:** Content-sized row tracks for `rows` and `autoRows`.
An auto row sizes to its tallest child's natural height at the resolved
column width, then stretches shorter children to fill the row. Use it — or
omit `autoRows` — when child heights are unknown, such as a card grid in a
scroll view. `auto` is vertical-only; `columns` rejects it. Grids built only
from fixed and `fr` tracks are unaffected and still lay each child out once.
- **Generated Styler field metadata:** Every generated Styler now exposes its
complete source-field inventory through
`StylerFieldMetadata.$stylerFieldNames`, allowing schema tooling to validate
coverage without maintaining duplicate string manifests. Handwritten and
previously generated Stylers do not implement this capability until they opt
in or are regenerated.

### Breaking changes

- **Omitted GridBox `autoRows` no longer throws:** Children needing more rows
than were declared previously required an explicit `autoRows` track, or the
Grid threw. Omitted `autoRows` now defaults to `GridTrack.auto()`, so
implicit rows size to their tallest child — both when no rows are declared
and when explicit rows run out. Fractional rows still require a bounded
height, and fixed tracks remain hard constraints. If you relied on the throw
to catch an under-declared Grid, declare `rows` explicitly or set `autoRows`
to the track you want repeated.

## 2.2.0-beta.4

### Fixes
Expand Down
8 changes: 4 additions & 4 deletions packages/mix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,14 @@ the runnable [WrapBox example](example/README.md).

### Grid Layouts

`GridBox` uses fixed and fractional tracks and can adapt to the width offered by
its own parent. Dot shorthand keeps nested track and breakpoint declarations
compact:
`GridBox` uses fixed and fractional columns plus vertical auto rows, and can
adapt to the width offered by its own parent. Dot shorthand keeps nested track
and breakpoint declarations compact. Omit `autoRows` (or set `.autoRows(.auto())`)
when row height should follow the tallest child:

```dart
final GridBoxStyler dashboardGrid = .equalColumns(3)
.gap(16)
.autoRows(.fixed(220))
.onConstraints(
.maxWidth(720),
.equalColumns(1).gap(12),
Expand Down
87 changes: 64 additions & 23 deletions packages/mix/doc/grid-layout.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Grid layout

`GridBox` arranges children in row-major order using fixed and fractional
tracks. Its style supports ordinary Mix variants and modifiers, plus
`onConstraints` for layout decisions based on the Grid's own available space.
`GridBox` arranges children in row-major order using fixed, fractional, and
vertical auto tracks. Its style supports ordinary Mix variants and modifiers,
plus `onConstraints` for layout decisions based on the Grid's own available
space.

## Basic usage

Expand All @@ -12,16 +13,19 @@ entry point, then chain the remaining geometry:
```dart
final GridBoxStyler style = .equalColumns(3)
.gap(16)
.autoRows(.fixed(160));
.autoRows(.auto());

GridBox(style: style, children: cards);
```

`GridTrack.fixed(size)` keeps its logical-pixel size. `GridTrack.fr(fraction)`
receives that fraction of the free space left after fixed tracks and gaps.
For example, with 300 pixels of remaining space, `[.fr(2), .fr(1)]` produces
tracks of 200 and 100 pixels. Fractional tracks require a bounded parent extent
on their axis. Use `columns` directly when tracks are intentionally mixed:
`GridTrack.fixed(size)` keeps its logical-pixel size and is a hard constraint:
children in a fixed row are stretched or clipped to that height.
`GridTrack.fr(fraction)` receives that fraction of the free space left after
fixed tracks, auto tracks, and gaps. For example, with 300 pixels of remaining space, `[.fr(2), .fr(1)]`
produces tracks of 200 and 100 pixels. Fractional tracks require a bounded
parent extent on their axis. `GridTrack.auto()` is vertical-only: the row
sizes to its tallest child's natural height at the resolved column width.
Use `columns` directly when tracks are intentionally mixed:

```dart
final GridBoxStyler sidebarAndContent = .columns([
Expand All @@ -38,7 +42,6 @@ the bounded maximum size offered to this Grid:
```dart
final GridBoxStyler cardGrid = .equalColumns(3)
.gap(16)
.autoRows(.fixed(220))
.onConstraints(
.maxWidth(760),
.equalColumns(2).gap(12),
Expand Down Expand Up @@ -69,22 +72,43 @@ Two GridBoxes on the same screen can therefore select different

Children fill columns from left to right, then continue on the next row.
Provide explicit `rows` for known row geometry, or `autoRows` for each repeated
row needed beyond the explicit list:
row needed beyond the explicit list. Omitted `autoRows` defaults to
`GridTrack.auto()`, so a two-column Grid with no row declaration sizes each
implicit row to its tallest child:

```dart
final GridBoxStyler gallery = .equalColumns(2).rows([
final GridBoxStyler gallery = .equalColumns(2).gap(12);

final GridBoxStyler mixed = .equalColumns(2).rows([
.fixed(180),
]).autoRows(.fixed(180)).columnGap(12).rowGap(12);
]).autoRows(.auto()).columnGap(12).rowGap(12);
```

With five children and two columns, the example needs three rows: the first
uses the explicit 180-pixel row and the next two repeat `autoRows`. When
`rows` is empty, every required row uses `autoRows`.
With five children and two columns, `mixed` needs three rows: the first uses
the explicit 180-pixel row and the next two repeat `autoRows`. When `rows` is
empty, every required row uses `autoRows` (or `auto` when it is omitted).

Use `GridTrack.auto()` — or omit `autoRows` — in a vertical
`SingleChildScrollView` when child heights are unknown. Auto-row children are
measured at their column width, and a child shorter than the resolved row is
then laid out a second time so it fills the row. A child that already fills
its row — always the tallest, and every child when a row holds one — is handed
back the constraint it was measured with, so its subtree is not relaid out.
That measure pass runs only for children in auto rows; explicit fixed/`fr`
Grids still lay each child out once.

Nesting therefore stays flat rather than compounding: a leaf inside three
nested auto Grids is laid out once, the same as inside one. The extra pass is
paid only by the shorter siblings a row actually has to stretch, so rows of
uneven children cost more than uniform ones.

If children require more rows than declared and `autoRows` is absent, GridBox
reports an actionable layout error rather than guessing a content-sizing rule.
Use fixed rows on an unbounded vertical axis such as `SingleChildScrollView`.
Fractional rows and fractional `autoRows` require bounded height.
Fixed rows stay hard heights. Fractional rows and fractional `autoRows` still
require bounded height. Auto rows require children with a finite natural
height; `Expanded`, `Spacer`, or another expanding child inside an auto row
in a scroll view is an error.

`GridTrack.auto()` is rejected on `columns`. Content-sized columns need a
separate two-axis design and are not part of this API.

## Design tokens

Expand Down Expand Up @@ -151,6 +175,7 @@ Animation compatibility is positional:

- fixed tracks interpolate with fixed tracks;
- fractional tracks interpolate with fractional tracks;
- auto tracks stay compatible with auto tracks;
- the two track lists must keep the same length and track kinds;
- compatible `autoRows`, `columnGap`, and `rowGap` values interpolate too.

Expand Down Expand Up @@ -179,11 +204,27 @@ contained:
final GridBoxStyler clipped = .clipBehavior(.hardEdge);
```

Auto rows are diagnosed the same way. Measured row heights that add up to more
than a bounded parent offers overflow the Grid's box and report the same
indicator, so putting tall content in a bounded parent is still visible rather
than silent.

A child that outgrows its own cell is not diagnosed. The Grid compares its
total track extent against the space the parent offered; it never asks whether
an individual child fits the cell it was given. A fixed track is therefore a
hard constraint in exactly the way a tight `SizedBox` is, and content taller
than that track is constrained without a warning. This is deliberate: probing
every fixed cell for its natural height would cost a speculative measure pass
per child and would break children that require a bounded height. Use `auto`
rows whenever the content height is unknown, and keep `.fixed(...)` for cells
where constraining the child is the intent.

## Current track model

GridBox intentionally supports fixed and fractional tracks with row-major
auto-placement. Content-sized tracks, spans, named areas, direction-aware
placement, and baseline alignment are not part of the current API.
GridBox supports fixed and fractional tracks on both axes and content-sized
`auto` tracks on rows only, with row-major auto-placement. Content-sized
columns, spans, named areas, direction-aware placement, and baseline alignment
are not part of the current API.

Run the card, dashboard, gallery, and animation examples from
`packages/mix/example`:
Expand Down
1 change: 1 addition & 0 deletions packages/mix/example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/failures/
4 changes: 2 additions & 2 deletions packages/mix/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ rather than the viewport:
```dart
final GridBoxStyler cardGrid = .equalColumns(3)
.gap(16)
.autoRows(.fixed(220))
.onConstraints(
.maxWidth(760),
.equalColumns(2).gap(12),
Expand All @@ -45,7 +44,8 @@ columns to one.
<img src="test/goldens/grid_dashboard_compact.png" alt="Compact GridBox dashboard" width="24%">
</p>

The same track model handles repeated product cards and a denser media gallery:
The card catalog omits `autoRows` so implicit rows size to unequal copy. The
media gallery keeps fixed automatic rows for a dense, even tile height:

<p>
<img src="test/goldens/grid_catalog_wide.png" alt="Wide GridBox card catalog" width="66%">
Expand Down
37 changes: 27 additions & 10 deletions packages/mix/example/lib/grid_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -444,50 +444,58 @@ class CatalogGridPreview extends StatelessWidget {
Widget build(BuildContext context) {
final GridBoxStyler style = .equalColumns(3)
.gap(16)
.autoRows(.fixed(220))
.onConstraints(.maxWidth(760), .equalColumns(2).gap(12))
.onConstraints(
.maxWidth(520),
.equalColumns(1).gap(10).autoRows(.fixed(190)),
);
.onConstraints(.maxWidth(520), .equalColumns(1).gap(10));

return GridBox(
key: const Key('catalog-grid'),
style: style,
children: const [
_ProductCard(
key: Key('catalog-card-0'),
'Canvas tote',
r'$48',
'A roomy everyday bag.',
Icons.shopping_bag_rounded,
Color(0xFFE9E6FF),
),
_ProductCard(
key: Key('catalog-card-1'),
'Desk lamp',
r'$72',
'Adjustable arm and a warm-dim LED. Built for late editing sessions when the rest of the room has gone dark.',
Icons.light_rounded,
Color(0xFFFFE9D8),
),
_ProductCard(
key: Key('catalog-card-2'),
'Travel mug',
r'$32',
'Keeps coffee hot through a standup.',
Icons.coffee_rounded,
Color(0xFFDDF4EE),
),
_ProductCard(
key: Key('catalog-card-3'),
'Studio clock',
r'$64',
'Quiet sweep, high-contrast face.',
Icons.schedule_rounded,
Color(0xFFDDEBFA),
),
_ProductCard(
key: Key('catalog-card-4'),
'Wool throw',
r'$96',
'Heavyweight merino for the couch.',
Icons.bed_rounded,
Color(0xFFF6E2EB),
),
_ProductCard(
key: Key('catalog-card-5'),
'Plant stand',
r'$58',
'Three-tier oak riser.',
Icons.eco_rounded,
Color(0xFFE4F2D8),
),
Expand All @@ -497,17 +505,26 @@ class CatalogGridPreview extends StatelessWidget {
}

class _ProductCard extends StatelessWidget {
const _ProductCard(this.name, this.price, this.icon, this.tint);
const _ProductCard(
this.name,
this.price,
this.blurb,
this.icon,
this.tint, {
super.key,
});

final String name;
final String price;
final String blurb;
final IconData icon;
final Color tint;

@override
Widget build(BuildContext context) {
return _CardSurface(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
Expand All @@ -519,15 +536,15 @@ class _ProductCard extends StatelessWidget {
),
child: Icon(icon, color: const Color(0xFF3C3D4A)),
),
const Spacer(),
const SizedBox(height: 16),
Text(
name,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
const SizedBox(height: 4),
const Text(
'Essential collection',
style: TextStyle(color: Color(0xFF77798A), fontSize: 12),
Text(
blurb,
style: const TextStyle(color: Color(0xFF77798A), fontSize: 12),
),
const SizedBox(height: 12),
Row(
Expand Down
Binary file modified packages/mix/example/test/goldens/grid_catalog_compact.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/mix/example/test/goldens/grid_catalog_wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions packages/mix/example/test/grid_box_example_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ void main() {
await tester.tap(find.text('Compact'));
await tester.pumpAndSettle();
expect(tester.getSize(find.byKey(const Key('catalog-grid'))).width, 334);
expect(
tester.getSize(find.byKey(const Key('catalog-card-1'))).height,
greaterThan(
tester.getSize(find.byKey(const Key('catalog-card-0'))).height,
),
);

await tester.tap(find.text('Media gallery'));
await tester.pumpAndSettle();
Expand All @@ -38,6 +44,40 @@ void main() {
expect(find.byKey(const Key('animated-grid')), findsOneWidget);
});

testWidgets(
'catalog rows follow unequal card content instead of one height',
(tester) async {
await tester.binding.setSurfaceSize(const Size(1200, 900));
addTearDown(() => tester.binding.setSurfaceSize(null));

await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: SizedBox(width: 1120, child: CatalogGridPreview()),
),
),
),
);

final rowZero = tester
.getSize(find.byKey(const Key('catalog-card-0')))
.height;
final rowOne = tester
.getSize(find.byKey(const Key('catalog-card-3')))
.height;
expect(rowZero, isNot(rowOne));
expect(
tester.getSize(find.byKey(const Key('catalog-card-1'))).height,
rowZero,
);
expect(
tester.getSize(find.byKey(const Key('catalog-card-4'))).height,
rowOne,
);
},
);

testWidgets('animation example interpolates tracks, rows, and gaps', (
tester,
) async {
Expand Down
Loading
Loading