@@ -94,6 +94,64 @@ class QuillRawEditorState extends EditorState
94
94
bool get dirty => _dirty;
95
95
bool _dirty = false ;
96
96
97
+ // Completely copied from flutter:
98
+ // https://github.com/flutter/flutter/blob/3.29.0/packages/flutter/lib/src/widgets/editable_text.dart#L3741
99
+ // Finds the closest scroll offset to the current scroll offset that fully
100
+ // reveals the given caret rect. If the given rect's main axis extent is too
101
+ // large to be fully revealed in `renderEditable`, it will be centered along
102
+ // the main axis.
103
+ //
104
+ // If this is a multiline EditableText (which means the Editable can only
105
+ // scroll vertically), the given rect's height will first be extended to match
106
+ // `renderEditable.preferredLineHeight`, before the target scroll offset is
107
+ // calculated.
108
+ RevealedOffset _getOffsetToRevealCaret (Rect rect) {
109
+ if (! _scrollController.position.allowImplicitScrolling) {
110
+ return RevealedOffset (offset: _scrollController.offset, rect: rect);
111
+ }
112
+
113
+ final Size editableSize = renderEditable.size;
114
+ final double additionalOffset;
115
+ final Offset unitOffset;
116
+
117
+ if (! _isMultiline) {
118
+ additionalOffset = rect.width >= editableSize.width
119
+ // Center `rect` if it's oversized.
120
+ ? editableSize.width / 2 - rect.center.dx
121
+ // Valid additional offsets range from (rect.right - size.width)
122
+ // to (rect.left). Pick the closest one if out of range.
123
+ : clampDouble (0.0 , rect.right - editableSize.width, rect.left);
124
+ unitOffset = const Offset (1 , 0 );
125
+ } else {
126
+ // The caret is vertically centered within the line. Expand the caret's
127
+ // height so that it spans the line because we're going to ensure that the
128
+ // entire expanded caret is scrolled into view.
129
+ final Rect expandedRect = Rect .fromCenter (
130
+ center: rect.center,
131
+ width: rect.width,
132
+ height: math.max (rect.height, renderEditable.preferredLineHeight),
133
+ );
134
+
135
+ additionalOffset = expandedRect.height >= editableSize.height
136
+ ? editableSize.height / 2 - expandedRect.center.dy
137
+ : clampDouble (
138
+ 0.0 , expandedRect.bottom - editableSize.height, expandedRect.top);
139
+ unitOffset = const Offset (0 , 1 );
140
+ }
141
+
142
+ // No overscrolling when encountering tall fonts/scripts that extend past
143
+ // the ascent.
144
+ final double targetOffset = clampDouble (
145
+ additionalOffset + _scrollController.offset,
146
+ _scrollController.position.minScrollExtent,
147
+ _scrollController.position.maxScrollExtent,
148
+ );
149
+
150
+ final double offsetDelta = _scrollController.offset - targetOffset;
151
+ return RevealedOffset (
152
+ rect: rect.shift (unitOffset * offsetDelta), offset: targetOffset);
153
+ }
154
+
97
155
@override
98
156
void insertContent (KeyboardInsertedContent content) {
99
157
assert (widget.config.contentInsertionConfiguration? .allowedMimeTypes
@@ -537,7 +595,6 @@ class QuillRawEditorState extends EditorState
537
595
final requestKeyboardFocusOnCheckListChanged =
538
596
widget.config.requestKeyboardFocusOnCheckListChanged;
539
597
if (! (widget.config.checkBoxReadOnly ?? widget.config.readOnly)) {
540
- _disableScrollControllerAnimateOnce = true ;
541
598
final currentSelection = controller.selection.copyWith ();
542
599
final attribute = value ? Attribute .checked : Attribute .unchecked;
543
600
@@ -1031,7 +1088,7 @@ class QuillRawEditorState extends EditorState
1031
1088
if (ignoreCaret) {
1032
1089
return ;
1033
1090
}
1034
- _showCaretOnScreen ( );
1091
+ _scheduleShowCaretOnScreen (withAnimation : true );
1035
1092
_cursorCont.startOrStopCursorTimerIfNeeded (_hasFocus, controller.selection);
1036
1093
if (hasConnection) {
1037
1094
// To keep the cursor from blinking while typing, we want to restart the
@@ -1101,7 +1158,7 @@ class QuillRawEditorState extends EditorState
1101
1158
_updateOrDisposeSelectionOverlayIfNeeded ();
1102
1159
if (_hasFocus) {
1103
1160
WidgetsBinding .instance.addObserver (this );
1104
- _showCaretOnScreen ( );
1161
+ _scheduleShowCaretOnScreen (withAnimation : true );
1105
1162
} else {
1106
1163
WidgetsBinding .instance.removeObserver (this );
1107
1164
}
@@ -1120,53 +1177,95 @@ class QuillRawEditorState extends EditorState
1120
1177
return widget.config.linkActionPickerDelegate (context, link, linkNode);
1121
1178
}
1122
1179
1123
- bool _showCaretOnScreenScheduled = false ;
1180
+ // Animation configuration for scrolling the caret back on screen.
1181
+ static const Duration _caretAnimationDuration = Duration (milliseconds: 100 );
1182
+ static const Curve _caretAnimationCurve = Curves .fastOutSlowIn;
1124
1183
1125
- // This is a workaround for checkbox tapping issue
1126
- // https://github.com/singerdmx/flutter-quill/issues/619
1127
- // We cannot treat {"list": "checked"} and {"list": "unchecked"} as
1128
- // block of the same style
1129
- // This causes controller.selection to go to offset 0
1130
- bool _disableScrollControllerAnimateOnce = false ;
1184
+ bool _showCaretOnScreenScheduled = false ;
1131
1185
1132
- void _showCaretOnScreen () {
1133
- if (! widget.config.showCursor || _showCaretOnScreenScheduled) {
1186
+ // Completely copied from flutter:
1187
+ // https://github.com/flutter/flutter/blob/3.29.0/packages/flutter/lib/src/widgets/editable_text.dart#L4228
1188
+ void _scheduleShowCaretOnScreen ({required bool withAnimation}) {
1189
+ if (_showCaretOnScreenScheduled) {
1134
1190
return ;
1135
1191
}
1136
-
1137
1192
_showCaretOnScreenScheduled = true ;
1138
- SchedulerBinding .instance.addPostFrameCallback ((_) {
1139
- if (widget.config.scrollable || _scrollController.hasClients) {
1140
- _showCaretOnScreenScheduled = false ;
1193
+ SchedulerBinding .instance.addPostFrameCallback ((Duration _) {
1194
+ _showCaretOnScreenScheduled = false ;
1195
+ // Since we are in a post frame callback, check currentContext in case
1196
+ // RenderEditable has been disposed (in which case it will be null).
1197
+ final RenderEditable ? renderEditable =
1198
+ _editableKey.currentContext? .findRenderObject () as RenderEditable ? ;
1199
+ if (renderEditable == null ||
1200
+ ! (renderEditable.selection? .isValid ?? false ) ||
1201
+ ! _scrollController.hasClients) {
1202
+ return ;
1203
+ }
1141
1204
1142
- if (! mounted) {
1143
- return ;
1144
- }
1205
+ final double lineHeight = renderEditable.preferredLineHeight;
1206
+
1207
+ // Enlarge the target rect by scrollPadding to ensure that caret is not
1208
+ // positioned directly at the edge after scrolling.
1209
+ double bottomSpacing = widget.scrollPadding.bottom;
1210
+ if (_selectionOverlay? .selectionControls != null ) {
1211
+ final double handleHeight = _selectionOverlay! .selectionControls!
1212
+ .getHandleSize (lineHeight)
1213
+ .height;
1214
+ final double interactiveHandleHeight =
1215
+ math.max (handleHeight, kMinInteractiveDimension);
1216
+ final Offset anchor =
1217
+ _selectionOverlay! .selectionControls! .getHandleAnchor (
1218
+ TextSelectionHandleType .collapsed,
1219
+ lineHeight,
1220
+ );
1221
+ final double handleCenter = handleHeight / 2 - anchor.dy;
1222
+ bottomSpacing =
1223
+ math.max (handleCenter + interactiveHandleHeight / 2 , bottomSpacing);
1224
+ }
1145
1225
1146
- final viewport = RenderAbstractViewport .of (renderEditor);
1147
- final editorOffset =
1148
- renderEditor.localToGlobal (const Offset (0 , 0 ), ancestor: viewport);
1149
- final offsetInViewport = _scrollController.offset + editorOffset.dy;
1226
+ final EdgeInsets caretPadding =
1227
+ widget.scrollPadding.copyWith (bottom: bottomSpacing);
1150
1228
1151
- final offset = renderEditor.getOffsetToRevealCursor (
1152
- _scrollController.position.viewportDimension,
1153
- _scrollController.offset,
1154
- offsetInViewport,
1155
- );
1229
+ final Rect caretRect =
1230
+ renderEditable.getLocalRectForCaret (renderEditable.selection! .extent);
1231
+ final RevealedOffset targetOffset = _getOffsetToRevealCaret (caretRect);
1156
1232
1157
- if (offset != null ) {
1158
- if (_disableScrollControllerAnimateOnce) {
1159
- _disableScrollControllerAnimateOnce = false ;
1160
- return ;
1161
- }
1162
- _scrollController.animateTo (
1163
- math.min (offset, _scrollController.position.maxScrollExtent),
1164
- duration: const Duration (milliseconds: 100 ),
1165
- curve: Curves .fastOutSlowIn,
1166
- );
1233
+ final Rect rectToReveal;
1234
+ final TextSelection selection = textEditingValue.selection;
1235
+ if (selection.isCollapsed) {
1236
+ rectToReveal = targetOffset.rect;
1237
+ } else {
1238
+ final List <TextBox > selectionBoxes =
1239
+ renderEditable.getBoxesForSelection (selection);
1240
+ // selectionBoxes may be empty if, for example, the selection does not
1241
+ // encompass a full character, like if it only contained part of an
1242
+ // extended grapheme cluster.
1243
+ if (selectionBoxes.isEmpty) {
1244
+ rectToReveal = targetOffset.rect;
1245
+ } else {
1246
+ rectToReveal = selection.baseOffset < selection.extentOffset
1247
+ ? selectionBoxes.last.toRect ()
1248
+ : selectionBoxes.first.toRect ();
1167
1249
}
1168
1250
}
1169
- });
1251
+
1252
+ if (withAnimation) {
1253
+ _scrollController.animateTo (
1254
+ targetOffset.offset,
1255
+ duration: _caretAnimationDuration,
1256
+ curve: _caretAnimationCurve,
1257
+ );
1258
+ renderEditable.showOnScreen (
1259
+ rect: caretPadding.inflateRect (rectToReveal),
1260
+ duration: _caretAnimationDuration,
1261
+ curve: _caretAnimationCurve,
1262
+ );
1263
+ } else {
1264
+ _scrollController.jumpTo (targetOffset.offset);
1265
+ renderEditable.showOnScreen (
1266
+ rect: caretPadding.inflateRect (rectToReveal));
1267
+ }
1268
+ }, debugLabel: 'EditableText.showCaret' );
1170
1269
}
1171
1270
1172
1271
/// The renderer for this widget's editor descendant.
@@ -1196,10 +1295,10 @@ class QuillRawEditorState extends EditorState
1196
1295
/// delay 500 milliseconds for waiting keyboard show up
1197
1296
Future .delayed (
1198
1297
const Duration (milliseconds: 500 ),
1199
- _showCaretOnScreen ,
1298
+ () => _scheduleShowCaretOnScreen (withAnimation : true ) ,
1200
1299
);
1201
1300
} else {
1202
- _showCaretOnScreen ( );
1301
+ _scheduleShowCaretOnScreen (withAnimation : true );
1203
1302
}
1204
1303
} else {
1205
1304
widget.config.focusNode.requestFocus ();
0 commit comments