Ver código fonte

Merge pull request #733 from LucasXu0/feat/flowy_editor_input_service

Implement bulleted-list, number-list, checkbox and heading style.
Nathan.fooo 2 anos atrás
pai
commit
39015f4d46
31 arquivos alterados com 920 adições e 2044 exclusões
  1. 45 0
      frontend/app_flowy/packages/flowy_editor/.vscode/launch.json
  2. 1 1
      frontend/app_flowy/packages/flowy_editor/example/assets/document.json
  3. 81 20
      frontend/app_flowy/packages/flowy_editor/example/assets/example.json
  4. 3 17
      frontend/app_flowy/packages/flowy_editor/example/lib/main.dart
  5. 0 102
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart
  6. 0 52
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart
  7. 0 758
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart
  8. 17 35
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart
  9. 0 352
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart
  10. 0 281
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart
  11. 0 33
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart
  12. 0 46
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart
  13. 0 17
      frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart
  14. 1 2
      frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart
  15. 58 0
      frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart
  16. 0 63
      frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart
  17. 0 88
      frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart
  18. 73 0
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart
  19. 123 0
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart
  20. 33 0
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart
  21. 48 164
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart
  22. 93 0
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart
  23. 74 0
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart
  24. 73 0
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart
  25. 12 5
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart
  26. 47 4
      frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart
  27. 1 1
      frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart
  28. 131 0
      frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart
  29. 1 0
      frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart
  30. 4 0
      frontend/app_flowy/packages/flowy_editor/lib/service/service.dart
  31. 1 3
      frontend/app_flowy/packages/flowy_editor/test/operation_test.dart

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

@@ -0,0 +1,45 @@
+{
+    // 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",
+            "cwd": "example",
+            "request": "launch",
+            "type": "dart"
+        },
+        {
+            "name": "example (profile mode)",
+            "cwd": "example",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "profile"
+        },
+        {
+            "name": "example (release mode)",
+            "cwd": "example",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "release"
+        },
+        {
+            "name": "flowy_editor",
+            "request": "launch",
+            "type": "dart"
+        },
+        {
+            "name": "flowy_editor (profile mode)",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "profile"
+        },
+        {
+            "name": "flowy_editor (release mode)",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "release"
+        },
+    ]
+}

+ 1 - 1
frontend/app_flowy/packages/flowy_editor/example/assets/document.json

@@ -68,7 +68,7 @@
           { "insert": " your ", "attributes": { "bold": true } },
           { "insert": "writing", "attributes": { "underline": true } },
           {
-            "insert": " howeverv you like.",
+            "insert": " however you like.",
             "attributes": { "strikethrough": true }
           }
         ],

+ 81 - 20
frontend/app_flowy/packages/flowy_editor/example/assets/example.json

@@ -3,10 +3,21 @@
     "type": "editor",
     "attributes": {},
     "children": [
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "subtype": "quote"
+        }
+      },
       {
         "type": "image",
         "attributes": {
-          "image_src": "https://images.pexels.com/photos/2253275/pexels-photo-2253275.jpeg?cs=srgb&dl=pexels-helena-lopes-2253275.jpg&fm=jpg"
+          "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png"
         }
       },
       {
@@ -17,6 +28,7 @@
           }
         ],
         "attributes": {
+          "subtype": "heading",
           "heading": "h1"
         }
       },
@@ -28,9 +40,18 @@
           }
         ],
         "attributes": {
+          "subtype": "heading",
           "heading": "h2"
         }
       },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
+          }
+        ]
+      },
       {
         "type": "text",
         "delta": [
@@ -39,6 +60,7 @@
           }
         ],
         "attributes": {
+          "subtype": "heading",
           "heading": "h3"
         }
       },
@@ -48,11 +70,7 @@
           { "insert": "Click " },
           { "insert": "anywhere", "attributes": { "underline": true } },
           { "insert": " and just typing." }
-        ],
-        "attributes": {
-          "list": "todo",
-          "todo": true
-        }
+        ]
       },
       {
         "type": "text",
@@ -67,11 +85,7 @@
           {
             "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc."
           }
-        ],
-        "attributes": {
-          "list": "todo",
-          "todo": true
-        }
+        ]
       },
       {
         "type": "text",
@@ -83,21 +97,65 @@
           { "insert": " your ", "attributes": { "italic": true } },
           { "insert": "writing", "attributes": { "strikethrough": true } },
           { "insert": "." }
+        ]
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Here are the plugins:"
+          }
         ],
         "attributes": {
-          "list": "todo",
-          "todo": true
+          "subtype": "heading",
+          "heading": "h3"
         }
       },
       {
         "type": "text",
         "delta": [
           {
-            "insert": "Here are the examples:"
+            "insert": "Hello world"
           }
         ],
         "attributes": {
-          "heading": "h3"
+          "subtype": "checkbox",
+          "checkbox": false
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": false
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "subtype": "checkbox",
+          "checkbox": false
+        }
+      },
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "subtype": "bullet-list"
         }
       },
       {
@@ -108,7 +166,7 @@
           }
         ],
         "attributes": {
-          "list": "bullet"
+          "subtype": "bullet-list"
         }
       },
       {
@@ -119,7 +177,7 @@
           }
         ],
         "attributes": {
-          "list": "bullet"
+          "subtype": "bullet-list"
         }
       },
       {
@@ -130,7 +188,7 @@
           }
         ],
         "attributes": {
-          "list": "bullet"
+          "subtype": "quote"
         }
       },
       {
@@ -141,7 +199,7 @@
           }
         ],
         "attributes": {
-          "quote": true
+          "subtype": "quote"
         }
       },
       {
@@ -152,7 +210,7 @@
           }
         ],
         "attributes": {
-          "quote": true
+          "subtype": "quote"
         }
       },
       {
@@ -163,6 +221,7 @@
           }
         ],
         "attributes": {
+          "subtype": "number-list",
           "number": 1
         }
       },
@@ -174,6 +233,7 @@
           }
         ],
         "attributes": {
+          "subtype": "number-list",
           "number": 2
         }
       },
@@ -185,6 +245,7 @@
           }
         ],
         "attributes": {
+          "subtype": "number-list",
           "number": 3
         }
       }

+ 3 - 17
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -1,12 +1,7 @@
 import 'dart:convert';
 
 import 'package:example/expandable_floating_action_button.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/image_node_widget.dart';
-import 'package:example/plugin/old_text_node_widget.dart';
-import 'package:example/plugin/text_with_check_box_node_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/services.dart';
@@ -59,19 +54,8 @@ class MyHomePage extends StatefulWidget {
 }
 
 class _MyHomePageState extends State<MyHomePage> {
-  final RenderPlugins renderPlugins = RenderPlugins();
   late EditorState _editorState;
   int page = 0;
-  @override
-  void initState() {
-    super.initState();
-
-    renderPlugins
-      ..register('editor', EditorNodeWidgetBuilder.create)
-      ..register('image', ImageNodeBuilder.create)
-      ..register('text/with-checkbox', TextWithCheckBoxNodeBuilder.create)
-      ..register('text/with-heading', TextWithHeadingNodeBuilder.create);
-  }
 
   @override
   Widget build(BuildContext context) {
@@ -130,11 +114,13 @@ class _MyHomePageState extends State<MyHomePage> {
           final document = StateTree.fromJson(data);
           _editorState = EditorState(
             document: document,
-            renderPlugins: renderPlugins,
           );
           return FlowyEditor(
             editorState: _editorState,
             keyEventHandlers: const [],
+            customBuilders: {
+              'image': ImageNodeBuilder(),
+            },
             shortcuts: [
               // TODO: this won't work, just a example for now.
               {

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

@@ -1,102 +0,0 @@
-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;
-  }
-}

+ 0 - 52
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart

@@ -1,52 +0,0 @@
-import 'package:flowy_editor/flowy_editor.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 context) {
-    return SingleChildScrollView(
-      key: key,
-      child: _EditorNodeWidget(
-        node: node,
-        editorState: editorState,
-      ),
-    );
-  }
-}
-
-class _EditorNodeWidget extends StatelessWidget {
-  final Node node;
-  final EditorState editorState;
-
-  const _EditorNodeWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  @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,
-                ),
-              ),
-            )
-            .toList(),
-      ),
-    );
-  }
-}

+ 0 - 758
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart

