Skip to content

Commit 6bfbc7a

Browse files
committed
Refactor _scheduleShowCaretOnScreen to fit with flutter_quill
1 parent 8e8516d commit 6bfbc7a

File tree

5 files changed

+81
-137
lines changed

5 files changed

+81
-137
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
1111
## [Unreleased]
1212

13+
- Fix caret not auto scroll to visible when page has multiple editors [#2570](https://github.com/singerdmx/flutter-quill/pull/2570).
14+
**BREAKING**: QuillEditorConfig is now accept `EdgeInsets padding` instead of `EdgeInsetsGeometry`.
15+
1316
## [11.4.1] - 2025-05-15
1417

1518
### Added

lib/src/editor/config/editor_config.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ class QuillEditorConfig {
214214

215215
/// Additional space around the content of this editor.
216216
/// by default will be [EdgeInsets.zero]
217-
final EdgeInsetsGeometry padding;
217+
final EdgeInsets padding;
218218

219219
/// Whether this editor should focus itself if nothing else is already
220220
/// focused.
@@ -484,7 +484,7 @@ class QuillEditorConfig {
484484
bool? scrollable,
485485
double? scrollBottomInset,
486486
bool? enableAlwaysIndentOnTab,
487-
EdgeInsetsGeometry? padding,
487+
EdgeInsets? padding,
488488
bool? autoFocus,
489489
bool? onTapOutsideEnabled,
490490
Function(PointerDownEvent event, FocusNode focusNode)? onTapOutside,

lib/src/editor/editor.dart

Lines changed: 4 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,71 +1222,6 @@ class RenderEditor extends RenderEditableContainerBox
12221222
);
12231223
}
12241224

1225-
/// Returns the y-offset of the editor at which [selection] is visible.
1226-
///
1227-
/// The offset is the distance from the top of the editor and is the minimum
1228-
/// from the current scroll position until [selection] becomes visible.
1229-
/// Returns null if [selection] is already visible.
1230-
///
1231-
/// Finds the closest scroll offset that fully reveals the editing cursor.
1232-
///
1233-
/// The `scrollOffset` parameter represents current scroll offset in the
1234-
/// parent viewport.
1235-
///
1236-
/// The `offsetInViewport` parameter represents the editor's vertical offset
1237-
/// in the parent viewport. This value should normally be 0.0 if this editor
1238-
/// is the only child of the viewport or if it's the topmost child. Otherwise
1239-
/// it should be a positive value equal to total height of all siblings of
1240-
/// this editor from above it.
1241-
///
1242-
/// Returns `null` if the cursor is currently visible.
1243-
double? getOffsetToRevealCursor(
1244-
double viewportHeight, double scrollOffset, double offsetInViewport) {
1245-
// Endpoints coordinates represents lower left or lower right corner of
1246-
// the selection. If we want to scroll up to reveal the caret we need to
1247-
// adjust the dy value by the height of the line. We also add a small margin
1248-
// so that the caret is not too close to the edge of the viewport.
1249-
final endpoints = getEndpointsForSelection(selection);
1250-
1251-
// when we drag the right handle, we should get the last point
1252-
TextSelectionPoint endpoint;
1253-
if (selection.isCollapsed) {
1254-
endpoint = endpoints.first;
1255-
} else {
1256-
if (selection is DragTextSelection) {
1257-
endpoint = (selection as DragTextSelection).first
1258-
? endpoints.first
1259-
: endpoints.last;
1260-
} else {
1261-
endpoint = endpoints.first;
1262-
}
1263-
}
1264-
1265-
// Collapsed selection => caret
1266-
final child = childAtPosition(selection.extent);
1267-
const kMargin = 8.0;
1268-
1269-
final caretTop = endpoint.point.dy -
1270-
child.preferredLineHeight(TextPosition(
1271-
offset: selection.extentOffset - child.container.documentOffset)) -
1272-
kMargin +
1273-
offsetInViewport +
1274-
scrollBottomInset;
1275-
final caretBottom =
1276-
endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset;
1277-
double? dy;
1278-
if (caretTop < scrollOffset) {
1279-
dy = caretTop;
1280-
} else if (caretBottom > scrollOffset + viewportHeight) {
1281-
dy = caretBottom - viewportHeight;
1282-
}
1283-
if (dy == null) {
1284-
return null;
1285-
}
1286-
// Clamping to 0.0 so that the content does not jump unnecessarily.
1287-
return math.max(dy, 0);
1288-
}
1289-
12901225
@override
12911226
Rect getLocalRectForCaret(TextPosition position) {
12921227
final targetChild = childAtPosition(position);
@@ -1298,6 +1233,10 @@ class RenderEditor extends RenderEditableContainerBox
12981233
return childLocalRect.shift(Offset(0, boxParentData.offset.dy));
12991234
}
13001235

1236+
TextPosition get caretTextPosition => _floatingCursorRect == null
1237+
? selection.extent
1238+
: _floatingCursorTextPosition;
1239+
13011240
// Start floating cursor
13021241

13031242
FloatingCursorPainter get _floatingCursorPainter => FloatingCursorPainter(

lib/src/editor/raw_editor/config/raw_editor_config.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ class QuillRawEditorConfig {
159159
final KeyEventResult? Function(KeyEvent event, Node? node)? onKeyPressed;
160160

161161
/// Additional space around the editor contents.
162-
final EdgeInsetsGeometry padding;
162+
final EdgeInsets padding;
163163

164164
/// Enables always indenting when the TAB key is pressed.
165165
///

lib/src/editor/raw_editor/raw_editor_state.dart

Lines changed: 71 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'dart:ui' as ui hide TextStyle;
66
import 'package:collection/collection.dart';
77
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
88
import 'package:flutter/material.dart';
9-
import 'package:flutter/rendering.dart' show RenderAbstractViewport;
9+
import 'package:flutter/rendering.dart' show RevealedOffset;
1010
import 'package:flutter/scheduler.dart' show SchedulerBinding;
1111
import 'package:flutter/services.dart';
1212
import 'package:flutter_keyboard_visibility_temp_fork/flutter_keyboard_visibility_temp_fork.dart'
@@ -94,7 +94,7 @@ class QuillRawEditorState extends EditorState
9494
bool get dirty => _dirty;
9595
bool _dirty = false;
9696

97-
// Completely copied from flutter:
97+
// Completely copied from flutter with some changes to fit flutter_quill:
9898
// https://github.com/flutter/flutter/blob/3.29.0/packages/flutter/lib/src/widgets/editable_text.dart#L3741
9999
// Finds the closest scroll offset to the current scroll offset that fully
100100
// reveals the given caret rect. If the given rect's main axis extent is too
@@ -110,44 +110,37 @@ class QuillRawEditorState extends EditorState
110110
return RevealedOffset(offset: _scrollController.offset, rect: rect);
111111
}
112112

113-
final Size editableSize = renderEditable.size;
113+
final editableSize = renderEditor.size;
114114
final double additionalOffset;
115115
final Offset unitOffset;
116116

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-
);
117+
// The caret is vertically centered within the line. Expand the caret's
118+
// height so that it spans the line because we're going to ensure that the
119+
// entire expanded caret is scrolled into view.
120+
final expandedRect = Rect.fromCenter(
121+
center: rect.center,
122+
width: rect.width,
123+
height: math.max(
124+
rect.height,
125+
renderEditor.preferredLineHeight(renderEditor.caretTextPosition),
126+
),
127+
);
134128

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-
}
129+
additionalOffset = expandedRect.height >= editableSize.height
130+
? editableSize.height / 2 - expandedRect.center.dy
131+
: ui.clampDouble(
132+
0, expandedRect.bottom - editableSize.height, expandedRect.top);
133+
unitOffset = const Offset(0, 1);
141134

