From 175465f8f0c69f7a6ad91363b4bff049469fc735 Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Sat, 25 Jan 2025 16:33:01 -0300 Subject: [PATCH] [SuperEditor][SuperTextField] - Improve RTL support (Resolves #2472) (#2518) --- .../lib/src/default_editor/list_items.dart | 138 +++++++++------ .../lib/src/default_editor/paragraph.dart | 64 +++---- .../lib/src/default_editor/tasks.dart | 71 ++++---- .../android/android_textfield.dart | 141 +++++++++------- .../desktop/desktop_textfield.dart | 144 +++++++++------- .../super_textfield/ios/ios_textfield.dart | 158 +++++++++++------- .../src/super_textfield/super_textfield.dart | 7 +- .../super_editor/supereditor_test_tools.dart | 1 + ...st-character-ordered-list-item-android.png | Bin 0 -> 3946 bytes ...ftmost-character-ordered-list-item-iOS.png | Bin 0 -> 2128 bytes ...most-character-ordered-list-item-linux.png | Bin 0 -> 2573 bytes ...most-character-ordered-list-item-macOS.png | Bin 0 -> 2117 bytes ...st-character-ordered-list-item-windows.png | Bin 0 -> 2117 bytes ...t-leftmost-character-paragraph-android.png | Bin 0 -> 3427 bytes ...et-at-leftmost-character-paragraph-iOS.png | Bin 0 -> 2054 bytes ...-at-leftmost-character-paragraph-linux.png | Bin 0 -> 2051 bytes ...-at-leftmost-character-paragraph-macOS.png | Bin 0 -> 2051 bytes ...t-leftmost-character-paragraph-windows.png | Bin 0 -> 2051 bytes ...ret-at-leftmost-character-task-android.png | Bin 0 -> 3780 bytes ...l-caret-at-leftmost-character-task-iOS.png | Bin 0 -> 2419 bytes ...caret-at-leftmost-character-task-linux.png | Bin 0 -> 2422 bytes ...caret-at-leftmost-character-task-macOS.png | Bin 0 -> 2422 bytes ...ret-at-leftmost-character-task-windows.png | Bin 0 -> 2422 bytes ...-character-unordered-list-item-android.png | Bin 0 -> 3828 bytes ...most-character-unordered-list-item-iOS.png | Bin 0 -> 2441 bytes ...st-character-unordered-list-item-linux.png | Bin 0 -> 2429 bytes ...st-character-unordered-list-item-macOS.png | Bin 0 -> 2429 bytes ...-character-unordered-list-item-windows.png | Bin 0 -> 2429 bytes .../editor/supereditor_rtl_test.dart | 128 ++++++++++++++ ...tl-caret-at-leftmost-character-android.png | Bin 0 -> 2921 bytes ...ld_rtl-caret-at-leftmost-character-iOS.png | Bin 0 -> 2925 bytes ..._rtl-caret-at-leftmost-character-linux.png | Bin 0 -> 2929 bytes ..._rtl-caret-at-leftmost-character-macOS.png | Bin 0 -> 2929 bytes ...tl-caret-at-leftmost-character-windows.png | Bin 0 -> 2929 bytes .../super_textfield_rtl_test.dart | 62 +++++++ super_text_layout/lib/src/super_text.dart | 3 + 36 files changed, 626 insertions(+), 291 deletions(-) create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-android.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-iOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-linux.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-macOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-windows.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-android.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-iOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-linux.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-macOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-windows.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-android.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-iOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-linux.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-macOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-windows.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-android.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-iOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-linux.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-macOS.png create mode 100644 super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-windows.png create mode 100644 super_editor/test_goldens/editor/supereditor_rtl_test.dart create mode 100644 super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-android.png create mode 100644 super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-iOS.png create mode 100644 super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-linux.png create mode 100644 super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-macOS.png create mode 100644 super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-windows.png create mode 100644 super_editor/test_goldens/super_textfield/super_textfield_rtl_test.dart diff --git a/super_editor/lib/src/default_editor/list_items.dart b/super_editor/lib/src/default_editor/list_items.dart index f663143c21..f701d6b55f 100644 --- a/super_editor/lib/src/default_editor/list_items.dart +++ b/super_editor/lib/src/default_editor/list_items.dart @@ -9,6 +9,7 @@ import 'package:super_editor/src/core/editor.dart'; import 'package:super_editor/src/core/styles.dart'; import 'package:super_editor/src/default_editor/attributions.dart'; import 'package:super_editor/src/default_editor/blocks/indentation.dart'; +import 'package:super_editor/src/default_editor/text_tools.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/keyboard.dart'; @@ -161,11 +162,16 @@ class ListItemComponentBuilder implements ComponentBuilder { ordinalValue = computeListItemOrdinalValue(node, document); } + final textDirection = getParagraphDirection(node.text.toPlainText()); + final textAlignment = textDirection == TextDirection.ltr ? TextAlign.left : TextAlign.right; + return switch (node.type) { ListItemType.unordered => UnorderedListItemComponentViewModel( nodeId: node.id, indent: node.indent, text: node.text, + textDirection: textDirection, + textAlignment: textAlignment, textStyleBuilder: noStyleBuilder, selectionColor: const Color(0x00000000), ), @@ -174,6 +180,8 @@ class ListItemComponentBuilder implements ComponentBuilder { indent: node.indent, ordinalValue: ordinalValue, text: node.text, + textDirection: textDirection, + textAlignment: textAlignment, textStyleBuilder: noStyleBuilder, selectionColor: const Color(0x00000000), ), @@ -196,6 +204,8 @@ class ListItemComponentBuilder implements ComponentBuilder { indent: componentViewModel.indent, dotStyle: componentViewModel.dotStyle, textSelection: componentViewModel.selection, + textDirection: componentViewModel.textDirection, + textAlignment: componentViewModel.textAlignment, selectionColor: componentViewModel.selectionColor, highlightWhenEmpty: componentViewModel.highlightWhenEmpty, underlines: componentViewModel.createUnderlines(), @@ -206,6 +216,8 @@ class ListItemComponentBuilder implements ComponentBuilder { indent: componentViewModel.indent, listIndex: componentViewModel.ordinalValue!, text: componentViewModel.text, + textDirection: componentViewModel.textDirection, + textAlignment: componentViewModel.textAlignment, styleBuilder: componentViewModel.textStyleBuilder, numeralStyle: componentViewModel.numeralStyle, textSelection: componentViewModel.selection, @@ -281,6 +293,7 @@ abstract class ListItemComponentViewModel extends SingleColumnLayoutComponentVie indent == other.indent && text == other.text && textDirection == other.textDirection && + textAlignment == other.textAlignment && selection == other.selection && selectionColor == other.selectionColor && highlightWhenEmpty == other.highlightWhenEmpty && @@ -298,6 +311,7 @@ abstract class ListItemComponentViewModel extends SingleColumnLayoutComponentVie indent.hashCode ^ text.hashCode ^ textDirection.hashCode ^ + textAlignment.hashCode ^ selection.hashCode ^ selectionColor.hashCode ^ highlightWhenEmpty.hashCode ^ @@ -355,6 +369,7 @@ class UnorderedListItemComponentViewModel extends ListItemComponentViewModel { textStyleBuilder: textStyleBuilder, dotStyle: dotStyle, textDirection: textDirection, + textAlignment: textAlignment, selection: selection, selectionColor: selectionColor, composingRegion: composingRegion, @@ -423,6 +438,7 @@ class OrderedListItemComponentViewModel extends ListItemComponentViewModel { text: text, textStyleBuilder: textStyleBuilder, textDirection: textDirection, + textAlignment: textAlignment, selection: selection, selectionColor: selectionColor, composingRegion: composingRegion, @@ -493,6 +509,8 @@ class UnorderedListItemComponent extends StatefulWidget { Key? key, required this.componentKey, required this.text, + this.textDirection = TextDirection.ltr, + this.textAlignment = TextAlign.left, required this.styleBuilder, this.inlineWidgetBuilders = const [], this.dotBuilder = _defaultUnorderedListItemDotBuilder, @@ -510,6 +528,8 @@ class UnorderedListItemComponent extends StatefulWidget { final GlobalKey componentKey; final AttributedText text; + final TextDirection textDirection; + final TextAlign textAlignment; final AttributionStyleBuilder styleBuilder; final InlineWidgetBuilderChain inlineWidgetBuilders; final UnorderedListItemDotBuilder dotBuilder; @@ -558,34 +578,39 @@ class _UnorderedListItemComponentState extends State return ProxyTextDocumentComponent( key: widget.componentKey, textComponentKey: _innerTextComponentKey, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: indentSpace, - decoration: BoxDecoration( - border: widget.showDebugPaint ? Border.all(width: 1, color: Colors.grey) : null, - ), - child: SizedBox( - height: lineHeight, - child: widget.dotBuilder(context, widget), + child: Directionality( + textDirection: widget.textDirection, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: indentSpace, + decoration: BoxDecoration( + border: widget.showDebugPaint ? Border.all(width: 1, color: Colors.grey) : null, + ), + child: SizedBox( + height: lineHeight, + child: widget.dotBuilder(context, widget), + ), ), - ), - Expanded( - child: TextComponent( - key: _innerTextComponentKey, - text: widget.text, - textStyleBuilder: widget.styleBuilder, - inlineWidgetBuilders: widget.inlineWidgetBuilders, - textSelection: widget.textSelection, - textScaler: textScaler, - selectionColor: widget.selectionColor, - highlightWhenEmpty: widget.highlightWhenEmpty, - underlines: widget.underlines, - showDebugPaint: widget.showDebugPaint, + Expanded( + child: TextComponent( + key: _innerTextComponentKey, + text: widget.text, + textDirection: widget.textDirection, + textAlign: widget.textAlignment, + textStyleBuilder: widget.styleBuilder, + inlineWidgetBuilders: widget.inlineWidgetBuilders, + textSelection: widget.textSelection, + textScaler: textScaler, + selectionColor: widget.selectionColor, + highlightWhenEmpty: widget.highlightWhenEmpty, + underlines: widget.underlines, + showDebugPaint: widget.showDebugPaint, + ), ), - ), - ], + ], + ), ), ); } @@ -659,6 +684,8 @@ class OrderedListItemComponent extends StatefulWidget { required this.componentKey, required this.listIndex, required this.text, + this.textDirection = TextDirection.ltr, + this.textAlignment = TextAlign.left, required this.styleBuilder, this.inlineWidgetBuilders = const [], this.numeralBuilder = _defaultOrderedListItemNumeralBuilder, @@ -677,6 +704,8 @@ class OrderedListItemComponent extends StatefulWidget { final GlobalKey componentKey; final int listIndex; final AttributedText text; + final TextDirection textDirection; + final TextAlign textAlignment; final AttributionStyleBuilder styleBuilder; final InlineWidgetBuilderChain inlineWidgetBuilders; final OrderedListItemNumeralBuilder numeralBuilder; @@ -725,35 +754,40 @@ class _OrderedListItemComponentState extends State { return ProxyTextDocumentComponent( key: widget.componentKey, textComponentKey: _innerTextComponentKey, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: indentSpace, - height: lineHeight, - decoration: BoxDecoration( - border: widget.showDebugPaint ? Border.all(width: 1, color: Colors.grey) : null, - ), - child: SizedBox( + child: Directionality( + textDirection: widget.textDirection, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: indentSpace, height: lineHeight, - child: widget.numeralBuilder(context, widget), + decoration: BoxDecoration( + border: widget.showDebugPaint ? Border.all(width: 1, color: Colors.grey) : null, + ), + child: SizedBox( + height: lineHeight, + child: widget.numeralBuilder(context, widget), + ), ), - ), - Expanded( - child: TextComponent( - key: _innerTextComponentKey, - text: widget.text, - textStyleBuilder: widget.styleBuilder, - inlineWidgetBuilders: widget.inlineWidgetBuilders, - textSelection: widget.textSelection, - textScaler: textScaler, - selectionColor: widget.selectionColor, - highlightWhenEmpty: widget.highlightWhenEmpty, - underlines: widget.underlines, - showDebugPaint: widget.showDebugPaint, + Expanded( + child: TextComponent( + key: _innerTextComponentKey, + text: widget.text, + textDirection: widget.textDirection, + textAlign: widget.textAlignment, + textStyleBuilder: widget.styleBuilder, + inlineWidgetBuilders: widget.inlineWidgetBuilders, + textSelection: widget.textSelection, + textScaler: textScaler, + selectionColor: widget.selectionColor, + highlightWhenEmpty: widget.highlightWhenEmpty, + underlines: widget.underlines, + showDebugPaint: widget.showDebugPaint, + ), ), - ), - ], + ], + ), ), ); } diff --git a/super_editor/lib/src/default_editor/paragraph.dart b/super_editor/lib/src/default_editor/paragraph.dart index dfadf61136..c4b3db7eff 100644 --- a/super_editor/lib/src/default_editor/paragraph.dart +++ b/super_editor/lib/src/default_editor/paragraph.dart @@ -331,38 +331,42 @@ class _ParagraphComponentState extends State @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Indent spacing on left. - SizedBox( - width: widget.viewModel.indentCalculator( - widget.viewModel.textStyleBuilder({}), - widget.viewModel.indent, + return Directionality( + textDirection: widget.viewModel.textDirection, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Indent spacing on left. + SizedBox( + width: widget.viewModel.indentCalculator( + widget.viewModel.textStyleBuilder({}), + widget.viewModel.indent, + ), ), - ), - // The actual paragraph UI. - Expanded( - child: TextComponent( - key: _textKey, - text: widget.viewModel.text, - textAlign: widget.viewModel.textAlignment, - textScaler: widget.viewModel.textScaler, - textStyleBuilder: widget.viewModel.textStyleBuilder, - inlineWidgetBuilders: widget.viewModel.inlineWidgetBuilders, - metadata: widget.viewModel.blockType != null - ? { - 'blockType': widget.viewModel.blockType, - } - : {}, - textSelection: widget.viewModel.selection, - selectionColor: widget.viewModel.selectionColor, - highlightWhenEmpty: widget.viewModel.highlightWhenEmpty, - underlines: widget.viewModel.createUnderlines(), - showDebugPaint: widget.showDebugPaint, + // The actual paragraph UI. + Expanded( + child: TextComponent( + key: _textKey, + text: widget.viewModel.text, + textDirection: widget.viewModel.textDirection, + textAlign: widget.viewModel.textAlignment, + textScaler: widget.viewModel.textScaler, + textStyleBuilder: widget.viewModel.textStyleBuilder, + inlineWidgetBuilders: widget.viewModel.inlineWidgetBuilders, + metadata: widget.viewModel.blockType != null + ? { + 'blockType': widget.viewModel.blockType, + } + : {}, + textSelection: widget.viewModel.selection, + selectionColor: widget.viewModel.selectionColor, + highlightWhenEmpty: widget.viewModel.highlightWhenEmpty, + underlines: widget.viewModel.createUnderlines(), + showDebugPaint: widget.showDebugPaint, + ), ), - ), - ], + ], + ), ); } } diff --git a/super_editor/lib/src/default_editor/tasks.dart b/super_editor/lib/src/default_editor/tasks.dart index daf3116b1b..180bd84f37 100644 --- a/super_editor/lib/src/default_editor/tasks.dart +++ b/super_editor/lib/src/default_editor/tasks.dart @@ -13,6 +13,7 @@ import 'package:super_editor/src/default_editor/blocks/indentation.dart'; import 'package:super_editor/src/default_editor/multi_node_editing.dart'; import 'package:super_editor/src/default_editor/paragraph.dart'; import 'package:super_editor/src/default_editor/text.dart'; +import 'package:super_editor/src/default_editor/text_tools.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/composable_text.dart'; @@ -163,6 +164,8 @@ class TaskComponentBuilder implements ComponentBuilder { return null; } + final textDirection = getParagraphDirection(node.text.toPlainText()); + return TaskComponentViewModel( nodeId: node.id, padding: EdgeInsets.zero, @@ -177,6 +180,8 @@ class TaskComponentBuilder implements ComponentBuilder { ]); }, text: node.text, + textDirection: textDirection, + textAlignment: textDirection == TextDirection.ltr ? TextAlign.left : TextAlign.right, textStyleBuilder: noStyleBuilder, selectionColor: const Color(0x00000000), ); @@ -273,6 +278,7 @@ class TaskComponentViewModel extends SingleColumnLayoutComponentViewModel with T textStyleBuilder: textStyleBuilder, inlineWidgetBuilders: inlineWidgetBuilders, textDirection: textDirection, + textAlignment: textAlignment, selection: selection, selectionColor: selectionColor, highlightWhenEmpty: highlightWhenEmpty, @@ -376,39 +382,44 @@ class _TaskComponentState extends State with ProxyDocumentCompone @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: widget.viewModel.indentCalculator( - widget.viewModel.textStyleBuilder({}), - widget.viewModel.indent, + return Directionality( + textDirection: widget.viewModel.textDirection, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: widget.viewModel.indentCalculator( + widget.viewModel.textStyleBuilder({}), + widget.viewModel.indent, + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 16, right: 4), - child: Checkbox( - visualDensity: Theme.of(context).visualDensity, - value: widget.viewModel.isComplete, - onChanged: (newValue) { - widget.viewModel.setComplete(newValue!); - }, + Padding( + padding: const EdgeInsets.only(left: 16, right: 4), + child: Checkbox( + visualDensity: Theme.of(context).visualDensity, + value: widget.viewModel.isComplete, + onChanged: (newValue) { + widget.viewModel.setComplete(newValue!); + }, + ), ), - ), - Expanded( - child: TextComponent( - key: _textKey, - text: widget.viewModel.text, - textStyleBuilder: _computeStyles, - inlineWidgetBuilders: widget.viewModel.inlineWidgetBuilders, - textSelection: widget.viewModel.selection, - selectionColor: widget.viewModel.selectionColor, - highlightWhenEmpty: widget.viewModel.highlightWhenEmpty, - underlines: widget.viewModel.createUnderlines(), - showDebugPaint: widget.showDebugPaint, + Expanded( + child: TextComponent( + key: _textKey, + text: widget.viewModel.text, + textDirection: widget.viewModel.textDirection, + textAlign: widget.viewModel.textAlignment, + textStyleBuilder: _computeStyles, + inlineWidgetBuilders: widget.viewModel.inlineWidgetBuilders, + textSelection: widget.viewModel.selection, + selectionColor: widget.viewModel.selectionColor, + highlightWhenEmpty: widget.viewModel.highlightWhenEmpty, + underlines: widget.viewModel.createUnderlines(), + showDebugPaint: widget.showDebugPaint, + ), ), - ), - ], + ], + ), ); } } diff --git a/super_editor/lib/src/super_textfield/android/android_textfield.dart b/super_editor/lib/src/super_textfield/android/android_textfield.dart index e4aefd1128..6275f1fa80 100644 --- a/super_editor/lib/src/super_textfield/android/android_textfield.dart +++ b/super_editor/lib/src/super_textfield/android/android_textfield.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:follow_the_leader/follow_the_leader.dart'; +import 'package:super_editor/src/default_editor/text_tools.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; import 'package:super_editor/src/infrastructure/flutter/build_context.dart'; import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart'; @@ -32,7 +33,7 @@ class SuperAndroidTextField extends StatefulWidget { this.focusNode, this.tapRegionGroupId, this.textController, - this.textAlign = TextAlign.left, + this.textAlign, this.textStyleBuilder = defaultTextFieldStyleBuilder, this.hintBehavior = HintBehavior.displayHintUntilFocus, this.hintBuilder, @@ -63,7 +64,10 @@ class SuperAndroidTextField extends StatefulWidget { final ImeAttributedTextEditingController? textController; /// The alignment to use for text in this text field. - final TextAlign textAlign; + /// + /// If `null`, the text alignment is determined by the text direction + /// of the content. + final TextAlign? textAlign; /// Text style factory that creates styles for the content in /// [textController] based on the attributions in that content. @@ -175,6 +179,21 @@ class SuperAndroidTextFieldState extends State late ImeAttributedTextEditingController _textEditingController; + /// The text direction of the first character in the text. + /// + /// Used to align and position the caret depending on whether + /// the text is RTL or LTR. + TextDirection? _contentTextDirection; + + /// The text direction applied to the inner text. + TextDirection get _textDirection => _contentTextDirection ?? TextDirection.ltr; + + TextAlign get _textAlign => + widget.textAlign ?? + ((_textDirection == TextDirection.ltr) // + ? TextAlign.left + : TextAlign.right); + final _magnifierLayerLink = LeaderLink(); late AndroidEditingOverlayController _editingOverlayController; @@ -217,6 +236,8 @@ class SuperAndroidTextFieldState extends State magnifierFocalPoint: _magnifierLayerLink, ); + _contentTextDirection = getParagraphDirection(_textEditingController.text.toPlainText()); + WidgetsBinding.instance.addObserver(this); if (_focusNode.hasFocus) { @@ -432,6 +453,10 @@ class SuperAndroidTextFieldState extends State if (_textEditingController.selection.isCollapsed) { _editingOverlayController.hideToolbar(); } + + setState(() { + _contentTextDirection = getParagraphDirection(_textEditingController.text.toPlainText()); + }); } void _onTextScrollChange() { @@ -576,7 +601,7 @@ class SuperAndroidTextFieldState extends State textScrollController: _textScrollController, textKey: _textContentKey, textEditingController: _textEditingController, - textAlign: widget.textAlign, + textAlign: _textAlign, minLines: widget.minLines, maxLines: widget.maxLines, lineHeight: widget.lineHeight, @@ -609,61 +634,65 @@ class SuperAndroidTextFieldState extends State ? _textEditingController.text.computeTextSpan(widget.textStyleBuilder) : TextSpan(text: "", style: widget.textStyleBuilder({})); - return SuperText( - key: _textContentKey, - richText: textSpan, - textAlign: widget.textAlign, - textScaler: MediaQuery.textScalerOf(context), - layerBeneathBuilder: (context, textLayout) { - final isTextEmpty = _textEditingController.text.isEmpty; - final showHint = widget.hintBuilder != null && - ((isTextEmpty && widget.hintBehavior == HintBehavior.displayHintUntilTextEntered) || - (isTextEmpty && !_focusNode.hasFocus && widget.hintBehavior == HintBehavior.displayHintUntilFocus)); - - return Stack( - children: [ - if (widget.textController?.selection.isValid == true) - // Selection highlight beneath the text. - TextLayoutSelectionHighlight( - textLayout: textLayout, - style: SelectionHighlightStyle( - color: widget.selectionColor, - ), - selection: widget.textController?.selection, - ), - // Underline beneath the composing region. - if (widget.textController?.composingRegion.isValid == true && widget.showComposingUnderline) - TextUnderlineLayer( - textLayout: textLayout, - style: StraightUnderlineStyle( - color: widget.textStyleBuilder({}).color ?? // - (Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), + return Directionality( + textDirection: _textDirection, + child: SuperText( + key: _textContentKey, + richText: textSpan, + textAlign: _textAlign, + textDirection: _textDirection, + textScaler: MediaQuery.textScalerOf(context), + layerBeneathBuilder: (context, textLayout) { + final isTextEmpty = _textEditingController.text.isEmpty; + final showHint = widget.hintBuilder != null && + ((isTextEmpty && widget.hintBehavior == HintBehavior.displayHintUntilTextEntered) || + (isTextEmpty && !_focusNode.hasFocus && widget.hintBehavior == HintBehavior.displayHintUntilFocus)); + + return Stack( + children: [ + if (widget.textController?.selection.isValid == true) + // Selection highlight beneath the text. + TextLayoutSelectionHighlight( + textLayout: textLayout, + style: SelectionHighlightStyle( + color: widget.selectionColor, + ), + selection: widget.textController?.selection, ), - underlines: [ - TextLayoutUnderline( - range: widget.textController!.composingRegion, + // Underline beneath the composing region. + if (widget.textController?.composingRegion.isValid == true && widget.showComposingUnderline) + TextUnderlineLayer( + textLayout: textLayout, + style: StraightUnderlineStyle( + color: widget.textStyleBuilder({}).color ?? // + (Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), ), - ], - ), - if (showHint) // - widget.hintBuilder!(context), - ], - ); - }, - layerAboveBuilder: (context, textLayout) { - if (!_focusNode.hasFocus) { - return const SizedBox(); - } - - return TextLayoutCaret( - textLayout: textLayout, - style: widget.caretStyle, - position: _textEditingController.selection.isCollapsed // - ? _textEditingController.selection.extent - : null, - blinkController: _caretBlinkController, - ); - }, + underlines: [ + TextLayoutUnderline( + range: widget.textController!.composingRegion, + ), + ], + ), + if (showHint) // + widget.hintBuilder!(context), + ], + ); + }, + layerAboveBuilder: (context, textLayout) { + if (!_focusNode.hasFocus) { + return const SizedBox(); + } + + return TextLayoutCaret( + textLayout: textLayout, + style: widget.caretStyle, + position: _textEditingController.selection.isCollapsed // + ? _textEditingController.selection.extent + : null, + blinkController: _caretBlinkController, + ); + }, + ), ); } diff --git a/super_editor/lib/src/super_textfield/desktop/desktop_textfield.dart b/super_editor/lib/src/super_textfield/desktop/desktop_textfield.dart index c61db35ea7..00685894a0 100644 --- a/super_editor/lib/src/super_textfield/desktop/desktop_textfield.dart +++ b/super_editor/lib/src/super_textfield/desktop/desktop_textfield.dart @@ -1,13 +1,13 @@ import 'dart:math'; import 'dart:ui' as ui; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide SelectableText; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:super_editor/src/core/document_layout.dart'; +import 'package:super_editor/src/default_editor/text_tools.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/actions.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; @@ -23,7 +23,6 @@ import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; import 'package:super_editor/src/infrastructure/platforms/mac/mac_ime.dart'; import 'package:super_editor/src/infrastructure/platforms/platform.dart'; import 'package:super_editor/src/infrastructure/text_input.dart'; -import 'package:super_editor/src/super_textfield/infrastructure/text_field_gestures_interaction_overrides.dart'; import 'package:super_editor/src/super_textfield/infrastructure/text_field_scroller.dart'; import 'package:super_editor/src/super_textfield/super_textfield.dart'; import 'package:super_text_layout/super_text_layout.dart'; @@ -52,7 +51,7 @@ class SuperDesktopTextField extends StatefulWidget { this.tapRegionGroupId, this.textController, this.textStyleBuilder = defaultTextFieldStyleBuilder, - this.textAlign = TextAlign.left, + this.textAlign, this.hintBehavior = HintBehavior.displayHintUntilFocus, this.hintBuilder, this.selectionHighlightStyle = const SelectionHighlightStyle( @@ -102,7 +101,10 @@ class SuperDesktopTextField extends StatefulWidget { final WidgetBuilder? hintBuilder; /// The alignment to use for text in this text field. - final TextAlign textAlign; + /// + /// If `null`, the text alignment is determined by the text direction + /// of the content. + final TextAlign? textAlign; /// The visual representation of the user's selection highlight. final SelectionHighlightStyle selectionHighlightStyle; @@ -170,6 +172,22 @@ class SuperDesktopTextFieldState extends State implements late SuperTextFieldContext _textFieldContext; late ImeAttributedTextEditingController _controller; + + /// The text direction of the first character in the text. + /// + /// Used to align and position the caret depending on whether + /// the text is RTL or LTR. + TextDirection? _contentTextDirection; + + /// The text direction applied to the inner text. + TextDirection get _textDirection => _contentTextDirection ?? TextDirection.ltr; + + TextAlign get _textAlign => + widget.textAlign ?? + ((_textDirection == TextDirection.ltr) // + ? TextAlign.left + : TextAlign.right); + late ScrollController _scrollController; late TextFieldScroller _textFieldScroller; @@ -198,6 +216,8 @@ class SuperDesktopTextFieldState extends State implements // Check if we need to update the selection. _updateSelectionAndComposingRegionOnFocusChange(); + + _contentTextDirection = getParagraphDirection(_controller.text.toPlainText()); } @override @@ -316,6 +336,12 @@ class SuperDesktopTextFieldState extends State implements // so that any pending visual content changes can happen before // attempting to calculate the visual position of the selection extent. onNextFrame((_) => _updateViewportHeight()); + + // Even though we calling `onNextFrame`, it doesn't necessarily mean + // a new frame will be scheduled. Call setState to ensure the text direction is updated. + setState(() { + _contentTextDirection = getParagraphDirection(_controller.text.toPlainText()); + }); } /// Returns true if the viewport height changed, false otherwise. @@ -433,7 +459,7 @@ class SuperDesktopTextFieldState extends State implements key: _textScrollKey, textKey: _textKey, textController: _controller, - textAlign: widget.textAlign, + textAlign: _textAlign, scrollController: _scrollController, viewportHeight: _viewportHeight, estimatedLineHeight: _getEstimatedLineHeight(), @@ -495,60 +521,64 @@ class SuperDesktopTextFieldState extends State implements } Widget _buildSelectableText() { - return SuperText( - key: _textKey, - richText: _controller.text.computeTextSpan(widget.textStyleBuilder), - textAlign: widget.textAlign, - textScaler: _textScaler, - layerBeneathBuilder: (context, textLayout) { - final isTextEmpty = _controller.text.isEmpty; - final showHint = widget.hintBuilder != null && - ((isTextEmpty && widget.hintBehavior == HintBehavior.displayHintUntilTextEntered) || - (isTextEmpty && !_focusNode.hasFocus && widget.hintBehavior == HintBehavior.displayHintUntilFocus)); - - return Stack( - children: [ - if (widget.textController?.selection.isValid == true) - // Selection highlight beneath the text. - TextLayoutSelectionHighlight( - textLayout: textLayout, - style: widget.selectionHighlightStyle, - selection: widget.textController?.selection, - ), - // Underline beneath the composing region. - if (widget.textController?.composingRegion.isValid == true && _shouldShowComposingUnderline) - TextUnderlineLayer( - textLayout: textLayout, - style: StraightUnderlineStyle( - color: widget.textStyleBuilder({}).color ?? // - (Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), + return Directionality( + textDirection: _textDirection, + child: SuperText( + key: _textKey, + richText: _controller.text.computeTextSpan(widget.textStyleBuilder), + textAlign: _textAlign, + textDirection: _textDirection, + textScaler: _textScaler, + layerBeneathBuilder: (context, textLayout) { + final isTextEmpty = _controller.text.isEmpty; + final showHint = widget.hintBuilder != null && + ((isTextEmpty && widget.hintBehavior == HintBehavior.displayHintUntilTextEntered) || + (isTextEmpty && !_focusNode.hasFocus && widget.hintBehavior == HintBehavior.displayHintUntilFocus)); + + return Stack( + children: [ + if (widget.textController?.selection.isValid == true) + // Selection highlight beneath the text. + TextLayoutSelectionHighlight( + textLayout: textLayout, + style: widget.selectionHighlightStyle, + selection: widget.textController?.selection, ), - underlines: [ - TextLayoutUnderline( - range: widget.textController!.composingRegion, + // Underline beneath the composing region. + if (widget.textController?.composingRegion.isValid == true && _shouldShowComposingUnderline) + TextUnderlineLayer( + textLayout: textLayout, + style: StraightUnderlineStyle( + color: widget.textStyleBuilder({}).color ?? // + (Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), ), - ], - ), - if (showHint) // - Align( - alignment: Alignment.centerLeft, - child: widget.hintBuilder!(context), - ), - ], - ); - }, - layerAboveBuilder: (context, textLayout) { - if (!_focusNode.hasFocus) { - return const SizedBox(); - } - - return TextLayoutCaret( - textLayout: textLayout, - style: widget.caretStyle, - position: _controller.selection.extent, - blinkTimingMode: widget.blinkTimingMode, - ); - }, + underlines: [ + TextLayoutUnderline( + range: widget.textController!.composingRegion, + ), + ], + ), + if (showHint) // + Align( + alignment: Alignment.centerLeft, + child: widget.hintBuilder!(context), + ), + ], + ); + }, + layerAboveBuilder: (context, textLayout) { + if (!_focusNode.hasFocus) { + return const SizedBox(); + } + + return TextLayoutCaret( + textLayout: textLayout, + style: widget.caretStyle, + position: _controller.selection.extent, + blinkTimingMode: widget.blinkTimingMode, + ); + }, + ), ); } } diff --git a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart index 6cdd9baaef..be00a479a0 100644 --- a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart +++ b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart @@ -2,6 +2,7 @@ import 'package:attributed_text/attributed_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:follow_the_leader/follow_the_leader.dart'; +import 'package:super_editor/src/default_editor/text_tools.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/flutter/build_context.dart'; @@ -41,7 +42,7 @@ class SuperIOSTextField extends StatefulWidget { this.tapHandlers = const [], this.textController, this.textStyleBuilder = defaultTextFieldStyleBuilder, - this.textAlign = TextAlign.left, + this.textAlign, this.padding, this.hintBehavior = HintBehavior.displayHintUntilFocus, this.hintBuilder, @@ -73,7 +74,10 @@ class SuperIOSTextField extends StatefulWidget { final ImeAttributedTextEditingController? textController; /// The alignment to use for text in this text field. - final TextAlign textAlign; + /// + /// If `null`, the text alignment is determined by the text direction + /// of the content. + final TextAlign? textAlign; /// Text style factory that creates styles for the content in /// [textController] based on the attributions in that content. @@ -181,6 +185,22 @@ class SuperIOSTextFieldState extends State late FocusNode _focusNode; late ImeAttributedTextEditingController _textEditingController; + + /// The text direction of the first character in the text. + /// + /// Used to align and position the caret depending on whether + /// the text is RTL or LTR. + TextDirection? _contentTextDirection; + + /// The text direction applied to the inner text. + TextDirection get _textDirection => _contentTextDirection ?? TextDirection.ltr; + + TextAlign get _textAlign => + widget.textAlign ?? + ((_textDirection == TextDirection.ltr) // + ? TextAlign.left + : TextAlign.right); + late FloatingCursorController _floatingCursorController; final _toolbarLeaderLink = LeaderLink(); @@ -237,6 +257,8 @@ class SuperIOSTextFieldState extends State overlayController: _overlayController, ); + _contentTextDirection = getParagraphDirection(_textEditingController.text.toPlainText()); + WidgetsBinding.instance.addObserver(this); if (_focusNode.hasFocus) { @@ -451,6 +473,10 @@ class SuperIOSTextFieldState extends State if (_textEditingController.selection.isCollapsed) { _editingOverlayController.hideToolbar(); } + + setState(() { + _contentTextDirection = getParagraphDirection(_textEditingController.text.toPlainText()); + }); } void _onTextScrollChange() { @@ -575,7 +601,7 @@ class SuperIOSTextFieldState extends State textScrollController: _textScrollController, textKey: _textContentKey, textEditingController: _textEditingController, - textAlign: widget.textAlign, + textAlign: _textAlign, minLines: widget.minLines, maxLines: widget.maxLines, lineHeight: widget.lineHeight, @@ -615,70 +641,74 @@ class SuperIOSTextFieldState extends State caretStyle = caretStyle.copyWith(color: caretColorOverride); } - return SuperText( - key: _textContentKey, - richText: textSpan, - textAlign: widget.textAlign, - textScaler: MediaQuery.textScalerOf(context), - layerBeneathBuilder: (context, textLayout) { - final isTextEmpty = _textEditingController.text.isEmpty; - final showHint = widget.hintBuilder != null && - ((isTextEmpty && widget.hintBehavior == HintBehavior.displayHintUntilTextEntered) || - (isTextEmpty && !_focusNode.hasFocus && widget.hintBehavior == HintBehavior.displayHintUntilFocus)); - - return Stack( - clipBehavior: Clip.none, - children: [ - if (_textEditingController.selection.isValid == true) - // Selection highlight beneath the text. - TextLayoutSelectionHighlight( - textLayout: textLayout, - style: SelectionHighlightStyle( - color: widget.selectionColor, - ), - selection: _textEditingController.selection, - ), - // Underline beneath the composing region. - if (_textEditingController.composingRegion.isValid == true && widget.showComposingUnderline) - TextUnderlineLayer( - textLayout: textLayout, - style: StraightUnderlineStyle( - color: widget.textStyleBuilder({}).color ?? // - (Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), + return Directionality( + textDirection: _textDirection, + child: SuperText( + key: _textContentKey, + richText: textSpan, + textAlign: _textAlign, + textDirection: _textDirection, + textScaler: MediaQuery.textScalerOf(context), + layerBeneathBuilder: (context, textLayout) { + final isTextEmpty = _textEditingController.text.isEmpty; + final showHint = widget.hintBuilder != null && + ((isTextEmpty && widget.hintBehavior == HintBehavior.displayHintUntilTextEntered) || + (isTextEmpty && !_focusNode.hasFocus && widget.hintBehavior == HintBehavior.displayHintUntilFocus)); + + return Stack( + clipBehavior: Clip.none, + children: [ + if (_textEditingController.selection.isValid == true) + // Selection highlight beneath the text. + TextLayoutSelectionHighlight( + textLayout: textLayout, + style: SelectionHighlightStyle( + color: widget.selectionColor, + ), + selection: _textEditingController.selection, ), - underlines: [ - TextLayoutUnderline( - range: _textEditingController.composingRegion, + // Underline beneath the composing region. + if (_textEditingController.composingRegion.isValid == true && widget.showComposingUnderline) + TextUnderlineLayer( + textLayout: textLayout, + style: StraightUnderlineStyle( + color: widget.textStyleBuilder({}).color ?? // + (Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), ), - ], + underlines: [ + TextLayoutUnderline( + range: _textEditingController.composingRegion, + ), + ], + ), + if (showHint) // + widget.hintBuilder!(context), + ], + ); + }, + layerAboveBuilder: (context, textLayout) { + if (!_focusNode.hasFocus) { + return const SizedBox(); + } + + return Stack( + clipBehavior: Clip.none, + children: [ + TextLayoutCaret( + textLayout: textLayout, + style: widget.caretStyle, + position: _textEditingController.selection.isCollapsed // + ? _textEditingController.selection.extent + : null, + blinkController: _caretBlinkController, ), - if (showHint) // - widget.hintBuilder!(context), - ], - ); - }, - layerAboveBuilder: (context, textLayout) { - if (!_focusNode.hasFocus) { - return const SizedBox(); - } - - return Stack( - clipBehavior: Clip.none, - children: [ - TextLayoutCaret( - textLayout: textLayout, - style: widget.caretStyle, - position: _textEditingController.selection.isCollapsed // - ? _textEditingController.selection.extent - : null, - blinkController: _caretBlinkController, - ), - IOSFloatingCursor( - controller: _floatingCursorController, - ), - ], - ); - }, + IOSFloatingCursor( + controller: _floatingCursorController, + ), + ], + ); + }, + ), ); } diff --git a/super_editor/lib/src/super_textfield/super_textfield.dart b/super_editor/lib/src/super_textfield/super_textfield.dart index 2e1c172176..37ff87fa89 100644 --- a/super_editor/lib/src/super_textfield/super_textfield.dart +++ b/super_editor/lib/src/super_textfield/super_textfield.dart @@ -60,7 +60,7 @@ class SuperTextField extends StatefulWidget { this.tapRegionGroupId, this.configuration, this.textController, - this.textAlign = TextAlign.left, + this.textAlign, this.textStyleBuilder = defaultTextFieldStyleBuilder, this.hintBehavior = HintBehavior.displayHintUntilFocus, this.hintBuilder, @@ -98,7 +98,10 @@ class SuperTextField extends StatefulWidget { final AttributedTextEditingController? textController; /// The alignment of the text in this text field. - final TextAlign textAlign; + /// + /// If `null`, the text alignment is determined by the text direction + /// of the content. + final TextAlign? textAlign; /// Text style factory that creates styles for the content in /// [textController] based on the attributions in that content. diff --git a/super_editor/test/super_editor/supereditor_test_tools.dart b/super_editor/test/super_editor/supereditor_test_tools.dart index 9399e5d83a..e33d1fcfd6 100644 --- a/super_editor/test/super_editor/supereditor_test_tools.dart +++ b/super_editor/test/super_editor/supereditor_test_tools.dart @@ -683,6 +683,7 @@ class _TestSuperEditorState extends State<_TestSuperEditor> { componentBuilders: [ ...widget.testConfiguration.addedComponents, ...(widget.testConfiguration.componentBuilders ?? defaultComponentBuilders), + if (widget.testConfiguration.componentBuilders == null) TaskComponentBuilder(widget.testDocumentContext.editor) ], scrollController: widget.testConfiguration.scrollController, documentOverlayBuilders: _createOverlayBuilders(), diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-android.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-android.png new file mode 100644 index 0000000000000000000000000000000000000000..fb114e12cdca6cd2fcd1def7226d779ab94c3372 GIT binary patch literal 3946 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 literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-iOS.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-iOS.png new file mode 100644 index 0000000000000000000000000000000000000000..9429d84e92b9f405a5bdd25ade6722eb9b479dd7 GIT binary patch literal 2128 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|WanPZ!6KiaBrZD*CAeia1<+#n)Dw!0y^~a3haS*AmAKd@KSb# zlbNjCUkO{cywZ)c^77xg^&3X}ZD3$XV&Y~{P(x*O=Rbe-t0?N7{=PTGrL~#*)x7op z%Jd(IbCni*KYg&Ui(zNPU3pp|8sE%<`eBuY9K zALd#dWMb&x)M03taR8a2YxeqW*%yI;>K*m-=E>c+-(zoWKhMvPzwHc5vHA6S`+fWN z__n*-yY)|uj$O0t_Uzpklg~^O#xMrO;2ZZ|Ffz<&KU=l$y4JZ$^RmO7Vhj#G35*O! zScqVV&Hj4r?|%jcMfK5KGg@?v7Mr8FfG)Yk44ofy`glX(f`uoOFa zhHwBu4M$1`0|V!FPZ!6KiaBp@-(P*dl=1k-@0JxCiw)f#>1|swGay9k^aQ=sqkkNa z%sRHI`#^Vs8*?kmdahr}J*Q^I=6K%DSd*&Fox|}c!+5d5nY=mcq&IipeRII_%^W=* zZMQ|Yi=ThK`>o=@^G7v#cI&_2oqYah-Lo*U@NjkphM7NC0?C+M7BL0~p9DsRBclw` z10lS({o5P4&)mi0G5&vEbgo>^SkNm7jDEvLRt5oK3K&OCpHFm`2bpYKZTsoP;d4)8 zCdZ3c>phTV;$~1#+rYq(1Z1pBSU=# z?Ty91KOYN!oH&1M-}3K!tN+iwf5Z5t-aqFA#si`Wm6erqYF<9E__jK}Prd%nlZw~! z`M$QD-xW46B(&}Qw*C6C&HHQ1-qcs!xA=MK^OCP=xcYc&i=OMXX{SW>S@KX zR?m}k_x?S<{dn>5`O@tCvJ&m}brr8HV_1OxH-2(;|2`|bzc<$=XJ%%;(u`q>7i2oX z6B!ltYky(s-}ZI#{k5+*e%yUuM&ABzeZtx8AOBT01GT+7xZc*H?A?z1oZZE@Z)`|B zERo!^_`J;ADzW#DK!@|}dcLmu81wFZR@>_9e((5r*xb*yo! zm7bn4`?KJosa|05io?e?er)RvKL0@I=R|8-rT2BSs~FBq0tU@W^%${prKvlk&a++@ z0*1u&gP%TqviN)KW#PX+KQ}&Ajy|@l^!2iNN2EUKJ27<3{rP)l;r;NkzW)2aHhw(0 zcD_V;ZQ}2Wr~2!CY`aZWHZUlB+xy<$uJ7-!8@ucGe|W_G*!uq8Z%bxJL<3Vml6qbJ zqo!*2y26w>*5!GVY}eQVE!;TUzUnjU_q_X6zat_dW~A_E=jWvsmX^K}45-LB|8C>= zm3zhS{6F~EUS{|HebtkE4{UvTMc%Jh-@f2~#pg4#lY0!Wt&g|odt-0$Ztw4VZzSF4 zTUwR9`*rF69qSL1o0G5q-MuNj;A4#|2g62dyW-;&AAbrzermtZCM+!MSnl?FW_h=Y zf`9eM#@{gv6WgmiGZg4hz2>vC&H91Z-?q8)2QQTh9dKc`T#k{U!1N;k44ofy`glX(f`uoOFa zhHwBu4M$1`0|WbePZ!6KiaBrZ8v02Gia1=9>^|15C~Do6qB3D&f~?2_5l)RrK~bG! zbLT4gs;p8Gx09T&{P$Sz{nzXEnHYbrE4T1rU`PpD%+A0NA=SvrARr91-f)ybav&T{ zniHS5>}pkAsoXyM9y#6yW#tVF3`tDf3<_!l80x^#Pmm0IUHOG0|IyEvslQ6ftg3qM zd`q&vd-q{35*O!Si~3{e9#!~&woA3idy@ezj|H%x;LroA3o3j&QvFeI|@H< zO#gAVuDoLV|ERr>+Y8@*egB5_52rb94YW%R){?diH7cvW+ElfeC$9g0I_P^-@*_5^ z!DaToy#4SC#RUa*J7>z(+wA-G_x1Gr%g;Wio$x8}nfB|;xxBCJB5=qK?U@z(eQBFR@f$Q-HzK)z4*}Q$iB}SMT0^ literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-windows.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-ordered-list-item-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..b4042d8bf491ab81ebb11d85069bb6dfe261d56f GIT binary patch literal 2117 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|WbePZ!6KiaBrZ8v02Gia1=9>^|15C~Do6qB3D&f~?2_5l)RrK~bG! zbLT4gs;p8Gx09T&{P$Sz{nzXEnHYbrE4T1rU`PpD%+A0NA=SvrARr91-f)ybav&T{ zniHS5>}pkAsoXyM9y#6yW#tVF3`tDf3<_!l80x^#Pmm0IUHOG0|IyEvslQ6ftg3qM zd`q&vd-q{35*O!Si~3{e9#!~&woA3idy@ezj|H%x;LroA3o3j&QvFeI|@H< zO#gAVuDoLV|ERr>+Y8@*egB5_52rb94YW%R){?diH7cvW+ElfeC$9g0I_P^-@*_5^ z!DaToy#4SC#RUa*J7>z(+wA-G_x1Gr%g;Wio$x8}nfB|;xxBCJB5=qK?U@z(eQBFR@f$Q-HzK)z4*}Q$iB}SMT0^ literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-android.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-android.png new file mode 100644 index 0000000000000000000000000000000000000000..d2e21accd45851c8adeed0f47040baeb7c809c8c GIT binary patch literal 3427 zcmeHKYdDl?AAe@Th@CB|HY7x(134|xMCGt522BlyMmbE*nQ>}{kV?CuB1D;N)**+X ziCOG8ZSANTH4|Z3Mwu9tnM#eu@x4dAA9t_!!~0(Em!2=rbKlSZ{QLdy>wo{~=IVr0 zQdfc?2#&T8I`A?SCx^^o0-HaMiH3X(VhSBA6? z`@x{|D|bN2yS@tjQwhSzQ!r@geJbbd9Bt|Bbog*Yg7Pj;pZ5PoElc}0LA*L7I(#b! zyR#A{Sn~cgDAB7aVB*9=CaFi@!_40Pk8rj9HAtWBHQl|p=TYCz%E%Zap?Wz9x#s@` z=yE#xSf4Xz8b{ciQ0^rXjo(!xRM(^Z4cE{r@D+rKtdc-tLWLyVN;tW5fo#MdQTorw z%FAOh#Hc%Bn4aQ}B5`{7FpkzLNtu~n{Mffwo1RQt<7tPVc-X_Jrtqf;jPN1d&wImG z*Bd5Rd)Zm8+)-q_(bXsB(vBjW+MOUz<$Lxm5Y>HF-TC zZLmd9aUcL@2n2QyeclD0%c9L>dT;ju34*x_R=H2CIn6mtGQV@9ymFbv7nkgnrE9>&Xi1w29EIida3BLK;2 zU=s#xl4iou*H<8Bg(e79RRp^^F1?out4nT!SHuT*XTHDE+TuoW9Q0phi)VxlxKk9| z^1-hLp=6QSWoBVx}>FmGa(;F|NSfGN+b>S^XuN4sUM+a|`OdMYxe*|Na(f^?@M(}SpsIfVUE@@}0h9wM;%?Ky3Pqpl z%rrsAr0v@Rl#w3M-*_B`BUC-@iWLm46(*|M+Cg09nk&|7rbZuo;RgVF&Zz!Pru7s? zwpnWhPH%M!%*7D&sdS-V`wKws;xR4TGRb*XXQAb961gJK_U0j1@A!+NQb}J7+>=>b zoBi2zBMD((?bCj)ecAu>byYCy?We)XXFmDrh_H{!yOE7vI6bw}Zi~#dLGCTjAa1@p z#j)>60jl_xcko7I9`Ps^{t+NGxC0@k}JB5wuKMr)F$^fiABg!zKzo%+~PqbGKctbi|!}~kMjnd zPXuVzKeM>WsfmdHh1nRH)T3!ea52XEk|MmCPVVc`w2K7!z0L^#3nyw0^_9*&;O0Dj z9JCaGT(v6a{@$dJ#ur)FOyl@(9Pt|CzTiAls|oA(5;Z2)?U8a_GMM}cf)S`Z<4(e> zhyLnkP(?|g{KBw#E`uocea$|CZh4-wl=S$~IpWI_UDUf^$D%^O^ZTqB*Xy9sjEY0Q z&na`l8_i|D-CHe#1D%mjmtN(K$-N!rk&hxcCT!p32^1`QtA{+ONs@XxsUyC>ema9r zm}#Mt!kBN!3mK;7=9tXgu|fGZk+J!t51qu$L9al5)P0U&nI=BB+Q*Y(%_A&NKn9H~ qz79QK=f1xgN50O-|Env)o7M_reu1i0$_8*zf}9;(?P>e{&ixC{Z@-)X literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-iOS.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-iOS.png new file mode 100644 index 0000000000000000000000000000000000000000..e8f985c152ca30326c3802d10b72f8c41b1ec12d GIT binary patch literal 2054 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|Wa8PZ!6KiaBp@8)iudia1;hypU(&IrXeWbnEFgCzf(srx>rxN!5L2 zqIpR|n5j{^HoBP4$JpRQTzRFz?8}9bY77iU^WFFv7z#33#26fW5*Qhdj50_Kgl&u2 z8Ggj?i>>|t`1fx4>*@1ZBgDmZ7#e0AU}EUtB%5)g>`>Q?{?ph*}%Y%#Kg^@ zphhLeHcQ#*sb2&b7^WX#rJ}Laa^9#pqme@01V5Uh21kn8&(LtUUSjtNZzEt`&fw|l K=d#Wzp$P!%+_Jd< literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-linux.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..38c198d71204d71c0753baae565a6d1dc95e553c GIT binary patch literal 2051 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|Wb7PZ!6KiaBp@8+tJZia1>K^mrWAv+~aRR=wk=RI+F09Saf^WWHWw z@Z4cmW8?Q(1(gP~Q{%34GBlhi`vNrXfA0Y%h7L|0hK3oV3=#w3MjkK(m%hFG?Dq51 zd*W)v9+d4&U}QMLBF5n0Lq4NyF+0PKZI^$V$jZsP^?zKv{rclA`|rnp>Dzm3qj)@i z8)`oN`C_E|xoYbD&zHORz9|i^zAMzo${-*NOjCw*WNbWZwEp!AMFy?gyU%VvKmF(9 z_3ujTeA|z=Yx|Y%B+5fIt{i`~_y6n?c(5M7m&pk49P=uMhD%247rQeQ9AsixF`D{E li-6JMZ!{OsJQv)se!ZnPfNkO~ePGSY;OXk;vd$@?2>^KVSup?r literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-macOS.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-macOS.png new file mode 100644 index 0000000000000000000000000000000000000000..38c198d71204d71c0753baae565a6d1dc95e553c GIT binary patch literal 2051 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|Wb7PZ!6KiaBp@8+tJZia1>K^mrWAv+~aRR=wk=RI+F09Saf^WWHWw z@Z4cmW8?Q(1(gP~Q{%34GBlhi`vNrXfA0Y%h7L|0hK3oV3=#w3MjkK(m%hFG?Dq51 zd*W)v9+d4&U}QMLBF5n0Lq4NyF+0PKZI^$V$jZsP^?zKv{rclA`|rnp>Dzm3qj)@i z8)`oN`C_E|xoYbD&zHORz9|i^zAMzo${-*NOjCw*WNbWZwEp!AMFy?gyU%VvKmF(9 z_3ujTeA|z=Yx|Y%B+5fIt{i`~_y6n?c(5M7m&pk49P=uMhD%247rQeQ9AsixF`D{E li-6JMZ!{OsJQv)se!ZnPfNkO~ePGSY;OXk;vd$@?2>^KVSup?r literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-windows.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-paragraph-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..38c198d71204d71c0753baae565a6d1dc95e553c GIT binary patch literal 2051 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|Wb7PZ!6KiaBp@8+tJZia1>K^mrWAv+~aRR=wk=RI+F09Saf^WWHWw z@Z4cmW8?Q(1(gP~Q{%34GBlhi`vNrXfA0Y%h7L|0hK3oV3=#w3MjkK(m%hFG?Dq51 zd*W)v9+d4&U}QMLBF5n0Lq4NyF+0PKZI^$V$jZsP^?zKv{rclA`|rnp>Dzm3qj)@i z8)`oN`C_E|xoYbD&zHORz9|i^zAMzo${-*NOjCw*WNbWZwEp!AMFy?gyU%VvKmF(9 z_3ujTeA|z=Yx|Y%B+5fIt{i`~_y6n?c(5M7m&pk49P=uMhD%247rQeQ9AsixF`D{E li-6JMZ!{OsJQv)se!ZnPfNkO~ePGSY;OXk;vd$@?2>^KVSup?r literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-android.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-android.png new file mode 100644 index 0000000000000000000000000000000000000000..b250555fc2cae82add436c53279438fb2a1d9538 GIT binary patch literal 3780 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 literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-linux.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-task-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6219ccbf46dde0940dd0c49e028f4a9d207dc7 GIT binary patch literal 2422 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 Sv8&*lE7<4e?MijVUi>GQXyEMt literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-iOS.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-iOS.png new file mode 100644 index 0000000000000000000000000000000000000000..ec432fbebfbd1ce901d2786455e739fc64380773 GIT binary patch literal 2441 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|TeAr;B4q#hkZycX!96$}~L87x#<^dQ(5aR3Pw!P@uDvqhglk4Xq}D z{1p-hgAQJD;^yX7?G;}n*0rFw>xP8lMI~|F5S=?Fx0ZQaYKi2QR#lk%_3jzZPc{X! zjOEX~e;v0k@m%Drx%Y2KFfgP{6SrkxIB?Z1fsx?|ix`80&nSb$KzOsum7gK{;fICE zk58EIk1hE>@%Z=N-QVBGo$z6Zv7SSf@Vq{!S>MinTKF`zYT25PmA^NwZ;hXQSNwx{ zBP)Y|Ffer)VlXzo7PDpWIr-+#vDDw|o=#nQ`djR`um5j~7ibc4k_|gIgW9X_U+*8^ zDBss>|L;%oo4v*Fcf9V~eeB!Y+v2uj#R3rw28OSfUq2pv-p+F0uV1U{@5aZ*%C6u0 z>zBoc8;u*cJW<>j{7l`6VMfQz9J6~BpL+{W%bLfFgeUhDK07n>;}c{5p6d7e)7Q+8 zi$BqG#yx@YfJ@Adg2X+)|5-nNb93|mo$qeH-zR(AUv7U+ap6l~y7W1+e15H!-#jbJ z>$j%IS7x4mp7E{iVOhErX zy4Wr+yL;Vz)9bgI&+pyv6e^r_yifLV^LIO$+uL$~pD*RN{c)kdI;$J71$``F)AKUj1=EIj;pbpAeDyMHexx1KrnXO9*;gIZed^IJb2ebztz>*d$N zFTWSxbFE+%0)}W{w)OR6x6W;K?LVmYqf>xEL2UyALlV$22*$JcmJj@=wY|IL61{I+ zpZYZGu6T?$j`*CG_T1b|%$DT}pS+b>U(M&+uHWS=Ywqu;{{OD^49kJ7hdHU8-C;HY u%abEiH+-~k8ZDtmYp2m%fHfEVV+wse)7+`58Q6ScVDNPHb6Mw<&;$U;(!O2* literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-linux.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..3e2afda0c436379805d27427aa47eeca530a5f3c GIT binary patch literal 2429 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|TeJr;B4q#hkZy_jbpG$}~JQ?pD#!{APbbDnR5Vw<$eC6GyT}9S2>%me-u7{V|uOb-&^nYHSFimVQ83ffQg}l6N7Pg z%Nxc7FJSbpySXeYS^9Ce{{A1wpJ!)hKVB*xFXt|2 zZDlL>6;GdNKR#FeeuvV!V-k=5{QR8!c%}L=X8ybOX6JMDQr*Oq zH!vh9>BaA}xmWrAZQ;X1t@W|ruU?PueSf~D{_L|yuYg{g(GfgrT z&YRtp_nor)?f<^%I}a7Mt^U^Y{r=yCH@CLFj=wHj_vAyxr%v_tH)CRKftg0Q=*5GD zA5Zu1?`xN@`O)?M+O=!PR;$NL%h&$+7?hS=_)m+SK`pKJ{jVQyE}uXCZ0Fg+U$-y+ z>;6#h0dn=)_p$P?tFKSDWxM@AP!U`7oLSHDFdrC~r{xmeZ(RR>`s*(J9TP1MVvYW@ zpo~4umPG`VB=h%tJtjJTf7Pe08w*pCHH{eyl8c(CoXI!d{B!BrwN32|3_IrGa1&-C t9TmYEd85HH8Z5ZV_0hC}o;GgGKb<+P>OoWYd|(rb!PC{xWt~$(697M~3u6EP literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-macOS.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-macOS.png new file mode 100644 index 0000000000000000000000000000000000000000..3e2afda0c436379805d27427aa47eeca530a5f3c GIT binary patch literal 2429 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|TeJr;B4q#hkZy_jbpG$}~JQ?pD#!{APbbDnR5Vw<$eC6GyT}9S2>%me-u7{V|uOb-&^nYHSFimVQ83ffQg}l6N7Pg z%Nxc7FJSbpySXeYS^9Ce{{A1wpJ!)hKVB*xFXt|2 zZDlL>6;GdNKR#FeeuvV!V-k=5{QR8!c%}L=X8ybOX6JMDQr*Oq zH!vh9>BaA}xmWrAZQ;X1t@W|ruU?PueSf~D{_L|yuYg{g(GfgrT z&YRtp_nor)?f<^%I}a7Mt^U^Y{r=yCH@CLFj=wHj_vAyxr%v_tH)CRKftg0Q=*5GD zA5Zu1?`xN@`O)?M+O=!PR;$NL%h&$+7?hS=_)m+SK`pKJ{jVQyE}uXCZ0Fg+U$-y+ z>;6#h0dn=)_p$P?tFKSDWxM@AP!U`7oLSHDFdrC~r{xmeZ(RR>`s*(J9TP1MVvYW@ zpo~4umPG`VB=h%tJtjJTf7Pe08w*pCHH{eyl8c(CoXI!d{B!BrwN32|3_IrGa1&-C t9TmYEd85HH8Z5ZV_0hC}o;GgGKb<+P>OoWYd|(rb!PC{xWt~$(697M~3u6EP literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-windows.png b/super_editor/test_goldens/editor/goldens/super-editor-rtl-caret-at-leftmost-character-unordered-list-item-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..3e2afda0c436379805d27427aa47eeca530a5f3c GIT binary patch literal 2429 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1{5hWm74*i7>k44ofy`glX(f`uoOFa zhHwBu4M$1`0|TeJr;B4q#hkZy_jbpG$}~JQ?pD#!{APbbDnR5Vw<$eC6GyT}9S2>%me-u7{V|uOb-&^nYHSFimVQ83ffQg}l6N7Pg z%Nxc7FJSbpySXeYS^9Ce{{A1wpJ!)hKVB*xFXt|2 zZDlL>6;GdNKR#FeeuvV!V-k=5{QR8!c%}L=X8ybOX6JMDQr*Oq zH!vh9>BaA}xmWrAZQ;X1t@W|ruU?PueSf~D{_L|yuYg{g(GfgrT z&YRtp_nor)?f<^%I}a7Mt^U^Y{r=yCH@CLFj=wHj_vAyxr%v_tH)CRKftg0Q=*5GD zA5Zu1?`xN@`O)?M+O=!PR;$NL%h&$+7?hS=_)m+SK`pKJ{jVQyE}uXCZ0Fg+U$-y+ z>;6#h0dn=)_p$P?tFKSDWxM@AP!U`7oLSHDFdrC~r{xmeZ(RR>`s*(J9TP1MVvYW@ zpo~4umPG`VB=h%tJtjJTf7Pe08w*pCHH{eyl8c(CoXI!d{B!BrwN32|3_IrGa1&-C t9TmYEd85HH8Z5ZV_0hC}o;GgGKb<+P>OoWYd|(rb!PC{xWt~$(697M~3u6EP literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/editor/supereditor_rtl_test.dart b/super_editor/test_goldens/editor/supereditor_rtl_test.dart new file mode 100644 index 0000000000..651dfdd13f --- /dev/null +++ b/super_editor/test_goldens/editor/supereditor_rtl_test.dart @@ -0,0 +1,128 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_robots/flutter_test_robots.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:super_editor/src/test/ime.dart'; +import 'package:super_editor/src/test/super_editor_test/supereditor_robot.dart'; +import 'package:super_editor/super_editor.dart'; + +import '../../test/super_editor/supereditor_test_tools.dart'; +import '../test_tools_goldens.dart'; + +void main() { + group('SuperEditor > RTL mode >', () { + testGoldensOnAllPlatforms( + 'inserts text and paints caret on the left side of paragraph for downstream position', + (tester) async { + await tester // + .createDocument() + .withSingleEmptyParagraph() + .withInputSource(TextInputSource.ime) + .pump(); + + // Place the caret at the beginning of the paragraph. + await tester.placeCaretInParagraph('1', 0); + + // Type the text "Example". + await tester.ime.typeText( + 'مثال', + getter: imeClientGetter, + ); + + await screenMatchesGolden( + tester, 'super-editor-rtl-caret-at-leftmost-character-paragraph-${defaultTargetPlatform.name}'); + }, + windowSize: goldenSizeSmall, + ); + + testGoldensOnAllPlatforms( + 'inserts text and paints caret on the left side of unordered list item for downstream position', + (tester) async { + await tester // + .createDocument() + .withCustomContent( + MutableDocument( + nodes: [ + ListItemNode.unordered(id: '1', text: AttributedText()), + ], + ), + ) + .withInputSource(TextInputSource.ime) + .pump(); + + // Place the caret at the beginning of the list item. + await tester.placeCaretInParagraph('1', 0); + + // Type the text "Example". + await tester.ime.typeText( + 'مثال', + getter: imeClientGetter, + ); + + await screenMatchesGolden( + tester, 'super-editor-rtl-caret-at-leftmost-character-unordered-list-item-${defaultTargetPlatform.name}'); + }, + windowSize: goldenSizeSmall, + ); + + testGoldensOnAllPlatforms( + 'inserts text and paints caret on the left side of ordered list item for downstream position', + (tester) async { + await tester // + .createDocument() + .withCustomContent( + MutableDocument( + nodes: [ + ListItemNode.ordered(id: '1', text: AttributedText()), + ], + ), + ) + .withInputSource(TextInputSource.ime) + .pump(); + + // Place the caret at the beginning of the list item. + await tester.placeCaretInParagraph('1', 0); + + // Type the text "Example". + await tester.ime.typeText( + 'مثال', + getter: imeClientGetter, + ); + + await screenMatchesGolden( + tester, 'super-editor-rtl-caret-at-leftmost-character-ordered-list-item-${defaultTargetPlatform.name}'); + }, + windowSize: goldenSizeSmall, + ); + + testGoldensOnAllPlatforms( + 'inserts text and paints caret on the left side of task for downstream position', + (tester) async { + await tester // + .createDocument() + .withCustomContent( + MutableDocument( + nodes: [ + TaskNode(id: '1', text: AttributedText(), isComplete: false), + ], + ), + ) + .withInputSource(TextInputSource.ime) + .pump(); + + // Place the caret at the beginning of the task. + await tester.placeCaretInParagraph('1', 0); + + // Type the text "Example". + await tester.ime.typeText( + 'مثال', + getter: imeClientGetter, + ); + + await screenMatchesGolden( + tester, 'super-editor-rtl-caret-at-leftmost-character-task-${defaultTargetPlatform.name}'); + }, + windowSize: goldenSizeSmall, + ); + }); +} diff --git a/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-android.png b/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-android.png new file mode 100644 index 0000000000000000000000000000000000000000..ece7a4aa642ff468fe2b7148fbe6bce26f573202 GIT binary patch literal 2921 zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?4mP03zO)&4fD~hKkh>GZx^prwfgF}%C(jTL zAgJL;>0n^s7WQ;;45^s&_Vz~K!vPX*fzqk1JqJV*g`Xu)@j1wmIq}lXDJC2uMJJh` z&$;CFajNvg*%-AMv%fPTq zevjJjEBX8FHviB5z72;;OuZC}K&;N3_3UrqmzLEx?61Y&e&58(ARr8^3=A8Q8K1iZ z8fHiNw_g-qm-Ij4f9ig2Qp@g~xO1GZx^prwfgF}%C(jTL zAgJL;>0n^s7WZ^<45^s&_Vz(vra%d|i;~XVCJE9J4ZIOcd2cY}-e8CpD(kThcWaKR zw3@eBDMjr5r)U26YV34RvoJV3%lpE>(6ChyXr*BzD}#XWC}T7bMpMFQE*Rpu;P#p0 z?H8pPB07IRpU(fkzP767zis|^^O!GNpKjRwqWX7q_TBeQSj)THJ#q{Tm(AlQ$G%z~ zKmX?co&UGpXXR#4P}{)3ki}9-p%;a2-*PDD`lbONO)z4*}Q$iB}5$};? literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-linux.png b/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..59f46310cc725af514a453afcba3475f11744298 GIT binary patch literal 2929 zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?4mP03zO)&4fD~hKkh>GZx^prwfgF}%C(jTL zAgJL;>0n^smiBaU45^s&_O_uBOQ3|?#p_wy6jGFvqnEgy+LhB%5EN>pD;8iHBlmdD z7V95}qU*0l$IZK5%D^zea;7ZxY17S2JjOKzNo(poPE$_Z5 z&0wLl`*wR_@pp@35|4j<%6YTy-2Iw(zxU61s*mrwFZX5t-?hni-#4)`2nYkqTf;_V z#_cn53=EHj*PrD3dUyT#H~(+c-^QU5Q!j-gV5^gRHqN{Lo7 z7=wck3S*BF%LDhdzu&&$j8FO>Q=ht@8$$`ECKQpJ%F4LwHV@IV( tjfl|@8x1jRS$s5GVJ-QH4VE{ea=XmWeGTam0XCZ%JYD@<);T3K0RYOk>0$r? literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-macOS.png b/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-macOS.png new file mode 100644 index 0000000000000000000000000000000000000000..59f46310cc725af514a453afcba3475f11744298 GIT binary patch literal 2929 zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?4mP03zO)&4fD~hKkh>GZx^prwfgF}%C(jTL zAgJL;>0n^smiBaU45^s&_O_uBOQ3|?#p_wy6jGFvqnEgy+LhB%5EN>pD;8iHBlmdD z7V95}qU*0l$IZK5%D^zea;7ZxY17S2JjOKzNo(poPE$_Z5 z&0wLl`*wR_@pp@35|4j<%6YTy-2Iw(zxU61s*mrwFZX5t-?hni-#4)`2nYkqTf;_V z#_cn53=EHj*PrD3dUyT#H~(+c-^QU5Q!j-gV5^gRHqN{Lo7 z7=wck3S*BF%LDhdzu&&$j8FO>Q=ht@8$$`ECKQpJ%F4LwHV@IV( tjfl|@8x1jRS$s5GVJ-QH4VE{ea=XmWeGTam0XCZ%JYD@<);T3K0RYOk>0$r? literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-windows.png b/super_editor/test_goldens/super_textfield/goldens/super-text-field_rtl-caret-at-leftmost-character-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..59f46310cc725af514a453afcba3475f11744298 GIT binary patch literal 2929 zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?4mP03zO)&4fD~hKkh>GZx^prwfgF}%C(jTL zAgJL;>0n^smiBaU45^s&_O_uBOQ3|?#p_wy6jGFvqnEgy+LhB%5EN>pD;8iHBlmdD z7V95}qU*0l$IZK5%D^zea;7ZxY17S2JjOKzNo(poPE$_Z5 z&0wLl`*wR_@pp@35|4j<%6YTy-2Iw(zxU61s*mrwFZX5t-?hni-#4)`2nYkqTf;_V z#_cn53=EHj*PrD3dUyT#H~(+c-^QU5Q!j-gV5^gRHqN{Lo7 z7=wck3S*BF%LDhdzu&&$j8FO>Q=ht@8$$`ECKQpJ%F4LwHV@IV( tjfl|@8x1jRS$s5GVJ-QH4VE{ea=XmWeGTam0XCZ%JYD@<);T3K0RYOk>0$r? literal 0 HcmV?d00001 diff --git a/super_editor/test_goldens/super_textfield/super_textfield_rtl_test.dart b/super_editor/test_goldens/super_textfield/super_textfield_rtl_test.dart new file mode 100644 index 0000000000..610d6465c9 --- /dev/null +++ b/super_editor/test_goldens/super_textfield/super_textfield_rtl_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_robots/flutter_test_robots.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:super_editor/super_editor.dart'; +import 'package:super_editor/super_reader_test.dart'; + +import '../../test/super_textfield/super_textfield_robot.dart'; +import '../test_tools_goldens.dart'; + +void main() { + group('SuperTextfield > RTL mode >', () { + testGoldensOnAllPlatforms( + 'inserts text and paints caret on the left side for downstream position', + (tester) async { + await _pumpTestApp(tester); + + // Place the caret at the beginning of the text field. + await tester.placeCaretInSuperTextField(0); + + // Type the text "Example". + await tester.ime.typeText( + 'مثال', + getter: imeClientGetter, + ); + await tester.pumpAndSettle(); + + await screenMatchesGolden( + tester, 'super-text-field_rtl-caret-at-leftmost-character-${defaultTargetPlatform.name}'); + }, + windowSize: const Size(600, 600), + ); + }); +} + +/// Pump a widget tree with a centered multiline textfield with +/// a yellow background, so we can clearly see the bounds of the textfield. +Future _pumpTestApp(WidgetTester tester) async { + final controller = ImeAttributedTextEditingController(); + + await tester.pumpWidget( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: ColoredBox( + color: Colors.yellow, + child: SuperTextField( + textController: controller, + maxLines: 10, + lineHeight: 16, + ), + ), + ), + ), + ), + ), + ); +} diff --git a/super_text_layout/lib/src/super_text.dart b/super_text_layout/lib/src/super_text.dart index 34c4ba9285..936ff8b0a5 100644 --- a/super_text_layout/lib/src/super_text.dart +++ b/super_text_layout/lib/src/super_text.dart @@ -90,6 +90,7 @@ class SuperTextState extends ProseTextState with ProseTextBlock { text: LayoutAwareRichText( text: widget.richText, textAlign: widget.textAlign, + textDirection: widget.textDirection, textScaler: widget.textScaler ?? MediaQuery.textScalerOf(context), onMarkNeedsLayout: _invalidateParagraph, ), @@ -295,12 +296,14 @@ class LayoutAwareRichText extends RichText { Key? key, required InlineSpan text, TextAlign textAlign = TextAlign.left, + TextDirection textDirection = TextDirection.ltr, TextScaler textScaler = TextScaler.noScaling, required this.onMarkNeedsLayout, }) : super( key: key, text: text, textAlign: textAlign, + textDirection: textDirection, textScaler: textScaler, );