@@ -1,758 +0,0 @@
-// Copyright 2014 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
-
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/rendering.dart';
-
-import 'package:flutter/material.dart';
-
-/// An eyeballed value that moves the cursor slightly left of where it is
-/// rendered for text on Android so its positioning more accurately matches the
-/// native iOS text cursor positioning.
-///
-/// This value is in device pixels, not logical pixels as is typically used
-/// throughout the codebase.
-const int iOSHorizontalOffset = -2;
-
-class _TextSpanEditingController extends TextEditingController {
-  _TextSpanEditingController({required TextSpan textSpan})
-      : assert(textSpan != null),
-        _textSpan = textSpan,
-        super(text: textSpan.toPlainText(includeSemanticsLabels: false));
-
-  final TextSpan _textSpan;
-
-  @override
-  TextSpan buildTextSpan(
-      {required BuildContext context,
-      TextStyle? style,
-      required bool withComposing}) {
-    // This does not care about composing.
-    return TextSpan(
-      style: style,
-      children: <TextSpan>[_textSpan],
-    );
-  }
-
-  @override
-  set text(String? newText) {
-    // This should never be reached.
-    throw UnimplementedError();
-  }
-}
-
-class _SelectableTextSelectionGestureDetectorBuilder
-    extends TextSelectionGestureDetectorBuilder {
-  _SelectableTextSelectionGestureDetectorBuilder({
-    required _FlowySelectableTextState state,
-  })  : _state = state,
-        super(delegate: state);
-
-  final _FlowySelectableTextState _state;
-
-  @override
-  void onForcePressStart(ForcePressDetails details) {
-    super.onForcePressStart(details);
-    if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
-      editableText.showToolbar();
-    }
-  }
-
-  @override
-  void onForcePressEnd(ForcePressDetails details) {
-    // Not required.
-  }
-
-  @override
-  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
-    if (delegate.selectionEnabled) {
-      renderEditable.selectWordsInRange(
-        from: details.globalPosition - details.offsetFromOrigin,
-        to: details.globalPosition,
-        cause: SelectionChangedCause.longPress,
-      );
-    }
-  }
-
-  @override
-  void onSingleTapUp(TapUpDetails details) {
-    editableText.hideToolbar();
-    if (delegate.selectionEnabled) {
-      switch (Theme.of(_state.context).platform) {
-        case TargetPlatform.iOS:
-        case TargetPlatform.macOS:
-        // renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
-        // break;
-        case TargetPlatform.android:
-        case TargetPlatform.fuchsia:
-        case TargetPlatform.linux:
-        case TargetPlatform.windows:
-          renderEditable.selectPosition(cause: SelectionChangedCause.tap);
-          break;
-      }
-    }
-    _state.widget.onTap?.call();
-  }
-
-  @override
-  void onSingleLongTapStart(LongPressStartDetails details) {
-    if (delegate.selectionEnabled) {
-      renderEditable.selectWord(cause: SelectionChangedCause.longPress);
-      Feedback.forLongPress(_state.context);
-    }
-  }
-}
-
-/// A run of selectable text with a single style.
-///
-/// The [FlowySelectableText] widget displays a string of text with a single style.
-/// The string might break across multiple lines or might all be displayed on
-/// the same line depending on the layout constraints.
-///
-/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
-///
-/// The [style] argument is optional. When omitted, the text will use the style
-/// from the closest enclosing [DefaultTextStyle]. If the given style's
-/// [TextStyle.inherit] property is true (the default), the given style will
-/// be merged with the closest enclosing [DefaultTextStyle]. This merging
-/// behavior is useful, for example, to make the text bold while using the
-/// default font family and size.
-///
-/// {@macro flutter.material.textfield.wantKeepAlive}
-///
-/// {@tool snippet}
-///
-/// ```dart
-/// const SelectableText(
-///   'Hello! How are you?',
-///   textAlign: TextAlign.center,
-///   style: TextStyle(fontWeight: FontWeight.bold),
-/// )
-/// ```
-/// {@end-tool}
-///
-/// Using the [SelectableText.rich] constructor, the [FlowySelectableText] widget can
-/// display a paragraph with differently styled [TextSpan]s. The sample
-/// that follows displays "Hello beautiful world" with different styles
-/// for each word.
-///
-/// {@tool snippet}
-///
-/// ```dart
-/// const SelectableText.rich(
-///   TextSpan(
-///     text: 'Hello', // default text style
-///     children: <TextSpan>[
-///       TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
-///       TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
-///     ],
-///   ),
-/// )
-/// ```
-/// {@end-tool}
-///
-/// ## Interactivity
-///
-/// To make [FlowySelectableText] react to touch events, use callback [onTap] to achieve
-/// the desired behavior.
-///
-/// See also:
-///
-///  * [Text], which is the non selectable version of this widget.
-///  * [TextField], which is the editable version of this widget.
-class FlowySelectableText extends StatefulWidget {
-  /// Creates a selectable text widget.
-  ///
-  /// If the [style] argument is null, the text will use the style from the
-  /// closest enclosing [DefaultTextStyle].
-  ///
-
-  /// The [showCursor], [autofocus], [dragStartBehavior], [selectionHeightStyle],
-  /// [selectionWidthStyle] and [data] parameters must not be null. If specified,
-  /// the [maxLines] argument must be greater than zero.
-  const FlowySelectableText(
-    String this.data, {
-    Key? key,
-    this.focusNode,
-    this.style,
-    this.strutStyle,
-    this.textAlign,
-    this.textDirection,
-    this.textScaleFactor,
-    this.showCursor = false,
-    this.autofocus = false,
-    ToolbarOptions? toolbarOptions,
-    this.minLines,
-    this.maxLines,
-    this.cursorWidth = 2.0,
-    this.cursorHeight,
-    this.cursorRadius,
-    this.cursorColor,
-    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
-    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
-    this.dragStartBehavior = DragStartBehavior.start,
-    this.enableInteractiveSelection = true,
-    this.selectionControls,
-    this.onTap,
-    this.scrollPhysics,
-    this.semanticsLabel,
-    this.textHeightBehavior,
-    this.textWidthBasis,
-    this.onSelectionChanged,
-  })  : assert(showCursor != null),
-        assert(autofocus != null),
-        assert(dragStartBehavior != null),
-        assert(selectionHeightStyle != null),
-        assert(selectionWidthStyle != null),
-        assert(maxLines == null || maxLines > 0),
-        assert(minLines == null || minLines > 0),
-        assert(
-          (maxLines == null) || (minLines == null) || (maxLines >= minLines),
-          "minLines can't be greater than maxLines",
-        ),
-        assert(
-          data != null,
-          'A non-null String must be provided to a SelectableText widget.',
-        ),
-        textSpan = null,
-        toolbarOptions = toolbarOptions ??
-            const ToolbarOptions(
-              selectAll: true,
-              copy: true,
-            ),
-        super(key: key);
-
-  /// Creates a selectable text widget with a [TextSpan].
-  ///
-  /// The [textSpan] parameter must not be null and only contain [TextSpan] in
-  /// [textSpan].children. Other type of [InlineSpan] is not allowed.
-  ///
-  /// The [autofocus] and [dragStartBehavior] arguments must not be null.
-  const FlowySelectableText.rich(
-    TextSpan this.textSpan, {
-    Key? key,
-    this.focusNode,
-    this.style,
-    this.strutStyle,
-    this.textAlign,
-    this.textDirection,
-    this.textScaleFactor,
-    this.showCursor = false,
-    this.autofocus = false,
-    ToolbarOptions? toolbarOptions,
-    this.minLines,
-    this.maxLines,
-    this.cursorWidth = 2.0,
-    this.cursorHeight,
-    this.cursorRadius,
-    this.cursorColor,
-    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
-    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
-    this.dragStartBehavior = DragStartBehavior.start,
-    this.enableInteractiveSelection = true,
-    this.selectionControls,
-    this.onTap,
-    this.scrollPhysics,
-    this.semanticsLabel,
-    this.textHeightBehavior,
-    this.textWidthBasis,
-    this.onSelectionChanged,
-  })  : assert(showCursor != null),
-        assert(autofocus != null),
-        assert(dragStartBehavior != null),
-        assert(maxLines == null || maxLines > 0),
-        assert(minLines == null || minLines > 0),
-        assert(
-          (maxLines == null) || (minLines == null) || (maxLines >= minLines),
-          "minLines can't be greater than maxLines",
-        ),
-        assert(
-          textSpan != null,
-          'A non-null TextSpan must be provided to a SelectableText.rich widget.',
-        ),
-        data = null,
-        toolbarOptions = toolbarOptions ??
-            const ToolbarOptions(
-              selectAll: true,
-              copy: true,
-            ),
-        super(key: key);
-
-  /// The text to display.
-  ///
-  /// This will be null if a [textSpan] is provided instead.
-  final String? data;
-
-  /// The text to display as a [TextSpan].
-  ///
-  /// This will be null if [data] is provided instead.
-  final TextSpan? textSpan;
-
-  /// Defines the focus for this widget.
-  ///
-  /// Text is only selectable when widget is focused.
-  ///
-  /// The [focusNode] is a long-lived object that's typically managed by a
-  /// [StatefulWidget] parent. See [FocusNode] for more information.
-  ///
-  /// To give the focus to this widget, provide a [focusNode] and then
-  /// use the current [FocusScope] to request the focus:
-  ///
-  /// ```dart
-  /// FocusScope.of(context).requestFocus(myFocusNode);
-  /// ```
-  ///
-  /// This happens automatically when the widget is tapped.
-  ///
-  /// To be notified when the widget gains or loses the focus, add a listener
-  /// to the [focusNode]:
-  ///
-  /// ```dart
-  /// focusNode.addListener(() { print(myFocusNode.hasFocus); });
-  /// ```
-  ///
-  /// If null, this widget will create its own [FocusNode] with
-  /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
-  /// to be skipped over during focus traversal.
-  final FocusNode? focusNode;
-
-  /// The style to use for the text.
-  ///
-  /// If null, defaults [DefaultTextStyle] of context.
-  final TextStyle? style;
-
-  /// {@macro flutter.widgets.editableText.strutStyle}
-  final StrutStyle? strutStyle;
-
-  /// {@macro flutter.widgets.editableText.textAlign}
-  final TextAlign? textAlign;
-
-  /// {@macro flutter.widgets.editableText.textDirection}
-  final TextDirection? textDirection;
-
-  /// {@macro flutter.widgets.editableText.textScaleFactor}
-  final double? textScaleFactor;
-
-  /// {@macro flutter.widgets.editableText.autofocus}
-  final bool autofocus;
-
-  /// {@macro flutter.widgets.editableText.minLines}
-  final int? minLines;
-
-  /// {@macro flutter.widgets.editableText.maxLines}
-  final int? maxLines;
-
-  /// {@macro flutter.widgets.editableText.showCursor}
-  final bool showCursor;
-
-  /// {@macro flutter.widgets.editableText.cursorWidth}
-  final double cursorWidth;
-
-  /// {@macro flutter.widgets.editableText.cursorHeight}
-  final double? cursorHeight;
-
-  /// {@macro flutter.widgets.editableText.cursorRadius}
-  final Radius? cursorRadius;
-
-  /// The color to use when painting the cursor.
-  ///
-  /// Defaults to the theme's `cursorColor` when null.
-  final Color? cursorColor;
-
-  /// Controls how tall the selection highlight boxes are computed to be.
-  ///
-  /// See [ui.BoxHeightStyle] for details on available styles.
-  final ui.BoxHeightStyle selectionHeightStyle;
-
-  /// Controls how wide the selection highlight boxes are computed to be.
-  ///
-  /// See [ui.BoxWidthStyle] for details on available styles.
-  final ui.BoxWidthStyle selectionWidthStyle;
-
-  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
-  final bool enableInteractiveSelection;
-
-  /// {@macro flutter.widgets.editableText.selectionControls}
-  final TextSelectionControls? selectionControls;
-
-  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
-  final DragStartBehavior dragStartBehavior;
-
-  /// Configuration of toolbar options.
-  ///
-  /// Paste and cut will be disabled regardless.
-  ///
-  /// If not set, select all and copy will be enabled by default.
-  final ToolbarOptions toolbarOptions;
-
-  /// {@macro flutter.widgets.editableText.selectionEnabled}
-  bool get selectionEnabled => enableInteractiveSelection;
-
-  /// Called when the user taps on this selectable text.
-  ///
-  /// The selectable text builds a [GestureDetector] to handle input events like tap,
-  /// to trigger focus requests, to move the caret, adjust the selection, etc.
-  /// Handling some of those events by wrapping the selectable text with a competing
-  /// GestureDetector is problematic.
-  ///
-  /// To unconditionally handle taps, without interfering with the selectable text's
-  /// internal gesture detector, provide this callback.
-  ///
-  /// To be notified when the text field gains or loses the focus, provide a
-  /// [focusNode] and add a listener to that.
-  ///
-  /// To listen to arbitrary pointer events without competing with the
-  /// selectable text's internal gesture detector, use a [Listener].
-  final GestureTapCallback? onTap;
-
-  /// {@macro flutter.widgets.editableText.scrollPhysics}
-  final ScrollPhysics? scrollPhysics;
-
-  /// {@macro flutter.widgets.Text.semanticsLabel}
-  final String? semanticsLabel;
-
-  /// {@macro dart.ui.textHeightBehavior}
-  final TextHeightBehavior? textHeightBehavior;
-
-  /// {@macro flutter.painting.textPainter.textWidthBasis}
-  final TextWidthBasis? textWidthBasis;
-
-  /// {@macro flutter.widgets.editableText.onSelectionChanged}
-  final SelectionChangedCallback? onSelectionChanged;
-
-  @override
-  State<FlowySelectableText> createState() => _FlowySelectableTextState();
-
-  @override
-  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
-    super.debugFillProperties(properties);
-    properties
-        .add(DiagnosticsProperty<String>('data', data, defaultValue: null));
-    properties.add(DiagnosticsProperty<String>('semanticsLabel', semanticsLabel,
-        defaultValue: null));
-    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
-        defaultValue: null));
-    properties.add(
-        DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
-    properties.add(
-        DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
-    properties.add(DiagnosticsProperty<bool>('showCursor', showCursor,
-        defaultValue: false));
-    properties.add(IntProperty('minLines', minLines, defaultValue: null));
-    properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
-    properties.add(
-        EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
-    properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
-        defaultValue: null));
-    properties.add(
-        DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
-    properties
-        .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
-    properties
-        .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
-    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
-        defaultValue: null));
-    properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor,
-        defaultValue: null));
-    properties.add(FlagProperty('selectionEnabled',
-        value: selectionEnabled,
-        defaultValue: true,
-        ifFalse: 'selection disabled'));
-    properties.add(DiagnosticsProperty<TextSelectionControls>(
-        'selectionControls', selectionControls,
-        defaultValue: null));
-    properties.add(DiagnosticsProperty<ScrollPhysics>(
-        'scrollPhysics', scrollPhysics,
-        defaultValue: null));
-    properties.add(DiagnosticsProperty<TextHeightBehavior>(
-        'textHeightBehavior', textHeightBehavior,
-        defaultValue: null));
-  }
-}
-
-class _FlowySelectableTextState extends State<FlowySelectableText>
-    implements TextSelectionGestureDetectorBuilderDelegate {
-  EditableTextState? get _editableText => editableTextKey.currentState;
-
-  late _TextSpanEditingController _controller;
-
-  FocusNode? _focusNode;
-  FocusNode get _effectiveFocusNode =>
-      widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
-
-  bool _showSelectionHandles = false;
-
-  late _SelectableTextSelectionGestureDetectorBuilder
-      _selectionGestureDetectorBuilder;
-
-  // API for TextSelectionGestureDetectorBuilderDelegate.
-  @override
-  late bool forcePressEnabled;
-
-  @override
-  final GlobalKey<EditableTextState> editableTextKey =
-      GlobalKey<EditableTextState>();
-
-  @override
-  bool get selectionEnabled => widget.selectionEnabled;
-  // End of API for TextSelectionGestureDetectorBuilderDelegate.
-
-  @override
-  void initState() {
-    super.initState();
-    _selectionGestureDetectorBuilder =
-        _SelectableTextSelectionGestureDetectorBuilder(state: this);
-    _controller = _TextSpanEditingController(
-      textSpan: widget.textSpan ?? TextSpan(text: widget.data),
-    );
-    _controller.addListener(_onControllerChanged);
-  }
-
-  @override
-  void didUpdateWidget(FlowySelectableText oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (widget.data != oldWidget.data ||
-        widget.textSpan != oldWidget.textSpan) {
-      _controller.removeListener(_onControllerChanged);
-      _controller = _TextSpanEditingController(
-        textSpan: widget.textSpan ?? TextSpan(text: widget.data),
-      );
-      _controller.addListener(_onControllerChanged);
-    }
-    if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
-      _showSelectionHandles = false;
-    } else {
-      _showSelectionHandles = true;
-    }
-  }
-
-  @override
-  void dispose() {
-    _focusNode?.dispose();
-    _controller.removeListener(_onControllerChanged);
-    super.dispose();
-  }
-
-  void _onControllerChanged() {
-    final bool showSelectionHandles =
-        !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed;
-    if (showSelectionHandles == _showSelectionHandles) {
-      return;
-    }
-    setState(() {
-      _showSelectionHandles = showSelectionHandles;
-    });
-  }
-
-  TextSelection? _lastSeenTextSelection;
-
-  void _handleSelectionChanged(
-      TextSelection selection, SelectionChangedCause? cause) {
-    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
-    if (willShowSelectionHandles != _showSelectionHandles) {
-      setState(() {
-        _showSelectionHandles = willShowSelectionHandles;
-      });
-    }
-    // TODO(chunhtai): The selection may be the same. We should remove this
-    // check once this is fixed https://github.com/flutter/flutter/issues/76349.
-    if (widget.onSelectionChanged != null &&
-        _lastSeenTextSelection != selection) {
-      widget.onSelectionChanged!(selection, cause);
-    }
-    _lastSeenTextSelection = selection;
-
-    switch (Theme.of(context).platform) {
-      case TargetPlatform.iOS:
-      case TargetPlatform.macOS:
-        if (cause == SelectionChangedCause.longPress) {
-          _editableText?.bringIntoView(selection.base);
-        }
-        return;
-      case TargetPlatform.android:
-      case TargetPlatform.fuchsia:
-      case TargetPlatform.linux:
-      case TargetPlatform.windows:
-      // Do nothing.
-    }
-  }
-
-  /// Toggle the toolbar when a selection handle is tapped.
-  void _handleSelectionHandleTapped() {
-    if (_controller.selection.isCollapsed) {
-      _editableText!.toggleToolbar();
-    }
-  }
-
-  bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
-    // When the text field is activated by something that doesn't trigger the
-    // selection overlay, we shouldn't show the handles either.
-    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
-      return false;
-
-    if (_controller.selection.isCollapsed) return false;
-
-    if (cause == SelectionChangedCause.keyboard) return false;
-
-    if (cause == SelectionChangedCause.longPress) return true;
-
-    if (_controller.text.isNotEmpty) return true;
-
-    return false;
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    // TODO(garyq): Assert to block WidgetSpans from being used here are removed,
-    // but we still do not yet have nice handling of things like carets, clipboard,
-    // and other features. We should add proper support. Currently, caret handling
-    // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
-    // should be landed in SkParagraph after the switch is complete.
-    assert(debugCheckHasMediaQuery(context));
-    assert(debugCheckHasDirectionality(context));
-    assert(
-      !(widget.style != null &&
-          widget.style!.inherit == false &&
-          (widget.style!.fontSize == null ||
-              widget.style!.textBaseline == null)),
-      'inherit false style must supply fontSize and textBaseline',
-    );
-
-    final ThemeData theme = Theme.of(context);
-    final TextSelectionThemeData selectionTheme =
-        TextSelectionTheme.of(context);
-    final FocusNode focusNode = _effectiveFocusNode;
-
-    TextSelectionControls? textSelectionControls = widget.selectionControls;
-    final bool paintCursorAboveText;
-    final bool cursorOpacityAnimates;
-    Offset? cursorOffset;
-    Color? cursorColor = widget.cursorColor;
-    final Color selectionColor;
-    Radius? cursorRadius = widget.cursorRadius;
-
-    switch (theme.platform) {
-      case TargetPlatform.iOS:
-        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
-        forcePressEnabled = true;
-        textSelectionControls ??= cupertinoTextSelectionControls;
-        paintCursorAboveText = true;
-        cursorOpacityAnimates = true;
-        cursorColor ??=
-            selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
-        selectionColor = selectionTheme.selectionColor ??
-            cupertinoTheme.primaryColor.withOpacity(0.40);
-        cursorRadius ??= const Radius.circular(2.0);
-        cursorOffset = Offset(
-            iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
-        break;
-
-      case TargetPlatform.macOS:
-        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
-        forcePressEnabled = false;
-        textSelectionControls ??= cupertinoDesktopTextSelectionControls;
-        paintCursorAboveText = true;
-        cursorOpacityAnimates = true;
-        cursorColor ??=
-            selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
-        selectionColor = selectionTheme.selectionColor ??
-            cupertinoTheme.primaryColor.withOpacity(0.40);
-        cursorRadius ??= const Radius.circular(2.0);
-        cursorOffset = Offset(
-            iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
-        break;
-
-      case TargetPlatform.android:
-      case TargetPlatform.fuchsia:
-        forcePressEnabled = false;
-        textSelectionControls ??= materialTextSelectionControls;
-        paintCursorAboveText = false;
-        cursorOpacityAnimates = false;
-        cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
-        selectionColor = selectionTheme.selectionColor ??
-            theme.colorScheme.primary.withOpacity(0.40);
-        break;
-
-      case TargetPlatform.linux:
-      case TargetPlatform.windows:
-        forcePressEnabled = false;
-        textSelectionControls ??= desktopTextSelectionControls;
-        paintCursorAboveText = false;
-        cursorOpacityAnimates = false;
-        cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
-        selectionColor = selectionTheme.selectionColor ??
-            theme.colorScheme.primary.withOpacity(0.40);
-        break;
-    }
-
-    final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
-    TextStyle? effectiveTextStyle = widget.style;
-    if (effectiveTextStyle == null || effectiveTextStyle.inherit)
-      effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
-    if (MediaQuery.boldTextOverride(context))
-      effectiveTextStyle = effectiveTextStyle
-          .merge(const TextStyle(fontWeight: FontWeight.bold));
-    final Widget child = RepaintBoundary(
-      child: EditableText(
-        key: editableTextKey,
-        style: effectiveTextStyle,
-        readOnly: true,
-        textWidthBasis:
-            widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
-        textHeightBehavior:
-            widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
-        showSelectionHandles: _showSelectionHandles,
-        showCursor: widget.showCursor,
-        controller: _controller,
-        focusNode: focusNode,
-        strutStyle: widget.strutStyle ?? const StrutStyle(),
-        textAlign:
-            widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
-        textDirection: widget.textDirection,
-        textScaleFactor: widget.textScaleFactor,
-        autofocus: widget.autofocus,
-        forceLine: false,
-        toolbarOptions: widget.toolbarOptions,
-        minLines: widget.minLines,
-        maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
-        selectionColor: selectionColor,
-        selectionControls:
-            widget.selectionEnabled ? textSelectionControls : null,
-        onSelectionChanged: _handleSelectionChanged,
-        onSelectionHandleTapped: _handleSelectionHandleTapped,
-        rendererIgnoresPointer: true,
-        cursorWidth: widget.cursorWidth,
-        cursorHeight: widget.cursorHeight,
-        cursorRadius: cursorRadius,
-        cursorColor: cursorColor,
-        selectionHeightStyle: widget.selectionHeightStyle,
-        selectionWidthStyle: widget.selectionWidthStyle,
-        cursorOpacityAnimates: cursorOpacityAnimates,
-        cursorOffset: cursorOffset,
-        paintCursorAboveText: paintCursorAboveText,
-        backgroundCursorColor: CupertinoColors.inactiveGray,
-        enableInteractiveSelection: widget.enableInteractiveSelection,
-        dragStartBehavior: widget.dragStartBehavior,
-        scrollPhysics: widget.scrollPhysics,
-        autofillHints: null,
-      ),
-    );
-
-    return Semantics(
-      label: widget.semanticsLabel,
-      excludeSemantics: widget.semanticsLabel != null,
-      onLongPress: () {
-        _effectiveFocusNode.requestFocus();
-      },
-      child: _selectionGestureDetectorBuilder.buildGestureDetector(
-        behavior: HitTestBehavior.translucent,
-        child: child,
-      ),
-    );
-  }
-}

