Skip to content

Commit 8e8516d

Browse files
committed
Copy _scheduleShowCaretOnScreen from flutter editable_text
1 parent 65da8cb commit 8e8516d

File tree

1 file changed

+140
-41
lines changed

1 file changed

+140
-41
lines changed

lib/src/editor/raw_editor/raw_editor_state.dart

Lines changed: 140 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,64 @@ class QuillRawEditorState extends EditorState
9494
bool get dirty => _dirty;
9595
bool _dirty = false;
9696

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+
97155
@override
98156
void insertContent(KeyboardInsertedContent content) {
99157
assert(widget.config.contentInsertionConfiguration?.allowedMimeTypes
@@ -537,7 +595,6 @@ class QuillRawEditorState extends EditorState
537595
final requestKeyboardFocusOnCheckListChanged =
538596
widget.config.requestKeyboardFocusOnCheckListChanged;
539597
if (!(widget.config.checkBoxReadOnly ?? widget.config.readOnly)) {
540-
_disableScrollControllerAnimateOnce = true;
541598
final currentSelection = controller.selection.copyWith();
542599
final attribute = value ? Attribute.checked : Attribute.unchecked;
543600

@@ -1031,7 +1088,7 @@ class QuillRawEditorState extends EditorState
10311088
if (ignoreCaret) {
10321089
return;
10331090
}
1034-
_showCaretOnScreen();
1091+
_scheduleShowCaretOnScreen(withAnimation: true);
10351092
_cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, controller.selection);
10361093
if (hasConnection) {
10371094
// To keep the cursor from blinking while typing, we want to restart the
@@ -1101,7 +1158,7 @@ class QuillRawEditorState extends EditorState
11011158
_updateOrDisposeSelectionOverlayIfNeeded();
11021159
if (_hasFocus) {
11031160
WidgetsBinding.instance.addObserver(this);
1104-
_showCaretOnScreen();
1161+
_scheduleShowCaretOnScreen(withAnimation: true);
11051162
} else {
11061163
WidgetsBinding.instance.removeObserver(this);
11071164
}
@@ -1120,53 +1177,95 @@ class QuillRawEditorState extends EditorState
11201177
return widget.config.linkActionPickerDelegate(context, link, linkNode);
11211178
}
11221179

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;
11241183

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;
11311185

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) {
11341190
return;
11351191
}
1136-
11371192
_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+
}
11411204

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+
}
11451225

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);
11501228

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);
11561232

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();
11671249
}
11681250
}
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');
11701269
}
11711270

11721271
/// The renderer for this widget's editor descendant.
@@ -1196,10 +1295,10 @@ class QuillRawEditorState extends EditorState
11961295
/// delay 500 milliseconds for waiting keyboard show up
11971296
Future.delayed(
11981297
const Duration(milliseconds: 500),
1199-
_showCaretOnScreen,
1298+
() => _scheduleShowCaretOnScreen(withAnimation: true),
12001299
);
12011300
} else {
1202-
_showCaretOnScreen();
1301+
_scheduleShowCaretOnScreen(withAnimation: true);
12031302
}
12041303
} else {
12051304
widget.config.focusNode.requestFocus();

0 commit comments

Comments
 (0)