David PHAM-VAN

Add Wrap Widget

... ... @@ -5,6 +5,7 @@
* Add better debugPaint on Align Widget
* Fix Transform placement when Alignment and Origin are Null
* Add Transform.rotateBox constructor
* Add Wrap Widget
## 1.3.15
... ...
... ... @@ -42,3 +42,4 @@ part 'widgets/table.dart';
part 'widgets/text.dart';
part 'widgets/theme.dart';
part 'widgets/widget.dart';
part 'widgets/wrap.dart';
... ...
... ... @@ -46,6 +46,14 @@ class BoxConstraints {
minHeight = height != null ? height : double.infinity,
maxHeight = height != null ? height : double.infinity;
const BoxConstraints.tightForFinite({
double width = double.infinity,
double height = double.infinity,
}) : minWidth = width != double.infinity ? width : 0.0,
maxWidth = width != double.infinity ? width : double.infinity,
minHeight = height != double.infinity ? height : 0.0,
maxHeight = height != double.infinity ? height : double.infinity;
/// The minimum width that satisfies the constraints.
final double minWidth;
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
part of widget;
/// How [Wrap] should align objects.
enum WrapAlignment {
start,
end,
center,
spaceBetween,
spaceAround,
spaceEvenly
}
/// Who [Wrap] should align children within a run in the cross axis.
enum WrapCrossAlignment { start, end, center }
class _RunMetrics {
_RunMetrics(this.mainAxisExtent, this.crossAxisExtent, this.childCount);
final double mainAxisExtent;
final double crossAxisExtent;
final int childCount;
}
/// A widget that displays its children in multiple horizontal or vertical runs.
class Wrap extends MultiChildWidget {
/// Creates a wrap layout.
Wrap({
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
this.spacing = 0.0,
this.runAlignment = WrapAlignment.start,
this.runSpacing = 0.0,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.verticalDirection = VerticalDirection.down,
List<Widget> children = const <Widget>[],
}) : assert(direction != null),
assert(alignment != null),
assert(spacing != null),
assert(runAlignment != null),
assert(runSpacing != null),
assert(crossAxisAlignment != null),
super(children: children);
/// The direction to use as the main axis.
final Axis direction;
/// How the children within a run should be placed in the main axis.
final WrapAlignment alignment;
/// How much space to place between children in a run in the main axis.
final double spacing;
/// How the runs themselves should be placed in the cross axis.
final WrapAlignment runAlignment;
/// How much space to place between the runs themselves in the cross axis.
final double runSpacing;
/// How the children within a run should be aligned relative to each other in
/// the cross axis.
final WrapCrossAlignment crossAxisAlignment;
/// Determines the order to lay children out vertically and how to interpret
/// `start` and `end` in the vertical direction.
final VerticalDirection verticalDirection;
bool get textDirection => false;
bool _hasVisualOverflow = false;
bool get _debugHasNecessaryDirections {
assert(direction != null);
assert(alignment != null);
assert(runAlignment != null);
assert(crossAxisAlignment != null);
if (children.length > 1) {
// i.e. there's more than one child
switch (direction) {
case Axis.horizontal:
assert(textDirection != null,
'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.');
break;
case Axis.vertical:
assert(verticalDirection != null,
'Vertical $runtimeType with multiple children has a null verticalDirection, so the layout order is undefined.');
break;
}
}
if (alignment == WrapAlignment.start || alignment == WrapAlignment.end) {
switch (direction) {
case Axis.horizontal:
assert(textDirection != null,
'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
assert(verticalDirection != null,
'Vertical $runtimeType with alignment $alignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
}
}
if (runAlignment == WrapAlignment.start ||
runAlignment == WrapAlignment.end) {
switch (direction) {
case Axis.horizontal:
assert(verticalDirection != null,
'Horizontal $runtimeType with runAlignment $runAlignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
assert(textDirection != null,
'Vertical $runtimeType with runAlignment $runAlignment has a null textDirection, so the alignment cannot be resolved.');
break;
}
}
if (crossAxisAlignment == WrapCrossAlignment.start ||
crossAxisAlignment == WrapCrossAlignment.end) {
switch (direction) {
case Axis.horizontal:
assert(verticalDirection != null,
'Horizontal $runtimeType with crossAxisAlignment $crossAxisAlignment has a null verticalDirection, so the alignment cannot be resolved.');
break;
case Axis.vertical:
assert(textDirection != null,
'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.');
break;
}
}
return true;
}
double _getMainAxisExtent(Widget child) {
switch (direction) {
case Axis.horizontal:
return child.box.width;
case Axis.vertical:
return child.box.height;
}
return 0.0;
}
double _getCrossAxisExtent(Widget child) {
switch (direction) {
case Axis.horizontal:
return child.box.height;
case Axis.vertical:
return child.box.width;
}
return 0.0;
}
PdfPoint _getOffset(double mainAxisOffset, double crossAxisOffset) {
switch (direction) {
case Axis.horizontal:
return PdfPoint(mainAxisOffset, crossAxisOffset);
case Axis.vertical:
return PdfPoint(crossAxisOffset, mainAxisOffset);
}
return PdfPoint.zero;
}
double _getChildCrossAxisOffset(bool flipCrossAxis, double runCrossAxisExtent,
double childCrossAxisExtent) {
final double freeSpace = runCrossAxisExtent - childCrossAxisExtent;
switch (crossAxisAlignment) {
case WrapCrossAlignment.start:
return flipCrossAxis ? freeSpace : 0.0;
case WrapCrossAlignment.end:
return flipCrossAxis ? 0.0 : freeSpace;
case WrapCrossAlignment.center:
return freeSpace / 2.0;
}
return 0.0;
}
@override
void layout(Context context, BoxConstraints constraints,
{bool parentUsesSize = false}) {
assert(_debugHasNecessaryDirections);
_hasVisualOverflow = false;
if (children.isEmpty) {
box = PdfRect.fromPoints(PdfPoint.zero, constraints.smallest);
return;
}
BoxConstraints childConstraints;
double mainAxisLimit = 0.0;
bool flipMainAxis = false;
bool flipCrossAxis = false;
switch (direction) {
case Axis.horizontal:
childConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
mainAxisLimit = constraints.maxWidth;
if (verticalDirection == VerticalDirection.down) {
flipCrossAxis = true;
}
break;
case Axis.vertical:
childConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
mainAxisLimit = constraints.maxHeight;
if (verticalDirection == VerticalDirection.down) {
flipMainAxis = true;
}
break;
}
assert(childConstraints != null);
assert(mainAxisLimit != null);
final double spacing = this.spacing;
final double runSpacing = this.runSpacing;
final List<_RunMetrics> runMetrics = <_RunMetrics>[];
final Map<Widget, int> childRunMetrics = <Widget, int>{};
double mainAxisExtent = 0.0;
double crossAxisExtent = 0.0;
double runMainAxisExtent = 0.0;
double runCrossAxisExtent = 0.0;
int childCount = 0;
for (Widget child in children) {
child.layout(context, childConstraints, parentUsesSize: true);
final double childMainAxisExtent = _getMainAxisExtent(child);
final double childCrossAxisExtent = _getCrossAxisExtent(child);
if (childCount > 0 &&
runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit) {
mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
crossAxisExtent += runCrossAxisExtent;
if (runMetrics.isNotEmpty) {
crossAxisExtent += runSpacing;
}
runMetrics.add(
_RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount));
runMainAxisExtent = 0.0;
runCrossAxisExtent = 0.0;
childCount = 0;
}
runMainAxisExtent += childMainAxisExtent;
if (childCount > 0) {
runMainAxisExtent += spacing;
}
runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent);
childCount += 1;
childRunMetrics[child] = runMetrics.length;
}
if (childCount > 0) {
mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
crossAxisExtent += runCrossAxisExtent;
if (runMetrics.isNotEmpty) {
crossAxisExtent += runSpacing;
}
runMetrics
.add(_RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount));
}
final int runCount = runMetrics.length;
assert(runCount > 0);
double containerMainAxisExtent = 0.0;
double containerCrossAxisExtent = 0.0;
switch (direction) {
case Axis.horizontal:
box = PdfRect.fromPoints(PdfPoint.zero,
constraints.constrain(PdfPoint(mainAxisExtent, crossAxisExtent)));
containerMainAxisExtent = box.width;
containerCrossAxisExtent = box.height;
break;
case Axis.vertical:
box = PdfRect.fromPoints(PdfPoint.zero,
constraints.constrain(PdfPoint(crossAxisExtent, mainAxisExtent)));
containerMainAxisExtent = box.height;
containerCrossAxisExtent = box.width;
break;
}
_hasVisualOverflow = containerMainAxisExtent < mainAxisExtent ||
containerCrossAxisExtent < crossAxisExtent;
final double crossAxisFreeSpace =
math.max(0.0, containerCrossAxisExtent - crossAxisExtent);
double runLeadingSpace = 0.0;
double runBetweenSpace = 0.0;
switch (runAlignment) {
case WrapAlignment.start:
break;
case WrapAlignment.end:
runLeadingSpace = crossAxisFreeSpace;
break;
case WrapAlignment.center:
runLeadingSpace = crossAxisFreeSpace / 2.0;
break;
case WrapAlignment.spaceBetween:
runBetweenSpace =
runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0;
break;
case WrapAlignment.spaceAround:
runBetweenSpace = crossAxisFreeSpace / runCount;
runLeadingSpace = runBetweenSpace / 2.0;
break;
case WrapAlignment.spaceEvenly:
runBetweenSpace = crossAxisFreeSpace / (runCount + 1);
runLeadingSpace = runBetweenSpace;
break;
}
runBetweenSpace += runSpacing;
double crossAxisOffset = flipCrossAxis
? containerCrossAxisExtent - runLeadingSpace
: runLeadingSpace;
int currentWidget = 0;
for (int i = 0; i < runCount; ++i) {
final _RunMetrics metrics = runMetrics[i];
final double runMainAxisExtent = metrics.mainAxisExtent;
final double runCrossAxisExtent = metrics.crossAxisExtent;
final int childCount = metrics.childCount;
final double mainAxisFreeSpace =
math.max(0.0, containerMainAxisExtent - runMainAxisExtent);
double childLeadingSpace = 0.0;
double childBetweenSpace = 0.0;
switch (alignment) {
case WrapAlignment.start:
break;
case WrapAlignment.end:
childLeadingSpace = mainAxisFreeSpace;
break;
case WrapAlignment.center:
childLeadingSpace = mainAxisFreeSpace / 2.0;
break;
case WrapAlignment.spaceBetween:
childBetweenSpace =
childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0;
break;
case WrapAlignment.spaceAround:
childBetweenSpace = mainAxisFreeSpace / childCount;
childLeadingSpace = childBetweenSpace / 2.0;
break;
case WrapAlignment.spaceEvenly:
childBetweenSpace = mainAxisFreeSpace / (childCount + 1);
childLeadingSpace = childBetweenSpace;
break;
}
childBetweenSpace += spacing;
double childMainPosition = flipMainAxis
? containerMainAxisExtent - childLeadingSpace
: childLeadingSpace;
if (flipCrossAxis) {
crossAxisOffset -= runCrossAxisExtent;
}
for (Widget child in children.sublist(currentWidget)) {
final int runIndex = childRunMetrics[child];
if (runIndex != i) {
break;
}
currentWidget++;
final double childMainAxisExtent = _getMainAxisExtent(child);
final double childCrossAxisExtent = _getCrossAxisExtent(child);
final double childCrossAxisOffset = _getChildCrossAxisOffset(
flipCrossAxis, runCrossAxisExtent, childCrossAxisExtent);
if (flipMainAxis) {
childMainPosition -= childMainAxisExtent;
}
child.box = PdfRect.fromPoints(
_getOffset(
childMainPosition, crossAxisOffset + childCrossAxisOffset),
child.box.size);
if (flipMainAxis) {
childMainPosition -= childBetweenSpace;
} else {
childMainPosition += childMainAxisExtent + childBetweenSpace;
}
}
if (flipCrossAxis) {
crossAxisOffset -= runBetweenSpace;
} else {
crossAxisOffset += runCrossAxisExtent + runBetweenSpace;
}
}
}
@override
void paint(Context context) {
super.paint(context);
context.canvas.saveContext();
if (_hasVisualOverflow) {
context.canvas
..drawRect(box.left, box.bottom, box.width, box.height)
..clipPath();
}
final Matrix4 mat = Matrix4.identity();
mat.translate(box.x, box.y);
context.canvas.setTransform(mat);
for (Widget child in children) {
child.paint(context);
}
context.canvas.restoreContext();
}
}
... ...
... ... @@ -32,6 +32,7 @@ import 'widget_table_test.dart' as widget_table;
import 'widget_test.dart' as widget;
import 'widget_text_test.dart' as widget_text;
import 'widget_theme_test.dart' as widget_theme;
import 'widget_wrap_test.dart' as widget_wrap;
void main() {
annotations.main();
... ... @@ -49,5 +50,6 @@ void main() {
widget_table.main();
widget_text.main();
widget_theme.main();
widget_wrap.main();
widget.main();
}
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:io';
import 'dart:math' as math;
import 'package:test/test.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
Document pdf;
void main() {
setUpAll(() {
Document.debug = true;
pdf = Document();
});
test('Wrap Widget Horizontal 1', () {
final List<Widget> wraps = <Widget>[];
for (VerticalDirection direction in VerticalDirection.values) {
wraps.add(Text('$direction'));
for (WrapAlignment alignment in WrapAlignment.values) {
wraps.add(Text('$alignment'));
wraps.add(
Wrap(
direction: Axis.horizontal,
verticalDirection: direction,
alignment: alignment,
children: List<Widget>.generate(
40,
(int n) => Text('${n + 1}'),
),
),
);
}
}
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(400, 800),
margin: const EdgeInsets.all(10),
build: (Context context) => Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: wraps,
),
),
);
});
test('Wrap Widget Vertical 1', () {
final List<Widget> wraps = <Widget>[];
for (VerticalDirection direction in VerticalDirection.values) {
wraps.add(Transform.rotateBox(child: Text('$direction'), angle: 1.57));
for (WrapAlignment alignment in WrapAlignment.values) {
wraps.add(Transform.rotateBox(child: Text('$alignment'), angle: 1.57));
wraps.add(
Wrap(
direction: Axis.vertical,
verticalDirection: direction,
alignment: alignment,
children: List<Widget>.generate(
40,
(int n) => Text('${n + 1}'),
),
),
);
}
}
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(800, 400),
margin: const EdgeInsets.all(10),
build: (Context context) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: wraps,
),
),
);
});
test('Wrap Widget Horizontal 2', () {
final List<Widget> wraps = <Widget>[];
for (WrapCrossAlignment alignment in WrapCrossAlignment.values) {
final math.Random rnd = math.Random(42);
wraps.add(Text('$alignment'));
wraps.add(
Wrap(
direction: Axis.horizontal,
crossAxisAlignment: alignment,
runSpacing: 20,
spacing: 20,
children: List<Widget>.generate(
20,
(int n) => SizedBox(
width: rnd.nextDouble() * 100,
height: rnd.nextDouble() * 50,
child: Placeholder(),
)),
),
);
}
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(400, 800),
margin: const EdgeInsets.all(10),
build: (Context context) => Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: wraps,
),
),
);
});
test('Wrap Widget Vertical 2', () {
final List<Widget> wraps = <Widget>[];
for (WrapCrossAlignment alignment in WrapCrossAlignment.values) {
final math.Random rnd = math.Random(42);
wraps.add(Transform.rotateBox(child: Text('$alignment'), angle: 1.57));
wraps.add(
Wrap(
direction: Axis.vertical,
crossAxisAlignment: alignment,
runSpacing: 20,
spacing: 20,
children: List<Widget>.generate(
20,
(int n) => SizedBox(
width: rnd.nextDouble() * 50,
height: rnd.nextDouble() * 100,
child: Placeholder(),
)),
),
);
}
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(800, 400),
margin: const EdgeInsets.all(10),
build: (Context context) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: wraps,
),
),
);
});
test('Wrap Widget Horizontal 3', () {
final List<Widget> wraps = <Widget>[];
for (WrapAlignment alignment in WrapAlignment.values) {
final math.Random rnd = math.Random(42);
wraps.add(Text('$alignment'));
wraps.add(
SizedBox(
height: 110,
child: Wrap(
direction: Axis.horizontal,
runAlignment: alignment,
spacing: 20,
children: List<Widget>.generate(
15,
(int n) => SizedBox(
width: rnd.nextDouble() * 100,
height: 20,
child: Placeholder(),
)),
),
),
);
}
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(400, 800),
margin: const EdgeInsets.all(10),
build: (Context context) => Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: wraps,
),
),
);
});
test('Wrap Widget Vertical 3', () {
final List<Widget> wraps = <Widget>[];
for (WrapAlignment alignment in WrapAlignment.values) {
final math.Random rnd = math.Random(42);
wraps.add(Transform.rotateBox(child: Text('$alignment'), angle: 1.57));
wraps.add(
SizedBox(
width: 110,
child: Wrap(
direction: Axis.vertical,
runAlignment: alignment,
spacing: 20,
children: List<Widget>.generate(
15,
(int n) => SizedBox(
width: 20,
height: rnd.nextDouble() * 100,
child: Placeholder(),
)),
),
),
);
}
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(800, 400),
margin: const EdgeInsets.all(10),
build: (Context context) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: wraps,
),
),
);
});
test('Wrap Widget Overlay', () {
final math.Random rnd = math.Random(42);
pdf.addPage(
Page(
pageFormat: const PdfPageFormat(200, 200),
margin: const EdgeInsets.all(10),
build: (Context context) => Wrap(
spacing: 10,
runSpacing: 10,
children: List<Widget>.generate(
15,
(int n) => SizedBox(
width: rnd.nextDouble() * 100,
height: rnd.nextDouble() * 100,
child: Placeholder(),
)),
),
),
);
});
test('Wrap Widget Empty', () {
pdf.addPage(Page(build: (Context context) => Wrap()));
});
tearDownAll(() {
final File file = File('widgets-wrap.pdf');
file.writeAsBytesSync(pdf.save());
});
}
... ...