+ 17 - 35
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -1,40 +1,37 @@
-import 'package:flowy_editor/document/position.dart';
-import 'package:flowy_editor/document/selection.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/material.dart';
 
-class ImageNodeBuilder extends NodeWidgetBuilder {
-  ImageNodeBuilder.create({
-    required super.node,
-    required super.editorState,
-    required super.key,
-  }) : super.create();
-
+class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
   @override
-  Widget build(BuildContext context) {
-    return _ImageNodeWidget(
-      key: key,
-      node: node,
-      editorState: editorState,
+  Widget build(NodeWidgetContext<Node> context) {
+    return ImageNodeWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
     );
   }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return node.type == 'image';
+      });
 }
 
-class _ImageNodeWidget extends StatefulWidget {
+class ImageNodeWidget extends StatefulWidget {
   final Node node;
   final EditorState editorState;
 
-  const _ImageNodeWidget({
+  const ImageNodeWidget({
     Key? key,
     required this.node,
     required this.editorState,
   }) : super(key: key);
 
   @override
-  State<_ImageNodeWidget> createState() => __ImageNodeWidgetState();
+  State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
 }
 
-class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
+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;
@@ -90,23 +87,8 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
       children: [
         Image.network(
           src,
-          height: 150.0,
-        ),
-        if (node.children.isNotEmpty)
-          Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: node.children
-                .map(
-                  (e) => editorState.renderPlugins.buildWidget(
-                    context: NodeWidgetContext(
-                      buildContext: context,
-                      node: e,
-                      editorState: editorState,
-                    ),
-                  ),
-                )
-                .toList(),
-          ),
+          width: MediaQuery.of(context).size.width,
+        )
       ],
     );
   }

