瀏覽代碼

feat: refactor render plugin service

1. abstract render plugin as service.
2. simplify plugin development.
3. delete unused code
Lucas.Xu 2 年之前
父節點
當前提交
ed1dc8ccef
共有 26 個文件被更改,包括 352 次插入1938 次删除
  1. 11 0
      frontend/app_flowy/packages/flowy_editor/example/assets/example.json
  2. 3 17
      frontend/app_flowy/packages/flowy_editor/example/lib/main.dart
  3. 0 102
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/debuggable_rich_text.dart
  4. 0 52
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart
  5. 0 758
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/flowy_selectable_text.dart
  6. 15 18
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart
  7. 0 352
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/old_text_node_widget.dart
  8. 0 281
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart
  9. 0 33
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart
  10. 0 46
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_heading_node_widget.dart
  11. 0 29
      frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart
  12. 1 2
      frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart
  13. 58 0
      frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart
  14. 0 63
      frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart
  15. 0 88
      frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart
  16. 11 12
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart
  17. 16 18
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart
  18. 15 16
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart
  19. 11 12
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart
  20. 11 12
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart
  21. 12 12
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart
  22. 5 8
      frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart
  23. 47 4
      frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart
  24. 131 0
      frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart
  25. 4 0
      frontend/app_flowy/packages/flowy_editor/lib/service/service.dart
  26. 1 3
      frontend/app_flowy/packages/flowy_editor/test/operation_test.dart

+ 11 - 0
frontend/app_flowy/packages/flowy_editor/example/assets/example.json

@@ -3,6 +3,17 @@
     "type": "editor",
     "attributes": {},
     "children": [
+      {
+        "type": "text",
+        "delta": [
+          {
+            "insert": "Hello world"
+          }
+        ],
+        "attributes": {
+          "subtype": "quote"
+        }
+      },
       {
         "type": "image",
         "attributes": {

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

+ 15 - 18
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;

+ 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 - 29
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,10 +1,4 @@
 import 'dart:async';
-import 'package:flowy_editor/render/rich_text/bulleted_list_text.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/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:flowy_editor/service/service.dart';
 import 'package:flutter/material.dart';
 
@@ -14,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
@@ -30,7 +23,6 @@ class ApplyOptions {
 
 class EditorState {
   final StateTree document;
-  final RenderPlugins renderPlugins;
 
   List<Node> selectedNodes = [];
 
@@ -59,31 +51,10 @@ class EditorState {
 
   EditorState({
     required this.document,
-    required this.renderPlugins,
   }) {
-    // FIXME: abstract render plugins as a service.
-    renderPlugins.register('text', RichTextNodeWidgetBuilder.create);
-    renderPlugins.register('text/checkbox', CheckboxNodeWidgetBuilder.create);
-    renderPlugins.register('text/heading', HeadingTextNodeWidgetBuilder.create);
-    renderPlugins.register(
-        'text/bullet-list', BulletedListTextNodeWidgetBuilder.create);
-    renderPlugins.register(
-        'text/number-list', NumberListTextNodeWidgetBuilder.create);
-    renderPlugins.register('text/quote', QuotedTextNodeWidgetBuilder.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("/")');
-    }
-  }
-}

+ 11 - 12
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart

@@ -1,27 +1,26 @@
 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/node_widget_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/selection/selectable.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 
-class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder {
-  BulletedListTextNodeWidgetBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create();
-
+class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
-  Widget build(BuildContext context) {
+  Widget build(NodeWidgetContext<TextNode> context) {
     return BulletedListTextNodeWidget(
-      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;
+      });
 }
 
 class BulletedListTextNodeWidget extends StatefulWidget {

+ 16 - 18
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart

@@ -2,30 +2,27 @@ 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/node_widget_builder.dart';
-import 'package:flowy_editor/render/render_plugins.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/extensions/object_extensions.dart';
+import 'package:flowy_editor/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 
-class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder {
-  CheckboxNodeWidgetBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create();
-
+class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
-  Widget build(BuildContext context) {
+  Widget build(NodeWidgetContext<TextNode> context) {
     return CheckboxNodeWidget(
-      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 node.attributes.containsKey(StyleKey.check);
+      });
 }
 
 class CheckboxNodeWidget extends StatefulWidget {
@@ -67,7 +64,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
   }
 
   Widget _buildWithSingle(BuildContext context) {
-    final check = widget.textNode.attributes.checkbox;
+    final check = widget.textNode.attributes.check;
     return Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
@@ -107,9 +104,10 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
             Column(
               children: widget.textNode.children
                   .map(
-                    (child) => widget.editorState.renderPlugins.buildWidget(
-                      context: NodeWidgetContext(
-                        buildContext: context,
+                    (child) => widget.editorState.service.renderPluginService
+                        .buildPluginWidget(
+                      NodeWidgetContext(
+                        context: context,
                         node: child,
                         editorState: widget.editorState,
                       ),

+ 15 - 16
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart

@@ -4,29 +4,27 @@ 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/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/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);
@@ -152,9 +150,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
         _buildSingleRichText(context),
         ...widget.textNode.children
             .map(
-              (child) => widget.editorState.renderPlugins.buildWidget(
-                context: NodeWidgetContext(
-                  buildContext: context,
+              (child) => widget.editorState.service.renderPluginService
+                  .buildPluginWidget(
+                NodeWidgetContext(
+                  context: context,
                   node: child,
                   editorState: widget.editorState,
                 ),

+ 11 - 12
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart

@@ -1,27 +1,26 @@
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/editor_state.dart';
-import 'package:flowy_editor/render/node_widget_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 HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder {
-  HeadingTextNodeWidgetBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create();
-
+class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
-  Widget build(BuildContext context) {
+  Widget build(NodeWidgetContext<TextNode> context) {
     return HeadingTextNodeWidget(
-      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 node.attributes.heading != null;
+      });
 }
 
 class HeadingTextNodeWidget extends StatefulWidget {

+ 11 - 12
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart

@@ -1,28 +1,27 @@
 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/node_widget_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 NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder {
-  NumberListTextNodeWidgetBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create();
-
+class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
-  Widget build(BuildContext context) {
+  Widget build(NodeWidgetContext<TextNode> context) {
     return NumberListTextNodeWidget(
-      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 node.attributes.number != null;
+      });
 }
 
 class NumberListTextNodeWidget extends StatefulWidget {

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

@@ -1,27 +1,27 @@
 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/node_widget_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 QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder {
-  QuotedTextNodeWidgetBuilder.create({
-    required super.editorState,
-    required super.node,
-    required super.key,
-  }) : super.create();
-
+class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
-  Widget build(BuildContext context) {
+  Widget build(NodeWidgetContext<TextNode> context) {
     return QuotedTextNodeWidget(
-      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;
+      });
 }
 
 class QuotedTextNodeWidget extends StatefulWidget {

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

@@ -32,7 +32,7 @@ class StyleKey {
   static String code = 'code';
 
   static String subtype = 'subtype';
-  static String checkbox = 'checkbox';
+  static String check = 'checkbox';
   static String heading = 'heading';
 }
 
@@ -63,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 {
@@ -104,9 +101,9 @@ extension NodeAttributesExtensions on Attributes {
     return false;
   }
 
-  bool get checkbox {
-    if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) {
-      return this[StyleKey.checkbox];
+  bool get check {
+    if (containsKey(StyleKey.check) && this[StyleKey.check] is bool) {
+      return this[StyleKey.check];
     }
     return false;
   }

+ 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;
 
-  /// shortcuts
+  /// 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,
+              ),
+            ),
           ),
         ),
       ),

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

+ 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]);