Skip to content

Commit dc20c86

Browse files
committed
feat: Column spans
1 parent 6be38a9 commit dc20c86

File tree

2 files changed

+142
-61
lines changed

2 files changed

+142
-61
lines changed

pdf/lib/src/widgets/table.dart

Lines changed: 95 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TableRow {
3030
this.repeat = false,
3131
this.verticalAlignment,
3232
this.decoration,
33+
this.columnSpans,
3334
});
3435

3536
/// The widgets that comprise the cells in this row.
@@ -41,6 +42,8 @@ class TableRow {
4142
final BoxDecoration? decoration;
4243

4344
final TableCellVerticalAlignment? verticalAlignment;
45+
46+
final Map<int, int>? columnSpans;
4447
}
4548

4649
enum TableCellVerticalAlignment { bottom, middle, top, full }
@@ -91,40 +94,6 @@ class TableBorder extends Border {
9194

9295
final BorderSide horizontalInside;
9396
final BorderSide verticalInside;
94-
95-
void paintTable(Context context, PdfRect box,
96-
[List<double?>? widths, List<double>? heights]) {
97-
super.paint(context, box);
98-
99-
if (verticalInside.style.paint) {
100-
verticalInside.style.setStyle(context);
101-
var offset = box.x;
102-
for (final width in widths!.sublist(0, widths.length - 1)) {
103-
offset += width!;
104-
context.canvas.moveTo(offset, box.y);
105-
context.canvas.lineTo(offset, box.top);
106-
}
107-
context.canvas.setStrokeColor(verticalInside.color);
108-
context.canvas.setLineWidth(verticalInside.width);
109-
context.canvas.strokePath();
110-
111-
verticalInside.style.unsetStyle(context);
112-
}
113-
114-
if (horizontalInside.style.paint) {
115-
horizontalInside.style.setStyle(context);
116-
var offset = box.top;
117-
for (final height in heights!.sublist(0, heights.length - 1)) {
118-
offset -= height;
119-
context.canvas.moveTo(box.x, offset);
120-
context.canvas.lineTo(box.right, offset);
121-
}
122-
context.canvas.setStrokeColor(horizontalInside.color);
123-
context.canvas.setLineWidth(horizontalInside.width);
124-
context.canvas.strokePath();
125-
horizontalInside.style.unsetStyle(context);
126-
}
127-
}
12897
}
12998