142135
// No overscrolling when encountering tall fonts/scripts that extend past
143136
// the ascent.
144-
final double targetOffset = clampDouble(
137+
final targetOffset = ui.clampDouble(
145138
additionalOffset + _scrollController.offset,
146139
_scrollController.position.minScrollExtent,
147140
_scrollController.position.maxScrollExtent,
148141
);
149142

150-
final double offsetDelta = _scrollController.offset - targetOffset;
143+
final offsetDelta = _scrollController.offset - targetOffset;
151144
return RevealedOffset(
152145
rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
153146
}
@@ -1183,70 +1176,80 @@ class QuillRawEditorState extends EditorState
11831176

11841177
bool _showCaretOnScreenScheduled = false;
11851178

1186-
// Completely copied from flutter:
1179+
// Completely copied from flutter with some changes to fit flutter_quill:
11871180
// https://github.com/flutter/flutter/blob/3.29.0/packages/flutter/lib/src/widgets/editable_text.dart#L4228
11881181
void _scheduleShowCaretOnScreen({required bool withAnimation}) {
11891182
if (_showCaretOnScreenScheduled) {
11901183
return;
11911184
}
11921185
_showCaretOnScreenScheduled = true;
1193-
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
1186+
SchedulerBinding.instance.addPostFrameCallback((_) {
11941187
_showCaretOnScreenScheduled = false;
11951188
// Since we are in a post frame callback, check currentContext in case
11961189
// 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) ||
1190+
final renderEditor =
1191+
_editorKey.currentContext?.findRenderObject() as RenderEditor?;
1192+
if (renderEditor == null ||
1193+
!renderEditor.selection.isValid ||
12011194
!_scrollController.hasClients) {
12021195
return;
12031196
}
12041197

1205-
final double lineHeight = renderEditable.preferredLineHeight;
1198+
final lineHeight =
1199+
renderEditor.preferredLineHeight(renderEditor.caretTextPosition);
12061200

12071201
// Enlarge the target rect by scrollPadding to ensure that caret is not
12081202
// 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(
1203+
var bottomSpacing = widget.config.padding.bottom;
1204+
if (_selectionOverlay?.selectionCtrls != null) {
1205+
final handleHeight =
1206+
_selectionOverlay!.selectionCtrls.getHandleSize(lineHeight).height;
1207+
1208+
final double interactiveHandleHeight = math.max(
1209+
handleHeight,
1210+
kMinInteractiveDimension,
1211+
);
1212+
1213+
final anchor = _selectionOverlay!.selectionCtrls.getHandleAnchor(
12181214
TextSelectionHandleType.collapsed,
12191215
lineHeight,
12201216
);
1221-
final double handleCenter = handleHeight / 2 - anchor.dy;
1222-
bottomSpacing =
1223-
math.max(handleCenter + interactiveHandleHeight / 2, bottomSpacing);
1217+
1218+
final handleCenter = handleHeight / 2 - anchor.dy;
1219+
bottomSpacing = math.max(
1220+
handleCenter + interactiveHandleHeight / 2,
1221+
bottomSpacing,
1222+
);
12241223
}
12251224

1226-
final EdgeInsets caretPadding =
1227-
widget.scrollPadding.copyWith(bottom: bottomSpacing);
1225+
final caretPadding =
1226+
widget.config.padding.copyWith(bottom: bottomSpacing);
12281227

1229-
final Rect caretRect =
1230-
renderEditable.getLocalRectForCaret(renderEditable.selection!.extent);
1231-
final RevealedOffset targetOffset = _getOffsetToRevealCaret(caretRect);
1228+
final caretRect =
1229+
renderEditor.getLocalRectForCaret(renderEditor.caretTextPosition);
1230+
final targetOffset = _getOffsetToRevealCaret(caretRect);
12321231

1233-
final Rect rectToReveal;
1234-
final TextSelection selection = textEditingValue.selection;
1232+
Rect? rectToReveal;
1233+
final selection = textEditingValue.selection;
12351234
if (selection.isCollapsed) {
12361235
rectToReveal = targetOffset.rect;
12371236
} 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();
1249-
}
1237+
// TODO: I'm not sure how to get getBoxesForSelection in flutter_quill or do we even has it?
1238+
// Currently just return targetOffset.rect.
1239+
//
1240+
// final List<TextBox> selectionBoxes =
1241+
// renderEditor.getBoxesForSelection(selection);
1242+
// // selectionBoxes may be empty if, for example, the selection does not
1243+
// // encompass a full character, like if it only contained part of an
1244+
// // extended grapheme cluster.
1245+
// if (selectionBoxes.isEmpty) {
1246+
// rectToReveal = targetOffset.rect;
1247+
// } else {
1248+
// rectToReveal = selection.baseOffset < selection.extentOffset
1249+
// ? selectionBoxes.last.toRect()
1250+
// : selectionBoxes.first.toRect();
1251+
// }
1252+
rectToReveal = targetOffset.rect;
12501253
}
12511254

12521255
if (withAnimation) {
@@ -1255,15 +1258,14 @@ class QuillRawEditorState extends EditorState
12551258
duration: _caretAnimationDuration,
12561259
curve: _caretAnimationCurve,
12571260
);
1258-
renderEditable.showOnScreen(
1261+
renderEditor.showOnScreen(
12591262
rect: caretPadding.inflateRect(rectToReveal),
12601263
duration: _caretAnimationDuration,
12611264
curve: _caretAnimationCurve,
12621265
);
12631266
} else {
12641267
_scrollController.jumpTo(targetOffset.offset);
1265-
renderEditable.showOnScreen(
1266-
rect: caretPadding.inflateRect(rectToReveal));
1268+
renderEditor.showOnScreen(rect: caretPadding.inflateRect(rectToReveal));
12671269
}
12681270
}, debugLabel: 'EditableText.showCaret');
12691271
}

0 commit comments

Comments
 (0)