dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
760 lines
22 KiB
760 lines
22 KiB
4 years ago
|
import 'package:flutter/material.dart';
|
||
|
import 'package:flutter/rendering.dart';
|
||
|
|
||
9 months ago
|
import '../../../common/structs/vertical_spacing.dart';
|
||
|
import '../../../common/utils/font.dart';
|
||
|
import '../../../controller/quill_controller.dart';
|
||
|
import '../../../delta/delta_diff.dart';
|
||
|
import '../../../document/attribute.dart';
|
||
|
import '../../../document/nodes/block.dart';
|
||
|
import '../../../document/nodes/line.dart';
|
||
|
import '../../../toolbar/base_toolbar.dart';
|
||
|
import '../../editor.dart';
|
||
|
import '../../embed/embed_editor_builder.dart';
|
||
|
import '../../provider.dart';
|
||
|
import '../../style_widgets/bullet_point.dart';
|
||
|
import '../../style_widgets/checkbox_point.dart';
|
||
|
import '../../style_widgets/number_point.dart';
|
||
|
import '../box.dart';
|
||
|
import '../cursor.dart';
|
||
|
import '../default_styles.dart';
|
||
|
import '../delegate.dart';
|
||
|
import '../link.dart';
|
||
4 years ago
|
import 'text_line.dart';
|
||
9 months ago
|
import 'text_selection.dart';
|
||
4 years ago
|
|
||
3 years ago
|
const List<int> arabianRomanNumbers = [
|
||
|
1000,
|
||
|
900,
|
||
|
500,
|
||
|
400,
|
||
|
100,
|
||
|
90,
|
||
|
50,
|
||
|
40,
|
||
|
10,
|
||
|
9,
|
||
|
5,
|
||
|
4,
|
||
|
1
|
||
|
];
|
||
|
|
||
|
const List<String> romanNumbers = [
|
||
|
'M',
|
||
|
'CM',
|
||
|
'D',
|
||
|
'CD',
|
||
|
'C',
|
||
|
'XC',
|
||
|
'L',
|
||
|
'XL',
|
||
|
'X',
|
||
|
'IX',
|
||
|
'V',
|
||
|
'IV',
|
||
|
'I'
|
||
|
];
|
||
4 years ago
|
|
||
|
class EditableTextBlock extends StatelessWidget {
|
||
2 years ago
|
const EditableTextBlock({
|
||
|
required this.block,
|
||
|
required this.controller,
|
||
|
required this.textDirection,
|
||
|
required this.scrollBottomInset,
|
||
|
required this.verticalSpacing,
|
||
|
required this.textSelection,
|
||
|
required this.color,
|
||
|
required this.styles,
|
||
|
required this.enableInteractiveSelection,
|
||
|
required this.hasFocus,
|
||
|
required this.contentPadding,
|
||
|
required this.embedBuilder,
|
||
|
required this.linkActionPicker,
|
||
|
required this.cursorCont,
|
||
|
required this.indentLevelCounts,
|
||
|
required this.clearIndents,
|
||
|
required this.onCheckboxTap,
|
||
|
required this.readOnly,
|
||
1 year ago
|
this.checkBoxReadOnly,
|
||
2 years ago
|
this.onLaunchUrl,
|
||
|
this.customStyleBuilder,
|
||
|
this.customLinkPrefixes = const <String>[],
|
||
|
super.key,
|
||
|
});
|
||
4 years ago
|
|
||
|
final Block block;
|
||
3 years ago
|
final QuillController controller;
|
||
4 years ago
|
final TextDirection textDirection;
|
||
|
final double scrollBottomInset;
|
||
2 years ago
|
final VerticalSpacing verticalSpacing;
|
||
4 years ago
|
final TextSelection textSelection;
|
||
|
final Color color;
|
||
|
final DefaultStyles? styles;
|
||
|
final bool enableInteractiveSelection;
|
||
|
final bool hasFocus;
|
||
|
final EdgeInsets? contentPadding;
|
||
3 years ago
|
final EmbedsBuilder embedBuilder;
|
||
3 years ago
|
final LinkActionPicker linkActionPicker;
|
||
|
final ValueChanged<String>? onLaunchUrl;
|
||
4 years ago
|
final CustomStyleBuilder? customStyleBuilder;
|
||
4 years ago
|
final CursorCont cursorCont;
|
||
|
final Map<int, int> indentLevelCounts;
|
||
2 years ago
|
final bool clearIndents;
|
||
4 years ago
|
final Function(int, bool) onCheckboxTap;
|
||
4 years ago
|
final bool readOnly;
|
||
1 year ago
|
final bool? checkBoxReadOnly;
|
||
2 years ago
|
final List<String> customLinkPrefixes;
|
||
4 years ago
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
assert(debugCheckHasMediaQuery(context));
|
||
|
|
||
|
final defaultStyles = QuillStyles.getStyles(context, false);
|
||
|
return _EditableBlock(
|
||
2 years ago
|
block: block,
|
||
|
textDirection: textDirection,
|
||
|
padding: verticalSpacing,
|
||
|
scrollBottomInset: scrollBottomInset,
|
||
|
decoration:
|
||
|
_getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(),
|
||
|
contentPadding: contentPadding,
|
||
|
children: _buildChildren(
|
||
|
context,
|
||
|
indentLevelCounts,
|
||
|
clearIndents,
|
||
|
),
|
||
|
);
|
||
4 years ago
|
}
|
||
|
|
||
3 years ago
|
BoxDecoration? _getDecorationForBlock(
|
||
|
Block node, DefaultStyles? defaultStyles) {
|
||
4 years ago
|
final attrs = block.style.attributes;
|
||
|
if (attrs.containsKey(Attribute.blockQuote.key)) {
|
||
|
return defaultStyles!.quote!.decoration;
|
||
|
}
|
||
|
if (attrs.containsKey(Attribute.codeBlock.key)) {
|
||
|
return defaultStyles!.code!.decoration;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
2 years ago
|
List<Widget> _buildChildren(BuildContext context,
|
||
|
Map<int, int> indentLevelCounts, bool clearIndents) {
|
||
4 years ago
|
final defaultStyles = QuillStyles.getStyles(context, false);
|
||
|
final count = block.children.length;
|
||
|
final children = <Widget>[];
|
||
2 years ago
|
if (clearIndents) {
|
||
|
indentLevelCounts.clear();
|
||
|
}
|
||
4 years ago
|
var index = 0;
|
||
|
for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
|
||
|
index++;
|
||
|
final editableTextLine = EditableTextLine(
|
||
2 years ago
|
line,
|
||
1 year ago
|
_buildLeading(
|
||
|
context: context,
|
||
|
line: line,
|
||
|
index: index,
|
||
|
indentLevelCounts: indentLevelCounts,
|
||
|
count: count,
|
||
|
),
|
||
2 years ago
|
TextLine(
|
||
|
line: line,
|
||
|
textDirection: textDirection,
|
||
|
embedBuilder: embedBuilder,
|
||
|
customStyleBuilder: customStyleBuilder,
|
||
|
styles: styles!,
|
||
|
readOnly: readOnly,
|
||
|
controller: controller,
|
||
|
linkActionPicker: linkActionPicker,
|
||
|
onLaunchUrl: onLaunchUrl,
|
||
|
customLinkPrefixes: customLinkPrefixes,
|
||
|
),
|
||
|
_getIndentWidth(context, count),
|
||
|
_getSpacingForLine(line, index, count, defaultStyles),
|
||
|
textDirection,
|
||
|
textSelection,
|
||
|
color,
|
||
|
enableInteractiveSelection,
|
||
|
hasFocus,
|
||
|
MediaQuery.devicePixelRatioOf(context),
|
||
|
cursorCont,
|
||
|
);
|
||
3 years ago
|
final nodeTextDirection = getDirectionOfNode(line);
|
||
2 years ago
|
children.add(
|
||
|
Directionality(
|
||
|
textDirection: nodeTextDirection,
|
||
|
child: editableTextLine,
|
||
|
),
|
||
|
);
|
||
4 years ago
|
}
|
||
|
return children.toList(growable: false);
|
||
|
}
|
||
|
|
||
2 years ago
|
double _numberPointWidth(double fontSize, int count) {
|
||
|
final length = '$count'.length;
|
||
|
switch (length) {
|
||
|
case 1:
|
||
|
case 2:
|
||
|
return fontSize * 2;
|
||
|
default:
|
||
|
// 3 -> 2.5
|
||
|
// 4 -> 3
|
||
|
// 5 -> 3.5
|
||
|
return fontSize * (length - (length - 2) / 2);
|
||
|
}
|
||
|
}
|
||
|
|
||
1 year ago
|
Widget? _buildLeading({
|
||
|
required BuildContext context,
|
||
|
required Line line,
|
||
|
required int index,
|
||
|
required Map<int, int> indentLevelCounts,
|
||
|
required int count,
|
||
|
}) {
|
||
2 years ago
|
final defaultStyles = QuillStyles.getStyles(context, false)!;
|
||
|
final fontSize = defaultStyles.paragraph?.style.fontSize ?? 16;
|
||
4 years ago
|
final attrs = line.style.attributes;
|
||
2 years ago
|
|
||
1 year ago
|
// Of the color button
|
||
|
final fontColor =
|
||
|
line.toDelta().operations.first.attributes?[Attribute.color.key] != null
|
||
|
? hexToColor(
|
||
|
line
|
||
|
.toDelta()
|
||
|
.operations
|
||
|
.first
|
||
|
.attributes?[Attribute.color.key],
|
||
|
)
|
||
|
: null;
|
||
|
|
||
|
// Of the size button
|
||
|
final size =
|
||
|
line.toDelta().operations.first.attributes?[Attribute.size.key] != null
|
||
|
? getFontSizeAsDouble(
|
||
|
line.toDelta().operations.first.attributes?[Attribute.size.key],
|
||
|
defaultStyles: defaultStyles,
|
||
|
)
|
||
|
: null;
|
||
|
|
||
|
// Of the alignment buttons
|
||
|
// final textAlign = line.style.attributes[Attribute.align.key]?.value != null
|
||
|
// ? getTextAlign(line.style.attributes[Attribute.align.key]?.value)
|
||
|
// : null;
|
||
|
|
||
2 years ago
|
if (attrs[Attribute.list.key] == Attribute.ol) {
|
||
1 year ago
|
return QuillEditorNumberPoint(
|
||
2 years ago
|
index: index,
|
||
|
indentLevelCounts: indentLevelCounts,
|
||
|
count: count,
|
||
1 year ago
|
style: defaultStyles.leading!.style.copyWith(
|
||
|
fontSize: size,
|
||
|
color: context.quillEditorElementOptions?.orderedList
|
||
|
.useTextColorForDot ==
|
||
|
true
|
||
|
? fontColor
|
||
|
: null,
|
||
|
),
|
||
2 years ago
|
attrs: attrs,
|
||
2 years ago
|
width: _numberPointWidth(fontSize, count),
|
||
2 years ago
|
padding: fontSize / 2,
|
||
2 years ago
|
);
|
||
|
}
|
||
|
|
||
|
if (attrs[Attribute.list.key] == Attribute.ul) {
|
||
1 year ago
|
return QuillEditorBulletPoint(
|
||
1 year ago
|
style: defaultStyles.leading!.style.copyWith(
|
||
|
fontWeight: FontWeight.bold,
|
||
|
fontSize: size,
|
||
|
color: context.quillEditorElementOptions?.unorderedList
|
||
|
.useTextColorForDot ==
|
||
|
true
|
||
|
? fontColor
|
||
|
: null,
|
||
|
),
|
||
2 years ago
|
width: fontSize * 2,
|
||
|
padding: fontSize / 2,
|
||
2 years ago
|
);
|
||
|
}
|
||
4 years ago
|
|
||
2 years ago
|
if (attrs[Attribute.list.key] == Attribute.checked ||
|
||
|
attrs[Attribute.list.key] == Attribute.unchecked) {
|
||
1 year ago
|
return QuillEditorCheckboxPoint(
|
||
2 years ago
|
size: fontSize,
|
||
2 years ago
|
value: attrs[Attribute.list.key] == Attribute.checked,
|
||
1 year ago
|
enabled: !(checkBoxReadOnly ?? readOnly),
|
||
3 years ago
|
onChanged: (checked) => onCheckboxTap(line.documentOffset, checked),
|
||
2 years ago
|
uiBuilder: defaultStyles.lists?.checkboxUIBuilder,
|
||
4 years ago
|
);
|
||
|
}
|
||
2 years ago
|
if (attrs.containsKey(Attribute.codeBlock.key) &&
|
||
2 years ago
|
context.requireQuillEditorElementOptions.codeBlock.enableLineNumbers) {
|
||
1 year ago
|
return QuillEditorNumberPoint(
|
||
4 years ago
|
index: index,
|
||
|
indentLevelCounts: indentLevelCounts,
|
||
|
count: count,
|
||
2 years ago
|
style: defaultStyles.code!.style
|
||
3 years ago
|
.copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)),
|
||
2 years ago
|
width: _numberPointWidth(fontSize, count),
|
||
4 years ago
|
attrs: attrs,
|
||
2 years ago
|
padding: fontSize,
|
||
4 years ago
|
withDot: false,
|
||
|
);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
2 years ago
|
double _getIndentWidth(BuildContext context, int count) {
|
||
2 years ago
|
final defaultStyles = QuillStyles.getStyles(context, false)!;
|
||
|
final fontSize = defaultStyles.paragraph?.style.fontSize ?? 16;
|
||
4 years ago
|
final attrs = block.style.attributes;
|
||
|
|
||
|
final indent = attrs[Attribute.indent.key];
|
||
|
var extraIndent = 0.0;
|
||
|
if (indent != null && indent.value != null) {
|
||
2 years ago
|
extraIndent = fontSize * indent.value;
|
||
4 years ago
|
}
|
||
|
|
||
|
if (attrs.containsKey(Attribute.blockQuote.key)) {
|
||
2 years ago
|
return fontSize + extraIndent;
|
||
4 years ago
|
}
|
||
|
|
||
|
var baseIndent = 0.0;
|
||
|
|
||
2 years ago
|
if (attrs.containsKey(Attribute.list.key)) {
|
||
2 years ago
|
baseIndent = fontSize * 2;
|
||
2 years ago
|
if (attrs[Attribute.list.key] == Attribute.ol) {
|
||
|
baseIndent = _numberPointWidth(fontSize, count);
|
||
|
} else if (attrs.containsKey(Attribute.codeBlock.key)) {
|
||
|
baseIndent = _numberPointWidth(fontSize, count);
|
||
|
}
|
||
4 years ago
|
}
|
||
|
|
||
4 years ago
|
return baseIndent + extraIndent;
|
||
4 years ago
|
}
|
||
|
|
||
2 years ago
|
VerticalSpacing _getSpacingForLine(
|
||
1 year ago
|
Line node,
|
||
|
int index,
|
||
|
int count,
|
||
|
DefaultStyles? defaultStyles,
|
||
|
) {
|
||
4 years ago
|
var top = 0.0, bottom = 0.0;
|
||
|
|
||
|
final attrs = block.style.attributes;
|
||
|
if (attrs.containsKey(Attribute.header.key)) {
|
||
|
final level = attrs[Attribute.header.key]!.value;
|
||
|
switch (level) {
|
||
|
case 1:
|
||
2 years ago
|
top = defaultStyles!.h1!.verticalSpacing.top;
|
||
|
bottom = defaultStyles.h1!.verticalSpacing.bottom;
|
||
4 years ago
|
break;
|
||
|
case 2:
|
||
2 years ago
|
top = defaultStyles!.h2!.verticalSpacing.top;
|
||
|
bottom = defaultStyles.h2!.verticalSpacing.bottom;
|
||
4 years ago
|
break;
|
||
|
case 3:
|
||
2 years ago
|
top = defaultStyles!.h3!.verticalSpacing.top;
|
||
|
bottom = defaultStyles.h3!.verticalSpacing.bottom;
|
||
4 years ago
|
break;
|
||
1 year ago
|
case 4:
|
||
|
top = defaultStyles!.h4!.verticalSpacing.top;
|
||
|
bottom = defaultStyles.h4!.verticalSpacing.bottom;
|
||
|
break;
|
||
|
case 5:
|
||
|
top = defaultStyles!.h5!.verticalSpacing.top;
|
||
|
bottom = defaultStyles.h5!.verticalSpacing.bottom;
|
||
|
break;
|
||
|
case 6:
|
||
|
top = defaultStyles!.h6!.verticalSpacing.top;
|
||
|
bottom = defaultStyles.h6!.verticalSpacing.bottom;
|
||
|
break;
|
||
4 years ago
|
default:
|
||
1 year ago
|
throw ArgumentError('Invalid level $level');
|
||
4 years ago
|
}
|
||
|
} else {
|
||
1 year ago
|
final VerticalSpacing lineSpacing;
|
||
4 years ago
|
if (attrs.containsKey(Attribute.blockQuote.key)) {
|
||
|
lineSpacing = defaultStyles!.quote!.lineSpacing;
|
||
|
} else if (attrs.containsKey(Attribute.indent.key)) {
|
||
|
lineSpacing = defaultStyles!.indent!.lineSpacing;
|
||
|
} else if (attrs.containsKey(Attribute.list.key)) {
|
||
|
lineSpacing = defaultStyles!.lists!.lineSpacing;
|
||
|
} else if (attrs.containsKey(Attribute.codeBlock.key)) {
|
||
|
lineSpacing = defaultStyles!.code!.lineSpacing;
|
||
|
} else if (attrs.containsKey(Attribute.align.key)) {
|
||
|
lineSpacing = defaultStyles!.align!.lineSpacing;
|
||
3 years ago
|
} else {
|
||
|
// use paragraph linespacing as a default
|
||
|
lineSpacing = defaultStyles!.paragraph!.lineSpacing;
|
||
4 years ago
|
}
|
||
2 years ago
|
top = lineSpacing.top;
|
||
|
bottom = lineSpacing.bottom;
|
||
4 years ago
|
}
|
||
|
|
||
|
if (index == 1) {
|
||
|
top = 0.0;
|
||
|
}
|
||
|
|
||
|
if (index == count) {
|
||
|
bottom = 0.0;
|
||
|
}
|
||
|
|
||
2 years ago
|
return VerticalSpacing(top, bottom);
|
||
4 years ago
|
}
|
||
|
}
|
||
|
|
||
3 years ago
|
class RenderEditableTextBlock extends RenderEditableContainerBox
|
||
|
implements RenderEditableBox {
|
||
4 years ago
|
RenderEditableTextBlock({
|
||
|
required Block block,
|
||
1 year ago
|
required super.textDirection,
|
||
4 years ago
|
required EdgeInsetsGeometry padding,
|
||
1 year ago
|
required super.scrollBottomInset,
|
||
4 years ago
|
required Decoration decoration,
|
||
1 year ago
|
super.children,
|
||
4 years ago
|
EdgeInsets contentPadding = EdgeInsets.zero,
|
||
|
}) : _decoration = decoration,
|
||
3 years ago
|
_configuration = ImageConfiguration(textDirection: textDirection),
|
||
4 years ago
|
_savedPadding = padding,
|
||
|
_contentPadding = contentPadding,
|
||
|
super(
|
||
3 years ago
|
container: block,
|
||
|
padding: padding.add(contentPadding),
|
||
4 years ago
|
);
|
||
|
|
||
|
EdgeInsetsGeometry _savedPadding;
|
||
|
EdgeInsets _contentPadding;
|
||
|
|
||
|
set contentPadding(EdgeInsets value) {
|
||
|
if (_contentPadding == value) return;
|
||
|
_contentPadding = value;
|
||
|
super.setPadding(_savedPadding.add(_contentPadding));
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void setPadding(EdgeInsetsGeometry value) {
|
||
|
super.setPadding(value.add(_contentPadding));
|
||
|
_savedPadding = value;
|
||
|
}
|
||
|
|
||
|
BoxPainter? _painter;
|
||
|
|
||
|
Decoration get decoration => _decoration;
|
||
|
Decoration _decoration;
|
||
|
|
||
|
set decoration(Decoration value) {
|
||
|
if (value == _decoration) return;
|
||
|
_painter?.dispose();
|
||
|
_painter = null;
|
||
|
_decoration = value;
|
||
|
markNeedsPaint();
|
||
|
}
|
||
|
|
||
|
ImageConfiguration get configuration => _configuration;
|
||
|
ImageConfiguration _configuration;
|
||
|
|
||
|
set configuration(ImageConfiguration value) {
|
||
|
if (value == _configuration) return;
|
||
|
_configuration = value;
|
||
|
markNeedsPaint();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
TextRange getLineBoundary(TextPosition position) {
|
||
|
final child = childAtPosition(position);
|
||
|
final rangeInChild = child.getLineBoundary(TextPosition(
|
||
3 years ago
|
offset: position.offset - child.container.offset,
|
||
4 years ago
|
affinity: position.affinity,
|
||
|
));
|
||
|
return TextRange(
|
||
3 years ago
|
start: rangeInChild.start + child.container.offset,
|
||
|
end: rangeInChild.end + child.container.offset,
|
||
4 years ago
|
);
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Offset getOffsetForCaret(TextPosition position) {
|
||
|
final child = childAtPosition(position);
|
||
|
return child.getOffsetForCaret(TextPosition(
|
||
3 years ago
|
offset: position.offset - child.container.offset,
|
||
4 years ago
|
affinity: position.affinity,
|
||
|
)) +
|
||
|
(child.parentData as BoxParentData).offset;
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
TextPosition getPositionForOffset(Offset offset) {
|
||
3 years ago
|
final child = childAtOffset(offset);
|
||
4 years ago
|
final parentData = child.parentData as BoxParentData;
|
||
3 years ago
|
final localPosition =
|
||
|
child.getPositionForOffset(offset - parentData.offset);
|
||
4 years ago
|
return TextPosition(
|
||
3 years ago
|
offset: localPosition.offset + child.container.offset,
|
||
4 years ago
|
affinity: localPosition.affinity,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
TextRange getWordBoundary(TextPosition position) {
|
||
|
final child = childAtPosition(position);
|
||
3 years ago
|
final nodeOffset = child.container.offset;
|
||
3 years ago
|
final childWord = child
|
||
|
.getWordBoundary(TextPosition(offset: position.offset - nodeOffset));
|
||
4 years ago
|
return TextRange(
|
||
|
start: childWord.start + nodeOffset,
|
||
|
end: childWord.end + nodeOffset,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
TextPosition? getPositionAbove(TextPosition position) {
|
||
3 years ago
|
assert(position.offset < container.length);
|
||
4 years ago
|
|
||
|
final child = childAtPosition(position);
|
||
3 years ago
|
final childLocalPosition =
|
||
3 years ago
|
TextPosition(offset: position.offset - child.container.offset);
|
||
4 years ago
|
final result = child.getPositionAbove(childLocalPosition);
|
||
|
if (result != null) {
|
||
3 years ago
|
return TextPosition(offset: result.offset + child.container.offset);
|
||
4 years ago
|
}
|
||
|
|
||
|
final sibling = childBefore(child);
|
||
|
if (sibling == null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
final caretOffset = child.getOffsetForCaret(childLocalPosition);
|
||
3 years ago
|
final testPosition = TextPosition(offset: sibling.container.length - 1);
|
||
4 years ago
|
final testOffset = sibling.getOffsetForCaret(testPosition);
|
||
|
final finalOffset = Offset(caretOffset.dx, testOffset.dy);
|
||
3 years ago
|
return TextPosition(
|
||
3 years ago
|
offset: sibling.container.offset +
|
||
3 years ago
|
sibling.getPositionForOffset(finalOffset).offset);
|
||
4 years ago
|
}
|
||
|
|
||
|
@override
|
||
|
TextPosition? getPositionBelow(TextPosition position) {
|
||
3 years ago
|
assert(position.offset < container.length);
|
||
4 years ago
|
|
||
|
final child = childAtPosition(position);
|
||
3 years ago
|
final childLocalPosition =
|
||
3 years ago
|
TextPosition(offset: position.offset - child.container.offset);
|
||
4 years ago
|
final result = child.getPositionBelow(childLocalPosition);
|
||
|
if (result != null) {
|
||
3 years ago
|
return TextPosition(offset: result.offset + child.container.offset);
|
||
4 years ago
|
}
|
||
|
|
||
|
final sibling = childAfter(child);
|
||
|
if (sibling == null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
final caretOffset = child.getOffsetForCaret(childLocalPosition);
|
||
|
final testOffset = sibling.getOffsetForCaret(const TextPosition(offset: 0));
|
||
|
final finalOffset = Offset(caretOffset.dx, testOffset.dy);
|
||
3 years ago
|
return TextPosition(
|
||
3 years ago
|
offset: sibling.container.offset +
|
||
3 years ago
|
sibling.getPositionForOffset(finalOffset).offset);
|
||
4 years ago
|
}
|
||
|
|
||
|
@override
|
||
|
double preferredLineHeight(TextPosition position) {
|
||
|
final child = childAtPosition(position);
|
||
3 years ago
|
return child.preferredLineHeight(
|
||
3 years ago
|
TextPosition(offset: position.offset - child.container.offset));
|
||
4 years ago
|
}
|
||
|
|
||
|
@override
|
||
|
TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) {
|
||
|
if (selection.isCollapsed) {
|
||
|
return TextSelectionPoint(
|
||
1 year ago
|
Offset(0, preferredLineHeight(selection.extent)) +
|
||
|
getOffsetForCaret(selection.extent),
|
||
|
null,
|
||
|
);
|
||
4 years ago
|
}
|
||
|
|
||
1 year ago
|
final baseNode = container
|
||
|
.queryChild(
|
||
|
selection.start,
|
||
|
false,
|
||
|
)
|
||
|
.node;
|
||
4 years ago
|
var baseChild = firstChild;
|
||
|
while (baseChild != null) {
|
||
3 years ago
|
if (baseChild.container == baseNode) {
|
||
4 years ago
|
break;
|
||
|
}
|
||
|
baseChild = childAfter(baseChild);
|
||
|
}
|
||
|
assert(baseChild != null);
|
||
|
|
||
3 years ago
|
final basePoint = baseChild!.getBaseEndpointForSelection(
|
||
1 year ago
|
localSelection(
|
||
|
baseChild.container,
|
||
|
selection,
|
||
|
true,
|
||
|
),
|
||
|
);
|
||
3 years ago
|
return TextSelectionPoint(
|
||
1 year ago
|
basePoint.point + (baseChild.parentData as BoxParentData).offset,
|
||
|
basePoint.direction,
|
||
|
);
|
||
4 years ago
|
}
|
||
|
|
||
|
@override
|
||
|
TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) {
|
||
|
if (selection.isCollapsed) {
|
||
|
return TextSelectionPoint(
|
||
1 year ago
|
Offset(0, preferredLineHeight(selection.extent)) +
|
||
|
getOffsetForCaret(selection.extent),
|
||
|
null,
|
||
|
);
|
||
4 years ago
|
}
|
||
|
|
||
3 years ago
|
final extentNode = container.queryChild(selection.end, false).node;
|
||
4 years ago
|
|
||
|
var extentChild = firstChild;
|
||
|
while (extentChild != null) {
|
||
3 years ago
|
if (extentChild.container == extentNode) {
|
||
4 years ago
|
break;
|
||
|
}
|
||
|
extentChild = childAfter(extentChild);
|
||
|
}
|
||
|
assert(extentChild != null);
|
||
|
|
||
3 years ago
|
final extentPoint = extentChild!.getExtentEndpointForSelection(
|
||
1 year ago
|
localSelection(
|
||
|
extentChild.container,
|
||
|
selection,
|
||
|
true,
|
||
|
),
|
||
|
);
|
||
4 years ago
|
return TextSelectionPoint(
|
||
1 year ago
|
extentPoint.point + (extentChild.parentData as BoxParentData).offset,
|
||
|
extentPoint.direction,
|
||
|
);
|
||
4 years ago
|
}
|
||
|
|
||
|
@override
|
||
|
void detach() {
|
||
|
_painter?.dispose();
|
||
|
_painter = null;
|
||
|
super.detach();
|
||
|
markNeedsPaint();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void paint(PaintingContext context, Offset offset) {
|
||
|
_paintDecoration(context, offset);
|
||
|
defaultPaint(context, offset);
|
||
|
}
|
||
|
|
||
|
void _paintDecoration(PaintingContext context, Offset offset) {
|
||
|
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
|
||
|
|
||
|
final decorationPadding = resolvedPadding! - _contentPadding;
|
||
|
|
||
3 years ago
|
final filledConfiguration =
|
||
|
configuration.copyWith(size: decorationPadding.deflateSize(size));
|
||
4 years ago
|
final debugSaveCount = context.canvas.getSaveCount();
|
||
|
|
||
3 years ago
|
final decorationOffset =
|
||
|
offset.translate(decorationPadding.left, decorationPadding.top);
|
||
4 years ago
|
_painter!.paint(context.canvas, decorationOffset, filledConfiguration);
|
||
|
if (debugSaveCount != context.canvas.getSaveCount()) {
|
||
1 year ago
|
throw StateError(
|
||
|
'${_decoration.runtimeType} painter had mismatching save and '
|
||
|
'restore calls.',
|
||
|
);
|
||
4 years ago
|
}
|
||
|
if (decoration.isComplex) {
|
||
|
context.setIsComplexHint();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||
|
return defaultHitTestChildren(result, position: position);
|
||
|
}
|
||
4 years ago
|
|
||
|
@override
|
||
|
Rect getLocalRectForCaret(TextPosition position) {
|
||
|
final child = childAtPosition(position);
|
||
|
final localPosition = TextPosition(
|
||
3 years ago
|
offset: position.offset - child.container.offset,
|
||
4 years ago
|
affinity: position.affinity,
|
||
|
);
|
||
|
final parentData = child.parentData as BoxParentData;
|
||
|
return child.getLocalRectForCaret(localPosition).shift(parentData.offset);
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
TextPosition globalToLocalPosition(TextPosition position) {
|
||
9 months ago
|
assert(container.containsOffset(position.offset) || container.length == 0,
|
||
3 years ago
|
'The provided text position is not in the current node');
|
||
4 years ago
|
return TextPosition(
|
||
3 years ago
|
offset: position.offset - container.documentOffset,
|
||
4 years ago
|
affinity: position.affinity,
|
||
|
);
|
||
|
}
|
||
3 years ago
|
|
||
|
@override
|
||
|
Rect getCaretPrototype(TextPosition position) {
|
||
|
final child = childAtPosition(position);
|
||
|
final localPosition = TextPosition(
|
||
3 years ago
|
offset: position.offset - child.container.offset,
|
||
3 years ago
|
affinity: position.affinity,
|
||
|
);
|
||
|
return child.getCaretPrototype(localPosition);
|
||
|
}
|
||
4 years ago
|
}
|
||
|
|
||
|
class _EditableBlock extends MultiChildRenderObjectWidget {
|
||
2 years ago
|
const _EditableBlock(
|
||
3 years ago
|
{required this.block,
|
||
|
required this.textDirection,
|
||
|
required this.padding,
|
||
|
required this.scrollBottomInset,
|
||
|
required this.decoration,
|
||
|
required this.contentPadding,
|
||
1 year ago
|
required super.children});
|
||
4 years ago
|
|
||
|
final Block block;
|
||
|
final TextDirection textDirection;
|
||
2 years ago
|
final VerticalSpacing padding;
|
||
4 years ago
|
final double scrollBottomInset;
|
||
|
final Decoration decoration;
|
||
|
final EdgeInsets? contentPadding;
|
||
|
|
||
3 years ago
|
EdgeInsets get _padding =>
|
||
2 years ago
|
EdgeInsets.only(top: padding.top, bottom: padding.bottom);
|
||
4 years ago
|
|
||
|
EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero;
|
||
|
|
||
|
@override
|
||
|
RenderEditableTextBlock createRenderObject(BuildContext context) {
|
||
|
return RenderEditableTextBlock(
|
||
|
block: block,
|
||
|
textDirection: textDirection,
|
||
|
padding: _padding,
|
||
|
scrollBottomInset: scrollBottomInset,
|
||
|
decoration: decoration,
|
||
|
contentPadding: _contentPadding,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
@override
|
||
3 years ago
|
void updateRenderObject(
|
||
|
BuildContext context, covariant RenderEditableTextBlock renderObject) {
|
||
4 years ago
|
renderObject
|
||
|
..setContainer(block)
|
||
|
..textDirection = textDirection
|
||
|
..scrollBottomInset = scrollBottomInset
|
||
|
..setPadding(_padding)
|
||
|
..decoration = decoration
|
||
|
..contentPadding = _contentPadding;
|
||
|
}
|
||
|
}
|