From d7091990d193e892e2f782ac8d91fc0326aff4bc Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Tue, 23 May 2023 08:44:00 -0600 Subject: [PATCH] feat: Update CssBoxWidget to handle rtl marker boxes (#1270) --- example/lib/main.dart | 4 +- lib/src/builtins/styled_element_builtin.dart | 13 +- lib/src/css_box_widget.dart | 64 +- lib/src/css_parser.dart | 269 +++++-- lib/src/processing/margins.dart | 30 +- lib/src/style.dart | 128 ++-- lib/src/style/length.dart | 10 + lib/src/style/margin.dart | 198 ++++- lib/src/style/padding.dart | 214 ++++++ test/style/css_parsing/margin_test.dart | 712 ++++++++++++++++++ test/style/css_parsing/padding_test.dart | 713 +++++++++++++++++++ 11 files changed, 2177 insertions(+), 178 deletions(-) create mode 100644 lib/src/style/padding.dart create mode 100644 test/style/css_parsing/margin_test.dart create mode 100644 test/style/css_parsing/padding_test.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index db394d8ed0..cb9f545ad8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -296,11 +296,11 @@ class MyHomePageState extends State { backgroundColor: const Color.fromARGB(0x50, 0xee, 0xee, 0xee), ), "th": Style( - padding: const EdgeInsets.all(6), + padding: HtmlPaddings.all(6), backgroundColor: Colors.grey, ), "td": Style( - padding: const EdgeInsets.all(6), + padding: HtmlPaddings.all(6), border: const Border(bottom: BorderSide(color: Colors.grey)), ), 'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis), diff --git a/lib/src/builtins/styled_element_builtin.dart b/lib/src/builtins/styled_element_builtin.dart index 349486c595..038962e276 100644 --- a/lib/src/builtins/styled_element_builtin.dart +++ b/lib/src/builtins/styled_element_builtin.dart @@ -176,7 +176,6 @@ class StyledElementBuiltIn extends HtmlExtension { continue italics; case "div": styledElement.style = Style( - margin: Margins.all(0), display: Display.block, ); break; @@ -338,14 +337,22 @@ class StyledElementBuiltIn extends HtmlExtension { styledElement.style = Style( display: Display.block, listStyleType: ListStyleType.decimal, - padding: const EdgeInsets.only(left: 40), + padding: HtmlPaddings.only(inlineStart: 40), + margin: Margins( + blockStart: Margin(1, Unit.em), + blockEnd: Margin(1, Unit.em), + ), ); break; case "ul": styledElement.style = Style( display: Display.block, listStyleType: ListStyleType.disc, - padding: const EdgeInsets.only(left: 40), + padding: HtmlPaddings.only(inlineStart: 40), + margin: Margins( + blockStart: Margin(1, Unit.em), + blockEnd: Margin(1, Unit.em), + ), ); break; case "p": diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 5859975f96..85679ea4b7 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -56,16 +56,19 @@ class CssBoxWidget extends StatelessWidget { ? _generateMarkerBoxSpan(style) : null; + final direction = _checkTextDirection(context, textDirection); + final padding = style.padding?.resolve(direction); + return _CSSBoxRenderer( width: style.width ?? Width.auto(), height: style.height ?? Height.auto(), - paddingSize: style.padding?.collapsedSize ?? Size.zero, + paddingSize: padding?.collapsedSize ?? Size.zero, borderSize: style.border?.dimensions.collapsedSize ?? Size.zero, margins: style.margin ?? Margins.zero, display: style.display ?? Display.inline, childIsReplaced: childIsReplaced, emValue: _calculateEmValue(style, context), - textDirection: _checkTextDirection(context, textDirection), + textDirection: direction, shrinkWrap: shrinkWrap, children: [ Container( @@ -74,7 +77,7 @@ class CssBoxWidget extends StatelessWidget { color: style.backgroundColor, //Colors the padding and content boxes ), width: _shouldExpandToFillBlock() ? double.infinity : null, - padding: style.padding ?? EdgeInsets.zero, + padding: padding, child: top ? child : MediaQuery( @@ -224,8 +227,8 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { final bool shrinkWrap; @override - _RenderCSSBox createRenderObject(BuildContext context) { - return _RenderCSSBox( + RenderCSSBox createRenderObject(BuildContext context) { + return RenderCSSBox( display: display, width: width..normalize(emValue), height: height..normalize(emValue), @@ -239,7 +242,7 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { } @override - void updateRenderObject(BuildContext context, _RenderCSSBox renderObject) { + void updateRenderObject(BuildContext context, RenderCSSBox renderObject) { renderObject ..display = display ..width = (width..normalize(emValue)) @@ -253,10 +256,21 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { } Margins _preProcessMargins(Margins margins, bool shrinkWrap) { - Margin leftMargin = margins.left ?? Margin.zero(); - Margin rightMargin = margins.right ?? Margin.zero(); - Margin topMargin = margins.top ?? Margin.zero(); - Margin bottomMargin = margins.bottom ?? Margin.zero(); + late Margin leftMargin; + late Margin rightMargin; + Margin topMargin = margins.top ?? margins.blockStart ?? Margin.zero(); + Margin bottomMargin = margins.bottom ?? margins.blockEnd ?? Margin.zero(); + + switch (textDirection) { + case TextDirection.rtl: + leftMargin = margins.left ?? margins.inlineEnd ?? Margin.zero(); + rightMargin = margins.right ?? margins.inlineStart ?? Margin.zero(); + break; + case TextDirection.ltr: + leftMargin = margins.left ?? margins.inlineStart ?? Margin.zero(); + rightMargin = margins.right ?? margins.inlineEnd ?? Margin.zero(); + break; + } //Preprocess margins to a pixel value leftMargin.normalize(emValue); @@ -295,12 +309,14 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { } } +@visibleForTesting + /// Implements the CSS layout algorithm -class _RenderCSSBox extends RenderBox +class RenderCSSBox extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { - _RenderCSSBox({ + RenderCSSBox({ required Display display, required Width width, required Height height, @@ -593,7 +609,20 @@ class _RenderCSSBox extends RenderBox final offsetHeight = distance - (markerBox.getDistanceToBaseline(TextBaseline.alphabetic) ?? markerBox.size.height); - markerBoxParentData.offset = Offset(-markerBox.size.width, offsetHeight); + switch (_textDirection) { + case TextDirection.rtl: + markerBoxParentData.offset = Offset( + child.size.width, + offsetHeight, + ); + break; + case TextDirection.ltr: + markerBoxParentData.offset = Offset( + -markerBox.size.width, + offsetHeight, + ); + break; + } } } @@ -701,10 +730,11 @@ class _RenderCSSBox extends RenderBox } return Margins( - left: marginLeft, - right: marginRight, - top: margins.top, - bottom: margins.bottom); + left: marginLeft, + right: marginRight, + top: margins.top, + bottom: margins.bottom, + ); } @override diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 4859b3907b..c0fcf02f8b 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -402,7 +402,7 @@ Style declarationsToStyle(Map> declarations) { element is! css.NumberTerm && !(element.text == 'auto')); Margins margin = ExpressionMapping.expressionToMargins(marginLengths); - style.margin = (style.margin ?? Margins.all(0)).copyWith( + style.margin = (style.margin ?? const Margins()).copyWith( left: margin.left, right: margin.right, top: margin.top, @@ -410,21 +410,77 @@ Style declarationsToStyle(Map> declarations) { ); break; case 'margin-left': - style.margin = (style.margin ?? Margins.zero).copyWith( + style.margin = (style.margin ?? const Margins()).copyWith( left: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-right': - style.margin = (style.margin ?? Margins.zero).copyWith( + style.margin = (style.margin ?? const Margins()).copyWith( right: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-top': - style.margin = (style.margin ?? Margins.zero) + style.margin = (style.margin ?? const Margins()) .copyWith(top: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-bottom': - style.margin = (style.margin ?? Margins.zero).copyWith( + style.margin = (style.margin ?? const Margins()).copyWith( bottom: ExpressionMapping.expressionToMargin(value.first)); break; + case 'margin-inline': + List? marginLengths = + value.whereType().toList(); + + /// List might include other values than the ones we want for margin length, so make sure to remove those before passing it to [ExpressionMapping] + marginLengths.removeWhere((element) => + element is! css.LengthTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm && + !(element.text == 'auto')); + Margins margin = + ExpressionMapping.expressionToInlineMargins(marginLengths); + style.margin = (style.margin ?? const Margins()).copyWith( + inlineStart: margin.inlineStart, + inlineEnd: margin.inlineEnd, + ); + break; + case 'margin-inline-end': + style.margin = (style.margin ?? const Margins()).copyWith( + inlineEnd: ExpressionMapping.expressionToMargin(value.first), + ); + break; + case 'margin-inline-start': + style.margin = (style.margin ?? const Margins()).copyWith( + inlineStart: ExpressionMapping.expressionToMargin(value.first), + ); + break; + case 'margin-block': + List? marginLengths = + value.whereType().toList(); + + /// List might include other values than the ones we want for margin length, so make sure to remove those before passing it to [ExpressionMapping] + marginLengths.removeWhere((element) => + element is! css.LengthTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm && + !(element.text == 'auto')); + Margins margin = + ExpressionMapping.expressionToBlockMargins(marginLengths); + style.margin = (style.margin ?? const Margins()).copyWith( + blockStart: margin.blockStart, + blockEnd: margin.blockEnd, + ); + break; + case 'margin-block-end': + style.margin = (style.margin ?? const Margins()).copyWith( + blockEnd: ExpressionMapping.expressionToMargin(value.first), + ); + break; + case 'margin-block-start': + style.margin = (style.margin ?? const Margins()).copyWith( + blockStart: ExpressionMapping.expressionToMargin(value.first), + ); + break; case 'padding': List? paddingLengths = value.whereType().toList(); @@ -435,30 +491,84 @@ Style declarationsToStyle(Map> declarations) { element is! css.EmTerm && element is! css.RemTerm && element is! css.NumberTerm); - List padding = - ExpressionMapping.expressionToPadding(paddingLengths); - style.padding = (style.padding ?? EdgeInsets.zero).copyWith( - left: padding[0], - right: padding[1], - top: padding[2], - bottom: padding[3], + final padding = + ExpressionMapping.expressionToHtmlPaddings(paddingLengths); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + left: padding.left, + right: padding.right, + top: padding.top, + bottom: padding.bottom, ); break; case 'padding-left': - style.padding = (style.padding ?? EdgeInsets.zero).copyWith( - left: ExpressionMapping.expressionToPaddingLength(value.first)); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + left: ExpressionMapping.expressionToHtmlPadding(value.first)); break; case 'padding-right': - style.padding = (style.padding ?? EdgeInsets.zero).copyWith( - right: ExpressionMapping.expressionToPaddingLength(value.first)); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + right: ExpressionMapping.expressionToHtmlPadding(value.first)); break; case 'padding-top': - style.padding = (style.padding ?? EdgeInsets.zero).copyWith( - top: ExpressionMapping.expressionToPaddingLength(value.first)); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + top: ExpressionMapping.expressionToHtmlPadding(value.first)); break; case 'padding-bottom': - style.padding = (style.padding ?? EdgeInsets.zero).copyWith( - bottom: ExpressionMapping.expressionToPaddingLength(value.first)); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + bottom: ExpressionMapping.expressionToHtmlPadding(value.first)); + break; + case 'padding-inline': + List? paddingLengths = + value.whereType().toList(); + + /// List might include other values than the ones we want for padding length, so make sure to remove those before passing it to [ExpressionMapping] + paddingLengths.removeWhere((element) => + element is! css.LengthTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm); + HtmlPaddings padding = + ExpressionMapping.expressionToInlineHtmlPadding(paddingLengths); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + inlineStart: padding.inlineStart, + inlineEnd: padding.inlineEnd, + ); + break; + case 'padding-inline-end': + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + inlineEnd: ExpressionMapping.expressionToHtmlPadding(value.first), + ); + break; + case 'padding-inline-start': + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + inlineStart: ExpressionMapping.expressionToHtmlPadding(value.first), + ); + break; + case 'padding-block': + List? paddingLengths = + value.whereType().toList(); + + /// List might include other values than the ones we want for padding length, so make sure to remove those before passing it to [ExpressionMapping] + paddingLengths.removeWhere((element) => + element is! css.LengthTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm); + HtmlPaddings padding = + ExpressionMapping.expressionToBlockHtmlPadding(paddingLengths); + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + blockStart: padding.blockStart, + blockEnd: padding.blockEnd, + ); + break; + case 'padding-block-end': + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + blockEnd: ExpressionMapping.expressionToHtmlPadding(value.first), + ); + break; + case 'padding-block-start': + style.padding = (style.padding ?? const HtmlPaddings()).copyWith( + blockStart: ExpressionMapping.expressionToHtmlPadding(value.first), + ); break; case 'text-align': style.textAlign = @@ -984,6 +1094,30 @@ class ExpressionMapping { } } + static Margins expressionToInlineMargins(List? lengths) { + Margin? inlineStart; + Margin? inlineEnd; + + if (lengths != null && lengths.isNotEmpty) { + inlineStart = expressionToMargin(lengths.first); + inlineEnd = expressionToMargin(lengths.last); + } + + return Margins(inlineStart: inlineStart, inlineEnd: inlineEnd); + } + + static Margins expressionToBlockMargins(List? lengths) { + Margin? blockStart; + Margin? blockEnd; + + if (lengths != null && lengths.isNotEmpty) { + blockStart = expressionToMargin(lengths.first); + blockEnd = expressionToMargin(lengths.last); + } + + return Margins(blockStart: blockStart, blockEnd: blockEnd); + } + static Margins expressionToMargins(List? lengths) { Margin? left; Margin? right; @@ -1015,49 +1149,72 @@ class ExpressionMapping { return Margins(left: left, right: right, top: top, bottom: bottom); } - static List expressionToPadding(List? lengths) { - double? left; - double? right; - double? top; - double? bottom; + static HtmlPadding? expressionToHtmlPadding(css.Expression value) { + final computedValue = expressionToLengthOrPercent(value); + return HtmlPadding(computedValue.value, computedValue.unit); + } + + static HtmlPaddings expressionToInlineHtmlPadding( + List? lengths) { + HtmlPadding? inlineStart; + HtmlPadding? inlineEnd; + + if (lengths != null && lengths.isNotEmpty) { + inlineStart = expressionToHtmlPadding(lengths.first); + inlineEnd = expressionToHtmlPadding(lengths.last); + } + + return HtmlPaddings(inlineStart: inlineStart, inlineEnd: inlineEnd); + } + + static HtmlPaddings expressionToBlockHtmlPadding( + List? lengths) { + HtmlPadding? blockStart; + HtmlPadding? blockEnd; + + if (lengths != null && lengths.isNotEmpty) { + blockStart = expressionToHtmlPadding(lengths.first); + blockEnd = expressionToHtmlPadding(lengths.last); + } + + return HtmlPaddings(blockStart: blockStart, blockEnd: blockEnd); + } + + static HtmlPaddings expressionToHtmlPaddings(List? lengths) { + HtmlPadding? left; + HtmlPadding? right; + HtmlPadding? top; + HtmlPadding? bottom; + if (lengths != null && lengths.isNotEmpty) { - top = expressionToPaddingLength(lengths.first); + top = expressionToHtmlPadding(lengths.first); if (lengths.length == 4) { - right = expressionToPaddingLength(lengths[1]); - bottom = expressionToPaddingLength(lengths[2]); - left = expressionToPaddingLength(lengths.last); + right = expressionToHtmlPadding(lengths[1]); + bottom = expressionToHtmlPadding(lengths[2]); + left = expressionToHtmlPadding(lengths.last); } if (lengths.length == 3) { - left = expressionToPaddingLength(lengths[1]); - right = expressionToPaddingLength(lengths[1]); - bottom = expressionToPaddingLength(lengths.last); + left = expressionToHtmlPadding(lengths[1]); + right = expressionToHtmlPadding(lengths[1]); + bottom = expressionToHtmlPadding(lengths.last); } if (lengths.length == 2) { - bottom = expressionToPaddingLength(lengths.first); - left = expressionToPaddingLength(lengths.last); - right = expressionToPaddingLength(lengths.last); + bottom = expressionToHtmlPadding(lengths.first); + left = expressionToHtmlPadding(lengths.last); + right = expressionToHtmlPadding(lengths.last); } if (lengths.length == 1) { - bottom = expressionToPaddingLength(lengths.first); - left = expressionToPaddingLength(lengths.first); - right = expressionToPaddingLength(lengths.first); + bottom = expressionToHtmlPadding(lengths.first); + left = expressionToHtmlPadding(lengths.first); + right = expressionToHtmlPadding(lengths.first); } } - return [left, right, top, bottom]; - } - - static double? expressionToPaddingLength(css.Expression value) { - if (value is css.NumberTerm) { - return double.tryParse(value.text); - } else if (value is css.EmTerm) { - return double.tryParse(value.text); - } else if (value is css.RemTerm) { - return double.tryParse(value.text); - } else if (value is css.LengthTerm) { - return double.tryParse( - value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), '')); - } - return null; + return HtmlPaddings( + left: left, + right: right, + top: top, + bottom: bottom, + ); } static LengthOrPercent expressionToLengthOrPercent(css.Expression value) { @@ -1065,8 +1222,8 @@ class ExpressionMapping { return LengthOrPercent(double.parse(value.text)); } else if (value is css.EmTerm) { return LengthOrPercent(double.parse(value.text), Unit.em); - // } else if (value is css.RemTerm) { - // return LengthOrPercent(double.parse(value.text), Unit.rem); + } else if (value is css.RemTerm) { + return LengthOrPercent(double.parse(value.text), Unit.rem); // TODO there are several other available terms processed by the CSS parser } else if (value is css.LengthTerm) { double number = @@ -1075,7 +1232,7 @@ class ExpressionMapping { return LengthOrPercent(number, unit); } - //Ignore unparsable input + //Ignore un-parsable input return LengthOrPercent(0); } diff --git a/lib/src/processing/margins.dart b/lib/src/processing/margins.dart index 539ec2eaf2..fb1378912c 100644 --- a/lib/src/processing/margins.dart +++ b/lib/src/processing/margins.dart @@ -40,8 +40,12 @@ class MarginProcessing { // Handle case (1) from above. // Top margins cannot collapse if the element has padding if ((tree.style.padding?.top ?? 0) == 0) { - final parentTop = tree.style.margin?.top?.value ?? 0; - final firstChildTop = tree.children.first.style.margin?.top?.value ?? 0; + final parentTop = tree.style.margin?.top?.value ?? + tree.style.margin?.blockStart?.value ?? + 0; + final firstChildTop = tree.children.first.style.margin?.top?.value ?? + tree.children.first.style.margin?.blockStart?.value ?? + 0; final newOuterMarginTop = max(parentTop, firstChildTop); // Set the parent's margin @@ -63,10 +67,14 @@ class MarginProcessing { // Handle case (3) from above. // Bottom margins cannot collapse if the element has padding - if ((tree.style.padding?.bottom ?? 0) == 0) { - final parentBottom = tree.style.margin?.bottom?.value ?? 0; - final lastChildBottom = - tree.children.last.style.margin?.bottom?.value ?? 0; + if ((tree.style.padding?.bottom ?? tree.style.padding?.blockEnd ?? 0) == + 0) { + final parentBottom = tree.style.margin?.bottom?.value ?? + tree.style.margin?.blockEnd?.value ?? + 0; + final lastChildBottom = tree.children.last.style.margin?.bottom?.value ?? + tree.children.last.style.margin?.blockEnd?.value ?? + 0; final newOuterMarginBottom = max(parentBottom, lastChildBottom); // Set the parent's margin @@ -82,7 +90,7 @@ class MarginProcessing { tree.children.last.style.margin = Margins.zero; } else { tree.children.last.style.margin = - tree.children.last.style.margin!.copyWithEdge(bottom: 0); + tree.children.last.style.margin!.copyWith(bottom: Margin.zero()); } } @@ -90,8 +98,12 @@ class MarginProcessing { if (tree.children.length > 1) { for (int i = 1; i < tree.children.length; i++) { final previousSiblingBottom = - tree.children[i - 1].style.margin?.bottom?.value ?? 0; - final thisTop = tree.children[i].style.margin?.top?.value ?? 0; + tree.children[i - 1].style.margin?.bottom?.value ?? + tree.children[i - 1].style.margin?.blockEnd?.value ?? + 0; + final thisTop = tree.children[i].style.margin?.top?.value ?? + tree.children[i].style.margin?.blockStart?.value ?? + 0; final newInternalMargin = max(previousSiblingBottom, thisTop); final newTop = newInternalMargin - previousSiblingBottom; diff --git a/lib/src/style.dart b/lib/src/style.dart index 18d82a97a8..004b32f4e7 100644 --- a/lib/src/style.dart +++ b/lib/src/style.dart @@ -6,6 +6,7 @@ import 'package:flutter_html/src/css_parser.dart'; //Export Style value-unit APIs export 'package:flutter_html/src/style/margin.dart'; +export 'package:flutter_html/src/style/padding.dart'; export 'package:flutter_html/src/style/length.dart'; export 'package:flutter_html/src/style/size.dart'; export 'package:flutter_html/src/style/fontsize.dart'; @@ -117,12 +118,6 @@ class Style { /// Default: ListStylePosition.OUTSIDE ListStylePosition? listStylePosition; - /// CSS attribute "`padding`" - /// - /// Inherited: no, - /// Default: EdgeInsets.zero - EdgeInsets? padding; - /// CSS pseudo-element "`::marker`" /// /// Inherited: no, @@ -132,9 +127,15 @@ class Style { /// CSS attribute "`margin`" /// /// Inherited: no, - /// Default: EdgeInsets.zero + /// Default: Margins.zero Margins? margin; + /// CSS attribute "`padding`" + /// + /// Inherited: no, + /// Default: HtmlPaddings.zero + HtmlPaddings? padding; + /// CSS attribute "`text-align`" /// /// Inherited: yes, @@ -213,7 +214,6 @@ class Style { String? after; Border? border; Alignment? alignment; - Widget? markerContent; /// MaxLine /// @@ -267,7 +267,6 @@ class Style { this.after, this.border, this.alignment, - this.markerContent, this.maxLines, this.textOverflow, this.textTransform = TextTransform.none, @@ -316,8 +315,6 @@ class Style { shadows: textShadow, wordSpacing: wordSpacing, height: lineHeight?.size ?? 1.0, - //TODO background - //TODO textBaseline ); } @@ -346,10 +343,8 @@ class Style { listStyleImage: other.listStyleImage, listStyleType: other.listStyleType, listStylePosition: other.listStylePosition, - padding: other.padding, - //TODO merge EdgeInsets - margin: other.margin, - //TODO merge Margins + padding: padding?.merge(other.padding) ?? other.padding, + margin: margin?.merge(other.margin) ?? other.margin, marker: other.marker, textAlign: other.textAlign, textDecoration: other.textDecoration, @@ -361,13 +356,10 @@ class Style { whiteSpace: other.whiteSpace, width: other.width, wordSpacing: other.wordSpacing, - before: other.before, after: other.after, - border: other.border, - //TODO merge border + border: border?.merge(other.border) ?? other.border, alignment: other.alignment, - markerContent: other.markerContent, maxLines: other.maxLines, textOverflow: other.textOverflow, textTransform: other.textTransform, @@ -436,7 +428,7 @@ class Style { ListStyleImage? listStyleImage, ListStyleType? listStyleType, ListStylePosition? listStylePosition, - EdgeInsets? padding, + HtmlPaddings? padding, Margins? margin, Marker? marker, TextAlign? textAlign, @@ -496,7 +488,6 @@ class Style { after: beforeAfterNull == true ? null : after ?? this.after, border: border ?? this.border, alignment: alignment ?? this.alignment, - markerContent: markerContent ?? this.markerContent, maxLines: maxLines ?? this.maxLines, textOverflow: textOverflow ?? this.textOverflow, textTransform: textTransform ?? this.textTransform, @@ -527,58 +518,75 @@ class Style { /// Sets any dimensions set to rem or em to the computed size void setRelativeValues(double remValue, double emValue) { - if (width?.unit == Unit.rem) { - width = Width(width!.value * remValue); - } else if (width?.unit == Unit.em) { - width = Width(width!.value * emValue); + final calculatedWidth = width?.calculateRelativeValue(remValue, emValue); + if (calculatedWidth != null) { + width = Width(calculatedWidth); } - if (height?.unit == Unit.rem) { - height = Height(height!.value * remValue); - } else if (height?.unit == Unit.em) { - height = Height(height!.value * emValue); + final calculatedHeight = height?.calculateRelativeValue(remValue, emValue); + if (calculatedHeight != null) { + height = Height(calculatedHeight); } - if (fontSize?.unit == Unit.rem) { - fontSize = FontSize(fontSize!.value * remValue); - } else if (fontSize?.unit == Unit.em) { - fontSize = FontSize(fontSize!.value * emValue); + final calculatedFontSize = + fontSize?.calculateRelativeValue(remValue, emValue); + if (calculatedFontSize != null) { + fontSize = FontSize(calculatedFontSize); } - Margin? marginLeft; - Margin? marginTop; - Margin? marginRight; - Margin? marginBottom; + margin = margin?.copyWith( + left: margin?.left?.getRelativeValue(remValue, emValue), + top: margin?.top?.getRelativeValue(remValue, emValue), + right: margin?.right?.getRelativeValue(remValue, emValue), + bottom: margin?.bottom?.getRelativeValue(remValue, emValue), + inlineStart: margin?.inlineStart?.getRelativeValue(remValue, emValue), + inlineEnd: margin?.inlineEnd?.getRelativeValue(remValue, emValue), + blockStart: margin?.blockStart?.getRelativeValue(remValue, emValue), + blockEnd: margin?.blockEnd?.getRelativeValue(remValue, emValue), + ); + + padding = padding?.copyWith( + left: padding?.left?.getRelativeValue(remValue, emValue), + top: padding?.top?.getRelativeValue(remValue, emValue), + right: padding?.right?.getRelativeValue(remValue, emValue), + bottom: padding?.bottom?.getRelativeValue(remValue, emValue), + inlineStart: padding?.inlineStart?.getRelativeValue(remValue, emValue), + inlineEnd: padding?.inlineEnd?.getRelativeValue(remValue, emValue), + blockStart: padding?.blockStart?.getRelativeValue(remValue, emValue), + blockEnd: padding?.blockEnd?.getRelativeValue(remValue, emValue), + ); + } +} - if (margin?.left?.unit == Unit.rem) { - marginLeft = Margin(margin!.left!.value * remValue); - } else if (margin?.left?.unit == Unit.em) { - marginLeft = Margin(margin!.left!.value * emValue); +extension _MarginRelativeValues on Margin { + Margin? getRelativeValue(double remValue, double emValue) { + double? calculatedValue = calculateRelativeValue(remValue, emValue); + if (calculatedValue != null) { + return Margin(calculatedValue); } - if (margin?.top?.unit == Unit.rem) { - marginTop = Margin(margin!.top!.value * remValue); - } else if (margin?.top?.unit == Unit.em) { - marginTop = Margin(margin!.top!.value * emValue); - } + return null; + } +} - if (margin?.right?.unit == Unit.rem) { - marginRight = Margin(margin!.right!.value * remValue); - } else if (margin?.right?.unit == Unit.em) { - marginRight = Margin(margin!.right!.value * emValue); +extension _PaddingRelativeValues on HtmlPadding { + HtmlPadding? getRelativeValue(double remValue, double emValue) { + double? calculatedValue = calculateRelativeValue(remValue, emValue); + if (calculatedValue != null) { + return HtmlPadding(calculatedValue); } - if (margin?.bottom?.unit == Unit.rem) { - marginBottom = Margin(margin!.bottom!.value * remValue); - } else if (margin?.bottom?.unit == Unit.em) { - marginBottom = Margin(margin!.bottom!.value * emValue); - } + return null; + } +} - margin = margin?.copyWith( - left: marginLeft, - top: marginTop, - right: marginRight, - bottom: marginBottom, +extension MergeBorders on Border? { + Border? merge(Border? other) { + return Border( + top: other?.top ?? this?.top ?? BorderSide.none, + right: other?.right ?? this?.right ?? BorderSide.none, + bottom: other?.bottom ?? this?.bottom ?? BorderSide.none, + left: other?.left ?? this?.left ?? BorderSide.none, ); } } diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart index b63e8ebb55..2fdd3d0319 100644 --- a/lib/src/style/length.dart +++ b/lib/src/style/length.dart @@ -41,6 +41,16 @@ abstract class Dimension { Dimension(this.value, this.unit, UnitType dimensionUnitType) : assert(dimensionUnitType.matches(unit.unitType), "This Dimension was given a Unit that isn't specified."); + + double? calculateRelativeValue(double remValue, double emValue) { + if (unit == Unit.rem) { + return value * remValue; + } else if (unit == Unit.em) { + return value * emValue; + } + + return null; + } } /// This dimension takes a value with a length unit such as px or em. Note that diff --git a/lib/src/style/margin.dart b/lib/src/style/margin.dart index 4df4e7b890..d2cbe157d1 100644 --- a/lib/src/style/margin.dart +++ b/lib/src/style/margin.dart @@ -7,67 +7,203 @@ class Margin extends AutoOrLengthOrPercent { Margin.auto() : super(0, Unit.auto); Margin.zero() : super(0, Unit.px); + + @override + String toString() { + if (unit == Unit.auto) { + return "auto"; + } else { + return "$value${unit.name}"; + } + } + + @override + int get hashCode => Object.hash(value, unit); + + @override + bool operator ==(Object other) { + return other is Margin && other.value == value && other.unit == unit; + } } class Margins { final Margin? left; final Margin? right; + final Margin? inlineEnd; + final Margin? inlineStart; final Margin? top; final Margin? bottom; + final Margin? blockEnd; + final Margin? blockStart; - const Margins({this.left, this.right, this.top, this.bottom}); + const Margins({ + this.left, + this.right, + this.inlineEnd, + this.inlineStart, + this.top, + this.bottom, + this.blockEnd, + this.blockStart, + }); /// Auto margins already have a "value" of zero so can be considered collapsed. - Margins collapse() => Margins( + Margins collapse() { + return Margins( left: left?.unit == Unit.auto ? left : Margin(0, Unit.px), right: right?.unit == Unit.auto ? right : Margin(0, Unit.px), + inlineEnd: + inlineEnd?.unit == Unit.auto ? inlineEnd : Margin(0, Unit.px), + inlineStart: + inlineStart?.unit == Unit.auto ? inlineStart : Margin(0, Unit.px), top: top?.unit == Unit.auto ? top : Margin(0, Unit.px), bottom: bottom?.unit == Unit.auto ? bottom : Margin(0, Unit.px), - ); - - Margins copyWith( - {Margin? left, Margin? right, Margin? top, Margin? bottom}) => - Margins( - left: left ?? this.left, - right: right ?? this.right, - top: top ?? this.top, - bottom: bottom ?? this.bottom, - ); - - Margins copyWithEdge( - {double? left, double? right, double? top, double? bottom}) => - Margins( - left: left != null ? Margin(left, this.left?.unit) : this.left, - right: right != null ? Margin(right, this.right?.unit) : this.right, - top: top != null ? Margin(top, this.top?.unit) : this.top, - bottom: - bottom != null ? Margin(bottom, this.bottom?.unit) : this.bottom, - ); - - // bool get isAutoHorizontal => (left is MarginAuto) || (right is MarginAuto); + blockEnd: blockEnd?.unit == Unit.auto ? blockEnd : Margin(0, Unit.px), + blockStart: + blockStart?.unit == Unit.auto ? blockStart : Margin(0, Unit.px)); + } + + Margins copyWith({ + Margin? left, + Margin? right, + Margin? inlineEnd, + Margin? inlineStart, + Margin? top, + Margin? bottom, + Margin? blockEnd, + Margin? blockStart, + }) { + return Margins( + left: left ?? this.left, + right: right ?? this.right, + inlineEnd: inlineEnd ?? this.inlineEnd, + inlineStart: inlineStart ?? this.inlineStart, + top: top ?? this.top, + bottom: bottom ?? this.bottom, + blockEnd: blockEnd ?? this.blockEnd, + blockStart: blockStart ?? this.blockStart, + ); + } + + Margins copyWithEdge({ + double? left, + double? right, + double? inlineEnd, + double? inlineStart, + double? top, + double? bottom, + double? blockEnd, + double? blockStart, + }) { + return Margins( + left: left != null ? Margin(left, this.left?.unit) : this.left, + right: right != null ? Margin(right, this.right?.unit) : this.right, + inlineEnd: inlineEnd != null + ? Margin(inlineEnd, this.inlineEnd?.unit) + : this.inlineEnd, + inlineStart: inlineStart != null + ? Margin(inlineStart, this.inlineStart?.unit) + : this.inlineStart, + top: top != null ? Margin(top, this.top?.unit) : this.top, + bottom: bottom != null ? Margin(bottom, this.bottom?.unit) : this.bottom, + blockEnd: blockEnd != null + ? Margin(blockEnd, this.blockEnd?.unit) + : this.blockEnd, + blockStart: blockStart != null + ? Margin(blockStart, this.blockStart?.unit) + : this.blockStart, + ); + } /// Analogous to [EdgeInsets.zero] static Margins get zero => Margins.all(0); /// Analogous to [EdgeInsets.all] - Margins.all(double value, {Unit? unit}) + Margins.all(double value, [Unit? unit]) : left = Margin(value, unit), right = Margin(value, unit), + inlineEnd = null, + inlineStart = null, top = Margin(value, unit), - bottom = Margin(value, unit); + bottom = Margin(value, unit), + blockEnd = null, + blockStart = null; /// Analogous to [EdgeInsets.only] - Margins.only( - {double? left, double? right, double? top, double? bottom, Unit? unit}) - : left = Margin(left ?? 0, unit), + Margins.only({ + double? left, + double? right, + double? inlineEnd, + double? inlineStart, + double? top, + double? bottom, + double? blockEnd, + double? blockStart, + Unit? unit, + }) : left = Margin(left ?? 0, unit), right = Margin(right ?? 0, unit), + inlineEnd = inlineEnd != null ? Margin(inlineEnd, unit) : null, + inlineStart = inlineStart != null ? Margin(inlineStart, unit) : null, top = Margin(top ?? 0, unit), - bottom = Margin(bottom ?? 0, unit); + bottom = Margin(bottom ?? 0, unit), + blockEnd = blockEnd != null ? Margin(blockEnd, unit) : null, + blockStart = blockStart != null ? Margin(blockStart, unit) : null; /// Analogous to [EdgeInsets.symmetric] Margins.symmetric({double? horizontal, double? vertical, Unit? unit}) : left = Margin(horizontal ?? 0, unit), right = Margin(horizontal ?? 0, unit), + inlineEnd = null, + inlineStart = null, top = Margin(vertical ?? 0, unit), - bottom = Margin(vertical ?? 0, unit); + bottom = Margin(vertical ?? 0, unit), + blockEnd = null, + blockStart = null; + + Margins merge(Margins? other) { + return copyWith( + left: other?.left, + right: other?.right, + top: other?.top, + bottom: other?.bottom, + inlineEnd: other?.inlineEnd, + inlineStart: other?.inlineStart, + blockStart: other?.blockStart, + blockEnd: other?.blockEnd, + ); + } + + @override + String toString() { + return "<$top,$right,$bottom,$left,$inlineStart,$inlineEnd,$blockStart,$blockEnd>"; + } + + @override + int get hashCode { + return Object.hash( + left, right, inlineStart, inlineEnd, top, bottom, blockStart, blockEnd); + } + + @override + bool operator ==(Object other) { + return other is Margins && + (left == other.left || + (left?.value == 0 && + left?.unit != Unit.auto && + other.left == null)) && + (right == other.right || + (right?.value == 0 && + right?.unit != Unit.auto && + other.right == null)) && + (top == other.top || + (top?.value == 0 && top?.unit != Unit.auto && other.top == null)) && + (bottom == other.bottom || + (bottom?.value == 0 && + bottom?.unit != Unit.auto && + other.bottom == null)) && + inlineStart == other.inlineStart && + inlineEnd == other.inlineEnd && + blockStart == other.blockStart && + blockEnd == other.blockEnd; + } } diff --git a/lib/src/style/padding.dart b/lib/src/style/padding.dart new file mode 100644 index 0000000000..09e2997ec6 --- /dev/null +++ b/lib/src/style/padding.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/src/style/length.dart'; + +class HtmlPadding extends LengthOrPercent { + HtmlPadding(double value, [Unit? unit = Unit.px]) + : assert(value >= 0, "Padding must be non-negative"), + super(value, unit ?? Unit.px); + + HtmlPadding.zero() : super(0, Unit.px); + + @override + int get hashCode => Object.hash(value, unit); + + @override + bool operator ==(Object other) { + return other is HtmlPadding && other.value == value && other.unit == unit; + } +} + +class HtmlPaddings { + final HtmlPadding? left; + final HtmlPadding? right; + final HtmlPadding? inlineEnd; + final HtmlPadding? inlineStart; + final HtmlPadding? top; + final HtmlPadding? bottom; + final HtmlPadding? blockEnd; + final HtmlPadding? blockStart; + + const HtmlPaddings({ + this.left, + this.right, + this.inlineEnd, + this.inlineStart, + this.top, + this.bottom, + this.blockEnd, + this.blockStart, + }); + + HtmlPaddings copyWith({ + HtmlPadding? left, + HtmlPadding? right, + HtmlPadding? inlineEnd, + HtmlPadding? inlineStart, + HtmlPadding? top, + HtmlPadding? bottom, + HtmlPadding? blockEnd, + HtmlPadding? blockStart, + }) { + return HtmlPaddings( + left: left ?? this.left, + right: right ?? this.right, + inlineEnd: inlineEnd ?? this.inlineEnd, + inlineStart: inlineStart ?? this.inlineStart, + top: top ?? this.top, + bottom: bottom ?? this.bottom, + blockEnd: blockEnd ?? this.blockEnd, + blockStart: blockStart ?? this.blockStart, + ); + } + + HtmlPaddings copyWithEdge({ + double? left, + double? right, + double? inlineEnd, + double? inlineStart, + double? top, + double? bottom, + double? blockEnd, + double? blockStart, + }) { + return HtmlPaddings( + left: left != null ? HtmlPadding(left, this.left?.unit) : this.left, + right: right != null ? HtmlPadding(right, this.right?.unit) : this.right, + inlineEnd: inlineEnd != null + ? HtmlPadding(inlineEnd, this.inlineEnd?.unit) + : this.inlineEnd, + inlineStart: inlineStart != null + ? HtmlPadding(inlineStart, this.inlineStart?.unit) + : this.inlineStart, + top: top != null ? HtmlPadding(top, this.top?.unit) : this.top, + bottom: + bottom != null ? HtmlPadding(bottom, this.bottom?.unit) : this.bottom, + blockEnd: blockEnd != null + ? HtmlPadding(blockEnd, this.blockEnd?.unit) + : this.blockEnd, + blockStart: blockStart != null + ? HtmlPadding(blockStart, this.blockStart?.unit) + : this.blockStart, + ); + } + + /// Analogous to [EdgeInsets.zero] + static HtmlPaddings get zero => HtmlPaddings.all(0); + + /// Analogous to [EdgeInsets.all] + HtmlPaddings.all(double value, [Unit? unit]) + : left = HtmlPadding(value, unit), + right = HtmlPadding(value, unit), + inlineEnd = null, + inlineStart = null, + top = HtmlPadding(value, unit), + bottom = HtmlPadding(value, unit), + blockEnd = null, + blockStart = null; + + /// Analogous to [EdgeInsets.only] + HtmlPaddings.only({ + double? left, + double? right, + double? inlineEnd, + double? inlineStart, + double? top, + double? bottom, + double? blockEnd, + double? blockStart, + Unit? unit, + }) : left = left != null ? HtmlPadding(left, unit) : null, + right = right != null ? HtmlPadding(right, unit) : null, + inlineEnd = inlineEnd != null ? HtmlPadding(inlineEnd, unit) : null, + inlineStart = + inlineStart != null ? HtmlPadding(inlineStart, unit) : null, + top = top != null ? HtmlPadding(top, unit) : null, + bottom = bottom != null ? HtmlPadding(bottom, unit) : null, + blockEnd = blockEnd != null ? HtmlPadding(blockEnd, unit) : null, + blockStart = blockStart != null ? HtmlPadding(blockStart, unit) : null; + + /// Analogous to [EdgeInsets.symmetric] + HtmlPaddings.symmetric({double? horizontal, double? vertical, Unit? unit}) + : left = horizontal != null ? HtmlPadding(horizontal, unit) : null, + right = horizontal != null ? HtmlPadding(horizontal, unit) : null, + inlineEnd = null, + inlineStart = null, + top = vertical != null ? HtmlPadding(vertical, unit) : null, + bottom = vertical != null ? HtmlPadding(vertical, unit) : null, + blockEnd = null, + blockStart = null; + + HtmlPaddings merge(HtmlPaddings? other) { + return copyWith( + left: other?.left, + right: other?.right, + top: other?.top, + bottom: other?.bottom, + inlineEnd: other?.inlineEnd, + inlineStart: other?.inlineStart, + blockStart: other?.blockStart, + blockEnd: other?.blockEnd, + ); + } + + /// Calculates the padding EdgeInsets given the textDirection. + EdgeInsets resolve(TextDirection direction) { + late double? leftPad; + late double? rightPad; + double? topPad = top?.value ?? blockStart?.value ?? 0; + double? bottomPad = bottom?.value ?? blockEnd?.value ?? 0; + + switch (direction) { + case TextDirection.rtl: + leftPad = left?.value ?? inlineEnd?.value ?? 0; + rightPad = right?.value ?? inlineStart?.value ?? 0; + break; + case TextDirection.ltr: + leftPad = left?.value ?? inlineStart?.value ?? 0; + rightPad = right?.value ?? inlineEnd?.value ?? 0; + break; + } + + return EdgeInsets.fromLTRB(leftPad, topPad, rightPad, bottomPad); + } + + @override + int get hashCode { + return Object.hash( + left, right, inlineStart, inlineEnd, top, bottom, blockStart, blockEnd); + } + + @override + bool operator ==(Object other) { + return other is HtmlPaddings && + left == other.left && + right == other.right && + top == other.top && + bottom == other.bottom && + inlineStart == other.inlineStart && + inlineEnd == other.inlineEnd && + blockStart == other.blockStart && + blockEnd == other.blockEnd; + } +} + +extension PaddingsFromEdgeInsets on EdgeInsets { + HtmlPaddings get htmlPadding { + return HtmlPaddings( + top: HtmlPadding(top), + bottom: HtmlPadding(bottom), + left: HtmlPadding(left), + right: HtmlPadding(right), + ); + } +} + +extension PaddingsFromEdgeInsetsDirectional on EdgeInsetsDirectional { + HtmlPaddings get htmlPadding { + return HtmlPaddings( + top: HtmlPadding(top), + bottom: HtmlPadding(bottom), + inlineStart: HtmlPadding(start), + inlineEnd: HtmlPadding(end), + ); + } +} diff --git a/test/style/css_parsing/margin_test.dart b/test/style/css_parsing/margin_test.dart new file mode 100644 index 0000000000..0890cc8e98 --- /dev/null +++ b/test/style/css_parsing/margin_test.dart @@ -0,0 +1,712 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../test_utils.dart'; + +// Note that in these tests we add ... before and after +// the `div` to prevent its margins from collapsing into its parent's margins. + +void main() { + testWidgets( + 'Test that a normal div has no margin', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: "
Test
", + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.zero)); + }, + ); + + testWidgets( + 'Test that a div with inline styled margin has margin', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: + """...
Test
...""", + ), + ), + ); + + // Top and bottom margins will be merged with parent margins due to margin collapsing + expect(_getMargin("Test"), equals(Margins.all(8, Unit.px))); + }, + ); + + testWidgets( + 'Test that a div with styled margin has margin', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.all(8, Unit.px))); + }, + ); + + testWidgets( + 'Test margin-left in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.only(left: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-top in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.only(top: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-right in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.only(right: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-bottom in + ...
Test
... + """, + ), + ), + ); + expect( + _getMargin("Test"), equals(Margins.only(bottom: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-block-start in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(top: 8, blockStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-block-end in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(bottom: 8, blockEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-inline-start in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(inlineStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-inline-end in + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(inlineEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-left inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.only(left: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-top inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.only(top: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-right inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), equals(Margins.only(right: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-bottom inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect( + _getMargin("Test"), equals(Margins.only(bottom: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-block-start inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(top: 8, blockStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-block-end inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(bottom: 8, blockEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-inline-start inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(inlineStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test margin-inline-end inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + expect(_getMargin("Test"), + equals(Margins.only(inlineEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test that margin actually applies to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect(_getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.all(8))); + }, + ); + + testWidgets( + 'Test that two-argument margin actually applies to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.symmetric( + vertical: 4, + horizontal: 8, + ))); + }, + ); + + testWidgets( + 'Test that three-argument margin applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + top: 4, + right: 6, + left: 6, + bottom: 8, + ))); + }, + ); + + testWidgets( + 'Test that four-argument margin applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + top: 2, + right: 4, + bottom: 6, + left: 8, + ))); + }, + ); + + testWidgets( + 'Test that margin-block-start applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + top: 2, + ))); + }, + ); + + testWidgets( + 'Test that margin-block-end applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + bottom: 2, + ))); + }, + ); + + testWidgets( + 'Test that two-argument margin-block applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + top: 2, + bottom: 4, + ))); + }, + ); + + testWidgets( + 'Test that margin-inline-start applies correctly to ltr visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.ltr, + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + left: 2, + ))); + }, + ); + + testWidgets( + 'Test that margin-inline-end applies correctly to ltr visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.ltr, + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + right: 2, + ))); + }, + ); + + testWidgets( + 'Test that two-argument margin-inline applies correctly to ltr visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.ltr, + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + left: 2, + right: 4, + ))); + }, + ); + + testWidgets( + 'Test that margin-inline-start applies correctly to rtl visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.rtl, + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + right: 2, + ))); + }, + ); + + testWidgets( + 'Test that margin-inline-end applies correctly to rtl visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.rtl, + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + left: 2, + ))); + }, + ); + + testWidgets( + 'Test that two-argument margin-inline applies correctly to rtl visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.rtl, + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only( + right: 2, + left: 4, + ))); + }, + ); + + testWidgets( + 'Test that em margin applies correctly', + (tester) async { + await tester.pumpWidget( + TestApp( + child: DefaultTextStyle( + style: const TextStyle(fontSize: 14), + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.symmetric( + vertical: 14, + horizontal: 28, + ))); + }, + ); + + testWidgets( + 'Test that rem margin applies correctly', + (tester) async { + await tester.pumpWidget( + TestApp( + child: DefaultTextStyle( + style: const TextStyle(fontSize: 14), + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only(left: 28)), + ); + }, + ); + + testWidgets( + 'Test that dimensionless margin applies correctly', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + ...
Test
... + """, + ), + ), + ); + + expect( + _getDeepestRenderCSSBox("Test", tester).margins, + equals(Margins.only(right: 12)), + ); + }, + ); +} + +Margins? _getMargin(String textToFind) { + return findCssBox(find.text(textToFind, findRichText: true))!.style.margin; +} + +RenderCSSBox _getDeepestRenderCSSBox(String textToFind, WidgetTester tester) { + final objects = tester.renderObjectList( + find.byElementType(MultiChildRenderObjectElement), + ); + + return objects.lastWhere((e) { + return e is RenderCSSBox; + }) as RenderCSSBox; +} diff --git a/test/style/css_parsing/padding_test.dart b/test/style/css_parsing/padding_test.dart new file mode 100644 index 0000000000..feecc8c1fa --- /dev/null +++ b/test/style/css_parsing/padding_test.dart @@ -0,0 +1,713 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../test_utils.dart'; + +void main() { + testWidgets( + 'Test that a normal div has no padding', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: "
Test
", + ), + ), + ); + expect(_getPadding("Test"), isNull); + }, + ); + + testWidgets( + 'Test that a div with inline styled padding has padding', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """
Test
""", + ), + ), + ); + expect(_getPadding("Test"), equals(HtmlPaddings.all(8, Unit.px))); + }, + ); + + testWidgets( + 'Test that a div with styled padding has padding', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ + +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), equals(HtmlPaddings.all(8, Unit.px))); + }, + ); + + testWidgets( + 'Test padding-left in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(left: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-top in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(top: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-right in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(right: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-bottom in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(bottom: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-block-start in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(blockStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-block-end in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(blockEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-inline-start in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(inlineStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-inline-end in +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(inlineEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-left inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(left: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-top inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(top: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-right inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(right: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-bottom inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(bottom: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-block-start inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(blockStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-block-end inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(blockEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-inline-start inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(inlineStart: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test padding-inline-end inline', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + expect(_getPadding("Test"), + equals(HtmlPaddings.only(inlineEnd: 8, unit: Unit.px))); + }, + ); + + testWidgets( + 'Test that padding actually applies to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect(_getDivContainer("Test").padding, equals(const EdgeInsets.all(8))); + }, + ); + + testWidgets( + 'Test that two-argument padding actually applies to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ))); + }, + ); + + testWidgets( + 'Test that three-argument padding applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + top: 4, + right: 6, + left: 6, + bottom: 8, + ))); + }, + ); + + testWidgets( + 'Test that four-argument padding applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + top: 2, + right: 4, + bottom: 6, + left: 8, + ))); + }, + ); + + testWidgets( + 'Test that padding-block-start applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + top: 2, + ))); + }, + ); + + testWidgets( + 'Test that padding-block-end applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + bottom: 2, + ))); + }, + ); + + testWidgets( + 'Test that two-argument padding-block applies correctly to visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + top: 2, + bottom: 4, + ))); + }, + ); + + testWidgets( + 'Test that padding-inline-start applies correctly to ltr visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.ltr, + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + left: 2, + ))); + }, + ); + + testWidgets( + 'Test that padding-inline-end applies correctly to ltr visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.ltr, + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + right: 2, + ))); + }, + ); + + testWidgets( + 'Test that two-argument padding-inline applies correctly to ltr visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.ltr, + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + left: 2, + right: 4, + ))); + }, + ); + + testWidgets( + 'Test that padding-inline-start applies correctly to rtl visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.rtl, + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + right: 2, + ))); + }, + ); + + testWidgets( + 'Test that padding-inline-end applies correctly to rtl visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.rtl, + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + left: 2, + ))); + }, + ); + + testWidgets( + 'Test that two-argument padding-inline applies correctly to rtl visual layout', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Directionality( + textDirection: TextDirection.rtl, + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + right: 2, + left: 4, + ))); + }, + ); + + testWidgets( + 'Test that em padding applies correctly', + (tester) async { + await tester.pumpWidget( + TestApp( + child: DefaultTextStyle( + style: const TextStyle(fontSize: 14), + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only( + top: 14, + bottom: 28, + ))); + }, + ); + + testWidgets( + 'Test that rem padding applies correctly', + (tester) async { + await tester.pumpWidget( + TestApp( + child: DefaultTextStyle( + style: const TextStyle(fontSize: 14), + child: Html( + data: """ +
Test
+ """, + ), + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only(top: 28)), + ); + }, + ); + + testWidgets( + 'Test that dimensionless padding applies correctly', + (tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """ +
Test
+ """, + ), + ), + ); + + expect( + _getDivContainer("Test").padding, + equals(const EdgeInsets.only(right: 12)), + ); + }, + ); +} + +HtmlPaddings? _getPadding(String textToFind) { + return findCssBox(find.text(textToFind, findRichText: true))!.style.padding; +} + +Container _getDivContainer(String textToFind) { + final containers = List.from(find + .ancestor( + of: find.text("Test", findRichText: true), + matching: find.byType(Container), + ) + .evaluate()); + expect(containers.length, greaterThanOrEqualTo(1)); + + return containers.first.widget as Container; +}