mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-11-12 23:16:03 +03:00
feat: support selection overlay
This commit is contained in:
parent
ce953d802a
commit
e2f35dd5cc
25
frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json
vendored
Normal file
25
frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "example",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "example (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "example (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:example/plugin/document_node_widget.dart';
|
||||
import 'package:example/plugin/selected_text_node_widget.dart';
|
||||
import 'package:example/plugin/text_with_heading_node_widget.dart';
|
||||
import 'package:example/plugin/image_node_widget.dart';
|
||||
import 'package:example/plugin/text_node_widget.dart';
|
||||
@ -65,7 +66,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
|
||||
renderPlugins
|
||||
..register('editor', EditorNodeWidgetBuilder.create)
|
||||
..register('text', TextNodeBuilder.create)
|
||||
..register('text', SelectedTextNodeBuilder.create)
|
||||
..register('image', ImageNodeBuilder.create)
|
||||
..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create)
|
||||
..register('text/with-heading', TextWithHeadingNodeBuilder.create);
|
||||
|
@ -0,0 +1,102 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class DebuggableRichText extends StatefulWidget {
|
||||
final InlineSpan text;
|
||||
final GlobalKey textKey;
|
||||
|
||||
const DebuggableRichText({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.textKey,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DebuggableRichText> createState() => _DebuggableRichTextState();
|
||||
}
|
||||
|
||||
class _DebuggableRichTextState extends State<DebuggableRichText> {
|
||||
final List<Rect> _textRects = [];
|
||||
|
||||
RenderParagraph get _renderParagraph =>
|
||||
widget.textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_updateTextRects();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
painter: _BoxPainter(
|
||||
rects: _textRects,
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
key: widget.textKey,
|
||||
text: widget.text,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTextRects() {
|
||||
setState(() {
|
||||
_textRects
|
||||
..clear()
|
||||
..addAll(
|
||||
_computeLocalSelectionRects(
|
||||
TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: widget.text.toPlainText().length,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<Rect> _computeLocalSelectionRects(TextSelection selection) {
|
||||
final textBoxes = _renderParagraph.getBoxesForSelection(selection);
|
||||
return textBoxes.map((box) => box.toRect()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _BoxPainter extends CustomPainter {
|
||||
final List<Rect> _rects;
|
||||
final Paint _paint;
|
||||
|
||||
_BoxPainter({
|
||||
required List<Rect> rects,
|
||||
bool fill = false,
|
||||
}) : _rects = rects,
|
||||
_paint = Paint() {
|
||||
_paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final rect in _rects) {
|
||||
canvas.drawRect(
|
||||
rect,
|
||||
_paint
|
||||
..color = Color(
|
||||
(Random().nextDouble() * 0xFFFFFF).toInt(),
|
||||
).withOpacity(1.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditorNodeWidgetBuilder extends NodeWidgetBuilder {
|
||||
EditorNodeWidgetBuilder.create({
|
||||
required super.editorState,
|
||||
required super.node,
|
||||
required super.key,
|
||||
}) : super.create();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext buildContext) {
|
||||
return SingleChildScrollView(
|
||||
key: key,
|
||||
child: _EditorNodeWidget(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
@ -30,21 +33,49 @@ class _EditorNodeWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(e) => editorState.renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(
|
||||
buildContext: context,
|
||||
node: e,
|
||||
editorState: editorState,
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: {
|
||||
PanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
||||
() => PanGestureRecognizer(),
|
||||
(recognizer) {
|
||||
recognizer
|
||||
..onStart = _onPanStart
|
||||
..onUpdate = _onPanUpdate
|
||||
..onEnd = _onPanEnd;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(e) => editorState.renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(
|
||||
buildContext: context,
|
||||
node: e,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPanStart(DragStartDetails details) {
|
||||
editorState.panStartOffset = details.globalPosition;
|
||||
}
|
||||
|
||||
void _onPanUpdate(DragUpdateDetails details) {
|
||||
editorState.panEndOffset = details.globalPosition;
|
||||
editorState.updateSelection();
|
||||
}
|
||||
|
||||
void _onPanEnd(DragEndDetails details) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
@ -5,18 +5,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder {
|
||||
ImageNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.editorState,
|
||||
required super.key,
|
||||
}) : super.create();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext buildContext) {
|
||||
return _ImageNodeWidget(
|
||||
key: key,
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageNodeWidget extends StatelessWidget {
|
||||
class _ImageNodeWidget extends StatefulWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@ -26,7 +28,22 @@ class _ImageNodeWidget extends StatelessWidget {
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
String get src => node.attributes['image_src'] as String;
|
||||
@override
|
||||
State<_ImageNodeWidget> createState() => __ImageNodeWidgetState();
|
||||
}
|
||||
|
||||
class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
|
||||
Node get node => widget.node;
|
||||
EditorState get editorState => widget.editorState;
|
||||
String get src => widget.node.attributes['image_src'] as String;
|
||||
|
||||
@override
|
||||
List<Rect> getOverlayRectsInRange(Offset start, Offset end) {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
||||
return [boxOffset & size];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -0,0 +1,223 @@
|
||||
import 'package:example/plugin/debuggable_rich_text.dart';
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SelectedTextNodeBuilder extends NodeWidgetBuilder {
|
||||
SelectedTextNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.editorState,
|
||||
required super.key,
|
||||
}) : super.create() {
|
||||
nodeValidator = ((node) {
|
||||
return node.type == 'text';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext buildContext) {
|
||||
return _SelectedTextNodeWidget(
|
||||
key: key,
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectedTextNodeWidget extends StatefulWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
const _SelectedTextNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_SelectedTextNodeWidget> createState() =>
|
||||
_SelectedTextNodeWidgetState();
|
||||
}
|
||||
|
||||
class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
with Selectable {
|
||||
TextNode get node => widget.node as TextNode;
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
final _textKey = GlobalKey();
|
||||
|
||||
RenderParagraph get _renderParagraph =>
|
||||
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||
|
||||
@override
|
||||
List<Rect> getOverlayRectsInRange(Offset start, Offset end) {
|
||||
// Returns select all if the start or end exceeds the size of the box
|
||||
// TODO: don't need to compute everytime.
|
||||
var rects = _computeSelectionRects(
|
||||
TextSelection(baseOffset: 0, extentOffset: node.toRawString().length),
|
||||
);
|
||||
|
||||
if (end.dy > start.dy) {
|
||||
// downward
|
||||
if (end.dy >= rects.last.bottom) {
|
||||
return rects;
|
||||
}
|
||||
} else {
|
||||
// upward
|
||||
if (end.dy <= rects.first.top) {
|
||||
return rects;
|
||||
}
|
||||
}
|
||||
|
||||
final selectionBaseOffset = _getTextPositionAtOffset(start).offset;
|
||||
final selectionExtentOffset = _getTextPositionAtOffset(end).offset;
|
||||
final textSelection = TextSelection(
|
||||
baseOffset: selectionBaseOffset,
|
||||
extentOffset: selectionExtentOffset,
|
||||
);
|
||||
return _computeSelectionRects(textSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget richText;
|
||||
if (kDebugMode) {
|
||||
richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey);
|
||||
} else {
|
||||
richText = RichText(key: _textKey, text: node.toTextSpan());
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
richText,
|
||||
if (node.children.isNotEmpty)
|
||||
...node.children.map(
|
||||
(e) => editorState.renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(
|
||||
buildContext: context,
|
||||
node: e,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
TextPosition _getTextPositionAtOffset(Offset offset) {
|
||||
final textOffset = _renderParagraph.globalToLocal(offset);
|
||||
return _renderParagraph.getPositionForOffset(textOffset);
|
||||
}
|
||||
|
||||
List<Rect> _computeSelectionRects(TextSelection selection) {
|
||||
final textBoxes = _renderParagraph.getBoxesForSelection(selection);
|
||||
return textBoxes
|
||||
.map((box) =>
|
||||
_renderParagraph.localToGlobal(box.toRect().topLeft) &
|
||||
box.toRect().size)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension on TextNode {
|
||||
TextSpan toTextSpan() => TextSpan(
|
||||
children: delta.operations
|
||||
.whereType<TextInsert>()
|
||||
.map((op) => op.toTextSpan())
|
||||
.toList());
|
||||
}
|
||||
|
||||
extension on TextInsert {
|
||||
TextSpan toTextSpan() {
|
||||
FontWeight? fontWeight;
|
||||
FontStyle? fontStyle;
|
||||
TextDecoration? decoration;
|
||||
GestureRecognizer? gestureRecognizer;
|
||||
Color color = Colors.black;
|
||||
Color highLightColor = Colors.transparent;
|
||||
double fontSize = 16.0;
|
||||
final attributes = this.attributes;
|
||||
if (attributes?['bold'] == true) {
|
||||
fontWeight = FontWeight.bold;
|
||||
}
|
||||
if (attributes?['italic'] == true) {
|
||||
fontStyle = FontStyle.italic;
|
||||
}
|
||||
if (attributes?['underline'] == true) {
|
||||
decoration = TextDecoration.underline;
|
||||
}
|
||||
if (attributes?['strikethrough'] == true) {
|
||||
decoration = TextDecoration.lineThrough;
|
||||
}
|
||||
if (attributes?['highlight'] is String) {
|
||||
highLightColor = Color(int.parse(attributes!['highlight']));
|
||||
}
|
||||
if (attributes?['href'] is String) {
|
||||
color = const Color.fromARGB(255, 55, 120, 245);
|
||||
decoration = TextDecoration.underline;
|
||||
gestureRecognizer = TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(attributes?['href']);
|
||||
};
|
||||
}
|
||||
final heading = attributes?['heading'] as String?;
|
||||
if (heading != null) {
|
||||
// TODO: make it better
|
||||
if (heading == 'h1') {
|
||||
fontSize = 30.0;
|
||||
} else if (heading == 'h2') {
|
||||
fontSize = 20.0;
|
||||
}
|
||||
fontWeight = FontWeight.bold;
|
||||
}
|
||||
return TextSpan(
|
||||
text: content,
|
||||
style: TextStyle(
|
||||
fontWeight: fontWeight,
|
||||
fontStyle: fontStyle,
|
||||
decoration: decoration,
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
backgroundColor: highLightColor,
|
||||
),
|
||||
recognizer: gestureRecognizer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlowyPainter extends CustomPainter {
|
||||
final List<Rect> _rects;
|
||||
final Paint _paint;
|
||||
|
||||
FlowyPainter({
|
||||
Key? key,
|
||||
required Color color,
|
||||
required List<Rect> rects,
|
||||
bool fill = false,
|
||||
}) : _rects = rects,
|
||||
_paint = Paint()..color = color {
|
||||
_paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final rect in _rects) {
|
||||
canvas.drawRect(
|
||||
rect,
|
||||
_paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ class TextNodeBuilder extends NodeWidgetBuilder {
|
||||
TextNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.editorState,
|
||||
required super.key,
|
||||
}) : super.create() {
|
||||
nodeValidator = ((node) {
|
||||
return node.type == 'text';
|
||||
@ -20,7 +21,7 @@ class TextNodeBuilder extends NodeWidgetBuilder {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext buildContext) {
|
||||
return _TextNodeWidget(node: node, editorState: editorState);
|
||||
return _TextNodeWidget(key: key, node: node, editorState: editorState);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
|
||||
TextWithCheckBoxNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.editorState,
|
||||
required super.key,
|
||||
}) : super.create();
|
||||
|
||||
// TODO: check the type
|
||||
|
@ -5,6 +5,7 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder {
|
||||
TextWithHeadingNodeBuilder.create({
|
||||
required super.editorState,
|
||||
required super.node,
|
||||
required super.key,
|
||||
}) : super.create() {
|
||||
nodeValidator = (node) => node.attributes.containsKey('heading');
|
||||
}
|
||||
@ -15,9 +16,9 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
);
|
||||
} else if (heading == 'h1') {
|
||||
} else if (heading == 'h2') {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
);
|
||||
}
|
||||
return const Padding(
|
||||
|
@ -10,6 +10,8 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
final LinkedList<Node> children;
|
||||
final Attributes attributes;
|
||||
|
||||
GlobalKey? key;
|
||||
|
||||
String? get subtype {
|
||||
// TODO: make 'subtype' as a const value.
|
||||
if (attributes.containsKey('subtype')) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/operation/operation.dart';
|
||||
import 'package:flowy_editor/document/attributes.dart';
|
||||
import 'package:flowy_editor/render/selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import './document/state_tree.dart';
|
||||
@ -12,6 +12,10 @@ import './render/render_plugins.dart';
|
||||
class EditorState {
|
||||
final StateTree document;
|
||||
final RenderPlugins renderPlugins;
|
||||
|
||||
Offset? panStartOffset;
|
||||
Offset? panEndOffset;
|
||||
|
||||
Selection? cursorSelection;
|
||||
|
||||
EditorState({
|
||||
@ -48,4 +52,82 @@ class EditorState {
|
||||
document.textEdit(op.path, op.delta);
|
||||
}
|
||||
}
|
||||
|
||||
List<OverlayEntry> selectionOverlays = [];
|
||||
|
||||
void updateSelection() {
|
||||
final selectedNodes = _selectedNodes;
|
||||
if (selectedNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(panStartOffset != null && panEndOffset != null);
|
||||
|
||||
selectionOverlays
|
||||
..forEach((element) => element.remove())
|
||||
..clear();
|
||||
for (final node in selectedNodes) {
|
||||
final key = node.key;
|
||||
if (key != null && key.currentState is Selectable) {
|
||||
final selectable = key.currentState as Selectable;
|
||||
final overlayRects =
|
||||
selectable.getOverlayRectsInRange(panStartOffset!, panEndOffset!);
|
||||
for (final rect in overlayRects) {
|
||||
// TODO: refactor overlay implement.
|
||||
final overlay = OverlayEntry(builder: ((context) {
|
||||
return Positioned.fromRect(
|
||||
rect: rect,
|
||||
child: Container(
|
||||
color: Colors.yellow.withAlpha(100),
|
||||
),
|
||||
);
|
||||
}));
|
||||
selectionOverlays.add(overlay);
|
||||
Overlay.of(selectable.context)?.insert(overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Node> get _selectedNodes {
|
||||
if (panStartOffset == null || panEndOffset == null) {
|
||||
return [];
|
||||
}
|
||||
return _calculateSelectedNodes(
|
||||
document.root, panStartOffset!, panEndOffset!);
|
||||
}
|
||||
|
||||
List<Node> _calculateSelectedNodes(Node node, Offset start, Offset end) {
|
||||
List<Node> result = [];
|
||||
|
||||
/// Skip the node without parent because it is the topmost node.
|
||||
/// Skip the node without key because it cannot get the [RenderObject].
|
||||
if (node.parent != null && node.key != null) {
|
||||
if (_isNodeInRange(node, start, end)) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
for (final child in node.children) {
|
||||
result.addAll(_calculateSelectedNodes(child, start, end));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _isNodeInRange(Node node, Offset start, Offset end) {
|
||||
assert(node.key != null);
|
||||
final renderBox =
|
||||
node.key?.currentContext?.findRenderObject() as RenderBox?;
|
||||
|
||||
/// Return false directly if the [RenderBox] cannot found.
|
||||
if (renderBox == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final rect = Rect.fromPoints(start, end);
|
||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
||||
return rect.overlaps(boxOffset & renderBox.size);
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ library flowy_editor;
|
||||
export 'package:flowy_editor/document/state_tree.dart';
|
||||
export 'package:flowy_editor/document/node.dart';
|
||||
export 'package:flowy_editor/document/path.dart';
|
||||
export 'package:flowy_editor/document/text_delta.dart';
|
||||
export 'package:flowy_editor/render/render_plugins.dart';
|
||||
export 'package:flowy_editor/render/node_widget_builder.dart';
|
||||
export 'package:flowy_editor/render/selectable.dart';
|
||||
export 'package:flowy_editor/operation/transaction.dart';
|
||||
export 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
export 'package:flowy_editor/operation/operation.dart';
|
||||
|
@ -9,6 +9,7 @@ typedef NodeValidator<T extends Node> = bool Function(T node);
|
||||
class NodeWidgetBuilder<T extends Node> {
|
||||
final EditorState editorState;
|
||||
final T node;
|
||||
final Key key;
|
||||
|
||||
bool rebuildOnNodeChanged;
|
||||
NodeValidator<T>? nodeValidator;
|
||||
@ -18,14 +19,22 @@ class NodeWidgetBuilder<T extends Node> {
|
||||
NodeWidgetBuilder.create({
|
||||
required this.editorState,
|
||||
required this.node,
|
||||
required this.key,
|
||||
this.rebuildOnNodeChanged = true,
|
||||
});
|
||||
|
||||
/// Render the current [Node]
|
||||
/// and the layout style of [Node.Children].
|
||||
Widget build(BuildContext buildContext) => throw UnimplementedError();
|
||||
Widget build(
|
||||
BuildContext buildContext,
|
||||
) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
Widget call(BuildContext buildContext) {
|
||||
/// TODO: refactore this part.
|
||||
/// return widget embeded with ChangeNotifier and widget itself.
|
||||
Widget call(
|
||||
BuildContext buildContext,
|
||||
) {
|
||||
/// TODO: Validate the node
|
||||
/// if failed, stop call build function,
|
||||
/// return Empty widget, and throw Error.
|
||||
@ -34,11 +43,7 @@ class NodeWidgetBuilder<T extends Node> {
|
||||
'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }');
|
||||
}
|
||||
|
||||
if (rebuildOnNodeChanged) {
|
||||
return _buildNodeChangeNotifier(buildContext);
|
||||
} else {
|
||||
return build(buildContext);
|
||||
}
|
||||
return _buildNodeChangeNotifier(buildContext);
|
||||
}
|
||||
|
||||
Widget _buildNodeChangeNotifier(BuildContext buildContext) {
|
||||
|
@ -19,6 +19,7 @@ typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
|
||||
Function({
|
||||
required T node,
|
||||
required EditorState editorState,
|
||||
required GlobalKey key,
|
||||
});
|
||||
|
||||
// unused
|
||||
@ -63,9 +64,12 @@ class RenderPlugins {
|
||||
name += '/${node.subtype}';
|
||||
}
|
||||
final nodeWidgetBuilder = _nodeWidgetBuilder(name);
|
||||
final key = GlobalKey();
|
||||
node.key = key;
|
||||
return nodeWidgetBuilder(
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
key: key,
|
||||
)(context.buildContext);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///
|
||||
mixin Selectable<T extends StatefulWidget> on State<T> {
|
||||
/// Returns a [Rect] list for overlay.
|
||||
/// [start] and [end] are global offsets.
|
||||
List<Rect> getOverlayRectsInRange(Offset start, Offset end);
|
||||
}
|
Loading…
Reference in New Issue
Block a user