+ 0 - 352
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart

@@ -1,352 +0,0 @@
-// import 'package:flowy_editor/document/position.dart';
-// import 'package:flowy_editor/document/selection.dart';
-// import 'package:flutter/gestures.dart';
-// import 'package:flutter/material.dart';
-// import 'package:flowy_editor/flowy_editor.dart';
-// import 'package:flutter/services.dart';
-// import 'package:url_launcher/url_launcher_string.dart';
-// import 'flowy_selectable_text.dart';
-
-// class TextNodeBuilder extends NodeWidgetBuilder {
-//   TextNodeBuilder.create({
-//     required super.node,
-//     required super.editorState,
-//     required super.key,
-//   }) : super.create() {
-//     nodeValidator = ((node) {
-//       return node.type == 'text';
-//     });
-//   }
-
-//   @override
-//   Widget build(BuildContext context) {
-//     return _TextNodeWidget(key: key, node: node, editorState: editorState);
-//   }
-// }
-
-// class _TextNodeWidget extends StatefulWidget {
-//   final Node node;
-//   final EditorState editorState;
-
-//   const _TextNodeWidget({
-//     Key? key,
-//     required this.node,
-//     required this.editorState,
-//   }) : super(key: key);
-
-//   @override
-//   State<_TextNodeWidget> createState() => __TextNodeWidgetState();
-// }
-
-// class __TextNodeWidgetState extends State<_TextNodeWidget>
-//     implements DeltaTextInputClient {
-//   TextNode get node => widget.node as TextNode;
-//   EditorState get editorState => widget.editorState;
-//   bool _metaKeyDown = false;
-//   bool _shiftKeyDown = false;
-
-//   TextInputConnection? _textInputConnection;
-
-//   @override
-//   Widget build(BuildContext context) {
-//     return Column(
-//       crossAxisAlignment: CrossAxisAlignment.start,
-//       children: [
-//         FlowySelectableText.rich(
-//           node.toTextSpan(),
-//           showCursor: true,
-//           enableInteractiveSelection: true,
-//           onSelectionChanged: _onSelectionChanged,
-//           // autofocus: true,
-//           focusNode: FocusNode(
-//             onKey: _onKey,
-//           ),
-//         ),
-//         if (node.children.isNotEmpty)
-//           ...node.children.map(
-//             (e) => editorState.renderPlugins.buildWidget(
-//               context: NodeWidgetContext(
-//                 buildContext: context,
-//                 node: e,
-//                 editorState: editorState,
-//               ),
-//             ),
-//           ),
-//         const SizedBox(
-//           height: 10,
-//         ),
-//       ],
-//     );
-//   }
-
-//   KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) {
-//     debugPrint('key: $event');
-//     if (event is RawKeyDownEvent) {
-//       final sel = _globalSelectionToLocal(node, editorState.cursorSelection);
-//       if (event.logicalKey == LogicalKeyboardKey.backspace) {
-//         _backDeleteTextAtSelection(sel);
-//         return KeyEventResult.handled;
-//       } else if (event.logicalKey == LogicalKeyboardKey.delete) {
-//         _forwardDeleteTextAtSelection(sel);
-//         return KeyEventResult.handled;
-//       } else if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
-//           event.logicalKey == LogicalKeyboardKey.metaRight) {
-//         _metaKeyDown = true;
-//       } else if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
-//           event.logicalKey == LogicalKeyboardKey.shiftRight) {
-//         _shiftKeyDown = true;
-//       } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) {
-//         if (_shiftKeyDown) {
-//           editorState.undoManager.redo();
-//         } else {
-//           editorState.undoManager.undo();
-//         }
-//       }
-//     } else if (event is RawKeyUpEvent) {
-//       if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
-//           event.logicalKey == LogicalKeyboardKey.metaRight) {
-//         _metaKeyDown = false;
-//       }
-//       if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
-//           event.logicalKey == LogicalKeyboardKey.shiftRight) {
-//         _shiftKeyDown = false;
-//       }
-//     }
-//     return KeyEventResult.ignored;
-//   }
-
-//   void _onSelectionChanged(
-//       TextSelection selection, SelectionChangedCause? cause) {
-//     _textInputConnection?.close();
-//     _textInputConnection = TextInput.attach(
-//       this,
-//       const TextInputConfiguration(
-//         enableDeltaModel: true,
-//         inputType: TextInputType.multiline,
-//         textCapitalization: TextCapitalization.sentences,
-//       ),
-//     );
-//     editorState.cursorSelection = _localSelectionToGlobal(node, selection);
-//     _textInputConnection
-//       ?..show()
-//       ..setEditingState(
-//         TextEditingValue(
-//           text: node.toRawString(),
-//           selection: selection,
-//         ),
-//       );
-//   }
-
-//   _backDeleteTextAtSelection(TextSelection? sel) {
-//     if (sel == null) {
-//       return;
-//     }
-//     if (sel.start == 0) {
-//       return;
-//     }
-
-//     if (sel.isCollapsed) {
-//       TransactionBuilder(editorState)
-//         ..deleteText(node, sel.start - 1, 1)
-//         ..commit();
-//     } else {
-//       TransactionBuilder(editorState)
-//         ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset)
-//         ..commit();
-//     }
-
-//     _setEditingStateFromGlobal();
-//   }
-
-//   _forwardDeleteTextAtSelection(TextSelection? sel) {
-//     if (sel == null) {
-//       return;
-//     }
-
-//     if (sel.isCollapsed) {
-//       TransactionBuilder(editorState)
-//         ..deleteText(node, sel.start, 1)
-//         ..commit();
-//     } else {
-//       TransactionBuilder(editorState)
-//         ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset)
-//         ..commit();
-//     }
-//     _setEditingStateFromGlobal();
-//   }
-
-//   _setEditingStateFromGlobal() {
-//     _textInputConnection?.setEditingState(TextEditingValue(
-//         text: node.toRawString(),
-//         selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
-//             const TextSelection.collapsed(offset: 0)));
-//   }
-
-//   @override
-//   void connectionClosed() {
-//     // TODO: implement connectionClosed
-//   }
-
-//   @override
-//   // TODO: implement currentAutofillScope
-//   AutofillScope? get currentAutofillScope => throw UnimplementedError();
-
-//   @override
-//   // TODO: implement currentTextEditingValue
-//   TextEditingValue? get currentTextEditingValue => TextEditingValue(
-//       text: node.toRawString(),
-//       selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
-//           const TextSelection.collapsed(offset: 0));
-
-//   @override
-//   void insertTextPlaceholder(Size size) {
-//     // TODO: implement insertTextPlaceholder
-//   }
-
-//   @override
-//   void performAction(TextInputAction action) {}
-
-//   @override
-//   void performPrivateCommand(String action, Map<String, dynamic> data) {
-//     // TODO: implement performPrivateCommand
-//   }
-
-//   @override
-//   void removeTextPlaceholder() {
-//     // TODO: implement removeTextPlaceholder
-//   }
-
-//   @override
-//   void showAutocorrectionPromptRect(int start, int end) {
-//     // TODO: implement showAutocorrectionPromptRect
-//   }
-
-//   @override
-//   void showToolbar() {
-//     // TODO: implement showToolbar
-//   }
-
-//   @override
-//   void updateEditingValue(TextEditingValue value) {}
-
-//   @override
-//   void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
-//     for (final textDelta in textEditingDeltas) {
-//       if (textDelta is TextEditingDeltaInsertion) {
-//         TransactionBuilder(editorState)
-//           ..insertText(node, textDelta.insertionOffset, textDelta.textInserted)
-//           ..commit();
-//       } else if (textDelta is TextEditingDeltaDeletion) {
-//         TransactionBuilder(editorState)
-//           ..deleteText(node, textDelta.deletedRange.start,
-//               textDelta.deletedRange.end - textDelta.deletedRange.start)
-//           ..commit();
-//       }
-//     }
-//   }
-
-//   @override
-//   void updateFloatingCursor(RawFloatingCursorPoint point) {
-//     // TODO: implement updateFloatingCursor
-//   }
-// }
-
-// 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;
-//     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,
-//     );
-//   }
-// }
-
-// TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) {
-//   if (globalSel == null) {
-//     return null;
-//   }
-//   final nodePath = node.path;
-
-//   if (!pathEquals(nodePath, globalSel.start.path)) {
-//     return null;
-//   }
-//   if (globalSel.isCollapsed) {
-//     return TextSelection(
-//         baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
-//   } else {
-//     if (pathEquals(globalSel.start.path, globalSel.end.path)) {
-//       return TextSelection(
-//           baseOffset: globalSel.start.offset,
-//           extentOffset: globalSel.end.offset);
-//     }
-//   }
-//   return null;
-// }
-
-// Selection? _localSelectionToGlobal(Node node, TextSelection? sel) {
-//   if (sel == null) {
-//     return null;
-//   }
-//   final nodePath = node.path;
-
-//   return Selection(
-//     start: Position(path: nodePath, offset: sel.baseOffset),
-//     end: Position(path: nodePath, offset: sel.extentOffset),
-//   );
-// }

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

