feat: refactor render plugin service

1. abstract render plugin as service.
2. simplify plugin development.
3. delete unused code
This commit is contained in:
Lucas.Xu 2022-07-29 14:29:37 +08:00
parent c5e9008f4b
commit ed1dc8ccef
26 changed files with 352 additions and 1938 deletions

View File

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

View File

@ -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.
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),
],
);
}
}

View File

@ -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) {

View File

@ -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';

View File

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

View File

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

View File

@ -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("/")');
}
}
}

View File

@ -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 {

View File

@ -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,
),

View File

@ -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,
),

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

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

View File

@ -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,
),
),
),
),
),

View File

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

View File

@ -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');

View File

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