13099
class TableContext extends WidgetContext {
@@ -315,7 +284,6 @@ class Table extends Widget with SpanningWidget {
315284

316285
final TableWidth tableWidth;
317286

318-
final List<double?> _widths = <double?>[];
319287
final List<double> _heights = <double>[];
320288

321289
final TableContext _context = TableContext();
@@ -334,69 +302,110 @@ class Table extends Widget with SpanningWidget {
334302
_context.firstLine = _context.lastLine;
335303
}
336304

305+
List<Widget> _getFilledChildrenFromColumnSpans(TableRow row) {
306+
if (row.columnSpans == null) {
307+
return row.children;
308+
}
309+
final filledChildren = <Widget>[];
310+
var n = 0;
311+
// TODO(Gustl22): Handle intrinsic column widths:
312+
// Currently, every cell is calculated by filling the remaining spanned
313+
// cells with empty containers and then sum up their calculated widths.
314+
for (final child in row.children) {
315+
// Columns, which are currently spanned.
316+
final columnSpan = row.columnSpans![n] ?? 1;
317+
filledChildren.add(child);
318+
if (columnSpan > 1) {
319+
filledChildren
320+
.addAll(Iterable.generate(columnSpan - 1, (index) => Container()));
321+
}
322+
n += columnSpan;
323+
}
324+
return filledChildren;
325+
}
326+
327+
List<double?> _getSpannedWidths(List<double?> widths, TableRow row) {
328+
final spannedWidths = <double?>[];
329+
var n = 0;
330+
for (var i = 0; i < row.children.length; i++) {
331+
final columnSpan = row.columnSpans?[n] ?? 1;
332+
final indices = Iterable.generate(columnSpan, (span) => n + span);
333+
final width = indices.fold<double?>(null, (prev, curIndex) {
334+
final current = widths[curIndex];
335+
if (prev == null && current == null) {
336+
return null;
337+
}
338+
return (prev ?? 0) + (current ?? 0);
339+
});
340+
spannedWidths.add(width);
341+
n += columnSpan;
342+
}
343+
return spannedWidths;
344+
}
345+
337346
@override
338347
void layout(Context context, BoxConstraints constraints,
339348
{bool parentUsesSize = false}) {
340349
// Compute required width for all row/columns width flex
341350
final flex = <double?>[];
342-
_widths.clear();
351+
final widths = <double?>[];
343352
_heights.clear();
344353
var index = 0;
345354

346355
for (final row in children) {
347356
var n = 0;
348-
for (final child in row.children) {
357+
for (final child in _getFilledChildrenFromColumnSpans(row)) {
349358
final columnWidth = columnWidths != null && columnWidths![n] != null
350359
? columnWidths![n]!
351360
: defaultColumnWidth;
352361
final columnLayout = columnWidth.layout(child, context, constraints);
353362
if (flex.length < n + 1) {
354363
flex.add(columnLayout.flex);
355-
_widths.add(columnLayout.width);
364+
widths.add(columnLayout.width);
356365
} else {
357366
if (columnLayout.flex! > 0) {
358367
flex[n] = math.max(flex[n]!, columnLayout.flex!);
359368
}
360-
_widths[n] = math.max(_widths[n]!, columnLayout.width!);
369+
widths[n] = math.max(widths[n]!, columnLayout.width!);
361370
}
362371
n++;
363372
}
364373
}
365374

366-
if (_widths.isEmpty) {
375+
if (widths.isEmpty) {
367376
box = PdfRect.fromPoints(PdfPoint.zero, constraints.smallest);
368377
return;
369378
}
370379

371-
final maxWidth = _widths.reduce((double? a, double? b) => a! + b!);
380+
final maxWidth = widths.reduce((double? a, double? b) => a! + b!);
372381

373382
// Compute column widths using flex and estimated width
374383
if (constraints.hasBoundedWidth) {
375384
final totalFlex = flex.reduce((double? a, double? b) => a! + b!)!;
376385
var flexSpace = 0.0;
377-
for (var n = 0; n < _widths.length; n++) {
386+
for (var n = 0; n < widths.length; n++) {
378387
if (flex[n] == 0.0) {
379-
final newWidth = _widths[n]! / maxWidth! * constraints.maxWidth;
388+
final newWidth = widths[n]! / maxWidth! * constraints.maxWidth;
380389
if ((tableWidth == TableWidth.max && totalFlex == 0.0) ||
381-
newWidth < _widths[n]!) {
382-
_widths[n] = newWidth;
390+
newWidth < widths[n]!) {
391+
widths[n] = newWidth;
383392
}
384-
flexSpace += _widths[n]!;
393+
flexSpace += widths[n]!;
385394
}
386395
}
387396
final spacePerFlex = totalFlex > 0.0
388397
? ((constraints.maxWidth - flexSpace) / totalFlex)
389398
: double.nan;
390399

391-
for (var n = 0; n < _widths.length; n++) {
400+
for (var n = 0; n < widths.length; n++) {
392401
if (flex[n]! > 0.0) {
393402
final newWidth = spacePerFlex * flex[n]!;
394-
_widths[n] = newWidth;
403+
widths[n] = newWidth;
395404
}
396405
}
397406
}
398407

399-
final totalWidth = _widths.reduce((double? a, double? b) => a! + b!)!;
408+
final totalWidth = widths.reduce((double? a, double? b) => a! + b!)!;
400409

401410
// Compute final widths
402411
var totalHeight = 0.0;
@@ -406,17 +415,21 @@ class Table extends Widget with SpanningWidget {
406415
continue;
407416
}
408417

418+
final spannedWidths = _getSpannedWidths(widths, row);
419+
409420
var n = 0;
410421
var x = 0.0;
411422

412423
var lineHeight = 0.0;
424+
413425
for (final child in row.children) {
414-
final childConstraints = BoxConstraints.tightFor(width: _widths[n]);
426+
final childConstraints =
427+
BoxConstraints.tightFor(width: spannedWidths[n]);
415428
child.layout(context, childConstraints);
416429
assert(child.box != null);
417430
child.box =
418431
PdfRect(x, totalHeight, child.box!.width, child.box!.height);
419-
x += _widths[n]!;
432+
x += spannedWidths[n]!;
420433
lineHeight = math.max(lineHeight, child.box!.height);
421434
n++;
422435
}
@@ -428,13 +441,13 @@ class Table extends Widget with SpanningWidget {
428441
n = 0;
429442
x = 0;
430443
for (final child in row.children) {
431-
final childConstraints =
432-
BoxConstraints.tightFor(width: _widths[n], height: lineHeight);
444+
final childConstraints = BoxConstraints.tightFor(
445+
width: spannedWidths[n], height: lineHeight);
433446
child.layout(context, childConstraints);
434447
assert(child.box != null);
435448
child.box =
436449
PdfRect(x, totalHeight, child.box!.width, child.box!.height);
437-
x += _widths[n]!;
450+
x += spannedWidths[n]!;
438451
n++;
439452
}
440453
}
@@ -527,14 +540,36 @@ class Table extends Widget with SpanningWidget {
527540
);
528541
}
529542

530-
for (final child in row.children) {
543+
for (final cell in row.children) {
544+
final cellBox = cell.box!;
531545
context.canvas
532546
..saveContext()
533-
..drawRect(
534-
child.box!.x, child.box!.y, child.box!.width, child.box!.height)
547+
..drawRect(cellBox.x, cellBox.y, cellBox.width, cellBox.height)
535548
..clipPath();
536-
child.paint(context);
549+
cell.paint(context);
537550
context.canvas.restoreContext();
551+
if (border?.verticalInside.style.paint == true &&
552+
cell != row.children.first) {
553+
border!.verticalInside.style.setStyle(context);
554+
555+
context.canvas.moveTo(cellBox.x, cellBox.bottom);
556+
context.canvas.lineTo(cellBox.x, cellBox.top);
557+
context.canvas.setStrokeColor(border!.verticalInside.color);
558+
context.canvas.setLineWidth(border!.verticalInside.width);
559+
context.canvas.strokePath();
560+
561+
border!.verticalInside.style.unsetStyle(context);
562+
}
563+
if (border?.horizontalInside.style.paint == true &&
564+
row != children.first) {
565+
border!.horizontalInside.style.setStyle(context);
566+
context.canvas.moveTo(cellBox.left, cellBox.top);
567+
context.canvas.lineTo(cellBox.right, cellBox.top);
568+
context.canvas.setStrokeColor(border!.horizontalInside.color);
569+
context.canvas.setLineWidth(border!.horizontalInside.width);
570+
context.canvas.strokePath();
571+
border!.horizontalInside.style.unsetStyle(context);
572+
}
538573
}
539574
if (index >= _context.lastLine) {
540575
break;
@@ -568,8 +603,9 @@ class Table extends Widget with SpanningWidget {
568603

569604
context.canvas.restoreContext();
570605

606+
// Paint outside borders
571607
if (border != null) {
572-
border!.paintTable(context, box!, _widths, _heights);
608+
border!.paint(context, box!);
573609
}
574610
}
575611

pdf/test/widget_table_test.dart

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ import 'utils.dart';
2525

2626
late Document pdf;
2727

28-
List<TableRow> buildTable(
29-
{required Context? context, int count = 10, bool repeatHeader = false}) {
28+
List<TableRow> buildTable({
29+
required Context? context,
30+
int count = 10,
31+
bool repeatHeader = false,
32+
}) {
3033
final rows = <TableRow>[];
3134
{
3235
final tableRow = <Widget>[];
@@ -148,6 +151,48 @@ void main() {
148151
));
149152
});
150153

154+
test('Table Widget Column Span', () {
155+
pdf.addPage(Page(
156+
build: (Context context) => Table(
157+
children: [
158+
TableRow(
159+
columnSpans: const {0: 2, 2: 1},
160+
children: [
161+
Container(color: PdfColors.red, height: 20),
162+
Container(color: PdfColors.green, height: 20),
163+
],
164+
),
165+
TableRow(
166+
columnSpans: const {1: 2},
167+
children: [
168+
Container(color: PdfColors.green, height: 20),
169+
Container(color: PdfColors.blue, height: 20),
170+
],
171+
),
172+
TableRow(
173+
columnSpans: const {0: 3},
174+
children: [
175+
Container(color: PdfColors.red, height: 20),
176+
],
177+
),
178+
TableRow(
179+
children: [
180+
Container(color: PdfColors.red, height: 20),
181+
Container(color: PdfColors.blue, height: 20),
182+
Container(color: PdfColors.green, height: 20),
183+
],
184+
),
185+
],
186+
border: TableBorder.all(),
187+
columnWidths: <int, TableColumnWidth>{
188+
0: const FixedColumnWidth(80),
189+
1: const FlexColumnWidth(2),
190+
2: const FractionColumnWidth(.2),
191+
},
192+
),
193+
));
194+
});
195+
151196
test('Table Widget TableCellVerticalAlignment', () {
152197
pdf.addPage(
153198
MultiPage(

0 commit comments

Comments
 (0)