Skip to content
Open
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
58 changes: 58 additions & 0 deletions lib/src/flutter/pixel_boundary_box.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';

/// A widget that sizes itself and its [child] such that the child occupies an integer
/// value width and height.
///
/// When laying out a [PixelBoundaryBox], the incoming constraints are pushed to the nearest
/// integer boundary. The minimum width/height are made larger, to the nearest integer. The
/// maximum width/height are made smaller, to the nearest integer. Then, the [child] is laid
/// out within these integer bounds.
///
/// If the [child] reports a size that also has an integer width and height then that size
/// is honored and layout finishes. However, the [child] chooses a size that include a fractional
/// width and/or height, then the [child] is laid out a second time, with tight constraints, which
/// are set to the nearest larger integer for the [child]'s originally reported width and height.
class PixelBoundaryBox extends SingleChildRenderObjectWidget {
const PixelBoundaryBox({
super.key,
required super.child,
});

@override
RenderPixelBoundaryBox createRenderObject(BuildContext context) {
return RenderPixelBoundaryBox();
}
}

class RenderPixelBoundaryBox extends RenderProxyBox {
@override
void performLayout() {
if (child == null) {
size = Size.zero;
return;
}

final integerConstraints = constraints.copyWith(
minWidth: constraints.minWidth.ceilToDouble(),
maxWidth: constraints.maxWidth < double.infinity ? constraints.maxWidth.floorToDouble() : double.infinity,
minHeight: constraints.minHeight.ceilToDouble(),
maxHeight: constraints.maxHeight < double.infinity ? constraints.maxHeight.floorToDouble() : double.infinity,
);

// Let child choose its size within our parent's integer bounded constraints.
child!.layout(integerConstraints, parentUsesSize: true);

// Check if the child chose a non-integer size. If it did, re-run layout, forcing it
// to the nearest larger integer size.
final childSize = child!.size;
if (childSize.width != childSize.width.round() || childSize.height != childSize.height.round()) {
child!.layout(
BoxConstraints.tight(Size(childSize.width.ceilToDouble(), childSize.height.ceilToDouble())),
parentUsesSize: true,
);
}

size = child!.size;
}
}
7 changes: 6 additions & 1 deletion lib/src/scenes/film_strip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter/material.dart' hide Image;
import 'package:flutter/material.dart' as m;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_goldens/src/flutter/flutter_test_extensions.dart';
import 'package:flutter_test_goldens/src/flutter/pixel_boundary_box.dart';
import 'package:flutter_test_goldens/src/goldens/golden_camera.dart';
import 'package:flutter_test_goldens/src/goldens/golden_collections.dart';
import 'package:flutter_test_goldens/src/goldens/golden_comparisons.dart';
Expand Down Expand Up @@ -55,7 +56,11 @@ class FilmStrip {

_setup = _FilmStripSetup((tester) async {
final widgetTree = sceneBuilder();
await _tester.pumpWidget(widgetTree);
await _tester.pumpWidget(
PixelBoundaryBox(
child: widgetTree,
),
);
});

return this;
Expand Down
41 changes: 23 additions & 18 deletions lib/src/scenes/gallery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter/material.dart' hide Image;
import 'package:flutter/material.dart' as m;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_goldens/src/flutter/flutter_test_extensions.dart';
import 'package:flutter_test_goldens/src/flutter/pixel_boundary_box.dart';
import 'package:flutter_test_goldens/src/goldens/golden_camera.dart';
import 'package:flutter_test_goldens/src/goldens/golden_collections.dart';
import 'package:flutter_test_goldens/src/goldens/golden_comparisons.dart';
Expand Down Expand Up @@ -153,30 +154,34 @@ class Gallery {
} else if (item.builder != null) {
// Pump this gallery item, deferring to a `WidgetBuilder` for the content.
await _tester.pumpWidget(
_itemScaffold(
_tester,
GoldenImageBounds(
child: _itemDecorator != null
? _itemDecorator.call(
_tester,
Builder(builder: item.builder!),
)
: Builder(builder: item.builder!),
PixelBoundaryBox(
child: _itemScaffold(
_tester,
GoldenImageBounds(
child: _itemDecorator != null
? _itemDecorator.call(
_tester,
Builder(builder: item.builder!),
)
: Builder(builder: item.builder!),
),
),
),
);
} else {
// Pump this gallery item, deferring to a `Widget` for the content.
await _tester.pumpWidget(
_itemScaffold(
_tester,
GoldenImageBounds(
child: _itemDecorator != null
? _itemDecorator.call(
_tester,
item.child!,
)
: item.child!,
PixelBoundaryBox(
child: _itemScaffold(
_tester,
GoldenImageBounds(
child: _itemDecorator != null
? _itemDecorator.call(
_tester,
item.child!,
)
: item.child!,
),
),
),
);
Expand Down
1 change: 1 addition & 0 deletions lib/src/scenes/golden_scene.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Colors;
import 'package:flutter/widgets.dart';
import 'package:flutter_test_goldens/src/flutter/pixel_boundary_box.dart';
import 'package:flutter_test_goldens/src/goldens/golden_camera.dart';

class GoldenScene extends StatelessWidget {
Expand Down
113 changes: 113 additions & 0 deletions test/flutter/pixel_boundary_box_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_goldens/src/flutter/pixel_boundary_box.dart';

void main() {
group("Pixel boundary box >", () {
testWidgets("does not modify incoming integer constraints", (tester) async {
final pixelBoundaryKey = GlobalKey(debugLabel: "pixel-boundary");
final childKey = GlobalKey(debugLabel: "child");

await _pumpScaffold(
tester,
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 100, maxHeight: 75),
child: PixelBoundaryBox(
key: pixelBoundaryKey,
child: Container(
key: childKey,
width: double.infinity,
height: double.infinity,
color: Colors.red,
),
),
),
);

expect(tester.getSize(find.byKey(pixelBoundaryKey)), Size(100, 75));
expect(tester.getSize(find.byKey(childKey)), Size(100, 75));
});

testWidgets("adjusts incoming non-integer constraints", (tester) async {
final pixelBoundaryKey = GlobalKey(debugLabel: "pixel-boundary");
final childKey = GlobalKey(debugLabel: "child");

await _pumpScaffold(
tester,
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 100.7, maxHeight: 75.8),
child: PixelBoundaryBox(
key: pixelBoundaryKey,
child: Container(
key: childKey,
width: double.infinity,
height: double.infinity,
color: Colors.red,
),
),
),
);

expect(tester.getSize(find.byKey(pixelBoundaryKey)), Size(100, 75));
expect(tester.getSize(find.byKey(childKey)), Size(100, 75));
});

testWidgets("adjusts non-integer child size", (tester) async {
final pixelBoundaryKey = GlobalKey(debugLabel: "pixel-boundary");
final childKey = GlobalKey(debugLabel: "child");

// First, try a bounded size that's larger than the child.
await _pumpScaffold(
tester,
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 100, maxHeight: 75),
child: PixelBoundaryBox(
key: pixelBoundaryKey,
child: Container(
key: childKey,
width: 88.5,
height: 48.2,
color: Colors.red,
),
),
),
);

expect(tester.getSize(find.byKey(pixelBoundaryKey)), Size(89, 49));
expect(tester.getSize(find.byKey(childKey)), Size(89, 49));

// Second, try an unbounded parent.
await _pumpScaffold(
tester,
ConstrainedBox(
constraints: BoxConstraints(maxWidth: double.infinity, maxHeight: double.infinity),
child: PixelBoundaryBox(
key: pixelBoundaryKey,
child: Container(
key: childKey,
width: 88.5,
height: 48.2,
color: Colors.red,
),
),
),
);

expect(tester.getSize(find.byKey(pixelBoundaryKey)), Size(89, 49));
expect(tester.getSize(find.byKey(childKey)), Size(89, 49));
});
});
}

Future<void> _pumpScaffold(WidgetTester tester, Widget content) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: content,
),
),
),
);
}
Binary file modified test_goldens/flutter/buttons/button_extended_fab_gallery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion test_goldens/flutter/buttons/buttons_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,17 @@ void main() {
tester,
sceneName: "button_extended_fab_gallery",
layout: SceneLayout.row,
itemDecorator: (context, child) {
itemScaffold: (context, child) {
return FlutterWidgetScaffold(
child: child,
);
},
itemDecorator: (context, child) {
return Padding(
padding: const EdgeInsets.all(24),
child: child,
);
},
goldenBackground: Image.memory(
backgroundImageBytes,
fit: BoxFit.cover,
Expand Down
Binary file modified test_goldens/flutter/textfield/textfield_interactions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.