Prechádzať zdrojové kódy

feat: support selection overlay

Lucas.Xu 2 rokov pred
rodič
commit
e2f35dd5cc

+ 25 - 0
frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json

@@ -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"
+        }
+    ]
+}

+ 2 - 1
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -1,6 +1,7 @@
 import 'dart:convert';
 import 'dart:convert';
 
 
 import 'package:example/plugin/document_node_widget.dart';
 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/text_with_heading_node_widget.dart';
 import 'package:example/plugin/image_node_widget.dart';
 import 'package:example/plugin/image_node_widget.dart';
 import 'package:example/plugin/text_node_widget.dart';
 import 'package:example/plugin/text_node_widget.dart';
@@ -65,7 +66,7 @@ class _MyHomePageState extends State<MyHomePage> {
 
 
     renderPlugins
     renderPlugins
       ..register('editor', EditorNodeWidgetBuilder.create)
       ..register('editor', EditorNodeWidgetBuilder.create)
-      ..register('text', TextNodeBuilder.create)
+      ..register('text', SelectedTextNodeBuilder.create)
       ..register('image', ImageNodeBuilder.create)
       ..register('image', ImageNodeBuilder.create)
       ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create)
       ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create)
       ..register('text/with-heading', TextWithHeadingNodeBuilder.create);
       ..register('text/with-heading', TextWithHeadingNodeBuilder.create);

+ 102 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart

@@ -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;
+  }
+}

+ 44 - 13
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart

@@ -1,15 +1,18 @@
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
 class EditorNodeWidgetBuilder extends NodeWidgetBuilder {
 class EditorNodeWidgetBuilder extends NodeWidgetBuilder {
   EditorNodeWidgetBuilder.create({
   EditorNodeWidgetBuilder.create({
     required super.editorState,
     required super.editorState,
     required super.node,
     required super.node,
+    required super.key,
   }) : super.create();
   }) : super.create();
 
 
   @override
   @override
   Widget build(BuildContext buildContext) {
   Widget build(BuildContext buildContext) {
     return SingleChildScrollView(
     return SingleChildScrollView(
+      key: key,
       child: _EditorNodeWidget(
       child: _EditorNodeWidget(
         node: node,
         node: node,
         editorState: editorState,
         editorState: editorState,
@@ -30,21 +33,49 @@ class _EditorNodeWidget extends StatelessWidget {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   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
+  }
 }
 }

+ 19 - 2
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -5,18 +5,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder {
   ImageNodeBuilder.create({
   ImageNodeBuilder.create({
     required super.node,
     required super.node,
     required super.editorState,
     required super.editorState,
+    required super.key,
   }) : super.create();
   }) : super.create();
 
 
   @override
   @override
   Widget build(BuildContext buildContext) {
   Widget build(BuildContext buildContext) {
     return _ImageNodeWidget(
     return _ImageNodeWidget(
+      key: key,
       node: node,
       node: node,
       editorState: editorState,
       editorState: editorState,
     );
     );
   }
   }
 }
 }
 
 
-class _ImageNodeWidget extends StatelessWidget {
+class _ImageNodeWidget extends StatefulWidget {
   final Node node;
   final Node node;
   final EditorState editorState;
   final EditorState editorState;
 
 
@@ -26,7 +28,22 @@ class _ImageNodeWidget extends StatelessWidget {
     required this.editorState,
     required this.editorState,
   }) : super(key: key);
   }) : 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
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {

+ 223 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart

@@ -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;
+  }
+}

+ 2 - 1
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

@@ -12,6 +12,7 @@ class TextNodeBuilder extends NodeWidgetBuilder {
   TextNodeBuilder.create({
   TextNodeBuilder.create({
     required super.node,
     required super.node,
     required super.editorState,
     required super.editorState,
+    required super.key,
   }) : super.create() {
   }) : super.create() {
     nodeValidator = ((node) {
     nodeValidator = ((node) {
       return node.type == 'text';
       return node.type == 'text';
@@ -20,7 +21,7 @@ class TextNodeBuilder extends NodeWidgetBuilder {
 
 
   @override
   @override
   Widget build(BuildContext buildContext) {
   Widget build(BuildContext buildContext) {
-    return _TextNodeWidget(node: node, editorState: editorState);
+    return _TextNodeWidget(key: key, node: node, editorState: editorState);
   }
   }
 }
 }
 
 

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart

@@ -5,6 +5,7 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
   TextWithCheckBoxNodeBuilder.create({
   TextWithCheckBoxNodeBuilder.create({
     required super.node,
     required super.node,
     required super.editorState,
     required super.editorState,
+    required super.key,
   }) : super.create();
   }) : super.create();
 
 
   // TODO: check the type
   // TODO: check the type

+ 3 - 2
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart

@@ -5,6 +5,7 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder {
   TextWithHeadingNodeBuilder.create({
   TextWithHeadingNodeBuilder.create({
     required super.editorState,
     required super.editorState,
     required super.node,
     required super.node,
+    required super.key,
   }) : super.create() {
   }) : super.create() {
     nodeValidator = (node) => node.attributes.containsKey('heading');
     nodeValidator = (node) => node.attributes.containsKey('heading');
   }
   }
@@ -15,9 +16,9 @@ class TextWithHeadingNodeBuilder extends NodeWidgetBuilder {
       return const Padding(
       return const Padding(
         padding: EdgeInsets.only(top: 10),
         padding: EdgeInsets.only(top: 10),
       );
       );
-    } else if (heading == 'h1') {
+    } else if (heading == 'h2') {
       return const Padding(
       return const Padding(
-        padding: EdgeInsets.only(top: 10),
+        padding: EdgeInsets.only(top: 5),
       );
       );
     }
     }
     return const Padding(
     return const Padding(

+ 2 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -10,6 +10,8 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
   final LinkedList<Node> children;
   final LinkedList<Node> children;
   final Attributes attributes;
   final Attributes attributes;
 
 
+  GlobalKey? key;
+
   String? get subtype {
   String? get subtype {
     // TODO: make 'subtype' as a const value.
     // TODO: make 'subtype' as a const value.
     if (attributes.containsKey('subtype')) {
     if (attributes.containsKey('subtype')) {

+ 83 - 1
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,6 +1,6 @@
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/operation/operation.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 'package:flutter/material.dart';
 
 
 import './document/state_tree.dart';
 import './document/state_tree.dart';
@@ -12,6 +12,10 @@ import './render/render_plugins.dart';
 class EditorState {
 class EditorState {
   final StateTree document;
   final StateTree document;
   final RenderPlugins renderPlugins;
   final RenderPlugins renderPlugins;
+
+  Offset? panStartOffset;
+  Offset? panEndOffset;
+
   Selection? cursorSelection;
   Selection? cursorSelection;
 
 
   EditorState({
   EditorState({
@@ -48,4 +52,82 @@ class EditorState {
       document.textEdit(op.path, op.delta);
       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);
+  }
 }
 }

+ 2 - 0
frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart

@@ -3,8 +3,10 @@ library flowy_editor;
 export 'package:flowy_editor/document/state_tree.dart';
 export 'package:flowy_editor/document/state_tree.dart';
 export 'package:flowy_editor/document/node.dart';
 export 'package:flowy_editor/document/node.dart';
 export 'package:flowy_editor/document/path.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/render_plugins.dart';
 export 'package:flowy_editor/render/node_widget_builder.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.dart';
 export 'package:flowy_editor/operation/transaction_builder.dart';
 export 'package:flowy_editor/operation/transaction_builder.dart';
 export 'package:flowy_editor/operation/operation.dart';
 export 'package:flowy_editor/operation/operation.dart';

+ 13 - 8
frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart

@@ -9,6 +9,7 @@ typedef NodeValidator<T extends Node> = bool Function(T node);
 class NodeWidgetBuilder<T extends Node> {
 class NodeWidgetBuilder<T extends Node> {
   final EditorState editorState;
   final EditorState editorState;
   final T node;
   final T node;
+  final Key key;
 
 
   bool rebuildOnNodeChanged;
   bool rebuildOnNodeChanged;
   NodeValidator<T>? nodeValidator;
   NodeValidator<T>? nodeValidator;
@@ -18,14 +19,22 @@ class NodeWidgetBuilder<T extends Node> {
   NodeWidgetBuilder.create({
   NodeWidgetBuilder.create({
     required this.editorState,
     required this.editorState,
     required this.node,
     required this.node,
+    required this.key,
     this.rebuildOnNodeChanged = true,
     this.rebuildOnNodeChanged = true,
   });
   });
 
 
   /// Render the current [Node]
   /// Render the current [Node]
   /// and the layout style of [Node.Children].
   /// and the layout style of [Node.Children].
-  Widget build(BuildContext buildContext) => throw UnimplementedError();
-
-  Widget call(BuildContext buildContext) {
+  Widget build(
+    BuildContext buildContext,
+  ) =>
+      throw UnimplementedError();
+
+  /// TODO: refactore this part.
+  /// return widget embeded with ChangeNotifier and widget itself.
+  Widget call(
+    BuildContext buildContext,
+  ) {
     /// TODO: Validate the node
     /// TODO: Validate the node
     /// if failed, stop call build function,
     /// if failed, stop call build function,
     ///   return Empty widget, and throw Error.
     ///   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} }');
           '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) {
   Widget _buildNodeChangeNotifier(BuildContext buildContext) {

+ 4 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart

@@ -19,6 +19,7 @@ typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
     Function({
     Function({
   required T node,
   required T node,
   required EditorState editorState,
   required EditorState editorState,
+  required GlobalKey key,
 });
 });
 
 
 // unused
 // unused
@@ -63,9 +64,12 @@ class RenderPlugins {
       name += '/${node.subtype}';
       name += '/${node.subtype}';
     }
     }
     final nodeWidgetBuilder = _nodeWidgetBuilder(name);
     final nodeWidgetBuilder = _nodeWidgetBuilder(name);
+    final key = GlobalKey();
+    node.key = key;
     return nodeWidgetBuilder(
     return nodeWidgetBuilder(
       node: context.node,
       node: context.node,
       editorState: context.editorState,
       editorState: context.editorState,
+      key: key,
     )(context.buildContext);
     )(context.buildContext);
   }
   }
 
 

+ 8 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart

@@ -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);
+}