, | , | , , and are also
/// supported.
@@ -14,7 +17,14 @@ import 'package:flutter_layout_grid/flutter_layout_grid.dart';
///
///
class TableHtmlExtension extends HtmlExtension {
- const TableHtmlExtension();
+ /// It use shrinkWrap option to CssBoxWidgets. But in build method it fill available width.
+ /// Use only if something wrong with the view.
+ /// Uses shrinkWrap option in many widgets can be very expensive, because it means that you have to measure everything.
+ final bool shrinkAndFill;
+ bool hasFixedHeaders = false;
+ final tableHelper = TableHelper();
+
+ TableHtmlExtension({this.shrinkAndFill = false});
@override
Set get supportedTags => {
@@ -32,6 +42,8 @@ class TableHtmlExtension extends HtmlExtension {
@override
StyledElement prepare(ExtensionContext context, List children) {
if (context.elementName == "table") {
+ hasFixedHeaders = context.attributes.containsKey("data-fixed-header");
+
final cellDescendants = _getCellDescendants(children);
return TableElement(
@@ -105,6 +117,7 @@ class TableHtmlExtension extends HtmlExtension {
if (context.elementName == "table") {
return WidgetSpan(
child: CssBoxWidget(
+ shrinkWrap: shrinkAndFill,
style: context.styledElement!.style,
child: LayoutBuilder(
builder: (ctx, constraints) {
@@ -114,12 +127,25 @@ class TableHtmlExtension extends HtmlExtension {
} else {
width = MediaQuery.sizeOf(ctx).width - 32;
}
- return _layoutCells(
- context.styledElement as TableElement,
- context.builtChildrenMap!,
- context,
- width,
- );
+ if (hasFixedHeaders) {
+ return FixedHeadersTableWidget(
+ table: context.styledElement as TableElement,
+ parsedCells: context.builtChildrenMap!,
+ context: context,
+ width: width,
+ shrinkWrap: shrinkAndFill,
+ tableHelper: tableHelper,
+ );
+ } else {
+ return _layoutCells(
+ context.styledElement as TableElement,
+ context.builtChildrenMap!,
+ context,
+ width,
+ shrinkAndFill,
+ tableHelper,
+ );
+ }
},
),
),
@@ -151,15 +177,16 @@ List _getCellDescendants(List children) {
return descendants;
}
-Widget _layoutCells(TableElement table, Map parsedCells, ExtensionContext context, double width) {
- final minWidths = _getColWidths(table.tableStructure);
+Widget _layoutCells(
+ TableElement table, Map parsedCells, ExtensionContext context, double width, bool shrinkAndFill, TableHelper tableHelper) {
+ final minWidths = tableHelper.getColWidths(table.tableStructure);
double requiredWidth = 0;
for (final minWidth in minWidths) {
requiredWidth += minWidth;
}
List cellWidths;
- if (requiredWidth < width) {
+ if (shrinkAndFill || requiredWidth < width) {
final extra = (width - requiredWidth) / minWidths.length;
cellWidths = List.generate(minWidths.length, (index) => minWidths[index] + extra);
} else {
@@ -219,12 +246,13 @@ Widget _layoutCells(TableElement table, Map parsedCel
rowStart: rowi,
rowSpan: min(child.rowspan, rows.length - rowi),
child: CssBoxWidget(
+ shrinkWrap: shrinkAndFill,
style: child.style.merge(row.style),
child: Builder(builder: (context) {
final alignment = child.style.direction ?? Directionality.of(context);
return SizedBox.expand(
child: Container(
- alignment: _getCellAlignment(child, alignment),
+ alignment: tableHelper.getCellAlignment(child, alignment),
child: CssBoxWidget.withInlineSpanChildren(
children: [parsedCells[child] ?? const TextSpan(text: "error")],
style: Style(),
@@ -266,129 +294,6 @@ Widget _layoutCells(TableElement table, Map parsedCel
));
}
-List _getColWidths(List children) {
- final widths = [];
- for (final child in children) {
- List partialWidths = [];
- if (child is TableRowLayoutElement) {
- partialWidths = _getColWidthsFromRow(child);
- } else {
- partialWidths = _getColWidths(child.children);
- }
- if (partialWidths.isEmpty) continue;
- for (int i = 0; i < partialWidths.length; ++i) {
- double partial = partialWidths[i];
- if (widths.length <= i) {
- widths.add(partial);
- } else if (widths[i] < partial) {
- widths[i] = partial;
- }
- }
- }
- return widths;
-}
-
-List _getColWidthsFromRow(TableRowLayoutElement row) {
- List widths = [];
- for (final cell in row.children) {
- if (cell is TableCellElement) {
- WidthInfo info = WidthInfo();
- for (final child in cell.children) {
- _getCellInfo(child, info);
- }
- double minWidth = info.requiredWidth + 32;
- widths.add(minWidth);
- }
- }
- return widths;
-}
-
-void _getCellInfo(StyledElement element, WidthInfo info) {
- if (element is TextContentElement) {
- final regex = RegExp(r'\w+|\s+|[^\w\s]');
- final wordRegex = RegExp(r'\w+');
- final text = element.text;
- if (text == null || text.isEmpty) return;
- final words = regex.allMatches(text).map((m) => m.group(0)!).toList();
- for (final word in words) {
- double wordWidth = TextPainter.computeWidth(
- text: TextSpan(
- text: word,
- style: TextStyle(
- fontSize: element.style.fontSize?.value ?? 16,
- fontFamily: element.style.fontFamily,
- fontWeight: element.style.fontWeight,
- fontStyle: element.style.fontStyle,
- )),
- textDirection: TextDirection.ltr,
- );
- if (info.join && wordRegex.hasMatch(word)) {
- info.width += wordWidth;
- } else {
- info.width = wordWidth;
- }
- if (info.width > info.requiredWidth) {
- info.requiredWidth = info.width;
- }
- info.join = wordRegex.hasMatch(word);
- }
- } else {
- for (final child in element.children) {
- _getCellInfo(child, info);
- }
- }
-}
-
-class WidthInfo {
- double width = 0;
- double requiredWidth = 0;
- bool join = false;
-}
-
-Alignment _getCellAlignment(TableCellElement cell, TextDirection alignment) {
- Alignment verticalAlignment;
-
- switch (cell.style.verticalAlign) {
- case VerticalAlign.baseline:
- case VerticalAlign.sub:
- case VerticalAlign.sup:
- case VerticalAlign.top:
- verticalAlignment = Alignment.topCenter;
- break;
- case VerticalAlign.middle:
- verticalAlignment = Alignment.center;
- break;
- case VerticalAlign.bottom:
- verticalAlignment = Alignment.bottomCenter;
- break;
- }
-
- switch (cell.style.textAlign) {
- case TextAlign.left:
- return verticalAlignment + Alignment.centerLeft;
- case TextAlign.right:
- return verticalAlignment + Alignment.centerRight;
- case TextAlign.center:
- return verticalAlignment + Alignment.center;
- case null:
- case TextAlign.start:
- case TextAlign.justify:
- switch (alignment) {
- case TextDirection.rtl:
- return verticalAlignment + Alignment.centerRight;
- case TextDirection.ltr:
- return verticalAlignment + Alignment.centerLeft;
- }
- case TextAlign.end:
- switch (alignment) {
- case TextDirection.rtl:
- return verticalAlignment + Alignment.centerLeft;
- case TextDirection.ltr:
- return verticalAlignment + Alignment.centerRight;
- }
- }
-}
-
class TableCellElement extends StyledElement {
int colspan = 1;
int rowspan = 1;
diff --git a/lib/src/builtins/styled_element_builtin.dart b/lib/src/builtins/styled_element_builtin.dart
index f035eb2140..73aca0c8bb 100644
--- a/lib/src/builtins/styled_element_builtin.dart
+++ b/lib/src/builtins/styled_element_builtin.dart
@@ -390,18 +390,9 @@ class StyledElementBuiltIn extends HtmlExtension {
case "strong":
continue bold;
case "sub":
- if (_hasOnlyDigits(children)) {
- // We can use FontFeature.subscripts since this just looks nice
- styledElement.style = Style(
- fontFeatureSettings: [const FontFeature.subscripts()],
- );
- } else {
- // For the rest we should just reduce the height of the text
- // Using WidgetSpan with translation should not be used because line breaks can occur just before subscript text
- styledElement.style = Style(
- fontSize: FontSize(50, Unit.percent),
- );
- }
+ styledElement.style = Style(
+ fontSize: FontSize(50, Unit.percent),
+ );
break;
case "summary":
styledElement.style = Style(
@@ -429,20 +420,6 @@ class StyledElementBuiltIn extends HtmlExtension {
return styledElement;
}
- bool _hasOnlyDigits(List children) {
- if (children.isEmpty) return true;
- for (var child in children) {
- if (child is TextContentElement) {
- final text = child.text;
- if (text == null || text.isEmpty) continue;
- for (int codeUnit in text.codeUnits) {
- if ((codeUnit ^ 0x30) > 9) return false;
- }
- }
- }
- return true;
- }
-
@override
InlineSpan build(ExtensionContext context) {
if (context.styledElement!.style.display == Display.listItem ||
diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart
index 337e70c0a4..3b1869c291 100644
--- a/lib/src/css_box_widget.dart
+++ b/lib/src/css_box_widget.dart
@@ -76,16 +76,23 @@ class CssBoxWidget extends StatelessWidget {
Container(
decoration: BoxDecoration(
border: style.border,
- color: style.backgroundColor, //Colors the padding and content boxes
+ borderRadius: style.borderRadius,
+ ),
+ child: Container(
+ clipBehavior: Clip.hardEdge,
+ decoration: BoxDecoration(
+ borderRadius: style.borderRadius,
+ color: style.backgroundColor, //Colors the padding and content boxes
+ ),
+ width: _shouldExpandToFillBlock() ? double.infinity : null,
+ padding: padding,
+ child: top
+ ? child
+ : MediaQuery(
+ data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+ child: child,
+ ),
),
- width: _shouldExpandToFillBlock() ? double.infinity : null,
- padding: padding,
- child: top
- ? child
- : MediaQuery(
- data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
- child: child,
- ),
),
if (markerBox != null) ClickableRichText(text: markerBox),
],
diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart
index c0fcf02f8b..00e43d1a4c 100644
--- a/lib/src/css_parser.dart
+++ b/lib/src/css_parser.dart
@@ -61,6 +61,29 @@ Style declarationsToStyle(Map> declarations) {
style.border = ExpressionMapping.expressionToBorder(
borderWidths, borderStyles, borderColors);
break;
+ case 'border-radius':
+ List? borderRadiuses =
+ value.whereType().toList();
+
+ /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
+ borderRadiuses.removeWhere((element) =>
+ element == null ||
+ (element is! css.LengthTerm &&
+ element is! css.PercentageTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm));
+
+ css.LiteralTerm? borderRadius =
+ borderRadiuses.firstWhereOrNull((element) => element != null);
+
+ BorderRadius newBorderRadius = BorderRadius.all(Radius.circular(
+ ExpressionMapping.expressionToBorderRadius(
+ borderRadius)),
+
+ );
+ style.borderRadius = newBorderRadius;
+ break;
case 'border-left':
List? borderWidths =
value.whereType().toList();
@@ -875,6 +898,23 @@ class ExpressionMapping {
return BorderStyle.none;
}
+ static double expressionToBorderRadius(css.Expression? value) {
+ if (value is css.NumberTerm) {
+ return double.tryParse(value.text) ?? 1.0;
+ } else if (value is css.PercentageTerm) {
+ return (double.tryParse(value.text) ?? 400) / 100;
+ } else if (value is css.EmTerm) {
+ return double.tryParse(value.text) ?? 1.0;
+ } else if (value is css.RemTerm) {
+ return double.tryParse(value.text) ?? 1.0;
+ } else if (value is css.LengthTerm) {
+ return double.tryParse(
+ value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), '')) ??
+ 1.0;
+ }
+ return 4.0;
+ }
+
static Color? expressionToColor(css.Expression? value) {
if (value != null) {
if (value is css.HexColorTerm) {
diff --git a/lib/src/style.dart b/lib/src/style.dart
index 747b1d9016..d19696fd22 100644
--- a/lib/src/style.dart
+++ b/lib/src/style.dart
@@ -214,6 +214,7 @@ class Style {
String? after;
Border? border;
Alignment? alignment;
+ BorderRadius? borderRadius;
/// MaxLine
///
@@ -267,6 +268,7 @@ class Style {
this.after,
this.border,
this.alignment,
+ this.borderRadius,
this.maxLines,
this.textOverflow,
this.textTransform = TextTransform.none,
@@ -299,7 +301,6 @@ class Style {
TextStyle generateTextStyle() {
return TextStyle(
- backgroundColor: backgroundColor,
color: color,
decoration: textDecoration,
decorationColor: textDecorationColor,
@@ -359,6 +360,7 @@ class Style {
before: other.before,
after: other.after,
border: border?.merge(other.border) ?? other.border,
+ borderRadius: other.borderRadius,
alignment: other.alignment,
maxLines: other.maxLines,
textOverflow: other.textOverflow,
@@ -378,9 +380,6 @@ class Style {
: lineHeight;
return child.copyWith(
- backgroundColor: child.backgroundColor != Colors.transparent
- ? child.backgroundColor
- : backgroundColor,
color: child.color ?? color,
direction: child.direction ?? direction,
display: display == Display.none ? display : child.display,
@@ -452,6 +451,7 @@ class Style {
TextOverflow? textOverflow,
TextTransform? textTransform,
bool? beforeAfterNull,
+ BorderRadius? borderRadius,
}) {
return Style(
backgroundColor: backgroundColor ?? this.backgroundColor,
@@ -489,6 +489,7 @@ class Style {
before: beforeAfterNull == true ? null : before ?? this.before,
after: beforeAfterNull == true ? null : after ?? this.after,
border: border ?? this.border,
+ borderRadius: borderRadius ?? this.borderRadius,
alignment: alignment ?? this.alignment,
maxLines: maxLines ?? this.maxLines,
textOverflow: textOverflow ?? this.textOverflow,
diff --git a/lib/src/tables/fixed_headers/adapters/layout_info_to_sliver_adapter.dart b/lib/src/tables/fixed_headers/adapters/layout_info_to_sliver_adapter.dart
new file mode 100644
index 0000000000..16d835fb34
--- /dev/null
+++ b/lib/src/tables/fixed_headers/adapters/layout_info_to_sliver_adapter.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_html/src/tables/fixed_headers/model/builder_info.dart';
+import 'package:flutter_layout_grid/flutter_layout_grid.dart';
+
+import '../scroll_group_synchronizer.dart';
+
+class LayoutInfoToSliverAdapter {
+ final List rowSizes;
+ final ScrollGroupSynchronizer _verticalScrollGroupSynchronizer;
+ final ScrollGroupSynchronizer _horizontalScrollGroupSynchronizer;
+
+ LayoutInfoToSliverAdapter(this.rowSizes, this._verticalScrollGroupSynchronizer, this._horizontalScrollGroupSynchronizer);
+
+ SliverToBoxAdapter transform(LayoutBuilderInfo layoutBuilderInfo) {
+ final List headerChildren = [];
+ final List bodyChildren = [];
+
+ for (var e in layoutBuilderInfo.headersLayoutBuilderInfo) {
+ if (e?.gridPlacements != null) {
+ headerChildren.addAll(e!.gridPlacements);
+ }
+ }
+
+ final headerLayoutGrid = LayoutGrid(
+ columnSizes: layoutBuilderInfo.headerColumnSizes,
+ rowSizes: rowSizes,
+ children: headerChildren,
+ );
+ LayoutGrid? bodyLayoutGrid;
+ if (layoutBuilderInfo.bodyColumnSizes.isNotEmpty) {
+ for (var e in layoutBuilderInfo.bodiesLayoutBuilderInfo) {
+ if (e?.gridPlacements != null) {
+ for (var gridPlacementList in e!.gridPlacements) {
+ bodyChildren.addAll(gridPlacementList);
+ }
+ }
+ }
+ bodyLayoutGrid = LayoutGrid(
+ columnSizes: layoutBuilderInfo.bodyColumnSizes,
+ rowSizes: rowSizes,
+ children: bodyChildren,
+ );
+ }
+
+ final bodyVerticalController = ScrollController();
+ final bodyHorizontalController = ScrollController();
+ _verticalScrollGroupSynchronizer.addController(bodyVerticalController);
+ _horizontalScrollGroupSynchronizer.addController(bodyHorizontalController);
+
+ if (layoutBuilderInfo.bodyColumnSizes.isNotEmpty) {
+ final stickyColumnVerticalController = ScrollController();
+ final stickyColumnHorizontalController = ScrollController();
+ _verticalScrollGroupSynchronizer.addController(stickyColumnVerticalController);
+ _horizontalScrollGroupSynchronizer.addController(stickyColumnHorizontalController);
+
+ final sliver = SliverToBoxAdapter(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SingleChildScrollView(
+ controller: stickyColumnVerticalController,
+ scrollDirection: Axis.vertical,
+ physics: const ClampingScrollPhysics(),
+ child: headerLayoutGrid,
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ controller: bodyVerticalController,
+ scrollDirection: Axis.vertical,
+ physics: const ClampingScrollPhysics(),
+ child: SingleChildScrollView(
+ controller: bodyHorizontalController, scrollDirection: Axis.horizontal, physics: const ClampingScrollPhysics(), child: bodyLayoutGrid),
+ ),
+ ),
+ ],
+ ),
+ );
+ return sliver;
+ } else {
+ /// For full spannable headers
+ final sliver = SliverToBoxAdapter(
+ child: SingleChildScrollView(
+ controller: bodyHorizontalController,
+ scrollDirection: Axis.horizontal,
+ physics: const ClampingScrollPhysics(),
+ child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
+ SingleChildScrollView(
+ controller: bodyVerticalController, scrollDirection: Axis.vertical, physics: const ClampingScrollPhysics(), child: headerLayoutGrid),
+ ]),
+ ),
+ );
+ return sliver;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/src/tables/fixed_headers/fixed_headers_table_widget.dart b/lib/src/tables/fixed_headers/fixed_headers_table_widget.dart
new file mode 100644
index 0000000000..d2438b9bab
--- /dev/null
+++ b/lib/src/tables/fixed_headers/fixed_headers_table_widget.dart
@@ -0,0 +1,564 @@
+import 'dart:math';
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_html/src/tables/fixed_headers/model/builder_info.dart';
+import 'package:flutter_html/src/tables/fixed_headers/scroll_group_synchronizer.dart';
+import 'package:flutter_layout_grid/flutter_layout_grid.dart';
+
+import '../../../../flutter_html.dart';
+import '../table_helper.dart';
+import 'adapters/layout_info_to_sliver_adapter.dart';
+
+class FixedHeadersTableWidget extends StatefulWidget {
+ final TableElement table;
+ final Map parsedCells;
+ final ExtensionContext context;
+ final double width;
+ final bool shrinkWrap;
+ final TableHelper _tableHelper;
+
+ const FixedHeadersTableWidget(
+ {super.key,
+ required this.table,
+ required this.parsedCells,
+ required this.context,
+ required this.width,
+ required this.shrinkWrap,
+ required TableHelper tableHelper})
+ : _tableHelper = tableHelper;
+
+ @override
+ State createState() => _FixedHeadersTableWidgetState();
+}
+
+class _FixedHeadersTableWidgetState extends State {
+ late double width;
+
+ late ScrollGroupSynchronizer _verticalScrollGroupSynchronizer;
+ late ScrollGroupSynchronizer _horizontalScrollGroupSynchronizer;
+ final _horizontalRowScrollController = ScrollController();
+
+ final GlobalKey _headerContentKey = GlobalKey();
+ late List intrinsicHeightRowsKeys;
+ List _calculatedFullRowIntrinsicHeightList = [];
+ double? _calculatedHeaderHeight;
+ bool _isMeasured = false;
+
+ @override
+ void initState() {
+ width = widget.width;
+ _horizontalScrollGroupSynchronizer = ScrollGroupSynchronizer([_horizontalRowScrollController]);
+ _verticalScrollGroupSynchronizer = ScrollGroupSynchronizer();
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _measureWidgets();
+ });
+ super.initState();
+ }
+
+ void _measureWidgets() {
+ if (!mounted) return;
+ final headerContext = _headerContentKey.currentContext;
+ double? newHeaderHeight;
+ bool isContextNull = false;
+ if (headerContext != null) {
+ newHeaderHeight = headerContext.size?.height;
+ } else {
+ isContextNull = true;
+ }
+ List newCalculatedIntrinsicHeights = _calculatedFullRowIntrinsicHeightList;
+ bool isCalculatedHeightTableChanged = false;
+ for (var i = 0; i < intrinsicHeightRowsKeys.length; ++i) {
+ final hasIndex = i < _calculatedFullRowIntrinsicHeightList.length - 1;
+ final height = hasIndex ? _calculatedFullRowIntrinsicHeightList[i] : null;
+ final context = intrinsicHeightRowsKeys[i].currentContext;
+ if (context != null) {
+ final newHeight = context.size?.height;
+ if (newHeight != null && newHeight != height) {
+ isCalculatedHeightTableChanged = true;
+ if (hasIndex) {
+ newCalculatedIntrinsicHeights[i] = newHeight;
+ } else {
+ newCalculatedIntrinsicHeights.add(newHeight);
+ }
+ }
+ } else {
+ isContextNull = true;
+ }
+ }
+
+ if (isContextNull) {
+ if (mounted) {
+ setState(() {
+ _isMeasured = false;
+ });
+ }
+ return;
+ }
+
+ if (isCalculatedHeightTableChanged || (newHeaderHeight != null && newHeaderHeight != _calculatedHeaderHeight)) {
+ if (mounted) {
+ setState(() {
+ if ((newHeaderHeight != null && newHeaderHeight != _calculatedHeaderHeight)) {
+ _calculatedHeaderHeight = newHeaderHeight;
+ }
+ if (isCalculatedHeightTableChanged) {
+ _calculatedFullRowIntrinsicHeightList = newCalculatedIntrinsicHeights;
+ }
+ _isMeasured = true;
+ });
+ }
+ }
+ }
+
+ @override
+ void didUpdateWidget(covariant FixedHeadersTableWidget oldWidget) {
+ setState(() {
+ if (oldWidget.width != widget.width) {
+ width = widget.width;
+ }
+ _isMeasured = false;
+ _calculatedFullRowIntrinsicHeightList = [];
+ _calculatedHeaderHeight = null;
+ });
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted && !_isMeasured) {
+ _measureWidgets();
+ }
+ });
+ super.didUpdateWidget(oldWidget);
+ }
+
+ @override
+ void dispose() {
+ _horizontalScrollGroupSynchronizer.dispose();
+ _verticalScrollGroupSynchronizer.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final minWidths = widget._tableHelper.getColWidths(widget.table.tableStructure);
+ double requiredWidth = 0;
+ for (final minWidth in minWidths) {
+ requiredWidth += minWidth;
+ }
+
+ List cellWidths;
+ if (!widget.shrinkWrap && requiredWidth < width) {
+ final extra = (width - requiredWidth) / minWidths.length;
+ cellWidths = List.generate(minWidths.length, (index) => minWidths[index] + extra);
+ } else {
+ cellWidths = minWidths;
+ width = requiredWidth + 32;
+ }
+
+ final rows = [];
+ for (var child in widget.table.tableStructure) {
+ if (child is TableSectionLayoutElement) {
+ rows.addAll(child.children.whereType());
+ } else if (child is TableRowLayoutElement) {
+ rows.add(child);
+ }
+ }
+
+ // All table rows have a height intrinsic to their (spanned) contents
+ final rowSizes = List.generate(
+ rows.length,
+ (_) => const IntrinsicContentTrackSize(),
+ );
+
+ // Calculate column bounds
+ int columnMax = 0;
+ List rowSpanOffsets = [];
+ for (final row in rows) {
+ final cols = row.children.whereType().fold(0, (int value, child) => value + child.colspan) +
+ rowSpanOffsets.fold(0, (int offset, child) => child);
+ columnMax = max(cols, columnMax);
+ rowSpanOffsets = [
+ ...rowSpanOffsets.map((value) => value - 1).where((value) => value > 0),
+ ...row.children.whereType().map((cell) => cell.rowspan - 1),
+ ];
+ // Ignore width set in CSS, there is only one proper layout...
+ row.children.whereType().forEach((cell) => cell.style.width = null);
+ }
+
+ // Place the cells in the rows/columns
+ final bodyCells = [];
+ final columnRowOffset = List.generate(columnMax, (_) => 0);
+ final columnColspanOffset = List.generate(columnMax, (_) => 0);
+ int rowi = 0;
+ late LayoutGrid cornerLayoutGrid;
+ late List headersRowGridPlacement = [];
+
+ late List headersColumnGridPlacement = [];
+
+ /// Collect build info to rebuild widget with different height
+ List headersColumnBuildInfoElements = [];
+ List bodyCellsBuildInfoElements = [];
+ late GridPlacementBuildInfo cornerBuildInfoElement;
+ List headerRowBuildInfoElements = [];
+
+ for (var row in rows) {
+ int columni = 0;
+ int lastHeaderColspan = 1;
+ for (var child in row.children) {
+ if (columni > columnMax - 1) {
+ break;
+ }
+ if (child is TableCellElement) {
+ while (columnRowOffset[columni] > 0) {
+ columnRowOffset[columni] = columnRowOffset[columni] - 1;
+ columni += columnColspanOffset[columni].clamp(1, columnMax - columni - 1);
+ }
+ if (columni == 0 && rowi == 0) {
+ final cornerGridPlacement = buildGridPlacement(columni, child, columnMax, rowi, rows, row);
+ cornerBuildInfoElement = GridPlacementBuildInfo(columni, child, columnMax, rowi, rows, row);
+ cornerLayoutGrid = LayoutGrid(
+ columnSizes: [FixedTrackSize(cellWidths[columni])],
+ rowSizes: [rowSizes[rowi]],
+ children: [cornerGridPlacement],
+ );
+ } else if (columni == 0 && rowi > 0) {
+ final gridBuildInfo = GridPlacementBuildInfo(columni, child, columnMax, rowi - 1, rows, row);
+ final gridPlacement = buildGridPlacement(columni, child, columnMax, rowi - 1, rows, row);
+ headersColumnBuildInfoElements.add(gridBuildInfo);
+ headersColumnGridPlacement.add(gridPlacement);
+ lastHeaderColspan = child.colspan;
+ } else if (columni > 0 && rowi == 0) {
+ //before headerRow we use cornerGrid so we have to use (columni - 1)
+ final gridBuildInfo = GridPlacementBuildInfo(columni - 1, child, columnMax, rowi, rows, row);
+ headerRowBuildInfoElements.add(gridBuildInfo);
+ headersRowGridPlacement.add(buildGridPlacement(columni - 1, child, columnMax, rowi, rows, row));
+ } else {
+ //body is separate view so we use (columni - 1)
+ bodyCellsBuildInfoElements.add(GridPlacementBuildInfo(columni - lastHeaderColspan, child, columnMax, rowi - 1, rows, row));
+ bodyCells.add(buildGridPlacement(columni - lastHeaderColspan, child, columnMax, rowi - 1, rows, row));
+ }
+ columnRowOffset[columni] = child.rowspan - 1;
+ columnColspanOffset[columni] = child.colspan;
+ columni += child.colspan;
+ }
+ }
+ while (columni < columnRowOffset.length) {
+ columnRowOffset[columni] = columnRowOffset[columni] - 1;
+ columni++;
+ }
+ rowi++;
+ }
+
+ // Create column tracks (insofar there were no colgroups that already defined them)
+ List finalColumnSizes = List.generate(cellWidths.length, (index) => FixedTrackSize(cellWidths[index]));
+
+ if (finalColumnSizes.isEmpty || rowSizes.isEmpty) {
+ // No actual cells to show
+ return const SizedBox();
+ }
+
+ // If not measured yet, render whole view to calculate height
+ if (!_isMeasured) {
+ // Prepare data for render offstage (invisible view) to calculate its height.
+ List intrinsicHeightRowsList = _prepareIntrinsicHeightRowsList(finalColumnSizes, rowSizes, bodyCells, headersColumnGridPlacement);
+
+ // Header row is the first row of table. It will be sticky.
+ final headerRowLayoutGrid =
+ LayoutGrid(gridFit: GridFit.passthrough, columnSizes: finalColumnSizes.sublist(1), rowSizes: rowSizes, children: headersRowGridPlacement);
+
+ final headerRowContent = IntrinsicHeight(
+ key: _headerContentKey,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(width: (finalColumnSizes[0] as FixedTrackSize).sizeInPx, child: cornerLayoutGrid),
+ Expanded(
+ child: SingleChildScrollView(
+ controller: _horizontalRowScrollController,
+ scrollDirection: Axis.horizontal,
+ physics: const ClampingScrollPhysics(),
+ child: headerRowLayoutGrid,
+ ),
+ ),
+ ],
+ ),
+ );
+
+ return Stack(
+ children: [
+ Offstage(
+ child: SingleChildScrollView(
+ child: Column(
+ children: [headerRowContent, ...intrinsicHeightRowsList],
+ ),
+ )),
+ ],
+ );
+ }
+
+ final calculatedHeaderRowContent = _recreateHeaderRowContent(cornerBuildInfoElement, headerRowBuildInfoElements, finalColumnSizes, cellWidths, rowSizes);
+
+ // Initialize values before long for loop
+ List headerLayoutGrids = [];
+ List bodyLayoutGrids = [];
+ List layoutBuilderInfoList = [];
+
+ for (int i = 0; i < _calculatedFullRowIntrinsicHeightList.length; ++i) {
+ /// Get info about header column sizes for later comparison is header column sized was changed
+ final headerColumnI = headersColumnBuildInfoElements.where((e) => e.rowi == i).firstOrNull;
+ int? headerColumnSpan = getColumnSpan(headerColumnI?.child, columnMax, headerColumnI?.columni);
+ List headerColumnSizes = headerColumnSpan == null ? [] : finalColumnSizes.sublist(0, headerColumnSpan);
+
+ final bodyCellsI = bodyCellsBuildInfoElements.where((e) => e.rowi == i).toList(growable: false);
+ List bodyColumnSizes = [];
+ final headerNotNullSpan = headerColumnSpan ?? 0;
+ if (bodyCellsI.isNotEmpty) {
+ final headerColumnSize = bodyCellsI.fold(0, (previousValue, element) {
+ final columnSpan = getColumnSpan(element.child, columnMax, element.columni) ?? 0;
+ return previousValue + columnSpan;
+ });
+ bodyColumnSizes = finalColumnSizes.sublist(headerNotNullSpan, headerColumnSize + headerNotNullSpan);
+ }
+
+ /// Check is currentLayout is the same as previous
+ bool isDifferentLayout = true;
+ if ((layoutBuilderInfoList.isEmpty) ||
+ !hasSameColumnSize(layoutBuilderInfoList.last.headerColumnSizes, headerColumnSizes, layoutBuilderInfoList.last.bodyColumnSizes, bodyColumnSizes)) {
+ isDifferentLayout = true;
+ } else {
+ isDifferentLayout = false;
+ }
+
+ /// Build info about header and add it to appropriate category
+ HeaderLayoutBuilderInfo? matchingHeaderLayoutBuilderInfo;
+ if (headerColumnI != null) {
+ //if it is differentLayout we start from index 0
+ //else we get lastHeaderIndex
+ final rowi = isDifferentLayout ? 0 : (layoutBuilderInfoList.last.lastHeaderIdx ?? 0) + 1;
+ final headerGridPlacement = buildGridPlacement(
+ headerColumnI.columni, headerColumnI.child, headerColumnI.columnMax, rowi, headerColumnI.rows, headerColumnI.row,
+ height: _calculatedFullRowIntrinsicHeightList[i]);
+ headerColumnSizes = finalColumnSizes.sublist(0, headerColumnSpan);
+ matchingHeaderLayoutBuilderInfo = headerLayoutGrids.where((e) => e.lastIdx == (i - 1) && e.columnSizes == headerColumnSizes).firstOrNull;
+ if (matchingHeaderLayoutBuilderInfo != null) {
+ matchingHeaderLayoutBuilderInfo.add(headerGridPlacement, lastIdx: i);
+ } else {
+ matchingHeaderLayoutBuilderInfo = HeaderLayoutBuilderInfo(headerColumnSizes)..add(headerGridPlacement, lastIdx: i);
+ headerLayoutGrids.add(matchingHeaderLayoutBuilderInfo);
+ }
+ }
+
+ BodyLayoutBuilderInfo? matchingBodyLayoutBuilderInfo;
+ final List cellsFixedSizeRowElements = [];
+
+ /// Build info about cells and add it to appropriate category
+ for (var e in bodyCellsI) {
+ //if it is differentLayout we start from index 0
+ //else we get last body index
+ final rowi = isDifferentLayout ? 0 : (layoutBuilderInfoList.last.lastBodyIdx ?? 0) + 1;
+ final cellGridPlacement = buildGridPlacement(e.columni, e.child, e.columnMax, rowi, e.rows, e.row, height: _calculatedFullRowIntrinsicHeightList[i]);
+ cellsFixedSizeRowElements.add(cellGridPlacement);
+ }
+
+ matchingBodyLayoutBuilderInfo = bodyLayoutGrids.where((e) => e.lastIdx == (i - 1) && e.columnSizes == bodyColumnSizes).firstOrNull;
+ if (matchingBodyLayoutBuilderInfo != null) {
+ matchingBodyLayoutBuilderInfo.add(cellsFixedSizeRowElements, lastIdx: i);
+ } else {
+ matchingBodyLayoutBuilderInfo = BodyLayoutBuilderInfo(bodyColumnSizes)..add(cellsFixedSizeRowElements, lastIdx: i);
+ bodyLayoutGrids.add(matchingBodyLayoutBuilderInfo);
+ }
+
+ /// Add to layoutBuilderInfoList
+ if (isDifferentLayout) {
+ final layoutBuilderInfo = LayoutBuilderInfo(headerColumnSizes, bodyColumnSizes);
+ layoutBuilderInfo.add(matchingHeaderLayoutBuilderInfo, matchingBodyLayoutBuilderInfo);
+ layoutBuilderInfoList.add(layoutBuilderInfo);
+ } else {
+ layoutBuilderInfoList.last.add(matchingHeaderLayoutBuilderInfo, matchingBodyLayoutBuilderInfo);
+ }
+ }
+
+ final layoutToSliverAdapter = LayoutInfoToSliverAdapter(rowSizes, _verticalScrollGroupSynchronizer, _horizontalScrollGroupSynchronizer);
+ final sliverToBoxAdapterList = layoutBuilderInfoList.map((e) => layoutToSliverAdapter.transform(e)).toList(growable: false);
+
+ return SizedBox(
+ height: MediaQuery.of(context).size.height - 150,
+ child: CustomScrollView(
+ slivers: [
+ SliverPersistentHeader(
+ pinned: true,
+ delegate: _MySliverPersistentRowHeaderDelegate(
+ minHeight: _calculatedHeaderHeight!, maxHeight: _calculatedHeaderHeight!, child: calculatedHeaderRowContent),
+ ),
+ ...sliverToBoxAdapterList
+ ],
+ ),
+ );
+ }
+
+ /// Prepare list of IntrinsicHeight widgets.
+ /// This widget list can be used to compute the height of all complete rows.
+ /// The height of each row is determined by the tallest column within that row.
+ List _prepareIntrinsicHeightRowsList(
+ List finalColumnSizes,
+ List rowSizes,
+ List bodyCells,
+ List headersColumnGridPlacement,
+ ) {
+ final cellsLayoutGrid = LayoutGrid(columnSizes: finalColumnSizes.sublist(1), rowSizes: rowSizes, children: bodyCells);
+ final headerColumnLayoutGrid = LayoutGrid(columnSizes: finalColumnSizes.sublist(0, 1), rowSizes: rowSizes, children: headersColumnGridPlacement);
+
+ List intrinsicHeightRowsList = [];
+ intrinsicHeightRowsKeys = List.generate(headerColumnLayoutGrid.children.length, (_) => GlobalKey());
+
+ final cellsLayoutGridChildren = (cellsLayoutGrid.children as List);
+ int j = 0;
+ for (int i = 0; i < headerColumnLayoutGrid.children.length; ++i) {
+ final cellsRowElements = [];
+ final headerElement = headerColumnLayoutGrid.children[i] as GridPlacement;
+ for (; j < cellsLayoutGridChildren.length && cellsLayoutGridChildren[j].rowStart == headerElement.rowStart; ++j) {
+ final currentCellsGridPlacement = cellsLayoutGridChildren[j];
+ cellsRowElements.add(currentCellsGridPlacement);
+ }
+ final cellRowLayoutGrid =
+ LayoutGrid(gridFit: GridFit.passthrough, columnSizes: finalColumnSizes.sublist(1), rowSizes: rowSizes, children: cellsRowElements);
+
+ final headerElementLayoutGrid = LayoutGrid(
+ columnSizes: finalColumnSizes.sublist(0, headerElement.columnSpan),
+ rowSizes: rowSizes,
+ children: [headerElement],
+ );
+ final currentHeaderColumnSizes = finalColumnSizes.whereIndexed((idx, e) => idx < headerElement.columnSpan);
+ final headerColumnSize = currentHeaderColumnSizes.fold(0.0, (previousValue, element) {
+ return previousValue + (element as FixedTrackSize).sizeInPx;
+ });
+ final fullRow = IntrinsicHeight(
+ key: intrinsicHeightRowsKeys[i],
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(width: headerColumnSize, child: headerElementLayoutGrid),
+ Expanded(
+ child: cellRowLayoutGrid,
+ ),
+ ],
+ ),
+ );
+ intrinsicHeightRowsList.add(fullRow);
+ }
+ return intrinsicHeightRowsList;
+ }
+
+ int? getColumnSpan(TableCellElement? child, int columnMax, int? columni) {
+ if (child == null || columni == null) {
+ return null;
+ } else {
+ return min(child.colspan, columnMax - columni);
+ }
+ }
+
+ GridPlacement buildGridPlacement(int columni, TableCellElement child, int columnMax, int rowi, List rows, TableRowLayoutElement row,
+ {double? height}) {
+ return GridPlacement(
+ columnStart: columni,
+ columnSpan: min(child.colspan, columnMax - columni),
+ rowStart: rowi,
+ rowSpan: min(child.rowspan, rows.length - rowi),
+ child: CssBoxWidget(
+ shrinkWrap: widget.shrinkWrap,
+ style: child.style.merge(row.style),
+ child: Builder(builder: (context) {
+ final alignment = child.style.direction ?? Directionality.of(context);
+ if (height == null) {
+ return SizedBox.expand(
+ child: Container(
+ alignment: widget._tableHelper.getCellAlignment(child, alignment),
+ child: CssBoxWidget.withInlineSpanChildren(
+ children: [widget.parsedCells[child] ?? const TextSpan(text: "error")],
+ style: Style(),
+ ),
+ ),
+ );
+ } else {
+ return Container(
+ height: height,
+ alignment: widget._tableHelper.getCellAlignment(child, alignment),
+ child: CssBoxWidget.withInlineSpanChildren(
+ children: [widget.parsedCells[child] ?? const TextSpan(text: "error")],
+ style: Style(),
+ ),
+ );
+ }
+ }),
+ ),
+ );
+ }
+
+ bool hasSameColumnSize(
+ List headerColumnSizes, List headerColumnSizes2, List bodyColumnSizes, List bodyColumnSizes2) {
+ const eq = ListEquality();
+ return eq.equals(headerColumnSizes, headerColumnSizes2) && eq.equals(bodyColumnSizes, bodyColumnSizes2);
+ }
+
+ IntrinsicHeight _recreateHeaderRowContent(GridPlacementBuildInfo cornerBuildInfoElement, List headerRowBuildInfoElements,
+ List columnSizes, List cellWidths, List rowSizes) {
+ final cornerLayoutGrid = LayoutGrid(
+ columnSizes: [FixedTrackSize(cellWidths[cornerBuildInfoElement.columni])],
+ rowSizes: [FixedTrackSize(_calculatedHeaderHeight ?? 0.0)],
+ children: [
+ buildGridPlacement(cornerBuildInfoElement.columni, cornerBuildInfoElement.child, cornerBuildInfoElement.columnMax, cornerBuildInfoElement.rowi,
+ cornerBuildInfoElement.rows, cornerBuildInfoElement.row,height: _calculatedHeaderHeight)
+ ],
+ );
+ final headerRowLayoutGrid = LayoutGrid(
+ gridFit: GridFit.passthrough,
+ columnSizes: columnSizes.sublist(1),
+ rowSizes: rowSizes.map((e) => FixedTrackSize(_calculatedHeaderHeight ?? 0.0)).toList(growable: false),
+ children: headerRowBuildInfoElements
+ .map((e) => buildGridPlacement(e.columni, e.child, e.columnMax, e.rowi, e.rows, e.row, height: _calculatedHeaderHeight))
+ .toList(growable: false));
+
+ return IntrinsicHeight(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(width: (columnSizes[0] as FixedTrackSize).sizeInPx, child: cornerLayoutGrid),
+ Expanded(
+ child: SingleChildScrollView(
+ controller: _horizontalRowScrollController,
+ scrollDirection: Axis.horizontal,
+ physics: const ClampingScrollPhysics(),
+ child: headerRowLayoutGrid,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _MySliverPersistentRowHeaderDelegate extends SliverPersistentHeaderDelegate {
+ _MySliverPersistentRowHeaderDelegate({
+ required this.minHeight,
+ required this.maxHeight,
+ required this.child,
+ });
+
+ final double minHeight;
+ final double maxHeight;
+ final Widget child;
+
+ @override
+ double get minExtent => minHeight;
+
+ @override
+ double get maxExtent => maxHeight;
+
+ @override
+ Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+ return SizedBox.expand(child: child);
+ }
+
+ @override
+ bool shouldRebuild(_MySliverPersistentRowHeaderDelegate oldDelegate) {
+ return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child;
+ }
+}
diff --git a/lib/src/tables/fixed_headers/model/builder_info.dart b/lib/src/tables/fixed_headers/model/builder_info.dart
new file mode 100644
index 0000000000..f24a62e14f
--- /dev/null
+++ b/lib/src/tables/fixed_headers/model/builder_info.dart
@@ -0,0 +1,62 @@
+import 'dart:math';
+
+import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_layout_grid/flutter_layout_grid.dart';
+
+/// Properties that be needed to build GridPlacement
+class GridPlacementBuildInfo {
+ final int columni;
+ final TableCellElement child;
+ final int columnMax;
+ final int rowi;
+ final List rows;
+ final TableRowLayoutElement row;
+
+ GridPlacementBuildInfo(this.columni, this.child, this.columnMax, this.rowi, this.rows, this.row);
+}
+
+class HeaderLayoutBuilderInfo {
+ int? lastIdx;
+ final List columnSizes;
+ final List gridPlacements = [];
+
+ HeaderLayoutBuilderInfo(this.columnSizes);
+
+ void add(GridPlacement gridPlacement, {required int lastIdx}) {
+ gridPlacements.add(gridPlacement);
+ this.lastIdx = lastIdx;
+ }
+}
+
+class BodyLayoutBuilderInfo {
+ int? lastIdx;
+ final List columnSizes;
+ final List> gridPlacements = [];
+
+ BodyLayoutBuilderInfo(this.columnSizes);
+
+ void add(List gridPlacement, {required int lastIdx}) {
+ gridPlacements.add(gridPlacement);
+ this.lastIdx = lastIdx;
+ }
+}
+
+class LayoutBuilderInfo {
+ int? lastIdx;
+ int? lastBodyIdx;
+ int? lastHeaderIdx;
+ final List headerColumnSizes;
+ final List bodyColumnSizes;
+ final List headersLayoutBuilderInfo = [];
+ final List bodiesLayoutBuilderInfo = [];
+
+ LayoutBuilderInfo(this.headerColumnSizes, this.bodyColumnSizes);
+
+ void add(HeaderLayoutBuilderInfo? headerLayoutBuilderInfo, BodyLayoutBuilderInfo? bodyLayoutBuilderInfo) {
+ headersLayoutBuilderInfo.add(headerLayoutBuilderInfo);
+ bodiesLayoutBuilderInfo.add(bodyLayoutBuilderInfo);
+ lastHeaderIdx = headerLayoutBuilderInfo?.gridPlacements.lastOrNull?.rowStart;
+ lastBodyIdx = bodyLayoutBuilderInfo?.gridPlacements.lastOrNull?.lastOrNull?.rowStart;
+ lastIdx = max(lastHeaderIdx ?? 0, lastBodyIdx ?? 0);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/tables/fixed_headers/scroll_group_synchronizer.dart b/lib/src/tables/fixed_headers/scroll_group_synchronizer.dart
new file mode 100644
index 0000000000..4ec35a141c
--- /dev/null
+++ b/lib/src/tables/fixed_headers/scroll_group_synchronizer.dart
@@ -0,0 +1,83 @@
+import 'dart:collection';
+
+import 'package:flutter/material.dart';
+
+class ScrollGroupSynchronizer {
+ final List _controllers = [];
+ final Map _listeners = {};
+ bool _isSyncing = false; // Flag to avoid unnecassary updates
+
+ ScrollGroupSynchronizer([List? controllersToSync]) {
+ if (controllersToSync != null) {
+ for (var controller in controllersToSync) {
+ addController(controller);
+ }
+ }
+ }
+
+
+ UnmodifiableListView get controllers => UnmodifiableListView(_controllers);
+
+ void addController(ScrollController controller) {
+ if (!_controllers.contains(controller)) {
+ _controllers.add(controller);
+ listener() => _handleScroll(controller);
+ _listeners[controller] = listener;
+ controller.addListener(listener);
+ }
+ }
+
+ /// Usuwa [ScrollController] z grupy synchronizacji.
+ /// Usuwa również powiązany listener.
+ void removeController(ScrollController controller) {
+ if (_controllers.contains(controller)) {
+ final listener = _listeners.remove(controller);
+ if (listener != null) {
+ controller.removeListener(listener);
+ }
+ _controllers.remove(controller);
+ }
+ }
+
+ void _handleScroll(ScrollController sourceController) {
+ if (_isSyncing || !sourceController.hasClients) return;
+
+ _isSyncing = true;
+
+ final sourceOffset = sourceController.offset;
+
+ for (var targetController in _controllers) {
+ if (targetController == sourceController) continue;
+
+ if (targetController.hasClients && targetController.offset != sourceOffset) {
+ final targetListener = _listeners[targetController];
+ if (targetListener != null) {
+ // Temporary delete listener to avoid loop, during jumpTo
+ targetController.removeListener(targetListener);
+ }
+
+ targetController.jumpTo(sourceOffset);
+
+ if (targetListener != null) {
+ // Add listener after set position
+ targetController.addListener(targetListener);
+ }
+ }
+ }
+
+ _isSyncing = false;
+ }
+
+
+ void dispose() {
+ for (var controller in _controllers) {
+ final listener = _listeners[controller];
+ if (listener != null) {
+ controller.removeListener(listener);
+ }
+ controller.dispose();
+ }
+ _listeners.clear();
+ _controllers.clear();
+ }
+}
\ No newline at end of file
diff --git a/lib/src/tables/table_helper.dart b/lib/src/tables/table_helper.dart
new file mode 100644
index 0000000000..5b7fb7ecdc
--- /dev/null
+++ b/lib/src/tables/table_helper.dart
@@ -0,0 +1,130 @@
+import 'package:flutter/material.dart';
+
+import '../../flutter_html.dart';
+
+class TableHelper {
+ Alignment getCellAlignment(TableCellElement cell, TextDirection alignment) {
+ Alignment verticalAlignment;
+
+ switch (cell.style.verticalAlign) {
+ case VerticalAlign.baseline:
+ case VerticalAlign.sub:
+ case VerticalAlign.sup:
+ case VerticalAlign.top:
+ verticalAlignment = Alignment.topCenter;
+ break;
+ case VerticalAlign.middle:
+ verticalAlignment = Alignment.center;
+ break;
+ case VerticalAlign.bottom:
+ verticalAlignment = Alignment.bottomCenter;
+ break;
+ }
+
+ switch (cell.style.textAlign) {
+ case TextAlign.left:
+ return verticalAlignment + Alignment.centerLeft;
+ case TextAlign.right:
+ return verticalAlignment + Alignment.centerRight;
+ case TextAlign.center:
+ return verticalAlignment + Alignment.center;
+ case null:
+ case TextAlign.start:
+ case TextAlign.justify:
+ switch (alignment) {
+ case TextDirection.rtl:
+ return verticalAlignment + Alignment.centerRight;
+ case TextDirection.ltr:
+ return verticalAlignment + Alignment.centerLeft;
+ }
+ case TextAlign.end:
+ switch (alignment) {
+ case TextDirection.rtl:
+ return verticalAlignment + Alignment.centerLeft;
+ case TextDirection.ltr:
+ return verticalAlignment + Alignment.centerRight;
+ }
+ }
+ }
+
+
+ List getColWidths(List children) {
+ final widths = [];
+ for (final child in children) {
+ List partialWidths = [];
+ if (child is TableRowLayoutElement) {
+ partialWidths = _getColWidthsFromRow(child);
+ } else {
+ partialWidths = getColWidths(child.children);
+ }
+ if (partialWidths.isEmpty) continue;
+ for (int i = 0; i < partialWidths.length; ++i) {
+ double partial = partialWidths[i];
+ if (widths.length <= i) {
+ widths.add(partial);
+ } else if (widths[i] < partial) {
+ widths[i] = partial;
+ }
+ }
+ }
+ return widths;
+ }
+
+
+ List _getColWidthsFromRow(TableRowLayoutElement row) {
+ List widths = [];
+ for (final cell in row.children) {
+ if (cell is TableCellElement) {
+ WidthInfo info = WidthInfo();
+ for (final child in cell.children) {
+ _getCellInfo(child, info);
+ }
+ double minWidth = info.requiredWidth + 32;
+ widths.add(minWidth);
+ }
+ }
+ return widths;
+ }
+
+ void _getCellInfo(StyledElement element, WidthInfo info) {
+ if (element is TextContentElement) {
+ final regex = RegExp(r'\w+|\s+|[^\w\s]');
+ final wordRegex = RegExp(r'\w+');
+ final text = element.text;
+ if (text == null || text.isEmpty) return;
+ final words = regex.allMatches(text).map((m) => m.group(0)!).toList();
+ for (final word in words) {
+ double wordWidth = TextPainter.computeWidth(
+ text: TextSpan(
+ text: word,
+ style: TextStyle(
+ fontSize: element.style.fontSize?.value ?? 16,
+ fontFamily: element.style.fontFamily,
+ fontWeight: element.style.fontWeight,
+ fontStyle: element.style.fontStyle,
+ )),
+ textDirection: TextDirection.ltr,
+ );
+ if (info.join && wordRegex.hasMatch(word)) {
+ info.width += wordWidth;
+ } else {
+ info.width = wordWidth;
+ }
+ if (info.width > info.requiredWidth) {
+ info.requiredWidth = info.width;
+ }
+ info.join = wordRegex.hasMatch(word);
+ }
+ } else {
+ for (final child in element.children) {
+ _getCellInfo(child, info);
+ }
+ }
+ }
+}
+
+class WidthInfo {
+ double width = 0;
+ double requiredWidth = 0;
+ bool join = false;
+}
|