@@ -1,281 +0,0 @@
-import 'dart:math';
-
-import 'package:example/plugin/debuggable_rich_text.dart';
-import 'package:flowy_editor/document/selection.dart';
-import 'package:flowy_editor/document/position.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 context) {
-    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();
-  TextSelection? _textSelection;
-
-  RenderParagraph get _renderParagraph =>
-      _textKey.currentContext?.findRenderObject() as RenderParagraph;
-
-  @override
-  Selection getSelectionInRange(Offset start, Offset end) {
-    final localStart = _renderParagraph.globalToLocal(start);
-    final localEnd = _renderParagraph.globalToLocal(end);
-    final baseOffset = _getTextPositionAtOffset(localStart).offset;
-    final extentOffset = _getTextPositionAtOffset(localEnd).offset;
-    return Selection.single(
-      path: node.path,
-      startOffset: baseOffset,
-      endOffset: extentOffset,
-    );
-  }
-
-  @override
-  Offset localToGlobal(Offset offset) {
-    return _renderParagraph.localToGlobal(offset);
-  }
-
-  @override
-  List<Rect> getRectsInSelection(Selection selection) {
-    assert(pathEquals(selection.start.path, selection.end.path));
-    assert(pathEquals(selection.start.path, node.path));
-    final textSelection = TextSelection(
-      baseOffset: selection.start.offset,
-      extentOffset: selection.end.offset,
-    );
-    return _computeSelectionRects(textSelection);
-  }
-
-  @override
-  Rect getCursorRectInPosition(Position position) {
-    final textSelection = TextSelection.collapsed(offset: position.offset);
-    _textSelection = textSelection;
-    return _computeCursorRect(textSelection.baseOffset);
-  }
-
-  @override
-  Position getPositionInOffset(Offset start) {
-    final localStart = _renderParagraph.globalToLocal(start);
-    final baseOffset = _getTextPositionAtOffset(localStart).offset;
-    return Position(path: node.path, offset: baseOffset);
-  }
-
-  @override
-  TextSelection? getTextSelectionInSelection(Selection selection) {
-    assert(selection.isCollapsed);
-    if (!selection.isCollapsed) {
-      return null;
-    }
-    return TextSelection(
-      baseOffset: selection.start.offset,
-      extentOffset: selection.end.offset,
-    );
-  }
-
-  @override
-  Position start() => Position(path: node.path, offset: 0);
-
-  @override
-  Position end() =>
-      Position(path: node.path, offset: node.toRawString().length);
-
-  @override
-  Widget build(BuildContext context) {
-    Widget richText;
-    if (kDebugMode) {
-      richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey);
-    } else {
-      richText = RichText(key: _textKey, text: node.toTextSpan());
-    }
-
-    if (node.children.isEmpty) {
-      return richText;
-    }
-
-    return Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        SizedBox(
-          width: MediaQuery.of(context).size.width,
-          child: 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) {
-    return _renderParagraph.getPositionForOffset(offset);
-  }
-
-  List<Rect> _computeSelectionRects(TextSelection textSelection) {
-    final textBoxes = _renderParagraph.getBoxesForSelection(textSelection);
-    return textBoxes.map((box) => box.toRect()).toList();
-  }
-
-  Rect _computeCursorRect(int offset) {
-    final position = TextPosition(offset: offset);
-    final cursorOffset =
-        _renderParagraph.getOffsetForCaret(position, Rect.zero);
-    final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
-    if (cursorHeight != null) {
-      const cursorWidth = 2;
-      return Rect.fromLTWH(
-        cursorOffset.dx - (cursorWidth / 2),
-        cursorOffset.dy,
-        cursorWidth.toDouble(),
-        cursorHeight.toDouble(),
-      );
-    } else {
-      return Rect.zero;
-    }
-  }
-}
-
-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;
-  }
-}

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

@@ -1,33 +0,0 @@
-import 'package:flowy_editor/flowy_editor.dart';
-import 'package:flutter/material.dart';
-
-class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
-  TextWithCheckBoxNodeBuilder.create({
-    required super.node,
-    required super.editorState,
-    required super.key,
-  }) : super.create();
-
-  // TODO: check the type
-  bool get isCompleted => node.attributes['checkbox'] as bool;
-
-  @override
-  Widget build(BuildContext context) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        Checkbox(value: isCompleted, onChanged: (value) {}),
-        Expanded(
-          child: renderPlugins.buildWidget(
-            context: NodeWidgetContext(
-              buildContext: context,
-              node: node,
-              editorState: editorState,
-            ),
-            withSubtype: false,
-          ),
-        )
-      ],
-    );
-  }
-}

+ 0 - 46
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart

@@ -1,46 +0,0 @@
-import 'package:flowy_editor/flowy_editor.dart';
-import 'package:flutter/material.dart';
-
-class TextWithHeadingNodeBuilder extends NodeWidgetBuilder {
-  TextWithHeadingNodeBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create() {
-    nodeValidator = (node) => node.attributes.containsKey('heading');
-  }
-
-  String get heading => node.attributes['heading'] as String;
-  Widget buildPadding() {
-    if (heading == 'h1') {
-      return const Padding(
-        padding: EdgeInsets.only(top: 10),
-      );
-    } else if (heading == 'h2') {
-      return const Padding(
-        padding: EdgeInsets.only(top: 5),
-      );
-    }
-    return const Padding(
-      padding: EdgeInsets.only(top: 0),
-    );
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Column(
-      children: [
-        buildPadding(),
-        renderPlugins.buildWidget(
-          context: NodeWidgetContext(
-            buildContext: context,
-            node: node,
-            editorState: editorState,
-          ),
-          withSubtype: false,
-        ),
-        buildPadding(),
-      ],
-    );
-  }
-}

+ 0 - 17
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,5 +1,4 @@
 import 'dart:async';
-import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
 import 'package:flowy_editor/service/service.dart';
 import 'package:flutter/material.dart';
 
@@ -9,7 +8,6 @@ import 'package:flowy_editor/document/state_tree.dart';
 import 'package:flowy_editor/operation/operation.dart';
 import 'package:flowy_editor/operation/transaction.dart';
 import 'package:flowy_editor/undo_manager.dart';
