Rich text editor for Flutter
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.

632 lines
19 KiB

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:tuple/tuple.dart';
import '../../flutter_quill.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/line.dart';
import '../utils/delta.dart';
import 'box.dart';
import 'cursor.dart';
import 'delegate.dart';
import 'link.dart';
import 'text_line.dart';
import 'text_selection.dart';
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'
];
class EditableTextBlock extends StatelessWidget {
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.onCheckboxTap,
required this.readOnly,
this.onLaunchUrl,
this.customStyleBuilder,
Key? key});
final Block block;
final QuillController controller;
final TextDirection textDirection;
final double scrollBottomInset;
final Tuple2 verticalSpacing;
final TextSelection textSelection;
final Color color;
final DefaultStyles? styles;
final bool enableInteractiveSelection;
final bool hasFocus;
final EdgeInsets? contentPadding;
final EmbedsBuilder embedBuilder;
final LinkActionPicker linkActionPicker;
final ValueChanged<String>? onLaunchUrl;
final CustomStyleBuilder? customStyleBuilder;
final CursorCont cursorCont;
final Map<int, int> indentLevelCounts;
final Function(int, bool) onCheckboxTap;
final bool readOnly;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final defaultStyles = QuillStyles.getStyles(context, false);
return _EditableBlock(
block: block,
textDirection: textDirection,
padding: verticalSpacing as Tuple2<double, double>,
scrollBottomInset: scrollBottomInset,
decoration: _getDecorationForBlock(block, defaultStyles) ??
const BoxDecoration(),
contentPadding: contentPadding,
children: _buildChildren(context, indentLevelCounts));
}
3 years ago
BoxDecoration? _getDecorationForBlock(
Block node, DefaultStyles? defaultStyles) {
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;
}
3 years ago
List<Widget> _buildChildren(
BuildContext context, Map<int, int> indentLevelCounts) {
final defaultStyles = QuillStyles.getStyles(context, false);
final count = block.children.length;
final children = <Widget>[];
var index = 0;
for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
index++;
final editableTextLine = EditableTextLine(
line,
_buildLeading(context, line, index, indentLevelCounts, count),
TextLine(
line: line,
textDirection: textDirection,
embedBuilder: embedBuilder,
customStyleBuilder: customStyleBuilder,
styles: styles!,
readOnly: readOnly,
controller: controller,
linkActionPicker: linkActionPicker,
onLaunchUrl: onLaunchUrl,
),
_getIndentWidth(),
_getSpacingForLine(line, index, count, defaultStyles),
textDirection,
textSelection,
color,
enableInteractiveSelection,
hasFocus,
MediaQuery.of(context).devicePixelRatio,
cursorCont);
final nodeTextDirection = getDirectionOfNode(line);
children.add(Directionality(
textDirection: nodeTextDirection, child: editableTextLine));
}
return children.toList(growable: false);
}
3 years ago
Widget? _buildLeading(BuildContext context, Line line, int index,
Map<int, int> indentLevelCounts, int count) {
final defaultStyles = QuillStyles.getStyles(context, false);
final attrs = line.style.attributes;
if (attrs[Attribute.list.key] == Attribute.ol) {
return QuillNumberPoint(
index: index,
indentLevelCounts: indentLevelCounts,
count: count,
style: defaultStyles!.leading!.style,
attrs: attrs,
width: 32,
padding: 8,
);
}
if (attrs[Attribute.list.key] == Attribute.ul) {
return QuillBulletPoint(
3 years ago
style:
defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold),
width: 32,
);
}
if (attrs[Attribute.list.key] == Attribute.checked) {
return CheckboxPoint(
size: 14,
value: true,
enabled: !readOnly,
onChanged: (checked) => onCheckboxTap(line.documentOffset, checked),
uiBuilder: defaultStyles?.lists?.checkboxUIBuilder,
);
}
if (attrs[Attribute.list.key] == Attribute.unchecked) {
return CheckboxPoint(
size: 14,
value: false,
enabled: !readOnly,
onChanged: (checked) => onCheckboxTap(line.documentOffset, checked),
uiBuilder: defaultStyles?.lists?.checkboxUIBuilder,
);
}
if (attrs.containsKey(Attribute.codeBlock.key)) {
return QuillNumberPoint(
index: index,
indentLevelCounts: indentLevelCounts,
count: count,
3 years ago
style: defaultStyles!.code!.style
.copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)),
width: 32,
attrs: attrs,
padding: 16,
withDot: false,
);
}
return null;
}
double _getIndentWidth() {
final attrs = block.style.attributes;
final indent = attrs[Attribute.indent.key];
var extraIndent = 0.0;
if (indent != null && indent.value != null) {
extraIndent = 16.0 * indent.value;
}
if (attrs.containsKey(Attribute.blockQuote.key)) {
return 16.0 + extraIndent;
4 years ago
}
var baseIndent = 0.0;
3 years ago
if (attrs.containsKey(Attribute.list.key) ||
attrs.containsKey(Attribute.codeBlock.key)) {
baseIndent = 32.0;
}
return baseIndent + extraIndent;
}
3 years ago
Tuple2 _getSpacingForLine(
Line node, int index, int count, DefaultStyles? defaultStyles) {
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:
top = defaultStyles!.h1!.verticalSpacing.item1;
bottom = defaultStyles.h1!.verticalSpacing.item2;
break;
case 2:
top = defaultStyles!.h2!.verticalSpacing.item1;
bottom = defaultStyles.h2!.verticalSpacing.item2;
break;
case 3:
top = defaultStyles!.h3!.verticalSpacing.item1;
bottom = defaultStyles.h3!.verticalSpacing.item2;
break;
default:
throw 'Invalid level $level';
}
} else {
late Tuple2 lineSpacing;
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;
} else {
// use paragraph linespacing as a default
lineSpacing = defaultStyles!.paragraph!.lineSpacing;
}
top = lineSpacing.item1;
bottom = lineSpacing.item2;
}
if (index == 1) {
top = 0.0;
}
if (index == count) {
bottom = 0.0;
}
return Tuple2(top, bottom);
}
}
3 years ago
class RenderEditableTextBlock extends RenderEditableContainerBox
implements RenderEditableBox {
RenderEditableTextBlock({
required Block block,
required TextDirection textDirection,
required EdgeInsetsGeometry padding,
required double scrollBottomInset,
required Decoration decoration,
List<RenderEditableBox>? children,
EdgeInsets contentPadding = EdgeInsets.zero,
}) : _decoration = decoration,
_configuration = ImageConfiguration(textDirection: textDirection),
_savedPadding = padding,
_contentPadding = contentPadding,
super(
children: children,
container: block,
textDirection: textDirection,
scrollBottomInset: scrollBottomInset,
padding: padding.add(contentPadding),
);
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(
offset: position.offset - child.container.offset,
affinity: position.affinity,
));
return TextRange(
start: rangeInChild.start + child.container.offset,
end: rangeInChild.end + child.container.offset,
);
}
@override
Offset getOffsetForCaret(TextPosition position) {
final child = childAtPosition(position);
return child.getOffsetForCaret(TextPosition(
offset: position.offset - child.container.offset,
affinity: position.affinity,
)) +
(child.parentData as BoxParentData).offset;
}
@override
TextPosition getPositionForOffset(Offset offset) {
final child = childAtOffset(offset);
final parentData = child.parentData as BoxParentData;
3 years ago
final localPosition =
child.getPositionForOffset(offset - parentData.offset);
return TextPosition(
offset: localPosition.offset + child.container.offset,
affinity: localPosition.affinity,
);
}
@override
TextRange getWordBoundary(TextPosition position) {
final child = childAtPosition(position);
final nodeOffset = child.container.offset;
3 years ago
final childWord = child
.getWordBoundary(TextPosition(offset: position.offset - nodeOffset));
return TextRange(
start: childWord.start + nodeOffset,
end: childWord.end + nodeOffset,
);
}
@override
TextPosition? getPositionAbove(TextPosition position) {
assert(position.offset < container.length);
final child = childAtPosition(position);
3 years ago
final childLocalPosition =
TextPosition(offset: position.offset - child.container.offset);
final result = child.getPositionAbove(childLocalPosition);
if (result != null) {
return TextPosition(offset: result.offset + child.container.offset);
}
final sibling = childBefore(child);
if (sibling == null) {
return null;
}
final caretOffset = child.getOffsetForCaret(childLocalPosition);
final testPosition = TextPosition(offset: sibling.container.length - 1);
final testOffset = sibling.getOffsetForCaret(testPosition);
final finalOffset = Offset(caretOffset.dx, testOffset.dy);
3 years ago
return TextPosition(
offset: sibling.container.offset +
3 years ago
sibling.getPositionForOffset(finalOffset).offset);
}
@override
TextPosition? getPositionBelow(TextPosition position) {
assert(position.offset < container.length);
final child = childAtPosition(position);
3 years ago
final childLocalPosition =
TextPosition(offset: position.offset - child.container.offset);
final result = child.getPositionBelow(childLocalPosition);
if (result != null) {
return TextPosition(offset: result.offset + child.container.offset);
}
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(
offset: sibling.container.offset +
3 years ago
sibling.getPositionForOffset(finalOffset).offset);
}
@override
double preferredLineHeight(TextPosition position) {
final child = childAtPosition(position);
3 years ago
return child.preferredLineHeight(
TextPosition(offset: position.offset - child.container.offset));
}
@override
TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) {
if (selection.isCollapsed) {
return TextSelectionPoint(
3 years ago
Offset(0, preferredLineHeight(selection.extent)) +
getOffsetForCaret(selection.extent),
null);
}
final baseNode = container.queryChild(selection.start, false).node;
var baseChild = firstChild;
while (baseChild != null) {
if (baseChild.container == baseNode) {
break;
}
baseChild = childAfter(baseChild);
}
assert(baseChild != null);
3 years ago
final basePoint = baseChild!.getBaseEndpointForSelection(
localSelection(baseChild.container, selection, true));
3 years ago
return TextSelectionPoint(
basePoint.point + (baseChild.parentData as BoxParentData).offset,
basePoint.direction);
}
@override
TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) {
if (selection.isCollapsed) {
return TextSelectionPoint(
3 years ago
Offset(0, preferredLineHeight(selection.extent)) +
getOffsetForCaret(selection.extent),
null);
}
final extentNode = container.queryChild(selection.end, false).node;
var extentChild = firstChild;
while (extentChild != null) {
if (extentChild.container == extentNode) {
break;
}
extentChild = childAfter(extentChild);
}
assert(extentChild != null);
3 years ago
final extentPoint = extentChild!.getExtentEndpointForSelection(
localSelection(extentChild.container, selection, true));
return TextSelectionPoint(
3 years ago
extentPoint.point + (extentChild.parentData as BoxParentData).offset,
extentPoint.direction);
}
@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));
final debugSaveCount = context.canvas.getSaveCount();
3 years ago
final decorationOffset =
offset.translate(decorationPadding.left, decorationPadding.top);
_painter!.paint(context.canvas, decorationOffset, filledConfiguration);
if (debugSaveCount != context.canvas.getSaveCount()) {
throw '${_decoration.runtimeType} painter had mismatching save and '
'restore calls.';
}
if (decoration.isComplex) {
context.setIsComplexHint();
}
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
@override
Rect getLocalRectForCaret(TextPosition position) {
final child = childAtPosition(position);
final localPosition = TextPosition(
offset: position.offset - child.container.offset,
affinity: position.affinity,
);
final parentData = child.parentData as BoxParentData;
return child.getLocalRectForCaret(localPosition).shift(parentData.offset);
}
@override
TextPosition globalToLocalPosition(TextPosition position) {
assert(container.containsOffset(position.offset),
3 years ago
'The provided text position is not in the current node');
return TextPosition(
offset: position.offset - container.documentOffset,
affinity: position.affinity,
);
}
@override
Rect getCaretPrototype(TextPosition position) {
final child = childAtPosition(position);
final localPosition = TextPosition(
offset: position.offset - child.container.offset,
affinity: position.affinity,
);
return child.getCaretPrototype(localPosition);
}
}
class _EditableBlock extends MultiChildRenderObjectWidget {
3 years ago
_EditableBlock(
{required this.block,
required this.textDirection,
required this.padding,
required this.scrollBottomInset,
required this.decoration,
required this.contentPadding,
required List<Widget> children,
Key? key})
: super(key: key, children: children);
final Block block;
final TextDirection textDirection;
final Tuple2<double, double> padding;
final double scrollBottomInset;
final Decoration decoration;
final EdgeInsets? contentPadding;
3 years ago
EdgeInsets get _padding =>
EdgeInsets.only(top: padding.item1, bottom: padding.item2);
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) {
renderObject
..setContainer(block)
..textDirection = textDirection
..scrollBottomInset = scrollBottomInset
..setPadding(_padding)
..decoration = decoration
..contentPadding = _contentPadding;
}
}