Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Fixed a crash (assert in debug, fatal `RangeError` in release) when painting selection endpoints for a selection edge that produces no glyph boxes, e.g. an offset inside an emoji grapheme cluster [#2751](https://github.com/singerdmx/flutter-quill/pull/2751).
- Fixed a fatal `Bad state: No element` crash in `getLineBoundary` when resolving the line boundary of an empty line (e.g. line-boundary navigation with Home/End/Shift+Home on a hardware keyboard), which produced no glyph boxes; falls back to the caret position [#2751](https://github.com/singerdmx/flutter-quill/pull/2751).
- Fixed an issue where bullet points became visually detached from the text body when toggling text direction formatting (RTL) by locking the list leading block to the editor's base text direction.
- Fixed typed text being inserted at the previous caret position on Android after moving the caret with a tap/mouse by keeping the platform IME's editing state in sync with the selection even when the keyboard is hidden.

Expand Down
21 changes: 20 additions & 1 deletion lib/src/editor/widgets/text/text_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,17 @@ class RenderEditableTextLine extends RenderEditableBox {
);
}
final boxes = _getBoxes(textSelection);
assert(boxes.isNotEmpty);
if (boxes.isEmpty) {
// A selection edge can land on a position that produces no glyph
// boxes, e.g. inside a grapheme cluster such as an emoji. Fall back to
// the caret offset for that edge instead of crashing on boxes.first /
// boxes.last while painting the selection.
final edge = first ? textSelection.base : textSelection.extent;
return TextSelectionPoint(
Offset(0, preferredLineHeight(edge)) + getOffsetForCaret(edge),
null,
);
}
final targetBox = first ? boxes.first : boxes.last;
return TextSelectionPoint(
Offset(first ? targetBox.start : targetBox.end, targetBox.bottom),
Expand All @@ -1101,6 +1111,15 @@ class RenderEditableTextLine extends RenderEditableBox {
_getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1))
.where((element) => element.top < lineDy && element.bottom > lineDy)
.toList(growable: false);
if (lineBoxes.isEmpty) {
// An empty line (Line.length == 1, so the [0, line.length - 1] == [0, 0]
// box selection is collapsed and yields no glyph boxes) has no boxes to
// derive left/right edges from. Fall back to the caret position so
// line-boundary navigation (e.g. Home/End/Shift+Home on a hardware
// keyboard) collapses to the caret instead of crashing on
// lineBoxes.first / lineBoxes.last.
return TextRange(start: position.offset, end: position.offset);
}
return TextRange(
start: getPositionForOffset(Offset(lineBoxes.first.left, lineDy)).offset,
end: getPositionForOffset(Offset(lineBoxes.last.right, lineDy)).offset,
Expand Down
60 changes: 60 additions & 0 deletions test/bug_fix_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,64 @@ void main() {
);
},
);
group('selection endpoint crash when getBoxesForSelection is empty', () {
testWidgets(
'getEndpointsForSelection falls back to the caret offset when a '
'selection edge produces no glyph boxes',
(tester) async {
final controller = QuillController.basic()
..document.insert(0, '\u{1F44D} thumbs up');
addTearDown(controller.dispose);

await tester.pumpWidget(
QuillTestApp.withScaffold(QuillEditor.basic(controller: controller)),
);
await tester.pumpAndSettle();

final renderEditor = tester.allRenderObjects
.whereType<RenderEditor>()
.first;

// A selection edge inside the emoji's surrogate pair yields no glyph
// boxes from getBoxesForSelection; this used to crash on boxes.last
// (release) or fail an assert (debug) while painting the selection.
final endpoints = renderEditor.getEndpointsForSelection(
const TextSelection(baseOffset: 0, extentOffset: 1),
);
expect(endpoints, hasLength(2));
},
);
});
group('line boundary crash when a line has no glyph boxes', () {
testWidgets(
'getLineAtOffset falls back to the caret offset on an empty line '
'instead of crashing on lineBoxes.first/last',
(tester) async {
// An empty first paragraph: the caret at offset 0 sits on a line whose
// [0, line.length - 1] == [0, 0] box selection is collapsed and yields
// no glyph boxes.
final controller = QuillController.basic()
..document.insert(0, '\nsecond line');
addTearDown(controller.dispose);

await tester.pumpWidget(
QuillTestApp.withScaffold(QuillEditor.basic(controller: controller)),
);
await tester.pumpAndSettle();

final renderEditor = tester.allRenderObjects
.whereType<RenderEditor>()
.first;

// Line-boundary navigation (Home/End/Shift+Home on a hardware
// keyboard) resolves the line at the caret; on the empty line this
// used to throw 'Bad state: No element' from lineBoxes.first.
final line = renderEditor.getLineAtOffset(
const TextPosition(offset: 0),
);
expect(line.baseOffset, 0);
expect(line.extentOffset, 0);
},
);
});
}
Loading