-import 'package:flowy_editor/render/render_plugins.dart';
 
 class ApplyOptions {
   /// This flag indicates that
@@ -25,7 +23,6 @@ class ApplyOptions {
 
 class EditorState {
   final StateTree document;
-  final RenderPlugins renderPlugins;
 
   List<Node> selectedNodes = [];
 
@@ -54,24 +51,10 @@ class EditorState {
 
   EditorState({
     required this.document,
-    required this.renderPlugins,
   }) {
-    // FIXME: abstract render plugins as a service.
-    renderPlugins.register('text', RichTextNodeWidgetBuilder.create);
     undoManager.state = this;
   }
 
-  /// TODO: move to a better place.
-  Widget build(BuildContext context) {
-    return renderPlugins.buildWidget(
-      context: NodeWidgetContext(
-        buildContext: context,
-        node: document.root,
-        editorState: this,
-      ),
-    );
-  }
-
   apply(Transaction transaction,
       [ApplyOptions options = const ApplyOptions()]) {
     for (final op in transaction.operations) {

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

@@ -4,8 +4,6 @@ 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/selection/selectable.dart';
 export 'package:flowy_editor/operation/transaction.dart';
 export 'package:flowy_editor/operation/transaction_builder.dart';
@@ -14,3 +12,4 @@ export 'package:flowy_editor/editor_state.dart';
 export 'package:flowy_editor/service/editor_service.dart';
 export 'package:flowy_editor/document/selection.dart';
 export 'package:flowy_editor/document/position.dart';
+export 'package:flowy_editor/service/render_plugin_service.dart';

+ 58 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart

@@ -0,0 +1,58 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
+  @override
+  Widget build(NodeWidgetContext context) {
+    return EditorNodeWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return node.type == 'editor';
+      });
+}
+
+class EditorNodeWidget extends StatelessWidget {
+  const EditorNodeWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+
+  @override
+  Widget build(BuildContext context) {
+    return SingleChildScrollView(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: node.children
+            .map(
+              (child) =>
+                  editorState.service.renderPluginService.buildPluginWidget(
+                child is TextNode
+                    ? NodeWidgetContext<TextNode>(
+                        context: context,
+                        node: child,
+                        editorState: editorState,
+                      )
+                    : NodeWidgetContext<Node>(
+                        context: context,
+                        node: child,
+                        editorState: editorState,
+                      ),
+              ),
+            )
+            .toList(),
+      ),
+    );
+  }
+}

+ 0 - 63
frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart

@@ -1,63 +0,0 @@
-import 'package:flowy_editor/editor_state.dart';
-import 'package:flowy_editor/document/node.dart';
-import 'package:flowy_editor/render/render_plugins.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-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;
-
-  RenderPlugins get renderPlugins => editorState.renderPlugins;
-
-  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 context,
-  ) =>
-      throw UnimplementedError();
-
-  /// TODO: refactore this part.
-  /// return widget embedded with ChangeNotifier and widget itself.
-  Widget call(
-    BuildContext context,
-  ) {
-    /// TODO: Validate the node
-    /// if failed, stop call build function,
-    ///   return Empty widget, and throw Error.
-    if (nodeValidator != null && nodeValidator!(node) != true) {
-      throw Exception(
-          'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }');
-    }
-
-    return _build(context);
-  }
-
-  Widget _build(BuildContext context) {
-    return CompositedTransformTarget(
-      link: node.layerLink,
-      child: ChangeNotifierProvider.value(
-        value: node,
-        builder: (context, child) => Consumer<T>(
-          builder: ((context, value, child) {
-            debugPrint('Node is rebuilding...');
-            return build(context);
-          }),
-        ),
-      ),
-    );
-  }
-}

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

@@ -1,88 +0,0 @@
-import 'package:flutter/material.dart';
-import '../document/node.dart';
-import './node_widget_builder.dart';
-import 'package:flowy_editor/editor_state.dart';
-
-class NodeWidgetContext {
-  final BuildContext buildContext;
-  final Node node;
-  final EditorState editorState;
-
-  NodeWidgetContext({
-    required this.buildContext,
-    required this.node,
-    required this.editorState,
-  });
-}
-
-typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
-    Function({
-  required T node,
-  required EditorState editorState,
-  required GlobalKey key,
-});
-
-// unused
-// typedef NodeBuilder<T extends Node> = T Function(Node node);
-
-class RenderPlugins {
-  final Map<String, NodeWidgetBuilderF> _nodeWidgetBuilders = {};
-  // unused
-  // Map<String, NodeBuilder> nodeBuilders = {};
-
-  /// Register plugin to render specified [name].
-  ///
-  /// [name] should be [Node].type
-  ///   or [Node].type + '/' + [Node].attributes['subtype'].
-  ///
-  /// e.g. 'text', 'text/with-checkbox', or 'text/with-heading'
-  ///
-  /// [name] could be empty.
-  void register(String name, NodeWidgetBuilderF builder) {
-    _validatePluginName(name);
-
-    _nodeWidgetBuilders[name] = builder;
-  }
-
-  /// UnRegister plugin with specified [name].
-  void unRegister(String name) {
-    _validatePluginName(name);
-
-    _nodeWidgetBuilders.removeWhere((key, _) => key == name);
-  }
-
-  Widget buildWidget({
-    required NodeWidgetContext context,
-    bool withSubtype = true,
-  }) {
-    /// Find node widget builder
-    /// 1. If node's attributes contains subtype, return.
-    /// 2. If node's attributes do no contains substype, return.
-    final node = context.node;
-    var name = node.type;
-    if (withSubtype && node.subtype != null) {
-      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);
-  }
-
-  NodeWidgetBuilderF _nodeWidgetBuilder(String name) {
-    assert(_nodeWidgetBuilders.containsKey(name),
-        'Could not query the builder with this $name');
-    return _nodeWidgetBuilders[name]!;
-  }
-
-  void _validatePluginName(String name) {
-    final paths = name.split('/');
-    if (paths.length > 2) {
-      throw Exception('[Name] must contains zero or one slash("/")');
-    }
-  }
-}

+ 73 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart

@@ -0,0 +1,73 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
+  @override
+  Widget build(NodeWidgetContext<TextNode> context) {
+    return BulletedListTextNodeWidget(
+      key: context.node.key,
+      textNode: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return true;
+      });
+}
+
+class BulletedListTextNodeWidget extends StatefulWidget {
+  const BulletedListTextNodeWidget({
+    Key? key,
+    required this.textNode,
+    required this.editorState,
+  }) : super(key: key);
+
+  final TextNode textNode;
+  final EditorState editorState;
+
+  @override
+  State<BulletedListTextNodeWidget> createState() =>
+      _BulletedListTextNodeWidgetState();
+}
+
+// customize
+
+class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
+    with Selectable, DefaultSelectable {
+  final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text');
+  final leftPadding = 20.0;
+
+  @override
+  Selectable<StatefulWidget> get forward =>
+      _richTextKey.currentState as Selectable;
+
+  @override
+  Offset get baseOffset {
+    return Offset(leftPadding, 0);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        FlowySvg(
+          size: Size.square(leftPadding),
+          name: 'point',
+        ),
+        FlowyRichText(
+          key: _richTextKey,
+          textNode: widget.textNode,
+          editorState: widget.editorState,
+        ),
+      ],
+    );
+  }
+}

+ 123 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart

@@ -0,0 +1,123 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/operation/transaction_builder.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
+  @override
+  Widget build(NodeWidgetContext<TextNode> context) {
+    return CheckboxNodeWidget(
+      key: context.node.key,
+      textNode: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return node.attributes.containsKey(StyleKey.check);
+      });
+}
+
+class CheckboxNodeWidget extends StatefulWidget {
+  const CheckboxNodeWidget({
+    Key? key,
+    required this.textNode,
+    required this.editorState,
+  }) : super(key: key);
+
+  final TextNode textNode;
+  final EditorState editorState;
+
+  @override
+  State<CheckboxNodeWidget> createState() => _CheckboxNodeWidgetState();
+}
+
+class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
+    with Selectable, DefaultSelectable {
+  final _richTextKey = GlobalKey(debugLabel: 'checkbox_text');
+
+  final leftPadding = 20.0;
+
+  @override
+  Selectable<StatefulWidget> get forward =>
+      _richTextKey.currentState as Selectable;
+
+  @override
+  Offset get baseOffset {
+    return Offset(leftPadding, 0);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (widget.textNode.children.isEmpty) {
+      return _buildWithSingle(context);
+    } else {
+      return _buildWithChildren(context);
+    }
+  }
+
+  Widget _buildWithSingle(BuildContext context) {
+    final check = widget.textNode.attributes.check;
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        GestureDetector(
+          child: FlowySvg(
+            size: Size.square(leftPadding),
+            name: check ? 'check' : 'uncheck',
+          ),
+          onTap: () {
+            debugPrint('[Checkbox] onTap...');
+            TransactionBuilder(widget.editorState)
+              ..updateNode(widget.textNode, {
+                'checkbox': !check,
+              })
+              ..commit();
+          },
+        ),
+        FlowyRichText(
+          key: _richTextKey,
+          textNode: widget.textNode,
+          editorState: widget.editorState,
+        )
+      ],
+    );
+  }
+
+  Widget _buildWithChildren(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        _buildWithSingle(context),
+        Row(
+          children: [
+            const SizedBox(
+              width: 20,
+            ),
+            Column(
+              children: widget.textNode.children
+                  .map(
+                    (child) => widget.editorState.service.renderPluginService
+                        .buildPluginWidget(
+                      NodeWidgetContext(
+                        context: context,
+                        node: child,
+                        editorState: widget.editorState,
+                      ),
+                    ),
+                  )
+                  .toList(),
+            )
+          ],
+        )
+      ],
+    );
+  }
+}

+ 33 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart

@@ -0,0 +1,33 @@
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flutter/material.dart';
+
+mixin DefaultSelectable {
+  Selectable get forward;
+
+  Offset get baseOffset;
+
+  Position getPositionInOffset(Offset start) =>
+      forward.getPositionInOffset(start);
+
+  Rect getCursorRectInPosition(Position position) =>
+      forward.getCursorRectInPosition(position).shift(baseOffset);
+
+  List<Rect> getRectsInSelection(Selection selection) => forward
+      .getRectsInSelection(selection)
+      .map((rect) => rect.shift(baseOffset))
+      .toList(growable: false);
+
+  Selection getSelectionInRange(Offset start, Offset end) =>
+      forward.getSelectionInRange(start, end);
+
+  Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
+
+  Selection? getWorldBoundaryInOffset(Offset offset) =>
+      forward.getWorldBoundaryInOffset(offset);
+
+  Position start() => forward.start();
+
+  Position end() => forward.end();
+}

+ 48 - 164
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart

@@ -4,39 +4,37 @@ import 'package:flowy_editor/document/selection.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flowy_editor/editor_state.dart';
 import 'package:flowy_editor/document/path.dart';
-import 'package:flowy_editor/operation/transaction_builder.dart';
-import 'package:flowy_editor/render/node_widget_builder.dart';
-import 'package:flowy_editor/render/render_plugins.dart';
 import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
-import 'package:flowy_editor/infra/flowy_svg.dart';
-import 'package:flowy_editor/extensions/object_extensions.dart';
 import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
 
-class RichTextNodeWidgetBuilder extends NodeWidgetBuilder {
-  RichTextNodeWidgetBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create();
-
+class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
-  Widget build(BuildContext context) {
+  Widget build(NodeWidgetContext<TextNode> context) {
     return FlowyRichText(
-      key: key,
-      textNode: node as TextNode,
-      editorState: editorState,
+      key: context.node.key,
+      textNode: context.node,
+      editorState: context.editorState,
     );
   }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return true;
+      });
 }
 
+typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
+
 class FlowyRichText extends StatefulWidget {
   const FlowyRichText({
     Key? key,
     this.cursorHeight,
     this.cursorWidth = 2.0,
+    this.textSpanDecorator,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
@@ -45,6 +43,7 @@ class FlowyRichText extends StatefulWidget {
   final double cursorWidth;
   final TextNode textNode;
   final EditorState editorState;
+  final FlowyTextSpanDecorator? textSpanDecorator;
 
   @override
   State<FlowyRichText> createState() => _FlowyRichTextState();
@@ -52,49 +51,32 @@ class FlowyRichText extends StatefulWidget {
 
 class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
   final _textKey = GlobalKey();
-  final _decorationKey = GlobalKey();
 
-  EditorState get _editorState => widget.editorState;
-  TextNode get _textNode => widget.textNode;
   RenderParagraph get _renderParagraph =>
       _textKey.currentContext?.findRenderObject() as RenderParagraph;
 
   @override
   Widget build(BuildContext context) {
-    final attributes = _textNode.attributes;
-    // TODO: use factory method ??
-    if (attributes.list == 'todo') {
-      return _buildTodoListRichText(context);
-    } else if (attributes.list == 'bullet') {
-      return _buildBulletedListRichText(context);
-    } else if (attributes.quote == true) {
-      return _buildQuotedRichText(context);
-    } else if (attributes.heading != null) {
-      return _buildHeadingRichText(context);
-    } else if (attributes.number != null) {
-      return _buildNumberListRichText(context);
-    }
     return _buildRichText(context);
   }
 
   @override
-  Position start() => Position(path: _textNode.path, offset: 0);
+  Position start() => Position(path: widget.textNode.path, offset: 0);
 
   @override
-  Position end() =>
-      Position(path: _textNode.path, offset: _textNode.toRawString().length);
+  Position end() => Position(
+      path: widget.textNode.path, offset: widget.textNode.toRawString().length);
 
   @override
   Rect getCursorRectInPosition(Position position) {
     final textPosition = TextPosition(offset: position.offset);
-    final baseRect = frontWidgetRect();
     final cursorOffset =
         _renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
     final cursorHeight = widget.cursorHeight ??
         _renderParagraph.getFullHeightForCaret(textPosition) ??
         5.0; // default height
     return Rect.fromLTWH(
-      baseRect.centerRight.dx + cursorOffset.dx - (widget.cursorWidth / 2),
+      cursorOffset.dx - (widget.cursorWidth / 2),
       cursorOffset.dy,
       widget.cursorWidth,
       cursorHeight,
@@ -105,7 +87,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
   Position getPositionInOffset(Offset start) {
     final offset = _renderParagraph.globalToLocal(start);
     final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
-    return Position(path: _textNode.path, offset: baseOffset);
+    return Position(path: widget.textNode.path, offset: baseOffset);
   }
 
   @override
@@ -113,25 +95,24 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
     final localOffset = _renderParagraph.globalToLocal(offset);
     final textPosition = _renderParagraph.getPositionForOffset(localOffset);
     final textRange = _renderParagraph.getWordBoundary(textPosition);
-    final start = Position(path: _textNode.path, offset: textRange.start);
-    final end = Position(path: _textNode.path, offset: textRange.end);
+    final start = Position(path: widget.textNode.path, offset: textRange.start);
+    final end = Position(path: widget.textNode.path, offset: textRange.end);
     return Selection(start: start, end: end);
   }
 
   @override
   List<Rect> getRectsInSelection(Selection selection) {
     assert(pathEquals(selection.start.path, selection.end.path) &&
-        pathEquals(selection.start.path, _textNode.path));
+        pathEquals(selection.start.path, widget.textNode.path));
 
     final textSelection = TextSelection(
       baseOffset: selection.start.offset,
       extentOffset: selection.end.offset,
     );
-    final baseRect = frontWidgetRect();
-    return _renderParagraph.getBoxesForSelection(textSelection).map((box) {
-      final rect = box.toRect();
-      return rect.translate(baseRect.centerRight.dx, 0);
-    }).toList();
+    return _renderParagraph
+        .getBoxesForSelection(textSelection)
+        .map((box) => box.toRect())
+        .toList();
   }
 
   @override
@@ -141,32 +122,40 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
     final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
     final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
     return Selection.single(
-      path: _textNode.path,
+      path: widget.textNode.path,
       startOffset: baseOffset,
       endOffset: extentOffset,
     );
   }
 
   Widget _buildRichText(BuildContext context) {
-    if (_textNode.children.isEmpty) {
-      return _buildSingleRichText(context);
-    } else {
-      return _buildRichTextWithChildren(context);
-    }
+    return _buildSingleRichText(context);
   }
 
+  Widget _buildSingleRichText(BuildContext context) {
+    final textSpan = _textSpan;
+    return RichText(
+      key: _textKey,
+      text: widget.textSpanDecorator != null
+          ? widget.textSpanDecorator!(textSpan)
+          : textSpan,
+    );
+  }
+
+  // unused now.
   Widget _buildRichTextWithChildren(BuildContext context) {
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
         _buildSingleRichText(context),
-        ..._textNode.children
+        ...widget.textNode.children
             .map(
-              (child) => _editorState.renderPlugins.buildWidget(
-                context: NodeWidgetContext(
-                  buildContext: context,
+              (child) => widget.editorState.service.renderPluginService
+                  .buildPluginWidget(
+                NodeWidgetContext(
+                  context: context,
                   node: child,
-                  editorState: _editorState,
+                  editorState: widget.editorState,
                 ),
               ),
             )
@@ -175,118 +164,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
     );
   }
 
-  Widget _buildSingleRichText(BuildContext context) {
-    return SizedBox(
-      width:
-          MediaQuery.of(context).size.width - 20, // FIXME: use the const value
-      child: RichText(key: _textKey, text: _decorateTextSpanWithGlobalStyle),
-    );
-  }
-
-  Widget _buildTodoListRichText(BuildContext context) {
-    final name = _textNode.attributes.todo ? 'check' : 'uncheck';
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        GestureDetector(
-          child: FlowySvg(
-            key: _decorationKey,
-            name: name,
-          ),
-          onTap: () => TransactionBuilder(_editorState)
-            ..updateNode(_textNode, {
-              'todo': !_textNode.attributes.todo,
-            })
-            ..commit(),
-        ),
-        _buildRichText(context),
-      ],
-    );
-  }
-
-  Widget _buildBulletedListRichText(BuildContext context) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.center,
-      children: [
-        FlowySvg(
-          key: _decorationKey,
-          name: 'point',
-        ),
-        _buildRichText(context),
-      ],
-    );
-  }
-
-  Widget _buildNumberListRichText(BuildContext context) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.center,
-      children: [
-        FlowySvg(
-          key: _decorationKey,
-          number: _textNode.attributes.number,
-        ),
-        _buildRichText(context),
-      ],
-    );
-  }
-
-  Widget _buildQuotedRichText(BuildContext context) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        FlowySvg(
-          key: _decorationKey,
-          name: 'quote',
-        ),
-        _buildRichText(context),
-      ],
-    );
-  }
-
-  Widget _buildHeadingRichText(BuildContext context) {
-    // TODO: customize
-    return Column(
-      children: [
-        const Padding(padding: EdgeInsets.only(top: 5)),
-        _buildRichText(context),
-        const Padding(padding: EdgeInsets.only(top: 5)),
-      ],
-    );
-  }
-
-  Rect frontWidgetRect() {
-    // FIXME: find a more elegant way to solve this situation.
-    final renderBox = _decorationKey.currentContext
-        ?.findRenderObject()
-        ?.unwrapOrNull<RenderBox>();
-    if (renderBox != null) {
-      return renderBox.localToGlobal(Offset.zero) & renderBox.size;
-    }
-    return Rect.zero;
-  }
-
+  @override
   Offset localToGlobal(Offset offset) {
     return _renderParagraph.localToGlobal(offset);
   }
 
-  TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan(
-        children: _textSpan.children
-            ?.whereType<TextSpan>()
-            .map(
-              (span) => TextSpan(
-                text: span.text,
-                style: span.style?.copyWith(
-                  fontSize: _textNode.attributes.fontSize,
-                  color: _textNode.attributes.quoteColor,
-                ),
-                recognizer: span.recognizer,
-              ),
-            )
-            .toList(),
-      );
-
   TextSpan get _textSpan => TextSpan(
-      children: _textNode.delta.operations
+      children: widget.textNode.delta.operations
           .whereType<TextInsert>()
           .map((insert) => RichTextStyle(
                 attributes: insert.attributes ?? {},

+ 93 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart

@@ -0,0 +1,93 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
+  @override
+  Widget build(NodeWidgetContext<TextNode> context) {
+    return HeadingTextNodeWidget(
+      key: context.node.key,
+      textNode: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return node.attributes.heading != null;
+      });
+}
+
+class HeadingTextNodeWidget extends StatefulWidget {
+  const HeadingTextNodeWidget({
+    Key? key,
+    required this.textNode,
+    required this.editorState,
+  }) : super(key: key);
+
+  final TextNode textNode;
+  final EditorState editorState;
+
+  @override
+  State<HeadingTextNodeWidget> createState() => _HeadingTextNodeWidgetState();
+}
+
+// customize
+
+class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
+    with Selectable, DefaultSelectable {
+  final _richTextKey = GlobalKey(debugLabel: 'heading_text');
+  final topPadding = 5.0;
+  final bottomPadding = 2.0;
+
+  @override
+  Selectable<StatefulWidget> get forward =>
+      _richTextKey.currentState as Selectable;
+
+  @override
+  Offset get baseOffset {
+    return Offset(0, topPadding);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        Padding(
+          padding: EdgeInsets.only(
+            top: topPadding,
+            bottom: bottomPadding,
+          ),
+          child: FlowyRichText(
+            key: _richTextKey,
+            textSpanDecorator: _textSpanDecorator,
+            textNode: widget.textNode,
+            editorState: widget.editorState,
+          ),
+        )
+      ],
+    );
+  }
+
+  TextSpan _textSpanDecorator(TextSpan textSpan) {
+    return TextSpan(
+      children: textSpan.children
+          ?.whereType<TextSpan>()
+          .map(
+            (span) => TextSpan(
+              text: span.text,
+              style: span.style?.copyWith(
+                fontSize: widget.textNode.attributes.fontSize,
+              ),
+              recognizer: span.recognizer,
+            ),
+          )
+          .toList(),
+    );
+  }
+}

+ 74 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart

@@ -0,0 +1,74 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
+  @override
+  Widget build(NodeWidgetContext<TextNode> context) {
+    return NumberListTextNodeWidget(
+      key: context.node.key,
+      textNode: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return node.attributes.number != null;
+      });
+}
+
+class NumberListTextNodeWidget extends StatefulWidget {
+  const NumberListTextNodeWidget({
+    Key? key,
+    required this.textNode,
+    required this.editorState,
+  }) : super(key: key);
+
+  final TextNode textNode;
+  final EditorState editorState;
+
+  @override
+  State<NumberListTextNodeWidget> createState() =>
+      _NumberListTextNodeWidgetState();
+}
+
+// customize
+
+class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
+    with Selectable, DefaultSelectable {
+  final _richTextKey = GlobalKey(debugLabel: 'number_list_text');
+  final leftPadding = 20.0;
+
+  @override
+  Selectable<StatefulWidget> get forward =>
+      _richTextKey.currentState as Selectable;
+
+  @override
+  Offset get baseOffset {
+    return Offset(leftPadding, 0);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        FlowySvg(
+          size: Size.square(leftPadding),
+          number: widget.textNode.attributes.number,
+        ),
+        FlowyRichText(
+          key: _richTextKey,
+          textNode: widget.textNode,
+          editorState: widget.editorState,
+        ),
+      ],
+    );
+  }
+}

+ 73 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart

