feat: optimize the performance (#442)
This commit is contained in:
parent
ad55171c5b
commit
19f999f422
80 changed files with 4639 additions and 828 deletions
2
codecov.yml
Normal file
2
codecov.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ignore:
|
||||||
|
- "lib/src/flutter/"
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||||
|
|
@ -51,6 +52,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_jsonString = rootBundle.loadString('assets/example.json');
|
_jsonString = rootBundle.loadString('assets/example.json');
|
||||||
|
|
||||||
_widgetBuilder = (context) => Editor(
|
_widgetBuilder = (context) => Editor(
|
||||||
jsonString: _jsonString,
|
jsonString: _jsonString,
|
||||||
onEditorStateChange: (editorState) {
|
onEditorStateChange: (editorState) {
|
||||||
|
|
@ -109,6 +111,20 @@ class _HomePageState extends State<HomePage> {
|
||||||
final jsonString = rootBundle.loadString('assets/example.json');
|
final jsonString = rootBundle.loadString('assets/example.json');
|
||||||
_loadEditor(context, jsonString);
|
_loadEditor(context, jsonString);
|
||||||
}),
|
}),
|
||||||
|
_buildListTile(context, 'With Large Document (10000+ lines)', () {
|
||||||
|
final nodes = List.generate(
|
||||||
|
10000,
|
||||||
|
(index) =>
|
||||||
|
paragraphNode(text: '$index ${generateRandomString(50)}'),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: Document(root: pageNode(children: nodes)),
|
||||||
|
);
|
||||||
|
final jsonString = Future.value(
|
||||||
|
jsonEncode(editorState.document.toJson()),
|
||||||
|
);
|
||||||
|
_loadEditor(context, jsonString);
|
||||||
|
}),
|
||||||
_buildListTile(context, 'With Example.html', () async {
|
_buildListTile(context, 'With Example.html', () async {
|
||||||
final htmlString =
|
final htmlString =
|
||||||
await rootBundle.loadString('assets/example.html');
|
await rootBundle.loadString('assets/example.html');
|
||||||
|
|
@ -315,3 +331,10 @@ class _HomePageState extends State<HomePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String generateRandomString(int len) {
|
||||||
|
var r = Random();
|
||||||
|
return String.fromCharCodes(
|
||||||
|
List.generate(len, (index) => r.nextInt(33) + 89),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:example/home_page.dart';
|
import 'package:example/home_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@ class Editor extends StatelessWidget {
|
||||||
onEditorStateChange(editorState);
|
onEditorStateChange(editorState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final scrollController = ScrollController();
|
final editorScrollController = EditorScrollController(
|
||||||
|
editorState: editorState,
|
||||||
|
shrinkWrap: false,
|
||||||
|
);
|
||||||
if (PlatformExtension.isDesktopOrWeb) {
|
if (PlatformExtension.isDesktopOrWeb) {
|
||||||
return FloatingToolbar(
|
return FloatingToolbar(
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -54,11 +57,11 @@ class Editor extends StatelessWidget {
|
||||||
...alignmentItems,
|
...alignmentItems,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
scrollController: scrollController,
|
editorScrollController: editorScrollController,
|
||||||
child: _buildDesktopEditor(
|
child: _buildDesktopEditor(
|
||||||
context,
|
context,
|
||||||
editorState,
|
editorState,
|
||||||
scrollController,
|
editorScrollController,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (PlatformExtension.isMobile) {
|
} else if (PlatformExtension.isMobile) {
|
||||||
|
|
@ -68,7 +71,7 @@ class Editor extends StatelessWidget {
|
||||||
child: _buildMobileEditor(
|
child: _buildMobileEditor(
|
||||||
context,
|
context,
|
||||||
editorState,
|
editorState,
|
||||||
scrollController,
|
editorScrollController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MobileToolbar(
|
MobileToolbar(
|
||||||
|
|
@ -99,19 +102,19 @@ class Editor extends StatelessWidget {
|
||||||
Widget _buildMobileEditor(
|
Widget _buildMobileEditor(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
ScrollController? scrollController,
|
EditorScrollController? editorScrollController,
|
||||||
) {
|
) {
|
||||||
return AppFlowyEditor(
|
return AppFlowyEditor(
|
||||||
editorStyle: const EditorStyle.mobile(),
|
editorStyle: const EditorStyle.mobile(),
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
scrollController: scrollController,
|
editorScrollController: editorScrollController,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDesktopEditor(
|
Widget _buildDesktopEditor(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
ScrollController? scrollController,
|
EditorScrollController? editorScrollController,
|
||||||
) {
|
) {
|
||||||
final customBlockComponentBuilders = {
|
final customBlockComponentBuilders = {
|
||||||
...standardBlockComponentBuilderMap,
|
...standardBlockComponentBuilderMap,
|
||||||
|
|
@ -127,7 +130,8 @@ class Editor extends StatelessWidget {
|
||||||
};
|
};
|
||||||
return AppFlowyEditor(
|
return AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
scrollController: scrollController,
|
shrinkWrap: true,
|
||||||
|
editorScrollController: editorScrollController,
|
||||||
blockComponentBuilders: customBlockComponentBuilders,
|
blockComponentBuilders: customBlockComponentBuilders,
|
||||||
commandShortcutEvents: [
|
commandShortcutEvents: [
|
||||||
customToggleHighlightCommand(
|
customToggleHighlightCommand(
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies:
|
||||||
universal_html: ^2.0.8
|
universal_html: ^2.0.8
|
||||||
highlight: ^0.7.0
|
highlight: ^0.7.0
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
|
show_fps: ^1.0.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import 'dart:collection';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/non_secure.dart';
|
||||||
|
|
||||||
abstract class NodeExternalValues {
|
abstract class NodeExternalValues {
|
||||||
const NodeExternalValues();
|
const NodeExternalValues();
|
||||||
|
|
@ -39,8 +39,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
),
|
),
|
||||||
), // unlink the given children to avoid the error of "node has already a parent"
|
), // unlink the given children to avoid the error of "node has already a parent"
|
||||||
_attributes = attributes,
|
_attributes = attributes,
|
||||||
id = id ?? nanoid(10) {
|
id = id ?? nanoid(6) {
|
||||||
for (final child in this.children) {
|
for (final child in children) {
|
||||||
child.parent = this;
|
child.parent = this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,12 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
|
|
||||||
/// The children of the node.
|
/// The children of the node.
|
||||||
final LinkedList<Node> _children;
|
final LinkedList<Node> _children;
|
||||||
Iterable<Node> get children => _children.toList(growable: false);
|
List<Node> get children {
|
||||||
|
_cacheChildren ??= _children.toList(growable: false);
|
||||||
|
return _cacheChildren!;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Node>? _cacheChildren;
|
||||||
|
|
||||||
/// The attributes of the node.
|
/// The attributes of the node.
|
||||||
Attributes _attributes;
|
Attributes _attributes;
|
||||||
|
|
@ -141,7 +146,9 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
|
|
||||||
entry.parent = this;
|
entry.parent = this;
|
||||||
|
|
||||||
if (children.isEmpty) {
|
_cacheChildren = null;
|
||||||
|
|
||||||
|
if (_children.isEmpty) {
|
||||||
_children.add(entry);
|
_children.add(entry);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
|
|
@ -164,6 +171,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
entry.parent = parent;
|
entry.parent = parent;
|
||||||
super.insertAfter(entry);
|
super.insertAfter(entry);
|
||||||
|
|
||||||
|
parent?._cacheChildren = null;
|
||||||
|
|
||||||
// Notifies the new node.
|
// Notifies the new node.
|
||||||
parent?.notifyListeners();
|
parent?.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +182,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
entry.parent = parent;
|
entry.parent = parent;
|
||||||
super.insertBefore(entry);
|
super.insertBefore(entry);
|
||||||
|
|
||||||
|
parent?._cacheChildren = null;
|
||||||
|
|
||||||
// Notifies the new node.
|
// Notifies the new node.
|
||||||
parent?.notifyListeners();
|
parent?.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +196,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
Log.editor.debug('delete Node $this from path $path');
|
Log.editor.debug('delete Node $this from path $path');
|
||||||
super.unlink();
|
super.unlink();
|
||||||
|
|
||||||
|
parent?._cacheChildren = null;
|
||||||
|
|
||||||
parent?.notifyListeners();
|
parent?.notifyListeners();
|
||||||
parent = null;
|
parent = null;
|
||||||
}
|
}
|
||||||
|
|
@ -230,12 +243,12 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
}) {
|
}) {
|
||||||
final node = Node(
|
final node = Node(
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
id: nanoid(10),
|
id: nanoid(6),
|
||||||
attributes: attributes ?? {...this.attributes},
|
attributes: attributes ?? {...this.attributes},
|
||||||
children: children ?? [],
|
children: children ?? [],
|
||||||
);
|
);
|
||||||
if (children == null && this.children.isNotEmpty) {
|
if (children == null && _children.isNotEmpty) {
|
||||||
for (final child in this.children) {
|
for (final child in _children) {
|
||||||
node._children.add(
|
node._children.add(
|
||||||
child.copyWith()..parent = node,
|
child.copyWith()..parent = node,
|
||||||
);
|
);
|
||||||
|
|
@ -246,11 +259,12 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Path _computePath([Path previous = const []]) {
|
Path _computePath([Path previous = const []]) {
|
||||||
|
final parent = this.parent;
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
final index = parent!.children.toList().indexOf(this);
|
final index = parent.children.indexOf(this);
|
||||||
return parent!._computePath([index, ...previous]);
|
return parent._computePath([index, ...previous]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
typedef Path = List<int>;
|
typedef Path = List<int>;
|
||||||
|
|
@ -105,4 +106,10 @@ extension PathExtensions on Path {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool inSelection(Selection? selection) {
|
||||||
|
return selection != null &&
|
||||||
|
selection.start.path <= this &&
|
||||||
|
this <= selection.end.path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/block_component/base_component/selection/selection_area_painter.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/selection/cursor.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
final _deepEqual = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
enum BlockSelectionType {
|
||||||
|
cursor,
|
||||||
|
selection,
|
||||||
|
block,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [BlockSelectionArea] is a widget that renders the selection area or the cursor of a block.
|
||||||
|
class BlockSelectionArea extends StatefulWidget {
|
||||||
|
const BlockSelectionArea({
|
||||||
|
super.key,
|
||||||
|
required this.node,
|
||||||
|
required this.delegate,
|
||||||
|
required this.listenable,
|
||||||
|
required this.cursorColor,
|
||||||
|
required this.selectionColor,
|
||||||
|
required this.blockColor,
|
||||||
|
this.supportTypes = const [
|
||||||
|
BlockSelectionType.cursor,
|
||||||
|
BlockSelectionType.selection
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the cursor rect or selection rects from the delegate
|
||||||
|
final SelectableMixin delegate;
|
||||||
|
|
||||||
|
// get the selection from the listenable
|
||||||
|
final ValueListenable<Selection?> listenable;
|
||||||
|
|
||||||
|
// the color of the cursor
|
||||||
|
final Color cursorColor;
|
||||||
|
|
||||||
|
// the color of the selection
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
|
final Color blockColor;
|
||||||
|
|
||||||
|
// the node of the block
|
||||||
|
final Node node;
|
||||||
|
|
||||||
|
final List<BlockSelectionType> supportTypes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BlockSelectionArea> createState() => _BlockSelectionAreaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BlockSelectionAreaState extends State<BlockSelectionArea> {
|
||||||
|
// We need to keep the key to refresh the cursor status when typing continuously.
|
||||||
|
late GlobalKey cursorKey = GlobalKey(
|
||||||
|
debugLabel: 'cursor_${widget.node.path}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// keep the previous cursor rect to avoid unnecessary rebuild
|
||||||
|
Rect? prevCursorRect;
|
||||||
|
// keep the previous selection rects to avoid unnecessary rebuild
|
||||||
|
List<Rect>? prevSelectionRects;
|
||||||
|
// keep the block selection rect to avoid unnecessary rebuild
|
||||||
|
Rect? prevBlockRect;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_updateSelectionIfNeeded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
key: ValueKey(widget.node.id + widget.supportTypes.toString()),
|
||||||
|
valueListenable: widget.listenable,
|
||||||
|
builder: ((context, value, child) {
|
||||||
|
final sizedBox = child ?? const SizedBox.shrink();
|
||||||
|
final selection = value?.normalized;
|
||||||
|
|
||||||
|
if (selection == null) {
|
||||||
|
return sizedBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = widget.node.path;
|
||||||
|
if (!path.inSelection(selection)) {
|
||||||
|
return sizedBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.read<EditorState>().selectionType == SelectionType.block) {
|
||||||
|
if (!widget.supportTypes.contains(BlockSelectionType.block) ||
|
||||||
|
!path.equals(selection.start.path) ||
|
||||||
|
prevBlockRect == null) {
|
||||||
|
return sizedBox;
|
||||||
|
}
|
||||||
|
return Positioned.fromRect(
|
||||||
|
rect: prevBlockRect!,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.blockColor,
|
||||||
|
borderRadius: BorderRadius.circular(0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// show the cursor when the selection is collapsed
|
||||||
|
else if (selection.isCollapsed) {
|
||||||
|
if (!widget.supportTypes.contains(BlockSelectionType.cursor) ||
|
||||||
|
prevCursorRect == null) {
|
||||||
|
return sizedBox;
|
||||||
|
}
|
||||||
|
final cursor = Cursor(
|
||||||
|
key: cursorKey,
|
||||||
|
rect: prevCursorRect!,
|
||||||
|
shouldBlink: widget.delegate.shouldCursorBlink,
|
||||||
|
cursorStyle: widget.delegate.cursorStyle,
|
||||||
|
color: widget.cursorColor,
|
||||||
|
);
|
||||||
|
// force to show the cursor
|
||||||
|
cursorKey.currentState?.unwrapOrNull<CursorState>()?.show();
|
||||||
|
return cursor;
|
||||||
|
} else {
|
||||||
|
// show the selection area when the selection is not collapsed
|
||||||
|
if (!widget.supportTypes.contains(BlockSelectionType.selection) ||
|
||||||
|
prevSelectionRects == null ||
|
||||||
|
prevSelectionRects!.isEmpty) {
|
||||||
|
return sizedBox;
|
||||||
|
}
|
||||||
|
return SelectionAreaPaint(
|
||||||
|
rects: prevSelectionRects!,
|
||||||
|
selectionColor: widget.selectionColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
child: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateSelectionIfNeeded() {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = widget.listenable.value?.normalized;
|
||||||
|
final path = widget.node.path;
|
||||||
|
|
||||||
|
// the current path is in the selection
|
||||||
|
if (selection != null && path.inSelection(selection)) {
|
||||||
|
if (widget.supportTypes.contains(BlockSelectionType.block)) {
|
||||||
|
if (context.read<EditorState>().selectionType != SelectionType.block ||
|
||||||
|
!path.equals(selection.start.path)) {
|
||||||
|
if (prevBlockRect != null) {
|
||||||
|
setState(() {
|
||||||
|
prevBlockRect = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final rect = widget.delegate.getBlockRect();
|
||||||
|
if (prevBlockRect != rect) {
|
||||||
|
setState(() {
|
||||||
|
prevBlockRect = rect;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (widget.supportTypes.contains(BlockSelectionType.cursor) &&
|
||||||
|
selection.isCollapsed) {
|
||||||
|
final rect = widget.delegate.getCursorRectInPosition(selection.start);
|
||||||
|
if (rect != prevCursorRect) {
|
||||||
|
setState(() {
|
||||||
|
prevCursorRect = rect;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (widget.supportTypes.contains(BlockSelectionType.selection)) {
|
||||||
|
final rects = widget.delegate.getRectsInSelection(selection);
|
||||||
|
if (!_deepEqual(rects, prevSelectionRects)) {
|
||||||
|
setState(() {
|
||||||
|
prevSelectionRects = rects;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
_updateSelectionIfNeeded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class BlockSelectionContainer extends StatelessWidget {
|
||||||
|
const BlockSelectionContainer({
|
||||||
|
super.key,
|
||||||
|
required this.node,
|
||||||
|
required this.delegate,
|
||||||
|
required this.listenable,
|
||||||
|
this.cursorColor = Colors.black,
|
||||||
|
this.selectionColor = Colors.blue,
|
||||||
|
this.blockColor = Colors.blue,
|
||||||
|
this.supportTypes = const [
|
||||||
|
BlockSelectionType.cursor,
|
||||||
|
BlockSelectionType.selection
|
||||||
|
],
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the cursor rect, selection rects or block rect from the delegate
|
||||||
|
final SelectableMixin delegate;
|
||||||
|
|
||||||
|
// get the selection from the listenable
|
||||||
|
final ValueListenable<Selection?> listenable;
|
||||||
|
|
||||||
|
// the color of the cursor
|
||||||
|
final Color cursorColor;
|
||||||
|
|
||||||
|
// the color of the selection
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
|
// the color of the background of the block
|
||||||
|
final Color blockColor;
|
||||||
|
|
||||||
|
// the node of the block
|
||||||
|
final Node node;
|
||||||
|
|
||||||
|
final List<BlockSelectionType> supportTypes;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
BlockSelectionArea(
|
||||||
|
node: node,
|
||||||
|
delegate: delegate,
|
||||||
|
listenable: listenable,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionColor: selectionColor,
|
||||||
|
blockColor: blockColor,
|
||||||
|
supportTypes: supportTypes,
|
||||||
|
),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AnimatedSelectionAreaPaint extends StatefulWidget {
|
||||||
|
const AnimatedSelectionAreaPaint({
|
||||||
|
super.key,
|
||||||
|
required this.rects,
|
||||||
|
this.withAnimation = false,
|
||||||
|
s,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Rect> rects;
|
||||||
|
final bool withAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimatedSelectionAreaPaint> createState() =>
|
||||||
|
_AnimatedSelectionAreaPaintState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedSelectionAreaPaintState extends State<AnimatedSelectionAreaPaint>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late Animation<double> animation;
|
||||||
|
late AnimationController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (widget.withAnimation) {
|
||||||
|
controller = AnimationController(
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
animation = Tween<double>(begin: 0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: controller,
|
||||||
|
curve: Curves.bounceInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.repeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget builder(double value) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: AnimatedSelectionAreaPainter(
|
||||||
|
colors: const [
|
||||||
|
Color.fromARGB(255, 65, 88, 208),
|
||||||
|
Color.fromARGB(255, 200, 80, 192),
|
||||||
|
Color.fromARGB(255, 255, 204, 112),
|
||||||
|
],
|
||||||
|
animation: value,
|
||||||
|
rects: widget.rects,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.withAnimation) {
|
||||||
|
return builder(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (BuildContext context, _) {
|
||||||
|
return builder(animation.value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (widget.withAnimation) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectionAreaPaint extends StatelessWidget {
|
||||||
|
const SelectionAreaPaint({
|
||||||
|
super.key,
|
||||||
|
required this.rects,
|
||||||
|
required this.selectionColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Rect> rects;
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: SelectionAreaPainter(
|
||||||
|
rects: rects,
|
||||||
|
selectionColor: selectionColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectionAreaPainter extends CustomPainter {
|
||||||
|
SelectionAreaPainter({
|
||||||
|
required this.rects,
|
||||||
|
required this.selectionColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Rect> rects;
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = selectionColor
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
for (final rect in rects) {
|
||||||
|
canvas.drawRect(rect, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(SelectionAreaPainter oldDelegate) {
|
||||||
|
return selectionColor != oldDelegate.selectionColor ||
|
||||||
|
!const DeepCollectionEquality().equals(rects, oldDelegate.rects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimatedSelectionAreaPainter extends CustomPainter {
|
||||||
|
const AnimatedSelectionAreaPainter({
|
||||||
|
required this.rects,
|
||||||
|
required this.colors,
|
||||||
|
required this.animation,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Rect> rects;
|
||||||
|
final List<Color> colors;
|
||||||
|
final double animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
for (final rect in rects) {
|
||||||
|
final paint = Paint()
|
||||||
|
..shader = LinearGradient(
|
||||||
|
colors: colors,
|
||||||
|
transform: GradientRotation(animation * 2 * pi),
|
||||||
|
).createShader(rect)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
canvas.drawRect(rect, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant AnimatedSelectionAreaPainter oldDelegate) {
|
||||||
|
return animation != oldDelegate.animation ||
|
||||||
|
!const DeepCollectionEquality().equals(colors, oldDelegate.colors) ||
|
||||||
|
!const DeepCollectionEquality().equals(rects, oldDelegate.rects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,9 @@ export 'base_component/indent_command.dart';
|
||||||
export 'base_component/insert_newline_in_type_command.dart';
|
export 'base_component/insert_newline_in_type_command.dart';
|
||||||
export 'base_component/markdown_format_helper.dart';
|
export 'base_component/markdown_format_helper.dart';
|
||||||
export 'base_component/outdent_command.dart';
|
export 'base_component/outdent_command.dart';
|
||||||
|
// selection
|
||||||
|
export 'base_component/selection/block_selection_area.dart';
|
||||||
|
export 'base_component/selection/block_selection_container.dart';
|
||||||
export 'base_component/text_direction_mixin.dart';
|
export 'base_component/text_direction_mixin.dart';
|
||||||
export 'base_component/text_style_configuration.dart';
|
export 'base_component/text_style_configuration.dart';
|
||||||
export 'base_component/widget/full_screen_overlay_entry.dart';
|
export 'base_component/widget/full_screen_overlay_entry.dart';
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ class _BulletedListBlockComponentWidgetState
|
||||||
Flexible(
|
Flexible(
|
||||||
child: AppFlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
|
delegate: this,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
textAlign: alignment?.toTextAlign,
|
textAlign: alignment?.toTextAlign,
|
||||||
|
|
@ -146,6 +147,8 @@ class _BulletedListBlockComponentWidgetState
|
||||||
placeholderTextStyle,
|
placeholderTextStyle,
|
||||||
),
|
),
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -158,6 +161,17 @@ class _BulletedListBlockComponentWidgetState
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class DividerBlockKeys {
|
class DividerBlockKeys {
|
||||||
const DividerBlockKeys._();
|
const DividerBlockKeys._();
|
||||||
|
|
@ -77,7 +78,7 @@ class _DividerBlockComponentWidgetState
|
||||||
Node get node => widget.node;
|
Node get node => widget.node;
|
||||||
|
|
||||||
final dividerKey = GlobalKey();
|
final dividerKey = GlobalKey();
|
||||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -96,6 +97,19 @@ class _DividerBlockComponentWidgetState
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final editorState = context.read<EditorState>();
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
@ -104,7 +118,15 @@ class _DividerBlockComponentWidgetState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return child;
|
final selectionNotifier = editorState.selectionNotifier;
|
||||||
|
return BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: selectionNotifier,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -128,22 +150,38 @@ class _DividerBlockComponentWidgetState
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect? getCursorRectInPosition(Position position) {
|
Rect? getCursorRectInPosition(
|
||||||
final size = _renderBox.size;
|
Position position, {
|
||||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
|
if (_renderBox == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getRectsInSelection(
|
||||||
|
Selection.collapsed(position),
|
||||||
|
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||||
|
).firstOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(
|
||||||
|
Selection selection, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
|
if (_renderBox == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final parentBox = context.findRenderObject();
|
final parentBox = context.findRenderObject();
|
||||||
final dividerBox = dividerKey.currentContext?.findRenderObject();
|
final dividerBox = dividerKey.currentContext?.findRenderObject();
|
||||||
if (parentBox is RenderBox && dividerBox is RenderBox) {
|
if (parentBox is RenderBox && dividerBox is RenderBox) {
|
||||||
return [
|
return [
|
||||||
dividerBox.localToGlobal(Offset.zero, ancestor: parentBox) &
|
(shiftWithBaseOffset
|
||||||
|
? dividerBox.localToGlobal(Offset.zero, ancestor: parentBox)
|
||||||
|
: Offset.zero) &
|
||||||
dividerBox.size
|
dividerBox.size
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [Offset.zero & _renderBox.size];
|
return [Offset.zero & _renderBox!.size];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -154,7 +192,11 @@ class _DividerBlockComponentWidgetState
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
Offset localToGlobal(
|
||||||
|
Offset offset, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) =>
|
||||||
|
_renderBox!.localToGlobal(offset);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextDirection textDirection() {
|
TextDirection textDirection() {
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ class _HeadingBlockComponentWidgetState
|
||||||
Flexible(
|
Flexible(
|
||||||
child: AppFlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
|
delegate: this,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
textAlign: alignment?.toTextAlign,
|
textAlign: alignment?.toTextAlign,
|
||||||
|
|
@ -158,6 +159,8 @@ class _HeadingBlockComponentWidgetState
|
||||||
defaultTextStyle(level),
|
defaultTextStyle(level),
|
||||||
),
|
),
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -170,6 +173,17 @@ class _HeadingBlockComponentWidgetState
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
Node get node => widget.node;
|
Node get node => widget.node;
|
||||||
|
|
||||||
final imageKey = GlobalKey();
|
final imageKey = GlobalKey();
|
||||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
|
||||||
|
|
||||||
late final editorState = Provider.of<EditorState>(context, listen: false);
|
late final editorState = Provider.of<EditorState>(context, listen: false);
|
||||||
|
|
||||||
|
|
@ -163,6 +163,17 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
@ -186,7 +197,14 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
child!,
|
BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
if (value) widget.menuBuilder!(widget.node, this),
|
if (value) widget.menuBuilder!(widget.node, this),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -220,13 +238,25 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect? getCursorRectInPosition(Position position) {
|
Rect? getCursorRectInPosition(
|
||||||
final size = _renderBox.size;
|
Position position, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
|
if (_renderBox == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final size = _renderBox!.size;
|
||||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(
|
||||||
|
Selection selection, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
|
if (_renderBox == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final parentBox = context.findRenderObject();
|
final parentBox = context.findRenderObject();
|
||||||
final dividerBox = imageKey.currentContext?.findRenderObject();
|
final dividerBox = imageKey.currentContext?.findRenderObject();
|
||||||
if (parentBox is RenderBox && dividerBox is RenderBox) {
|
if (parentBox is RenderBox && dividerBox is RenderBox) {
|
||||||
|
|
@ -235,7 +265,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
dividerBox.size
|
dividerBox.size
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [Offset.zero & _renderBox.size];
|
return [Offset.zero & _renderBox!.size];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -246,7 +276,11 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
Offset localToGlobal(
|
||||||
|
Offset offset, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) =>
|
||||||
|
_renderBox!.localToGlobal(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AlignmentExtension on Alignment {
|
extension AlignmentExtension on Alignment {
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ class _NumberedListBlockComponentWidgetState
|
||||||
Flexible(
|
Flexible(
|
||||||
child: AppFlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
|
delegate: this,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
textAlign: alignment?.toTextAlign,
|
textAlign: alignment?.toTextAlign,
|
||||||
|
|
@ -152,6 +153,8 @@ class _NumberedListBlockComponentWidgetState
|
||||||
placeholderTextStyle,
|
placeholderTextStyle,
|
||||||
),
|
),
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -164,6 +167,17 @@ class _NumberedListBlockComponentWidgetState
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
||||||
Flexible(
|
Flexible(
|
||||||
child: AppFlowyRichText(
|
child: AppFlowyRichText(
|
||||||
key: forwardKey,
|
key: forwardKey,
|
||||||
|
delegate: this,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
textAlign: alignment?.toTextAlign,
|
textAlign: alignment?.toTextAlign,
|
||||||
|
|
@ -143,6 +144,8 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
||||||
placeholderTextStyle,
|
placeholderTextStyle,
|
||||||
),
|
),
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -156,6 +159,17 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||||
|
|
@ -6,6 +7,7 @@ import 'package:appflowy_editor/src/core/document/path.dart';
|
||||||
import 'package:appflowy_editor/src/core/document/text_delta.dart';
|
import 'package:appflowy_editor/src/core/document/text_delta.dart';
|
||||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/block_component/base_component/selection/block_selection_container.dart';
|
||||||
import 'package:appflowy_editor/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart';
|
import 'package:appflowy_editor/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart';
|
||||||
import 'package:appflowy_editor/src/editor/util/color_util.dart';
|
import 'package:appflowy_editor/src/editor/util/color_util.dart';
|
||||||
import 'package:appflowy_editor/src/editor_state.dart';
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
|
|
@ -28,7 +30,7 @@ class AppFlowyRichText extends StatefulWidget {
|
||||||
const AppFlowyRichText({
|
const AppFlowyRichText({
|
||||||
super.key,
|
super.key,
|
||||||
this.cursorHeight,
|
this.cursorHeight,
|
||||||
this.cursorWidth = 1.5,
|
this.cursorWidth = 2.0,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.textSpanDecorator,
|
this.textSpanDecorator,
|
||||||
this.placeholderText = ' ',
|
this.placeholderText = ' ',
|
||||||
|
|
@ -36,6 +38,9 @@ class AppFlowyRichText extends StatefulWidget {
|
||||||
this.textDirection = TextDirection.ltr,
|
this.textDirection = TextDirection.ltr,
|
||||||
this.textSpanDecoratorForCustomAttributes,
|
this.textSpanDecoratorForCustomAttributes,
|
||||||
this.textAlign,
|
this.textAlign,
|
||||||
|
this.cursorColor = const Color.fromARGB(255, 0, 0, 0),
|
||||||
|
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
||||||
|
required this.delegate,
|
||||||
required this.node,
|
required this.node,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
});
|
});
|
||||||
|
|
@ -68,6 +73,9 @@ class AppFlowyRichText extends StatefulWidget {
|
||||||
|
|
||||||
final TextAlign? textAlign;
|
final TextAlign? textAlign;
|
||||||
|
|
||||||
|
// get the cursor rect, selection rects or block rect from the delegate
|
||||||
|
final SelectableMixin delegate;
|
||||||
|
|
||||||
/// customize the text span for custom attributes
|
/// customize the text span for custom attributes
|
||||||
///
|
///
|
||||||
/// You can use this to customize the text span for custom attributes
|
/// You can use this to customize the text span for custom attributes
|
||||||
|
|
@ -75,6 +83,9 @@ class AppFlowyRichText extends StatefulWidget {
|
||||||
final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes;
|
final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
final Color cursorColor;
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppFlowyRichText> createState() => _AppFlowyRichTextState();
|
State<AppFlowyRichText> createState() => _AppFlowyRichTextState();
|
||||||
}
|
}
|
||||||
|
|
@ -84,8 +95,8 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
final textKey = GlobalKey();
|
final textKey = GlobalKey();
|
||||||
final placeholderTextKey = GlobalKey();
|
final placeholderTextKey = GlobalKey();
|
||||||
|
|
||||||
RenderParagraph get _renderParagraph =>
|
RenderParagraph? get _renderParagraph =>
|
||||||
textKey.currentContext?.findRenderObject() as RenderParagraph;
|
textKey.currentContext?.findRenderObject() as RenderParagraph?;
|
||||||
|
|
||||||
RenderParagraph? get _placeholderRenderParagraph =>
|
RenderParagraph? get _placeholderRenderParagraph =>
|
||||||
placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph?;
|
placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph?;
|
||||||
|
|
@ -96,7 +107,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
final child = MouseRegion(
|
||||||
cursor: SystemMouseCursors.text,
|
cursor: SystemMouseCursors.text,
|
||||||
child: widget.node.delta?.toPlainText().isEmpty ?? true
|
child: widget.node.delta?.toPlainText().isEmpty ?? true
|
||||||
? Stack(
|
? Stack(
|
||||||
|
|
@ -107,6 +118,15 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
)
|
)
|
||||||
: _buildRichText(context),
|
: _buildRichText(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return BlockSelectionContainer(
|
||||||
|
delegate: widget.delegate,
|
||||||
|
listenable: widget.editorState.selectionNotifier,
|
||||||
|
node: widget.node,
|
||||||
|
cursorColor: widget.cursorColor,
|
||||||
|
selectionColor: widget.selectionColor,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -124,12 +144,15 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect? getCursorRectInPosition(Position position) {
|
Rect? getCursorRectInPosition(
|
||||||
|
Position position, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
final textPosition = TextPosition(offset: position.offset);
|
final textPosition = TextPosition(offset: position.offset);
|
||||||
|
var cursorHeight = _renderParagraph?.getFullHeightForCaret(textPosition);
|
||||||
var cursorHeight = _renderParagraph.getFullHeightForCaret(textPosition);
|
|
||||||
var cursorOffset =
|
var cursorOffset =
|
||||||
_renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
|
_renderParagraph?.getOffsetForCaret(textPosition, Rect.zero) ??
|
||||||
|
Offset.zero;
|
||||||
if (cursorHeight == null) {
|
if (cursorHeight == null) {
|
||||||
cursorHeight =
|
cursorHeight =
|
||||||
_placeholderRenderParagraph?.getFullHeightForCaret(textPosition);
|
_placeholderRenderParagraph?.getFullHeightForCaret(textPosition);
|
||||||
|
|
@ -155,7 +178,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
cursorHeight = widget.cursorHeight;
|
cursorHeight = widget.cursorHeight;
|
||||||
}
|
}
|
||||||
final rect = Rect.fromLTWH(
|
final rect = Rect.fromLTWH(
|
||||||
cursorOffset.dx - (widget.cursorWidth / 2.0),
|
max(0, cursorOffset.dx - (widget.cursorWidth / 2.0)),
|
||||||
cursorOffset.dy,
|
cursorOffset.dy,
|
||||||
widget.cursorWidth,
|
widget.cursorWidth,
|
||||||
cursorHeight ?? 16.0,
|
cursorHeight ?? 16.0,
|
||||||
|
|
@ -165,16 +188,19 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Position getPositionInOffset(Offset start) {
|
Position getPositionInOffset(Offset start) {
|
||||||
final offset = _renderParagraph.globalToLocal(start);
|
final offset = _renderParagraph?.globalToLocal(start) ?? Offset.zero;
|
||||||
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
|
final baseOffset =
|
||||||
|
_renderParagraph?.getPositionForOffset(offset).offset ?? -1;
|
||||||
return Position(path: widget.node.path, offset: baseOffset);
|
return Position(path: widget.node.path, offset: baseOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Selection? getWordBoundaryInOffset(Offset offset) {
|
Selection? getWordBoundaryInOffset(Offset offset) {
|
||||||
final localOffset = _renderParagraph.globalToLocal(offset);
|
final localOffset = _renderParagraph?.globalToLocal(offset) ?? Offset.zero;
|
||||||
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
|
final textPosition = _renderParagraph?.getPositionForOffset(localOffset) ??
|
||||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
const TextPosition(offset: 0);
|
||||||
|
final textRange =
|
||||||
|
_renderParagraph?.getWordBoundary(textPosition) ?? TextRange.empty;
|
||||||
final start = Position(path: widget.node.path, offset: textRange.start);
|
final start = Position(path: widget.node.path, offset: textRange.start);
|
||||||
final end = Position(path: widget.node.path, offset: textRange.end);
|
final end = Position(path: widget.node.path, offset: textRange.end);
|
||||||
return Selection(start: start, end: end);
|
return Selection(start: start, end: end);
|
||||||
|
|
@ -183,37 +209,47 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
@override
|
@override
|
||||||
Selection? getWordBoundaryInPosition(Position position) {
|
Selection? getWordBoundaryInPosition(Position position) {
|
||||||
final textPosition = TextPosition(offset: position.offset);
|
final textPosition = TextPosition(offset: position.offset);
|
||||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
final textRange =
|
||||||
|
_renderParagraph?.getWordBoundary(textPosition) ?? TextRange.empty;
|
||||||
final start = Position(path: widget.node.path, offset: textRange.start);
|
final start = Position(path: widget.node.path, offset: textRange.start);
|
||||||
final end = Position(path: widget.node.path, offset: textRange.end);
|
final end = Position(path: widget.node.path, offset: textRange.end);
|
||||||
return Selection(start: start, end: end);
|
return Selection(start: start, end: end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(
|
||||||
|
Selection selection, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
final textSelection = textSelectionFromEditorSelection(selection);
|
final textSelection = textSelectionFromEditorSelection(selection);
|
||||||
if (textSelection == null) {
|
if (textSelection == null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final rects = _renderParagraph
|
final rects = _renderParagraph
|
||||||
.getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
|
?.getBoxesForSelection(
|
||||||
|
textSelection,
|
||||||
|
boxHeightStyle: BoxHeightStyle.max,
|
||||||
|
)
|
||||||
.map((box) => box.toRect())
|
.map((box) => box.toRect())
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
if (rects.isEmpty) {
|
|
||||||
|
if (rects == null || rects.isEmpty) {
|
||||||
// If the rich text widget does not contain any text,
|
// If the rich text widget does not contain any text,
|
||||||
// there will be no selection boxes,
|
// there will be no selection boxes,
|
||||||
// so we need to return to the default selection.
|
// so we need to return to the default selection.
|
||||||
return [Rect.fromLTWH(0, 0, 0, _renderParagraph.size.height)];
|
return [Rect.fromLTWH(0, 0, 0, _renderParagraph?.size.height ?? 0)];
|
||||||
}
|
}
|
||||||
return rects;
|
return rects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Selection getSelectionInRange(Offset start, Offset end) {
|
Selection getSelectionInRange(Offset start, Offset end) {
|
||||||
final localStart = _renderParagraph.globalToLocal(start);
|
final localStart = _renderParagraph?.globalToLocal(start) ?? Offset.zero;
|
||||||
final localEnd = _renderParagraph.globalToLocal(end);
|
final localEnd = _renderParagraph?.globalToLocal(end) ?? Offset.zero;
|
||||||
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
|
final baseOffset =
|
||||||
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
|
_renderParagraph?.getPositionForOffset(localStart).offset ?? -1;
|
||||||
|
final extentOffset =
|
||||||
|
_renderParagraph?.getPositionForOffset(localEnd).offset ?? -1;
|
||||||
return Selection.single(
|
return Selection.single(
|
||||||
path: widget.node.path,
|
path: widget.node.path,
|
||||||
startOffset: baseOffset,
|
startOffset: baseOffset,
|
||||||
|
|
@ -222,8 +258,11 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset localToGlobal(Offset offset) {
|
Offset localToGlobal(
|
||||||
return _renderParagraph.localToGlobal(offset);
|
Offset offset, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
|
return _renderParagraph?.localToGlobal(offset) ?? Offset.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,15 @@ mixin DefaultSelectableMixin {
|
||||||
SelectableMixin<StatefulWidget> get forward =>
|
SelectableMixin<StatefulWidget> get forward =>
|
||||||
forwardKey.currentState as SelectableMixin;
|
forwardKey.currentState as SelectableMixin;
|
||||||
|
|
||||||
Offset get baseOffset {
|
Offset baseOffset({
|
||||||
final parentBox = containerKey.currentContext?.findRenderObject();
|
bool shiftWithBaseOffset = false,
|
||||||
final childBox = forwardKey.currentContext?.findRenderObject();
|
}) {
|
||||||
if (parentBox is RenderBox && childBox is RenderBox) {
|
if (shiftWithBaseOffset) {
|
||||||
return childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
final parentBox = containerKey.currentContext?.findRenderObject();
|
||||||
|
final childBox = forwardKey.currentContext?.findRenderObject();
|
||||||
|
if (parentBox is RenderBox && childBox is RenderBox) {
|
||||||
|
return childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Offset.zero;
|
return Offset.zero;
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +28,7 @@ mixin DefaultSelectableMixin {
|
||||||
if (parentBox is RenderBox && childBox is RenderBox) {
|
if (parentBox is RenderBox && childBox is RenderBox) {
|
||||||
final offset = childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
final offset = childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
||||||
final size = parentBox.size;
|
final size = parentBox.size;
|
||||||
return offset & (size - offset as Size);
|
return Offset.zero & (size - offset as Size);
|
||||||
}
|
}
|
||||||
return Rect.zero;
|
return Rect.zero;
|
||||||
}
|
}
|
||||||
|
|
@ -32,19 +36,42 @@ mixin DefaultSelectableMixin {
|
||||||
Position getPositionInOffset(Offset start) =>
|
Position getPositionInOffset(Offset start) =>
|
||||||
forward.getPositionInOffset(start);
|
forward.getPositionInOffset(start);
|
||||||
|
|
||||||
Rect? getCursorRectInPosition(Position position) =>
|
Rect? getCursorRectInPosition(
|
||||||
forward.getCursorRectInPosition(position)?.shift(baseOffset);
|
Position position, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) =>
|
||||||
|
forward.getCursorRectInPosition(position)?.shift(
|
||||||
|
baseOffset(
|
||||||
|
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
List<Rect> getRectsInSelection(Selection selection) => forward
|
List<Rect> getRectsInSelection(
|
||||||
.getRectsInSelection(selection)
|
Selection selection, {
|
||||||
.map((rect) => rect.shift(baseOffset))
|
bool shiftWithBaseOffset = false,
|
||||||
.toList(growable: false);
|
}) =>
|
||||||
|
forward
|
||||||
|
.getRectsInSelection(selection)
|
||||||
|
.map(
|
||||||
|
(rect) => rect.shift(
|
||||||
|
baseOffset(
|
||||||
|
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
Selection getSelectionInRange(Offset start, Offset end) =>
|
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||||
forward.getSelectionInRange(start, end);
|
forward.getSelectionInRange(start, end);
|
||||||
|
|
||||||
Offset localToGlobal(Offset offset) =>
|
Offset localToGlobal(
|
||||||
forward.localToGlobal(offset) - baseOffset;
|
Offset offset, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) =>
|
||||||
|
forward.localToGlobal(offset) -
|
||||||
|
baseOffset(
|
||||||
|
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||||
|
);
|
||||||
|
|
||||||
Selection? getWordBoundaryInOffset(Offset offset) =>
|
Selection? getWordBoundaryInOffset(Offset offset) =>
|
||||||
forward.getWordBoundaryInOffset(offset);
|
forward.getWordBoundaryInOffset(offset);
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,17 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = BlockSelectionContainer(
|
||||||
|
node: node,
|
||||||
|
delegate: this,
|
||||||
|
listenable: editorState.selectionNotifier,
|
||||||
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
|
supportTypes: const [
|
||||||
|
BlockSelectionType.block,
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.showActions && widget.actionBuilder != null) {
|
if (widget.showActions && widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
@ -188,12 +199,18 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
||||||
Position getPositionInOffset(Offset start) => end();
|
Position getPositionInOffset(Offset start) => end();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(
|
||||||
|
Selection selection, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
final parentBox = context.findRenderObject();
|
final parentBox = context.findRenderObject();
|
||||||
final tableBox = tableKey.currentContext?.findRenderObject();
|
final tableBox = tableKey.currentContext?.findRenderObject();
|
||||||
if (parentBox is RenderBox && tableBox is RenderBox) {
|
if (parentBox is RenderBox && tableBox is RenderBox) {
|
||||||
return [
|
return [
|
||||||
tableBox.localToGlobal(Offset.zero, ancestor: parentBox) & tableBox.size
|
(shiftWithBaseOffset
|
||||||
|
? tableBox.localToGlobal(Offset.zero, ancestor: parentBox)
|
||||||
|
: Offset.zero) &
|
||||||
|
tableBox.size
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [Offset.zero & _renderBox.size];
|
return [Offset.zero & _renderBox.size];
|
||||||
|
|
@ -213,7 +230,11 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
||||||
CursorStyle get cursorStyle => CursorStyle.cover;
|
CursorStyle get cursorStyle => CursorStyle.cover;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
Offset localToGlobal(
|
||||||
|
Offset offset, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) =>
|
||||||
|
_renderBox.localToGlobal(offset);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect getBlockRect() {
|
Rect getBlockRect() {
|
||||||
|
|
@ -221,7 +242,10 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect? getCursorRectInPosition(Position position) {
|
Rect? getCursorRectInPosition(
|
||||||
|
Position position, {
|
||||||
|
bool shiftWithBaseOffset = false,
|
||||||
|
}) {
|
||||||
final size = _renderBox.size;
|
final size = _renderBox.size;
|
||||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue