First step of improving the quill editor

pull/1510/head
Ellet 1 year ago
parent 39154183b7
commit 2a2c0620a6
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 7
      lib/src/widgets/editor/editor.dart
  2. 2534
      lib/src/widgets/raw_editor/raw_editor.dart
  3. 578
      lib/src/widgets/raw_editor/raw_editor_actions.dart
  4. 1620
      lib/src/widgets/raw_editor/raw_editor_state.dart
  5. 292
      lib/src/widgets/raw_editor/raw_editor_text_boundaries.dart
  6. 86
      lib/src/widgets/raw_editor/raw_editor_widget.dart
  7. 1
      test/widgets/editor_test.dart

@ -1,8 +1,9 @@
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/cupertino.dart'
show CupertinoTheme, cupertinoTextSelectionControls;
import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

File diff suppressed because it is too large Load Diff

@ -0,0 +1,578 @@
import 'package:flutter/material.dart';
import '../../models/documents/attribute.dart';
import '../editor/editor.dart';
import '../toolbar/buttons/link_style2.dart';
import '../toolbar/buttons/search/search_dialog.dart';
import 'raw_editor_state.dart';
import 'raw_editor_text_boundaries.dart';
// ------------------------------- Text Actions -------------------------------
class QuillEditorDeleteTextAction<T extends DirectionalTextEditingIntent>
extends ContextAction<T> {
QuillEditorDeleteTextAction(this.state, this.getTextBoundariesForIntent);
final QuillRawEditorState state;
final QuillEditorTextBoundary Function(T intent) getTextBoundariesForIntent;
TextRange _expandNonCollapsedRange(TextEditingValue value) {
final TextRange selection = value.selection;
assert(selection.isValid);
assert(!selection.isCollapsed);
final atomicBoundary = QuillEditorCharacterBoundary(value);
return TextRange(
start: atomicBoundary
.getLeadingTextBoundaryAt(TextPosition(offset: selection.start))
.offset,
end: atomicBoundary
.getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1))
.offset,
);
}
@override
Object? invoke(T intent, [BuildContext? context]) {
final selection = state.textEditingValue.selection;
assert(selection.isValid);
if (!selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(
state.textEditingValue,
'',
_expandNonCollapsedRange(state.textEditingValue),
SelectionChangedCause.keyboard),
);
}
final textBoundary = getTextBoundariesForIntent(intent);
if (!textBoundary.textEditingValue.selection.isValid) {
return null;
}
if (!textBoundary.textEditingValue.selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(
state.textEditingValue,
'',
_expandNonCollapsedRange(textBoundary.textEditingValue),
SelectionChangedCause.keyboard),
);
}
return Actions.invoke(
context!,
ReplaceTextIntent(
textBoundary.textEditingValue,
'',
textBoundary
.getTextBoundaryAt(textBoundary.textEditingValue.selection.base),
SelectionChangedCause.keyboard,
),
);
}
@override
bool get isActionEnabled =>
!state.widget.readOnly && state.textEditingValue.selection.isValid;
}
class QuillEditorUpdateTextSelectionAction<
T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
QuillEditorUpdateTextSelectionAction(this.state,
this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent);
final QuillRawEditorState state;
final bool ignoreNonCollapsedSelection;
final QuillEditorTextBoundary Function(T intent) getTextBoundariesForIntent;
@override
Object? invoke(T intent, [BuildContext? context]) {
final selection = state.textEditingValue.selection;
assert(selection.isValid);
final collapseSelection =
intent.collapseSelection || !state.widget.selectionEnabled;
// Collapse to the logical start/end.
TextSelection collapse(TextSelection selection) {
assert(selection.isValid);
assert(!selection.isCollapsed);
return selection.copyWith(
baseOffset: intent.forward ? selection.end : selection.start,
extentOffset: intent.forward ? selection.end : selection.start,
);
}
if (!selection.isCollapsed &&
!ignoreNonCollapsedSelection &&
collapseSelection) {
return Actions.invoke(
context!,
UpdateSelectionIntent(
state.textEditingValue,
collapse(selection),
SelectionChangedCause.keyboard,
),
);
}
final textBoundary = getTextBoundariesForIntent(intent);
final textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return null;
}
if (!textBoundarySelection.isCollapsed &&
!ignoreNonCollapsedSelection &&
collapseSelection) {
return Actions.invoke(
context!,
UpdateSelectionIntent(state.textEditingValue,
collapse(textBoundarySelection), SelectionChangedCause.keyboard),
);
}
final extent = textBoundarySelection.extent;
final newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
final newSelection = collapseSelection
? TextSelection.fromPosition(newExtent)
: textBoundarySelection.extendTo(newExtent);
// If collapseAtReversal is true and would have an effect, collapse it.
if (!selection.isCollapsed &&
intent.collapseAtReversal &&
(selection.baseOffset < selection.extentOffset !=
newSelection.baseOffset < newSelection.extentOffset)) {
return Actions.invoke(
context!,
UpdateSelectionIntent(
state.textEditingValue,
TextSelection.fromPosition(selection.base),
SelectionChangedCause.keyboard,
),
);
}
return Actions.invoke(
context!,
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection,
SelectionChangedCause.keyboard),
);
}
@override
bool get isActionEnabled => state.textEditingValue.selection.isValid;
}
class QuillEditorExtendSelectionOrCaretPositionAction extends ContextAction<
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> {
QuillEditorExtendSelectionOrCaretPositionAction(
this.state, this.getTextBoundariesForIntent);
final QuillRawEditorState state;
final QuillEditorTextBoundary Function(
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent)
getTextBoundariesForIntent;
@override
Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent,
[BuildContext? context]) {
final selection = state.textEditingValue.selection;
assert(selection.isValid);
final textBoundary = getTextBoundariesForIntent(intent);
final textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return null;
}
final extent = textBoundarySelection.extent;
final newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
final newSelection = (newExtent.offset - textBoundarySelection.baseOffset) *
(textBoundarySelection.extentOffset -
textBoundarySelection.baseOffset) <
0
? textBoundarySelection.copyWith(
extentOffset: textBoundarySelection.baseOffset,
affinity: textBoundarySelection.extentOffset >
textBoundarySelection.baseOffset
? TextAffinity.downstream
: TextAffinity.upstream,
)
: textBoundarySelection.extendTo(newExtent);
return Actions.invoke(
context!,
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection,
SelectionChangedCause.keyboard),
);
}
@override
bool get isActionEnabled =>
state.widget.selectionEnabled && state.textEditingValue.selection.isValid;
}
class QuillEditorUpdateTextSelectionToAdjacentLineAction<
T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
QuillEditorUpdateTextSelectionToAdjacentLineAction(this.state);
final QuillRawEditorState state;
QuillVerticalCaretMovementRun? _verticalMovementRun;
TextSelection? _runSelection;
void stopCurrentVerticalRunIfSelectionChanges() {
final runSelection = _runSelection;
if (runSelection == null) {
assert(_verticalMovementRun == null);
return;
}
_runSelection = state.textEditingValue.selection;
final currentSelection = state.controller.selection;
final continueCurrentRun = currentSelection.isValid &&
currentSelection.isCollapsed &&
currentSelection.baseOffset == runSelection.baseOffset &&
currentSelection.extentOffset == runSelection.extentOffset;
if (!continueCurrentRun) {
_verticalMovementRun = null;
_runSelection = null;
}
}
@override
void invoke(T intent, [BuildContext? context]) {
assert(state.textEditingValue.selection.isValid);
final collapseSelection =
intent.collapseSelection || !state.widget.selectionEnabled;
final value = state.textEditingValue;
if (!value.selection.isValid) {
return;
}
final currentRun = _verticalMovementRun ??
state.renderEditor
.startVerticalCaretMovement(state.renderEditor.selection.extent);
final shouldMove =
intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
final newExtent = shouldMove
? currentRun.current
: (intent.forward
? TextPosition(offset: state.textEditingValue.text.length)
: const TextPosition(offset: 0));
final newSelection = collapseSelection
? TextSelection.fromPosition(newExtent)
: value.selection.extendTo(newExtent);
Actions.invoke(
context!,
UpdateSelectionIntent(
value, newSelection, SelectionChangedCause.keyboard),
);
if (state.textEditingValue.selection == newSelection) {
_verticalMovementRun = currentRun;
_runSelection = newSelection;
}
}
@override
bool get isActionEnabled => state.textEditingValue.selection.isValid;
}
class QuillEditorSelectAllAction extends ContextAction<SelectAllTextIntent> {
QuillEditorSelectAllAction(this.state);
final QuillRawEditorState state;
@override
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
return Actions.invoke(
context!,
UpdateSelectionIntent(
state.textEditingValue,
TextSelection(
baseOffset: 0, extentOffset: state.textEditingValue.text.length),
intent.cause,
),
);
}
@override
bool get isActionEnabled => state.widget.selectionEnabled;
}
class QuillEditorCopySelectionAction
extends ContextAction<CopySelectionTextIntent> {
QuillEditorCopySelectionAction(this.state);
final QuillRawEditorState state;
@override
void invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
if (intent.collapseSelection) {
state.cutSelection(intent.cause);
} else {
state.copySelection(intent.cause);
}
}
@override
bool get isActionEnabled =>
state.textEditingValue.selection.isValid &&
!state.textEditingValue.selection.isCollapsed;
}
//Intent class for "escape" key to dismiss selection toolbar in Windows platform
class HideSelectionToolbarIntent extends Intent {
const HideSelectionToolbarIntent();
}
class QuillEditorHideSelectionToolbarAction
extends ContextAction<HideSelectionToolbarIntent> {
QuillEditorHideSelectionToolbarAction(this.state);
final QuillRawEditorState state;
@override
void invoke(HideSelectionToolbarIntent intent, [BuildContext? context]) {
state.hideToolbar();
}
@override
bool get isActionEnabled => state.textEditingValue.selection.isValid;
}
class QuillEditorUndoKeyboardAction extends ContextAction<UndoTextIntent> {
QuillEditorUndoKeyboardAction(this.state);
final QuillRawEditorState state;
@override
void invoke(UndoTextIntent intent, [BuildContext? context]) {
if (state.controller.hasUndo) {
state.controller.undo();
}
}
@override
bool get isActionEnabled => true;
}
class QuillEditorRedoKeyboardAction extends ContextAction<RedoTextIntent> {
QuillEditorRedoKeyboardAction(this.state);
final QuillRawEditorState state;
@override
void invoke(RedoTextIntent intent, [BuildContext? context]) {
if (state.controller.hasRedo) {
state.controller.redo();
}
}
@override
bool get isActionEnabled => true;
}
class ToggleTextStyleIntent extends Intent {
const ToggleTextStyleIntent(this.attribute);
final Attribute attribute;
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorToggleTextStyleAction extends Action<ToggleTextStyleIntent> {
QuillEditorToggleTextStyleAction(this.state);
final QuillRawEditorState state;
bool _isStyleActive(Attribute styleAttr, Map<String, Attribute> attrs) {
if (styleAttr.key == Attribute.list.key) {
final attribute = attrs[styleAttr.key];
if (attribute == null) {
return false;
}
return attribute.value == styleAttr.value;
}
return attrs.containsKey(styleAttr.key);
}
@override
void invoke(ToggleTextStyleIntent intent, [BuildContext? context]) {
final isActive = _isStyleActive(
intent.attribute, state.controller.getSelectionStyle().attributes);
state.controller.formatSelection(
isActive ? Attribute.clone(intent.attribute, null) : intent.attribute);
}
@override
bool get isActionEnabled => true;
}
class IndentSelectionIntent extends Intent {
const IndentSelectionIntent(this.isIncrease);
final bool isIncrease;
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorIndentSelectionAction extends Action<IndentSelectionIntent> {
QuillEditorIndentSelectionAction(this.state);
final QuillRawEditorState state;
@override
void invoke(IndentSelectionIntent intent, [BuildContext? context]) {
state.controller.indentSelection(intent.isIncrease);
}
@override
bool get isActionEnabled => true;
}
class OpenSearchIntent extends Intent {
const OpenSearchIntent();
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorOpenSearchAction extends ContextAction<OpenSearchIntent> {
QuillEditorOpenSearchAction(this.state);
final QuillRawEditorState state;
@override
Future invoke(OpenSearchIntent intent, [BuildContext? context]) async {
if (context == null) {
throw ArgumentError(
'The context should not be null to use invoke() method',
);
}
await showDialog<String>(
context: context,
builder: (_) => QuillToolbarSearchDialog(
controller: state.controller,
text: '',
),
);
}
@override
bool get isActionEnabled => true;
}
class QuillEditorApplyHeaderIntent extends Intent {
const QuillEditorApplyHeaderIntent(this.header);
final Attribute header;
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorApplyHeaderAction
extends Action<QuillEditorApplyHeaderIntent> {
QuillEditorApplyHeaderAction(this.state);
final QuillRawEditorState state;
Attribute<dynamic> _getHeaderValue() {
return state.controller
.getSelectionStyle()
.attributes[Attribute.header.key] ??
Attribute.header;
}
@override
void invoke(QuillEditorApplyHeaderIntent intent, [BuildContext? context]) {
final attribute =
_getHeaderValue() == intent.header ? Attribute.header : intent.header;
state.controller.formatSelection(attribute);
}
@override
bool get isActionEnabled => true;
}
class QuillEditorApplyCheckListIntent extends Intent {
const QuillEditorApplyCheckListIntent();
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorApplyCheckListAction
extends Action<QuillEditorApplyCheckListIntent> {
QuillEditorApplyCheckListAction(this.state);
final QuillRawEditorState state;
bool _getIsToggled() {
final attrs = state.controller.getSelectionStyle().attributes;
var attribute = state.controller.toolbarButtonToggler[Attribute.list.key];
if (attribute == null) {
attribute = attrs[Attribute.list.key];
} else {
// checkbox tapping causes controller.selection to go to offset 0
state.controller.toolbarButtonToggler.remove(Attribute.list.key);
}
if (attribute == null) {
return false;
}
return attribute.value == Attribute.unchecked.value ||
attribute.value == Attribute.checked.value;
}
@override
void invoke(QuillEditorApplyCheckListIntent intent, [BuildContext? context]) {
state.controller.formatSelection(_getIsToggled()
? Attribute.clone(Attribute.unchecked, null)
: Attribute.unchecked);
}
@override
bool get isActionEnabled => true;
}
class QuillEditorApplyLinkIntent extends Intent {
const QuillEditorApplyLinkIntent();
}
class QuillEditorApplyLinkAction extends Action<QuillEditorApplyLinkIntent> {
QuillEditorApplyLinkAction(this.state);
final QuillRawEditorState state;
@override
Object? invoke(QuillEditorApplyLinkIntent intent) async {
final initialTextLink = QuillTextLink.prepare(state.controller);
final textLink = await showDialog<QuillTextLink>(
context: state.context,
builder: (context) {
return LinkStyleDialog(
text: initialTextLink.text,
link: initialTextLink.link,
dialogTheme: state.widget.dialogTheme,
);
},
);
if (textLink != null) {
textLink.submit(state.controller);
}
return null;
}
}
class QuillEditorInsertEmbedIntent extends Intent {
const QuillEditorInsertEmbedIntent(this.type);
final Attribute type;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,292 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show TextLayoutMetrics;
/// An interface for retrieving the logical text boundary
/// (left-closed-right-open)
/// at a given location in a document.
///
/// Depending on the implementation of the [QuillEditorTextBoundary], the input
/// [TextPosition] can either point to a code unit, or a position between 2 code
/// units (which can be visually represented by the caret if the selection were
/// to collapse to that position).
///
/// For example, [QuillEditorLineBreak] interprets the input [TextPosition] as a caret
/// location, since in Flutter the caret is generally painted between the
/// character the [TextPosition] points to and its previous character, and
/// [QuillEditorLineBreak] cares about the affinity of the input [TextPosition]. Most
/// other text boundaries however, interpret the input [TextPosition] as the
/// location of a code unit in the document, since it's easier to reason about
/// the text boundary given a code unit in the text.
///
/// To convert a "code-unit-based" [QuillEditorTextBoundary] to "caret-location-based",
/// use the [QuillEditorCollapsedSelectionBoundary] combinator.
abstract class QuillEditorTextBoundary {
const QuillEditorTextBoundary();
TextEditingValue get textEditingValue;
/// Returns the leading text boundary at the given location, inclusive.
TextPosition getLeadingTextBoundaryAt(TextPosition position);
/// Returns the trailing text boundary at the given location, exclusive.
TextPosition getTrailingTextBoundaryAt(TextPosition position);
TextRange getTextBoundaryAt(TextPosition position) {
return TextRange(
start: getLeadingTextBoundaryAt(position).offset,
end: getTrailingTextBoundaryAt(position).offset,
);
}
}
// ----------------------------- Text Boundaries -----------------------------
// The word modifier generally removes the word boundaries around white spaces
// (and newlines), IOW white spaces and some other punctuations are considered
// a part of the next word in the search direction.
class QuillEditorWhitespaceBoundary extends QuillEditorTextBoundary {
const QuillEditorWhitespaceBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
for (var index = position.offset; index >= 0; index -= 1) {
if (!TextLayoutMetrics.isWhitespace(
textEditingValue.text.codeUnitAt(index))) {
return TextPosition(offset: index);
}
}
return const TextPosition(offset: 0);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
for (var index = position.offset;
index < textEditingValue.text.length;
index += 1) {
if (!TextLayoutMetrics.isWhitespace(
textEditingValue.text.codeUnitAt(index))) {
return TextPosition(offset: index + 1);
}
}
return TextPosition(offset: textEditingValue.text.length);
}
}
// Most apps delete the entire grapheme when the backspace key is pressed.
// Also always put the new caret location to character boundaries to avoid
// sending malformed UTF-16 code units to the paragraph builder.
class QuillEditorCharacterBoundary extends QuillEditorTextBoundary {
const QuillEditorCharacterBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
final int endOffset =
math.min(position.offset + 1, textEditingValue.text.length);
return TextPosition(
offset:
CharacterRange.at(textEditingValue.text, position.offset, endOffset)
.stringBeforeLength,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
final int endOffset =
math.min(position.offset + 1, textEditingValue.text.length);
final range =
CharacterRange.at(textEditingValue.text, position.offset, endOffset);
return TextPosition(
offset: textEditingValue.text.length - range.stringAfterLength,
);
}
@override
TextRange getTextBoundaryAt(TextPosition position) {
final int endOffset =
math.min(position.offset + 1, textEditingValue.text.length);
final range =
CharacterRange.at(textEditingValue.text, position.offset, endOffset);
return TextRange(
start: range.stringBeforeLength,
end: textEditingValue.text.length - range.stringAfterLength,
);
}
}
// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries.
class QuillEditorWordBoundary extends QuillEditorTextBoundary {
const QuillEditorWordBoundary(this.textLayout, this.textEditingValue);
final TextLayoutMetrics textLayout;
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getWordBoundary(position).start,
// Word boundary seems to always report downstream on many platforms.
affinity:
TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getWordBoundary(position).end,
// Word boundary seems to always report downstream on many platforms.
affinity:
TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
}
// The linebreaks of the current text layout. The input [TextPosition]s are
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware.
class QuillEditorLineBreak extends QuillEditorTextBoundary {
const QuillEditorLineBreak(this.textLayout, this.textEditingValue);
final TextLayoutMetrics textLayout;
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getLineAtOffset(position).start,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getLineAtOffset(position).end,
affinity: TextAffinity.upstream,
);
}
}
// The document boundary is unique and is a constant function of the input
// position.
class QuillEditorDocumentBoundary extends QuillEditorTextBoundary {
const QuillEditorDocumentBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) =>
const TextPosition(offset: 0);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textEditingValue.text.length,
affinity: TextAffinity.upstream,
);
}
}
// ------------------------ Text Boundary Combinators ------------------------
// Expands the innerTextBoundary with outerTextBoundary.
class QuillEditorExpandedTextBoundary extends QuillEditorTextBoundary {
QuillEditorExpandedTextBoundary(
this.innerTextBoundary, this.outerTextBoundary);
final QuillEditorTextBoundary innerTextBoundary;
final QuillEditorTextBoundary outerTextBoundary;
@override
TextEditingValue get textEditingValue {
assert(innerTextBoundary.textEditingValue ==
outerTextBoundary.textEditingValue);
return innerTextBoundary.textEditingValue;
}
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return outerTextBoundary.getLeadingTextBoundaryAt(
innerTextBoundary.getLeadingTextBoundaryAt(position),
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return outerTextBoundary.getTrailingTextBoundaryAt(
innerTextBoundary.getTrailingTextBoundaryAt(position),
);
}
}
// Force the innerTextBoundary to interpret the input [TextPosition]s as caret
// locations instead of code unit positions.
//
// The innerTextBoundary must be a [_TextBoundary] that interprets the input
// [TextPosition]s as code unit positions.
class QuillEditorCollapsedSelectionBoundary extends QuillEditorTextBoundary {
QuillEditorCollapsedSelectionBoundary(this.innerTextBoundary, this.isForward);
final QuillEditorTextBoundary innerTextBoundary;
final bool isForward;
@override
TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return isForward
? innerTextBoundary.getLeadingTextBoundaryAt(position)
: position.offset <= 0
? const TextPosition(offset: 0)
: innerTextBoundary.getLeadingTextBoundaryAt(
TextPosition(offset: position.offset - 1));
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return isForward
? innerTextBoundary.getTrailingTextBoundaryAt(position)
: position.offset <= 0
? const TextPosition(offset: 0)
: innerTextBoundary.getTrailingTextBoundaryAt(
TextPosition(offset: position.offset - 1));
}
}
// A _TextBoundary that creates a [TextRange] where its start is from the
// specified leading text boundary and its end is from the specified trailing
// text boundary.
class QuillEditorMixedBoundary extends QuillEditorTextBoundary {
QuillEditorMixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary);
final QuillEditorTextBoundary leadingTextBoundary;
final QuillEditorTextBoundary trailingTextBoundary;
@override
TextEditingValue get textEditingValue {
assert(leadingTextBoundary.textEditingValue ==
trailingTextBoundary.textEditingValue);
return leadingTextBoundary.textEditingValue;
}
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) =>
leadingTextBoundary.getLeadingTextBoundaryAt(position);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) =>
trailingTextBoundary.getTrailingTextBoundaryAt(position);
}

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ViewportOffset;
import '../../models/documents/document.dart';
import '../cursor.dart';
import '../editor/editor.dart';
class QuilRawEditorMultiChildRenderObjectWidget
extends MultiChildRenderObjectWidget {
const QuilRawEditorMultiChildRenderObjectWidget({
required super.children,
required this.document,
required this.textDirection,
required this.hasFocus,
required this.scrollable,
required this.selection,
required this.startHandleLayerLink,
required this.endHandleLayerLink,
required this.onSelectionChanged,
required this.onSelectionCompleted,
required this.scrollBottomInset,
required this.cursorController,
required this.floatingCursorDisabled,
super.key,
this.padding = EdgeInsets.zero,
this.maxContentWidth,
this.offset,
});
final ViewportOffset? offset;
final Document document;
final TextDirection textDirection;
final bool hasFocus;
final bool scrollable;
final TextSelection selection;
final LayerLink startHandleLayerLink;
final LayerLink endHandleLayerLink;
final TextSelectionChangedHandler onSelectionChanged;
final TextSelectionCompletedHandler onSelectionCompleted;
final double scrollBottomInset;
final EdgeInsetsGeometry padding;
final double? maxContentWidth;
final CursorCont cursorController;
final bool floatingCursorDisabled;
@override
RenderEditor createRenderObject(BuildContext context) {
return RenderEditor(
offset: offset,
document: document,
textDirection: textDirection,
hasFocus: hasFocus,
scrollable: scrollable,
selection: selection,
startHandleLayerLink: startHandleLayerLink,
endHandleLayerLink: endHandleLayerLink,
onSelectionChanged: onSelectionChanged,
onSelectionCompleted: onSelectionCompleted,
cursorController: cursorController,
padding: padding,
maxContentWidth: maxContentWidth,
scrollBottomInset: scrollBottomInset,
floatingCursorDisabled: floatingCursorDisabled,
);
}
@override
void updateRenderObject(
BuildContext context,
covariant RenderEditor renderObject,
) {
renderObject
..offset = offset
..document = document
..setContainer(document.root)
..textDirection = textDirection
..setHasFocus(hasFocus)
..setSelection(selection)
..setStartHandleLayerLink(startHandleLayerLink)
..setEndHandleLayerLink(endHandleLayerLink)
..onSelectionChanged = onSelectionChanged
..setScrollBottomInset(scrollBottomInset)
..setPadding(padding)
..maxContentWidth = maxContentWidth;
}
}

@ -3,6 +3,7 @@ import 'dart:convert' show jsonDecode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/src/widgets/raw_editor/raw_editor_state.dart';
import 'package:flutter_quill_test/flutter_quill_test.dart';
import 'package:flutter_test/flutter_test.dart';

Loading…
Cancel
Save