@@ -0,0 +1,73 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/infra/flowy_svg.dart';
+import 'package:flowy_editor/render/rich_text/default_selectable.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
+import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
+import 'package:flowy_editor/render/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
+import 'package:flutter/material.dart';
+
+class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
+  @override
+  Widget build(NodeWidgetContext<TextNode> context) {
+    return QuotedTextNodeWidget(
+      key: context.node.key,
+      textNode: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => ((node) {
+        return true;
+      });
+}
+
+class QuotedTextNodeWidget extends StatefulWidget {
+  const QuotedTextNodeWidget({
+    Key? key,
+    required this.textNode,
+    required this.editorState,
+  }) : super(key: key);
+
+  final TextNode textNode;
+  final EditorState editorState;
+
+  @override
+  State<QuotedTextNodeWidget> createState() => _QuotedTextNodeWidgetState();
+}
+
+// customize
+
+class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
+    with Selectable, DefaultSelectable {
+  final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
+  final leftPadding = 20.0;
+
+  @override
+  Selectable<StatefulWidget> get forward =>
+      _richTextKey.currentState as Selectable;
+
+  @override
+  Offset get baseOffset {
+    return Offset(leftPadding, 0);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        FlowySvg(
+          size: Size.square(leftPadding),
+          name: 'quote',
+        ),
+        FlowyRichText(
+          key: _richTextKey,
+          textNode: widget.textNode,
+          editorState: widget.editorState,
+        ),
+      ],
+    );
+  }
+}

+ 12 - 5
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart

@@ -25,12 +25,15 @@ class StyleKey {
   static String font = 'font';
   static String href = 'href';
 
-  static String heading = 'heading';
   static String quote = 'quote';
   static String list = 'list';
   static String number = 'number';
   static String todo = 'todo';
   static String code = 'code';
+
+  static String subtype = 'subtype';
+  static String check = 'checkbox';
+  static String heading = 'heading';
 }
 
 double baseFontSize = 16.0;
@@ -60,10 +63,7 @@ extension NodeAttributesExtensions on Attributes {
   }
 
   bool get quote {
-    if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) {
-      return this[StyleKey.quote];
-    }
-    return false;
+    return containsKey(StyleKey.quote);
   }
 
   Color? get quoteColor {
@@ -100,6 +100,13 @@ extension NodeAttributesExtensions on Attributes {
     }
     return false;
   }
+
+  bool get check {
+    if (containsKey(StyleKey.check) && this[StyleKey.check] is bool) {
+      return this[StyleKey.check];
+    }
+    return false;
+  }
 }
 
 extension DeltaAttributesExtensions on Attributes {

+ 47 - 4
frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart

@@ -1,6 +1,10 @@
+import 'package:flowy_editor/render/editor/editor_entry.dart';
+import 'package:flowy_editor/render/rich_text/checkbox_text.dart';
+import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
 import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
 import 'package:flowy_editor/service/input_service.dart';
 import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
 import 'package:flowy_editor/service/shortcut_service.dart';
 import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
 import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
@@ -9,21 +13,41 @@ import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handle
 import 'package:flowy_editor/service/keyboard_service.dart';
 import 'package:flowy_editor/service/selection_service.dart';
 import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart';
+import 'package:flowy_editor/render/rich_text/heading_text.dart';
+import 'package:flowy_editor/render/rich_text/number_list_text.dart';
+import 'package:flowy_editor/render/rich_text/quoted_text.dart';
 
 import 'package:flutter/material.dart';
 
+NodeWidgetBuilders defaultBuilders = {
+  'editor': EditorEntryWidgetBuilder(),
+  'text': RichTextNodeWidgetBuilder(),
+  'text/checkbox': CheckboxNodeWidgetBuilder(),
+  'text/heading': HeadingTextNodeWidgetBuilder(),
+  'text/bullet-list': BulletedListTextNodeWidgetBuilder(),
+  'text/number-list': NumberListTextNodeWidgetBuilder(),
+  'text/quote': QuotedTextNodeWidgetBuilder(),
+};
+
 class FlowyEditor extends StatefulWidget {
   const FlowyEditor({
     Key? key,
     required this.editorState,
-    required this.keyEventHandlers,
-    required this.shortcuts,
+    this.customBuilders = const {},
+    this.keyEventHandlers = const [],
+    this.shortcuts = const [],
   }) : super(key: key);
 
   final EditorState editorState;
+
+  /// Render plugins.
+  final NodeWidgetBuilders customBuilders;
+
+  /// Keyboard event handlers.
   final List<FlowyKeyEventHandler> keyEventHandlers;
 
-  /// Shortcusts
+  /// Shortcuts
   final FloatingShortcuts shortcuts;
 
   @override
@@ -33,6 +57,19 @@ class FlowyEditor extends StatefulWidget {
 class _FlowyEditorState extends State<FlowyEditor> {
   EditorState get editorState => widget.editorState;
 
+  @override
+  void initState() {
+    super.initState();
+
+    editorState.service.renderPluginService = FlowyRenderPlugin(
+      editorState: editorState,
+      builders: {
+        ...defaultBuilders,
+        ...widget.customBuilders,
+      },
+    );
+  }
+
   @override
   Widget build(BuildContext context) {
     return FlowySelection(
@@ -57,7 +94,13 @@ class _FlowyEditorState extends State<FlowyEditor> {
             size: const Size(200, 150), // TODO: support customize size.
             editorState: editorState,
             floatingShortcuts: widget.shortcuts,
-            child: editorState.build(context),
+            child: editorState.service.renderPluginService.buildPluginWidget(
+              NodeWidgetContext(
+                context: context,
+                node: editorState.document.root,
+                editorState: editorState,
+              ),
+            ),
           ),
         ),
       ),

+ 1 - 1
frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart

@@ -90,7 +90,7 @@ class _FlowyInputState extends State<FlowyInput>
 
   @override
   void apply(List<TextEditingDelta> deltas) {
-// TODO: implement the detail
+    // TODO: implement the detail
     for (final delta in deltas) {
       if (delta is TextEditingDeltaInsertion) {
       } else if (delta is TextEditingDeltaDeletion) {

+ 131 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart

@@ -0,0 +1,131 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+typedef NodeValidator<T extends Node> = bool Function(T node);
+
+abstract class NodeWidgetBuilder<T extends Node> {
+  NodeValidator get nodeValidator;
+
+  Widget build(NodeWidgetContext<T> context);
+}
+
+typedef NodeWidgetBuilders = Map<String, NodeWidgetBuilder>;
+
+abstract class FlowyRenderPluginService {
+  /// Register render plugin with specified [name].
+  ///
+  /// [name] should be [Node].type
+  ///   or [Node].type + '/' + [Node].attributes['subtype'].
+  ///
+  /// e.g. 'text', 'text/checkbox', or 'text/heading'
+  ///
+  /// [name] could be empty.
+  void register(String name, NodeWidgetBuilder builder);
+  void registerAll(Map<String, NodeWidgetBuilder> builders);
+
+  /// UnRegister plugin with specified [name].
+  void unRegister(String name);
+
+  Widget buildPluginWidget(NodeWidgetContext context);
+}
+
+class NodeWidgetContext<T extends Node> {
+  final BuildContext context;
+  final T node;
+  final EditorState editorState;
+
+  NodeWidgetContext({
+    required this.context,
+    required this.node,
+    required this.editorState,
+  });
+
+  NodeWidgetContext copyWith({
+    BuildContext? context,
+    T? node,
+    EditorState? editorState,
+  }) {
+    return NodeWidgetContext(
+      context: context ?? this.context,
+      node: node ?? this.node,
+      editorState: editorState ?? this.editorState,
+    );
+  }
+}
+
+class FlowyRenderPlugin extends FlowyRenderPluginService {
+  FlowyRenderPlugin({
+    required this.editorState,
+    required NodeWidgetBuilders builders,
+  }) {
+    registerAll(builders);
+  }
+
+  final NodeWidgetBuilders _builders = {};
+  final EditorState editorState;
+
+  @override
+  Widget buildPluginWidget(NodeWidgetContext context) {
+    final node = context.node;
+    final name =
+        node.subtype == null ? node.type : '${node.type}/${node.subtype!}';
+    final builder = _builders[name];
+    if (builder != null && builder.nodeValidator(node)) {
+      final key = GlobalKey(debugLabel: name);
+      node.key = key;
+      return _wrap(
+        builder.build(context),
+        context,
+      );
+    } else {
+      assert(false, 'Could not query the builder with this $name');
+      // TODO: return a placeholder widget with tips.
+      return Container();
+    }
+  }
+
+  @override
+  void register(String name, NodeWidgetBuilder<Node> builder) {
+    debugPrint('[Plugins] registering $name...');
+    _validatePlugin(name);
+    _builders[name] = builder;
+  }
+
+  @override
+  void registerAll(Map<String, NodeWidgetBuilder<Node>> builders) {
+    builders.forEach(register);
+  }
+
+  @override
+  void unRegister(String name) {
+    _validatePlugin(name);
+    _builders.remove(name);
+  }
+
+  Widget _wrap(Widget widget, NodeWidgetContext context) {
+    return CompositedTransformTarget(
+      link: context.node.layerLink,
+      child: ChangeNotifierProvider<Node>.value(
+        value: context.node,
+        builder: (context, child) => Consumer(
+          builder: ((context, value, child) {
+            debugPrint('Node is rebuilding...');
+            return widget;
+          }),
+        ),
+      ),
+    );
+  }
+
+  void _validatePlugin(String name) {
+    final paths = name.split('/');
+    if (paths.length > 2) {
+      throw Exception('Plugin name must contain at most one or zero slash');
+    }
+    if (_builders.containsKey(name)) {
+      throw Exception('Plugin name($name) already exists.');
+    }
+  }
+}

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart

@@ -250,6 +250,7 @@ class _FlowySelectionState extends State<FlowySelection>
     );
   }
 
+  @override
   List<Rect> rects() {
     return _rects;
   }

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

@@ -1,3 +1,4 @@
+import 'package:flowy_editor/service/render_plugin_service.dart';
 import 'package:flowy_editor/service/shortcut_service.dart';
 import 'package:flowy_editor/service/selection_service.dart';
 import 'package:flutter/material.dart';
@@ -17,6 +18,9 @@ class FlowyService {
   // input service
   final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');
 
+  // render plugin service
+  late FlowyRenderPlugin renderPluginService;
+
   // floating shortcut service
   final floatingShortcutServiceKey =
       GlobalKey(debugLabel: 'flowy_floating_shortcut_service');

+ 1 - 3
frontend/app_flowy/packages/flowy_editor/test/operation_test.dart

@@ -6,7 +6,6 @@ import 'package:flowy_editor/operation/operation.dart';
 import 'package:flowy_editor/operation/transaction_builder.dart';
 import 'package:flowy_editor/editor_state.dart';
 import 'package:flowy_editor/document/state_tree.dart';
-import 'package:flowy_editor/render/render_plugins.dart';
 
 void main() {
   group('transform path', () {
@@ -64,8 +63,7 @@ void main() {
             item2,
             item3,
           ]));
-    final state = EditorState(
-        document: StateTree(root: root), renderPlugins: RenderPlugins());
+    final state = EditorState(document: StateTree(root: root));
 
     expect(item1.path, [0]);
     expect(item2.path, [1]);