From 6704f8e1ab4c6e4fdf46e73daf6e5f58b897e23f Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Sun, 14 Aug 2022 18:56:46 -0300 Subject: [PATCH] [SuperTextField] Fix horizontal aligment (Resolves #668) (#716) --- .../android/android_textfield.dart | 28 +- .../desktop/desktop_textfield.dart | 42 ++- .../fill_width_if_constrained.dart | 87 ++++++ .../infrastructure/text_scrollview.dart | 35 ++- .../super_textfield/ios/ios_textfield.dart | 28 +- ...textfield_alignments_multiline_android.png | Bin 0 -> 21242 bytes ...textfield_alignments_multiline_desktop.png | Bin 0 -> 21242 bytes ...per_textfield_alignments_multiline_ios.png | Bin 0 -> 21242 bytes ...extfield_alignments_singleline_android.png | Bin 0 -> 21004 bytes ...extfield_alignments_singleline_desktop.png | Bin 0 -> 21004 bytes ...er_textfield_alignments_singleline_ios.png | Bin 0 -> 21004 bytes .../super_textfield_text_alignment_test.dart | 254 ++++++++++++++++++ 12 files changed, 435 insertions(+), 39 deletions(-) create mode 100644 super_editor/lib/src/infrastructure/super_textfield/infrastructure/fill_width_if_constrained.dart create mode 100644 super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_android.png create mode 100644 super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_desktop.png create mode 100644 super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_ios.png create mode 100644 super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_android.png create mode 100644 super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_desktop.png create mode 100644 super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_ios.png create mode 100644 super_editor/test/super_textfield/super_textfield_text_alignment_test.dart diff --git a/super_editor/lib/src/infrastructure/super_textfield/android/android_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/android/android_textfield.dart index ccae63bcc..a00fbd992 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/android/android_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/android/android_textfield.dart @@ -5,6 +5,7 @@ import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; import 'package:super_editor/src/infrastructure/focus.dart'; import 'package:super_editor/src/infrastructure/super_textfield/android/_editing_controls.dart'; import 'package:super_editor/src/infrastructure/super_textfield/android/_user_interaction.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/fill_width_if_constrained.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/hint_text.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart'; import 'package:super_editor/src/infrastructure/super_textfield/input_method_engine/_ime_text_editing_controller.dart'; @@ -472,6 +473,7 @@ class SuperAndroidTextFieldState extends State textScrollController: _textScrollController, textKey: _textContentKey, textEditingController: _textEditingController, + textAlign: widget.textAlign, minLines: widget.minLines, maxLines: widget.maxLines, lineHeight: widget.lineHeight, @@ -509,19 +511,21 @@ class SuperAndroidTextFieldState extends State ? _textEditingController.text.computeTextSpan(widget.textStyleBuilder) : TextSpan(text: "", style: widget.textStyleBuilder({})); - return SuperTextWithSelection.single( - key: _textContentKey, - richText: textSpan, - textAlign: widget.textAlign, - userSelection: UserSelection( - highlightStyle: SelectionHighlightStyle( - color: widget.selectionColor, - ), - caretStyle: CaretStyle( - color: widget.caretColor, + return FillWidthIfConstrained( + child: SuperTextWithSelection.single( + key: _textContentKey, + richText: textSpan, + textAlign: widget.textAlign, + userSelection: UserSelection( + highlightStyle: SelectionHighlightStyle( + color: widget.selectionColor, + ), + caretStyle: CaretStyle( + color: widget.caretColor, + ), + selection: _textEditingController.selection, + hasCaret: _focusNode.hasFocus, ), - selection: _textEditingController.selection, - hasCaret: _focusNode.hasFocus, ), ); } diff --git a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart index 2e816587e..11fa42986 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart @@ -18,6 +18,7 @@ import 'package:super_text_layout/super_text_layout.dart'; import '../../keyboard.dart'; import '../../multi_tap_gesture.dart'; +import '../infrastructure/fill_width_if_constrained.dart'; import '../styles.dart'; final _log = textFieldLog; @@ -301,6 +302,7 @@ class SuperDesktopTextFieldState extends State implements key: _textScrollKey, textKey: _textKey, textController: _controller, + textAlign: widget.textAlign, scrollController: _scrollController, viewportHeight: _viewportHeight, estimatedLineHeight: _getEstimatedLineHeight(), @@ -327,15 +329,17 @@ class SuperDesktopTextFieldState extends State implements } Widget _buildSelectableText() { - return SuperTextWithSelection.single( - key: _textKey, - richText: _controller.text.computeTextSpan(widget.textStyleBuilder), - textAlign: widget.textAlign, - userSelection: UserSelection( - highlightStyle: widget.selectionHighlightStyle, - caretStyle: widget.caretStyle, - selection: _controller.selection, - hasCaret: _focusNode.hasFocus, + return FillWidthIfConstrained( + child: SuperTextWithSelection.single( + key: _textKey, + richText: _controller.text.computeTextSpan(widget.textStyleBuilder), + textAlign: widget.textAlign, + userSelection: UserSelection( + highlightStyle: widget.selectionHighlightStyle, + caretStyle: widget.caretStyle, + selection: _controller.selection, + hasCaret: _focusNode.hasFocus, + ), ), ); } @@ -870,6 +874,7 @@ class SuperTextFieldScrollview extends StatefulWidget { required this.viewportHeight, required this.estimatedLineHeight, required this.isMultiline, + this.textAlign = TextAlign.left, required this.child, }) : super(key: key); @@ -899,6 +904,9 @@ class SuperTextFieldScrollview extends StatefulWidget { /// Whether or not this text field allows multiple lines of text. final bool isMultiline; + /// The text alignment within the scrollview. + final TextAlign textAlign; + /// The rest of the subtree for this text field. final Widget child; @@ -1127,6 +1135,22 @@ class SuperTextFieldScrollviewState extends State with } } + Alignment _getAlignment() { + switch (widget.textAlign) { + case TextAlign.left: + case TextAlign.justify: + return Alignment.topLeft; + case TextAlign.right: + return Alignment.topRight; + case TextAlign.center: + return Alignment.topCenter; + case TextAlign.start: + return Directionality.of(context) == TextDirection.ltr ? Alignment.topLeft : Alignment.topRight; + case TextAlign.end: + return Directionality.of(context) == TextDirection.ltr ? Alignment.topRight : Alignment.topLeft; + } + } + @override Widget build(BuildContext context) { return SizedBox( diff --git a/super_editor/lib/src/infrastructure/super_textfield/infrastructure/fill_width_if_constrained.dart b/super_editor/lib/src/infrastructure/super_textfield/infrastructure/fill_width_if_constrained.dart new file mode 100644 index 000000000..1ec184062 --- /dev/null +++ b/super_editor/lib/src/infrastructure/super_textfield/infrastructure/fill_width_if_constrained.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// Forces [child] to take up all available width when the +/// incoming width constraint is bounded, otherwise the [child] +/// is sized by its intrinsic width. +/// +/// This widget is used to correctly align the text of a multiline +/// [SuperTextWithSelection] with a constrained width. +class FillWidthIfConstrained extends SingleChildRenderObjectWidget { + const FillWidthIfConstrained({ + required Widget child, + }) : super(child: child); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderFillWidthIfConstrained( + minWidth: _getViewportWidth(context), + ); + } + + @override + void updateRenderObject(BuildContext context, RenderFillWidthIfConstrained renderObject) { + renderObject.minWidth = _getViewportWidth(context); + } + + double? _getViewportWidth(BuildContext context) { + final scrollable = Scrollable.of(context); + if (scrollable == null) { + return null; + } + + final direction = scrollable.axisDirection; + // We only need to specify the width if we are inside a horizontal scrollable, + // because in this case we might have an infinity maxWidth. + if (direction == AxisDirection.up || direction == AxisDirection.down) { + return null; + } + return (scrollable.context.findRenderObject() as RenderBox?)?.size.width; + } +} + +class RenderFillWidthIfConstrained extends RenderProxyBox { + RenderFillWidthIfConstrained({ + double? minWidth, + }) : _minWidth = minWidth; + + /// Sets the minimum width the child widget needs to be. + /// + /// This is needed when this widget is inside a horizontal Scrollable. + /// In this case, we might have an infinity maxWidth, so we need + /// to specify the Scrollable's width to force the child to + /// be at least this width. + set minWidth(double? value) { + _minWidth = value; + markNeedsLayout(); + } + + double? _minWidth; + + @override + void performLayout() { + BoxConstraints childConstraints = constraints; + + // If the available width is bounded, + // force the child to be as wide as the available width. + if (constraints.hasBoundedWidth) { + childConstraints = BoxConstraints( + minWidth: constraints.maxWidth, + minHeight: constraints.minHeight, + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, + ); + } else if (_minWidth != null) { + // If a minWidth is given, force the child to be at least this width. + // This is the case when this widget is placed inside an Scrollable. + childConstraints = BoxConstraints( + minWidth: _minWidth!, + minHeight: constraints.minHeight, + maxHeight: constraints.maxHeight, + ); + } + + child!.layout(childConstraints, parentUsesSize: true); + size = child!.size; + } +} diff --git a/super_editor/lib/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart b/super_editor/lib/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart index 2ffba2248..a907a6cdb 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart @@ -32,6 +32,7 @@ class TextScrollView extends StatefulWidget { this.lineHeight, this.perLineAutoScrollDuration = Duration.zero, this.showDebugPaint = false, + this.textAlign = TextAlign.left, required this.child, }) : super(key: key); @@ -89,6 +90,9 @@ class TextScrollView extends StatefulWidget { /// Whether to paint debug guides. final bool showDebugPaint; + /// The text alignment within the scrollview. + final TextAlign textAlign; + /// The child widget. final Widget child; @@ -451,15 +455,34 @@ class _TextScrollViewState extends State ); } + Alignment _getAlignment() { + switch (widget.textAlign) { + case TextAlign.left: + case TextAlign.justify: + return Alignment.topLeft; + case TextAlign.right: + return Alignment.topRight; + case TextAlign.center: + return Alignment.topCenter; + case TextAlign.start: + return Directionality.of(context) == TextDirection.ltr ? Alignment.topLeft : Alignment.topRight; + case TextAlign.end: + return Directionality.of(context) == TextDirection.ltr ? Alignment.topRight : Alignment.topLeft; + } + } + Widget _buildScrollView({ required Widget child, }) { - return SingleChildScrollView( - key: _textFieldViewportKey, - controller: _scrollController, - physics: const NeverScrollableScrollPhysics(), - scrollDirection: isMultiline ? Axis.vertical : Axis.horizontal, - child: widget.child, + return Align( + alignment: _getAlignment(), + child: SingleChildScrollView( + key: _textFieldViewportKey, + controller: _scrollController, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: isMultiline ? Axis.vertical : Axis.horizontal, + child: widget.child, + ), ); } diff --git a/super_editor/lib/src/infrastructure/super_textfield/ios/ios_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/ios/ios_textfield.dart index 83a49d4a4..47bef30a5 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/ios/ios_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/ios/ios_textfield.dart @@ -5,6 +5,7 @@ import 'package:super_editor/src/infrastructure/_listenable_builder.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; import 'package:super_editor/src/infrastructure/focus.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/fill_width_if_constrained.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/hint_text.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart'; import 'package:super_editor/src/infrastructure/super_textfield/input_method_engine/_ime_text_editing_controller.dart'; @@ -470,6 +471,7 @@ class SuperIOSTextFieldState extends State textScrollController: _textScrollController, textKey: _textContentKey, textEditingController: _textEditingController, + textAlign: widget.textAlign, minLines: widget.minLines, maxLines: widget.maxLines, lineHeight: widget.lineHeight, @@ -516,19 +518,21 @@ class SuperIOSTextFieldState extends State ? _textEditingController.text.computeTextSpan(widget.textStyleBuilder) : AttributedText(text: "").computeTextSpan(widget.textStyleBuilder); - return SuperTextWithSelection.single( - key: _textContentKey, - richText: textSpan, - textAlign: widget.textAlign, - userSelection: UserSelection( - highlightStyle: SelectionHighlightStyle( - color: widget.selectionColor, - ), - caretStyle: CaretStyle( - color: _floatingCursorController.isShowingFloatingCursor ? Colors.grey : widget.caretColor, + return FillWidthIfConstrained( + child: SuperTextWithSelection.single( + key: _textContentKey, + richText: textSpan, + textAlign: widget.textAlign, + userSelection: UserSelection( + highlightStyle: SelectionHighlightStyle( + color: widget.selectionColor, + ), + caretStyle: CaretStyle( + color: _floatingCursorController.isShowingFloatingCursor ? Colors.grey : widget.caretColor, + ), + selection: _textEditingController.selection, + hasCaret: _focusNode.hasFocus, ), - selection: _textEditingController.selection, - hasCaret: _focusNode.hasFocus, ), ); } diff --git a/super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_android.png b/super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_android.png new file mode 100644 index 0000000000000000000000000000000000000000..cc82aafea133ec6e4a574ab284da66bdd38d3d69 GIT binary patch literal 21242 zcmeHPYe-XJ7=E`l-89Qfs6<{UiJ*vy{g7mNVWK0k5W!H~EMd+%^T>DyneV2!R&|l@DdIiOSpU3P@ApgvvH{KJ{!VzP9z(r zB^bDD+`v1+hHwpU&SHVEESoJ9naw-EU7@Hxqa;v2A8Rrq2Sq**M!~@9ZA63DYXaAJaBKt9yi=fmLO+ zQa&UQsl8<(n;WJhVK40H-s1=kn1GFB$ z*RAG4YIBP@BKAuiZ*JzHnphx?h96I~o~Wf4tz76G6V#rfI3$z8*-BK9l* z!2(I^^Q2vBV^*eL=#Tw-=E5#HfYk?++<0^ZOCy#FOJ&dcDul70&CaLa_p?{2)^(ST zUGW@%3HV)PI(p_5b(#>M$-5IT5_(rk9H!}rn9w*NPZ%7K5J(7AO@;%2*?`%A*?`$V z5I`wFDL^SeDL^T}D%z6;*g~Mi02f9^6#a@2&%0tgXQ($idfT4aK2t}Wv{G-=zM1FU zRmWkPAczT#1M-9s3?#%#3DMj5{VYKECF*|x5Y3gMi3lQsxQa94444g=4VVp>4Fmy{ z0+a%j0+a%j0_^HgS@1tQ7@fr_o=0t>Cr^scn+L-H3<=(I(>dNJ!Eu-oEX0Hc0tvAe Zgy1wb6<=OZ+o!1?lV;{hx-v@Y{{WpJ0FVFx literal 0 HcmV?d00001 diff --git a/super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_desktop.png b/super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..cc82aafea133ec6e4a574ab284da66bdd38d3d69 GIT binary patch literal 21242 zcmeHPYe-XJ7=E`l-89Qfs6<{UiJ*vy{g7mNVWK0k5W!H~EMd+%^T>DyneV2!R&|l@DdIiOSpU3P@ApgvvH{KJ{!VzP9z(r zB^bDD+`v1+hHwpU&SHVEESoJ9naw-EU7@Hxqa;v2A8Rrq2Sq**M!~@9ZA63DYXaAJaBKt9yi=fmLO+ zQa&UQsl8<(n;WJhVK40H-s1=kn1GFB$ z*RAG4YIBP@BKAuiZ*JzHnphx?h96I~o~Wf4tz76G6V#rfI3$z8*-BK9l* z!2(I^^Q2vBV^*eL=#Tw-=E5#HfYk?++<0^ZOCy#FOJ&dcDul70&CaLa_p?{2)^(ST zUGW@%3HV)PI(p_5b(#>M$-5IT5_(rk9H!}rn9w*NPZ%7K5J(7AO@;%2*?`%A*?`$V z5I`wFDL^SeDL^T}D%z6;*g~Mi02f9^6#a@2&%0tgXQ($idfT4aK2t}Wv{G-=zM1FU zRmWkPAczT#1M-9s3?#%#3DMj5{VYKECF*|x5Y3gMi3lQsxQa94444g=4VVp>4Fmy{ z0+a%j0+a%j0_^HgS@1tQ7@fr_o=0t>Cr^scn+L-H3<=(I(>dNJ!Eu-oEX0Hc0tvAe Zgy1wb6<=OZ+o!1?lV;{hx-v@Y{{WpJ0FVFx literal 0 HcmV?d00001 diff --git a/super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_ios.png b/super_editor/test/super_textfield/goldens/super_textfield_alignments_multiline_ios.png new file mode 100644 index 0000000000000000000000000000000000000000..cc82aafea133ec6e4a574ab284da66bdd38d3d69 GIT binary patch literal 21242 zcmeHPYe-XJ7=E`l-89Qfs6<{UiJ*vy{g7mNVWK0k5W!H~EMd+%^T>DyneV2!R&|l@DdIiOSpU3P@ApgvvH{KJ{!VzP9z(r zB^bDD+`v1+hHwpU&SHVEESoJ9naw-EU7@Hxqa;v2A8Rrq2Sq**M!~@9ZA63DYXaAJaBKt9yi=fmLO+ zQa&UQsl8<(n;WJhVK40H-s1=kn1GFB$ z*RAG4YIBP@BKAuiZ*JzHnphx?h96I~o~Wf4tz76G6V#rfI3$z8*-BK9l* z!2(I^^Q2vBV^*eL=#Tw-=E5#HfYk?++<0^ZOCy#FOJ&dcDul70&CaLa_p?{2)^(ST zUGW@%3HV)PI(p_5b(#>M$-5IT5_(rk9H!}rn9w*NPZ%7K5J(7AO@;%2*?`%A*?`$V z5I`wFDL^SeDL^T}D%z6;*g~Mi02f9^6#a@2&%0tgXQ($idfT4aK2t}Wv{G-=zM1FU zRmWkPAczT#1M-9s3?#%#3DMj5{VYKECF*|x5Y3gMi3lQsxQa94444g=4VVp>4Fmy{ z0+a%j0+a%j0_^HgS@1tQ7@fr_o=0t>Cr^scn+L-H3<=(I(>dNJ!Eu-oEX0Hc0tvAe Zgy1wb6<=OZ+o!1?lV;{hx-v@Y{{WpJ0FVFx literal 0 HcmV?d00001 diff --git a/super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_android.png b/super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_android.png new file mode 100644 index 0000000000000000000000000000000000000000..1eec8b2213fed6af0d4fcad0bd8257cba35b35cb GIT binary patch literal 21004 zcmeI4OK1~O6oyY8Z6?)Ht)QaS2C2Fc5xTHd3noO{0j=70(Gn{0frTm*iBiFcgKjE9 zK-^T3?nNx^!UsMeT?sBMF0`c;6gNSP;>t&f)tSt_9qO{8Nc?7#GdE!t|M~y_oO^TU zUfSQ;I4Ke`jB$8YU@RMj<@i8kEaHkP=&yLRIAx$bP@`n3;X`w7MC z$5igKj>YOA>M!SmZ)2@~>6v2lqm5u0+yX>!+8=(AxmzUO_?^M02R9K+nunLQ#Q95OYIC z1*XibBq%Ddz{0#oQ{mTn&)WQyv%(?S$ocn;`OO*iyTPE>UYQPPVB!J+As~dH z23%7FE5Hh{0_6m#K>0Ft!)_|TM2QnZKnOt%s6hlPzzVPeQQLHTfTz#(u55!8ShXcm+cpaum5wSn3| zZP2bzN^0nx$P^EnDgTxjKh%HHIcV;xcb3+cuy=F=!*2e;MCkxR{4OEdXPVZ$Hu8m9 O=RxMK*7$HNdEqA+AXy>+ literal 0 HcmV?d00001 diff --git a/super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_desktop.png b/super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..1eec8b2213fed6af0d4fcad0bd8257cba35b35cb GIT binary patch literal 21004 zcmeI4OK1~O6oyY8Z6?)Ht)QaS2C2Fc5xTHd3noO{0j=70(Gn{0frTm*iBiFcgKjE9 zK-^T3?nNx^!UsMeT?sBMF0`c;6gNSP;>t&f)tSt_9qO{8Nc?7#GdE!t|M~y_oO^TU zUfSQ;I4Ke`jB$8YU@RMj<@i8kEaHkP=&yLRIAx$bP@`n3;X`w7MC z$5igKj>YOA>M!SmZ)2@~>6v2lqm5u0+yX>!+8=(AxmzUO_?^M02R9K+nunLQ#Q95OYIC z1*XibBq%Ddz{0#oQ{mTn&)WQyv%(?S$ocn;`OO*iyTPE>UYQPPVB!J+As~dH z23%7FE5Hh{0_6m#K>0Ft!)_|TM2QnZKnOt%s6hlPzzVPeQQLHTfTz#(u55!8ShXcm+cpaum5wSn3| zZP2bzN^0nx$P^EnDgTxjKh%HHIcV;xcb3+cuy=F=!*2e;MCkxR{4OEdXPVZ$Hu8m9 O=RxMK*7$HNdEqA+AXy>+ literal 0 HcmV?d00001 diff --git a/super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_ios.png b/super_editor/test/super_textfield/goldens/super_textfield_alignments_singleline_ios.png new file mode 100644 index 0000000000000000000000000000000000000000..1eec8b2213fed6af0d4fcad0bd8257cba35b35cb GIT binary patch literal 21004 zcmeI4OK1~O6oyY8Z6?)Ht)QaS2C2Fc5xTHd3noO{0j=70(Gn{0frTm*iBiFcgKjE9 zK-^T3?nNx^!UsMeT?sBMF0`c;6gNSP;>t&f)tSt_9qO{8Nc?7#GdE!t|M~y_oO^TU zUfSQ;I4Ke`jB$8YU@RMj<@i8kEaHkP=&yLRIAx$bP@`n3;X`w7MC z$5igKj>YOA>M!SmZ)2@~>6v2lqm5u0+yX>!+8=(AxmzUO_?^M02R9K+nunLQ#Q95OYIC z1*XibBq%Ddz{0#oQ{mTn&)WQyv%(?S$ocn;`OO*iyTPE>UYQPPVB!J+As~dH z23%7FE5Hh{0_6m#K>0Ft!)_|TM2QnZKnOt%s6hlPzzVPeQQLHTfTz#(u55!8ShXcm+cpaum5wSn3| zZP2bzN^0nx$P^EnDgTxjKh%HHIcV;xcb3+cuy=F=!*2e;MCkxR{4OEdXPVZ$Hu8m9 O=RxMK*7$HNdEqA+AXy>+ literal 0 HcmV?d00001 diff --git a/super_editor/test/super_textfield/super_textfield_text_alignment_test.dart b/super_editor/test/super_textfield/super_textfield_text_alignment_test.dart new file mode 100644 index 000000000..6ae8b5e13 --- /dev/null +++ b/super_editor/test/super_textfield/super_textfield_text_alignment_test.dart @@ -0,0 +1,254 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:super_editor/super_editor.dart'; + +import '../test_tools.dart'; + +void main() { + // These golden tests are being skipped on macOS because the text seems to be + // a bit bigger in this platform, causing the tests to fail. + group('SuperTextField', () { + group('single line', () { + group('displays different alignments', () { + testGoldens('(on Android)', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: "Left", + textAlign: TextAlign.left, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.android, + ), + _buildSuperTextField( + text: "Center", + textAlign: TextAlign.center, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.android, + ), + _buildSuperTextField( + text: "Right", + textAlign: TextAlign.right, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.android, + ), + ], + ); + + await screenMatchesGolden(tester, 'super_textfield_alignments_singleline_android'); + }, skip: Platform.isMacOS); + + testGoldens('(on iOS)', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: "Left", + textAlign: TextAlign.left, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.iOS, + ), + _buildSuperTextField( + text: "Center", + textAlign: TextAlign.center, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.iOS, + ), + _buildSuperTextField( + text: "Right", + textAlign: TextAlign.right, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.iOS, + ), + ], + ); + + await screenMatchesGolden(tester, 'super_textfield_alignments_singleline_ios'); + }, skip: Platform.isMacOS); + + testGoldens('(on Desktop)', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: "Left", + textAlign: TextAlign.left, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.desktop, + ), + _buildSuperTextField( + text: "Center", + textAlign: TextAlign.center, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.desktop, + ), + _buildSuperTextField( + text: "Right", + textAlign: TextAlign.right, + maxLines: 1, + configuration: SuperTextFieldPlatformConfiguration.desktop, + ), + ], + ); + + await screenMatchesGolden(tester, 'super_textfield_alignments_singleline_desktop'); + }, skip: Platform.isMacOS); + }); + }); + + group('multi line', () { + const multilineText = 'First Line\nSecond Line\nThird Line\nFourth Line'; + group('displays different alignments', () { + testGoldens('(on Android)', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.left, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.android, + ), + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.center, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.android, + ), + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.right, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.android, + ), + ], + ); + + await screenMatchesGolden(tester, 'super_textfield_alignments_multiline_android'); + }, skip: Platform.isMacOS); + + testGoldens('(on iOS)', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.left, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.iOS, + ), + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.center, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.iOS, + ), + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.right, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.iOS, + ), + ], + ); + + await screenMatchesGolden(tester, 'super_textfield_alignments_multiline_ios'); + }, skip: Platform.isMacOS); + + testGoldens('(on Desktop)', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.left, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.desktop, + ), + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.center, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.desktop, + ), + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.right, + maxLines: 4, + configuration: SuperTextFieldPlatformConfiguration.desktop, + ), + ], + ); + + await screenMatchesGolden(tester, 'super_textfield_alignments_multiline_desktop'); + }); + }, skip: Platform.isMacOS); + + testWidgetsOnAllPlatforms('makes scrollview fill all the field width', (tester) async { + await _pumpScaffold( + tester, + children: [ + _buildSuperTextField( + text: multilineText, + textAlign: TextAlign.center, + maxLines: 4, + ), + ], + ); + await tester.pump(); + + final textfieldWidth = tester.getSize(find.byType(SuperTextField)).width; + final scrollViewWidth = tester.getSize(find.byType(SingleChildScrollView)).width; + + // Ensure the scrollview occupies all the available width rathen than + // just width of the text. + expect(scrollViewWidth, equals(textfieldWidth)); + }); + }); + }); +} + +Widget _buildSuperTextField({ + required String text, + required TextAlign textAlign, + SuperTextFieldPlatformConfiguration? configuration, + int? maxLines, +}) { + final controller = AttributedTextEditingController( + text: AttributedText(text: text), + ); + + return SizedBox( + width: double.infinity, + child: SuperTextField( + configuration: configuration, + textController: controller, + textAlign: textAlign, + maxLines: maxLines, + minLines: 1, + lineHeight: 20, + textStyleBuilder: (_) { + return const TextStyle( + color: Colors.black, + fontSize: 20, + ); + }, + ), + ); +} + +Future _pumpScaffold( + WidgetTester tester, { + required List children, +}) async { + await tester.pumpWidget( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Column(children: children), + ), + ), + ); +}