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:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||
|
|
@ -51,6 +52,7 @@ class _HomePageState extends State<HomePage> {
|
|||
super.initState();
|
||||
|
||||
_jsonString = rootBundle.loadString('assets/example.json');
|
||||
|
||||
_widgetBuilder = (context) => Editor(
|
||||
jsonString: _jsonString,
|
||||
onEditorStateChange: (editorState) {
|
||||
|
|
@ -109,6 +111,20 @@ class _HomePageState extends State<HomePage> {
|
|||
final jsonString = rootBundle.loadString('assets/example.json');
|
||||
_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 {
|
||||
final htmlString =
|
||||
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:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ class Editor extends StatelessWidget {
|
|||
onEditorStateChange(editorState);
|
||||
}
|
||||
});
|
||||
final scrollController = ScrollController();
|
||||
final editorScrollController = EditorScrollController(
|
||||
editorState: editorState,
|
||||
shrinkWrap: false,
|
||||
);
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
return FloatingToolbar(
|
||||
items: [
|
||||
|
|
@ -54,11 +57,11 @@ class Editor extends StatelessWidget {
|
|||
...alignmentItems,
|
||||
],
|
||||
editorState: editorState,
|
||||
scrollController: scrollController,
|
||||
editorScrollController: editorScrollController,
|
||||
child: _buildDesktopEditor(
|
||||
context,
|
||||
editorState,
|
||||
scrollController,
|
||||
editorScrollController,
|
||||
),
|
||||
);
|
||||
} else if (PlatformExtension.isMobile) {
|
||||
|
|
@ -68,7 +71,7 @@ class Editor extends StatelessWidget {
|
|||
child: _buildMobileEditor(
|
||||
context,
|
||||
editorState,
|
||||
scrollController,
|
||||
editorScrollController,
|
||||
),
|
||||
),
|
||||
MobileToolbar(
|
||||
|
|
@ -99,19 +102,19 @@ class Editor extends StatelessWidget {
|
|||
Widget _buildMobileEditor(
|
||||
BuildContext context,
|
||||
EditorState editorState,
|
||||
ScrollController? scrollController,
|
||||
EditorScrollController? editorScrollController,
|
||||
) {
|
||||
return AppFlowyEditor(
|
||||
editorStyle: const EditorStyle.mobile(),
|
||||
editorState: editorState,
|
||||
scrollController: scrollController,
|
||||
editorScrollController: editorScrollController,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopEditor(
|
||||
BuildContext context,
|
||||
EditorState editorState,
|
||||
ScrollController? scrollController,
|
||||
EditorScrollController? editorScrollController,
|
||||
) {
|
||||
final customBlockComponentBuilders = {
|
||||
...standardBlockComponentBuilderMap,
|
||||
|
|
@ -127,7 +130,8 @@ class Editor extends StatelessWidget {
|
|||
};
|
||||
return AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
scrollController: scrollController,
|
||||
shrinkWrap: true,
|
||||
editorScrollController: editorScrollController,
|
||||
blockComponentBuilders: customBlockComponentBuilders,
|
||||
commandShortcutEvents: [
|
||||
customToggleHighlightCommand(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies:
|
|||
universal_html: ^2.0.8
|
||||
highlight: ^0.7.0
|
||||
http: ^0.13.5
|
||||
show_fps: ^1.0.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'dart:collection';
|
|||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
import 'package:nanoid/non_secure.dart';
|
||||
|
||||
abstract class 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"
|
||||
_attributes = attributes,
|
||||
id = id ?? nanoid(10) {
|
||||
for (final child in this.children) {
|
||||
id = id ?? nanoid(6) {
|
||||
for (final child in children) {
|
||||
child.parent = this;
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,12 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
|
||||
/// The children of the node.
|
||||
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.
|
||||
Attributes _attributes;
|
||||
|
|
@ -141,7 +146,9 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
|
||||
entry.parent = this;
|
||||
|
||||
if (children.isEmpty) {
|
||||
_cacheChildren = null;
|
||||
|
||||
if (_children.isEmpty) {
|
||||
_children.add(entry);
|
||||
notifyListeners();
|
||||
return;
|
||||
|
|
@ -164,6 +171,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
entry.parent = parent;
|
||||
super.insertAfter(entry);
|
||||
|
||||
parent?._cacheChildren = null;
|
||||
|
||||
// Notifies the new node.
|
||||
parent?.notifyListeners();
|
||||
}
|
||||
|
|
@ -173,6 +182,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
entry.parent = parent;
|
||||
super.insertBefore(entry);
|
||||
|
||||
parent?._cacheChildren = null;
|
||||
|
||||
// Notifies the new node.
|
||||
parent?.notifyListeners();
|
||||
}
|
||||
|
|
@ -185,6 +196,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
Log.editor.debug('delete Node $this from path $path');
|
||||
super.unlink();
|
||||
|
||||
parent?._cacheChildren = null;
|
||||
|
||||
parent?.notifyListeners();
|
||||
parent = null;
|
||||
}
|
||||
|
|
@ -230,12 +243,12 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
}) {
|
||||
final node = Node(
|
||||
type: type ?? this.type,
|
||||
id: nanoid(10),
|
||||
id: nanoid(6),
|
||||
attributes: attributes ?? {...this.attributes},
|
||||
children: children ?? [],
|
||||
);
|
||||
if (children == null && this.children.isNotEmpty) {
|
||||
for (final child in this.children) {
|
||||
if (children == null && _children.isNotEmpty) {
|
||||
for (final child in _children) {
|
||||
node._children.add(
|
||||
child.copyWith()..parent = node,
|
||||
);
|
||||
|
|
@ -246,11 +259,12 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
}
|
||||
|
||||
Path _computePath([Path previous = const []]) {
|
||||
final parent = this.parent;
|
||||
if (parent == null) {
|
||||
return previous;
|
||||
}
|
||||
final index = parent!.children.toList().indexOf(this);
|
||||
return parent!._computePath([index, ...previous]);
|
||||
final index = parent.children.indexOf(this);
|
||||
return parent._computePath([index, ...previous]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef Path = List<int>;
|
||||
|
|
@ -105,4 +106,10 @@ extension PathExtensions on Path {
|
|||
}
|
||||
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/markdown_format_helper.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_style_configuration.dart';
|
||||
export 'base_component/widget/full_screen_overlay_entry.dart';
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ class _BulletedListBlockComponentWidgetState
|
|||
Flexible(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
textAlign: alignment?.toTextAlign,
|
||||
|
|
@ -146,6 +147,8 @@ class _BulletedListBlockComponentWidgetState
|
|||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -158,6 +161,17 @@ class _BulletedListBlockComponentWidgetState
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DividerBlockKeys {
|
||||
const DividerBlockKeys._();
|
||||
|
|
@ -77,7 +78,7 @@ class _DividerBlockComponentWidgetState
|
|||
Node get node => widget.node;
|
||||
|
||||
final dividerKey = GlobalKey();
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -96,6 +97,19 @@ class _DividerBlockComponentWidgetState
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
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
|
||||
|
|
@ -128,22 +150,38 @@ class _DividerBlockComponentWidgetState
|
|||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
final size = _renderBox.size;
|
||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (_renderBox == null) {
|
||||
return null;
|
||||
}
|
||||
return getRectsInSelection(
|
||||
Selection.collapsed(position),
|
||||
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||
).firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (_renderBox == null) {
|
||||
return [];
|
||||
}
|
||||
final parentBox = context.findRenderObject();
|
||||
final dividerBox = dividerKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && dividerBox is RenderBox) {
|
||||
return [
|
||||
dividerBox.localToGlobal(Offset.zero, ancestor: parentBox) &
|
||||
(shiftWithBaseOffset
|
||||
? dividerBox.localToGlobal(Offset.zero, ancestor: parentBox)
|
||||
: Offset.zero) &
|
||||
dividerBox.size
|
||||
];
|
||||
}
|
||||
return [Offset.zero & _renderBox.size];
|
||||
return [Offset.zero & _renderBox!.size];
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -154,7 +192,11 @@ class _DividerBlockComponentWidgetState
|
|||
);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||
Offset localToGlobal(
|
||||
Offset offset, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
_renderBox!.localToGlobal(offset);
|
||||
|
||||
@override
|
||||
TextDirection textDirection() {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ class _HeadingBlockComponentWidgetState
|
|||
Flexible(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
textAlign: alignment?.toTextAlign,
|
||||
|
|
@ -158,6 +159,8 @@ class _HeadingBlockComponentWidgetState
|
|||
defaultTextStyle(level),
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -170,6 +173,17 @@ class _HeadingBlockComponentWidgetState
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
Node get node => widget.node;
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -163,6 +163,17 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
|
|
@ -186,7 +197,14 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
builder: (context, value, child) {
|
||||
return Stack(
|
||||
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),
|
||||
],
|
||||
);
|
||||
|
|
@ -220,13 +238,25 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
final size = _renderBox.size;
|
||||
Rect? getCursorRectInPosition(
|
||||
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);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (_renderBox == null) {
|
||||
return [];
|
||||
}
|
||||
final parentBox = context.findRenderObject();
|
||||
final dividerBox = imageKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && dividerBox is RenderBox) {
|
||||
|
|
@ -235,7 +265,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
dividerBox.size
|
||||
];
|
||||
}
|
||||
return [Offset.zero & _renderBox.size];
|
||||
return [Offset.zero & _renderBox!.size];
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -246,7 +276,11 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||
Offset localToGlobal(
|
||||
Offset offset, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
_renderBox!.localToGlobal(offset);
|
||||
}
|
||||
|
||||
extension AlignmentExtension on Alignment {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ class _NumberedListBlockComponentWidgetState
|
|||
Flexible(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
textAlign: alignment?.toTextAlign,
|
||||
|
|
@ -152,6 +153,8 @@ class _NumberedListBlockComponentWidgetState
|
|||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -164,6 +167,17 @@ class _NumberedListBlockComponentWidgetState
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
Flexible(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
textAlign: alignment?.toTextAlign,
|
||||
|
|
@ -143,6 +144,8 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -156,6 +159,17 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
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/location/position.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/util/color_util.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
|
|
@ -28,7 +30,7 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
const AppFlowyRichText({
|
||||
super.key,
|
||||
this.cursorHeight,
|
||||
this.cursorWidth = 1.5,
|
||||
this.cursorWidth = 2.0,
|
||||
this.lineHeight,
|
||||
this.textSpanDecorator,
|
||||
this.placeholderText = ' ',
|
||||
|
|
@ -36,6 +38,9 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
this.textDirection = TextDirection.ltr,
|
||||
this.textSpanDecoratorForCustomAttributes,
|
||||
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.editorState,
|
||||
});
|
||||
|
|
@ -68,6 +73,9 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
|
||||
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
|
||||
///
|
||||
/// You can use this to customize the text span for custom attributes
|
||||
|
|
@ -75,6 +83,9 @@ class AppFlowyRichText extends StatefulWidget {
|
|||
final TextSpanDecoratorForAttribute? textSpanDecoratorForCustomAttributes;
|
||||
final TextDirection textDirection;
|
||||
|
||||
final Color cursorColor;
|
||||
final Color selectionColor;
|
||||
|
||||
@override
|
||||
State<AppFlowyRichText> createState() => _AppFlowyRichTextState();
|
||||
}
|
||||
|
|
@ -84,8 +95,8 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
final textKey = GlobalKey();
|
||||
final placeholderTextKey = GlobalKey();
|
||||
|
||||
RenderParagraph get _renderParagraph =>
|
||||
textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||
RenderParagraph? get _renderParagraph =>
|
||||
textKey.currentContext?.findRenderObject() as RenderParagraph?;
|
||||
|
||||
RenderParagraph? get _placeholderRenderParagraph =>
|
||||
placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph?;
|
||||
|
|
@ -96,7 +107,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
final child = MouseRegion(
|
||||
cursor: SystemMouseCursors.text,
|
||||
child: widget.node.delta?.toPlainText().isEmpty ?? true
|
||||
? Stack(
|
||||
|
|
@ -107,6 +118,15 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
)
|
||||
: _buildRichText(context),
|
||||
);
|
||||
|
||||
return BlockSelectionContainer(
|
||||
delegate: widget.delegate,
|
||||
listenable: widget.editorState.selectionNotifier,
|
||||
node: widget.node,
|
||||
cursorColor: widget.cursorColor,
|
||||
selectionColor: widget.selectionColor,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -124,12 +144,15 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final textPosition = TextPosition(offset: position.offset);
|
||||
|
||||
var cursorHeight = _renderParagraph.getFullHeightForCaret(textPosition);
|
||||
var cursorHeight = _renderParagraph?.getFullHeightForCaret(textPosition);
|
||||
var cursorOffset =
|
||||
_renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
|
||||
_renderParagraph?.getOffsetForCaret(textPosition, Rect.zero) ??
|
||||
Offset.zero;
|
||||
if (cursorHeight == null) {
|
||||
cursorHeight =
|
||||
_placeholderRenderParagraph?.getFullHeightForCaret(textPosition);
|
||||
|
|
@ -155,7 +178,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
cursorHeight = widget.cursorHeight;
|
||||
}
|
||||
final rect = Rect.fromLTWH(
|
||||
cursorOffset.dx - (widget.cursorWidth / 2.0),
|
||||
max(0, cursorOffset.dx - (widget.cursorWidth / 2.0)),
|
||||
cursorOffset.dy,
|
||||
widget.cursorWidth,
|
||||
cursorHeight ?? 16.0,
|
||||
|
|
@ -165,16 +188,19 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
final offset = _renderParagraph.globalToLocal(start);
|
||||
final baseOffset = _renderParagraph.getPositionForOffset(offset).offset;
|
||||
final offset = _renderParagraph?.globalToLocal(start) ?? Offset.zero;
|
||||
final baseOffset =
|
||||
_renderParagraph?.getPositionForOffset(offset).offset ?? -1;
|
||||
return Position(path: widget.node.path, offset: baseOffset);
|
||||
}
|
||||
|
||||
@override
|
||||
Selection? getWordBoundaryInOffset(Offset offset) {
|
||||
final localOffset = _renderParagraph.globalToLocal(offset);
|
||||
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
|
||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||
final localOffset = _renderParagraph?.globalToLocal(offset) ?? Offset.zero;
|
||||
final textPosition = _renderParagraph?.getPositionForOffset(localOffset) ??
|
||||
const TextPosition(offset: 0);
|
||||
final textRange =
|
||||
_renderParagraph?.getWordBoundary(textPosition) ?? TextRange.empty;
|
||||
final start = Position(path: widget.node.path, offset: textRange.start);
|
||||
final end = Position(path: widget.node.path, offset: textRange.end);
|
||||
return Selection(start: start, end: end);
|
||||
|
|
@ -183,37 +209,47 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
@override
|
||||
Selection? getWordBoundaryInPosition(Position position) {
|
||||
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 end = Position(path: widget.node.path, offset: textRange.end);
|
||||
return Selection(start: start, end: end);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final textSelection = textSelectionFromEditorSelection(selection);
|
||||
if (textSelection == null) {
|
||||
return [];
|
||||
}
|
||||
final rects = _renderParagraph
|
||||
.getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
|
||||
?.getBoxesForSelection(
|
||||
textSelection,
|
||||
boxHeightStyle: BoxHeightStyle.max,
|
||||
)
|
||||
.map((box) => box.toRect())
|
||||
.toList(growable: false);
|
||||
if (rects.isEmpty) {
|
||||
|
||||
if (rects == null || rects.isEmpty) {
|
||||
// If the rich text widget does not contain any text,
|
||||
// there will be no selection boxes,
|
||||
// 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;
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) {
|
||||
final localStart = _renderParagraph.globalToLocal(start);
|
||||
final localEnd = _renderParagraph.globalToLocal(end);
|
||||
final baseOffset = _renderParagraph.getPositionForOffset(localStart).offset;
|
||||
final extentOffset = _renderParagraph.getPositionForOffset(localEnd).offset;
|
||||
final localStart = _renderParagraph?.globalToLocal(start) ?? Offset.zero;
|
||||
final localEnd = _renderParagraph?.globalToLocal(end) ?? Offset.zero;
|
||||
final baseOffset =
|
||||
_renderParagraph?.getPositionForOffset(localStart).offset ?? -1;
|
||||
final extentOffset =
|
||||
_renderParagraph?.getPositionForOffset(localEnd).offset ?? -1;
|
||||
return Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: baseOffset,
|
||||
|
|
@ -222,8 +258,11 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
return _renderParagraph.localToGlobal(offset);
|
||||
Offset localToGlobal(
|
||||
Offset offset, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
return _renderParagraph?.localToGlobal(offset) ?? Offset.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ mixin DefaultSelectableMixin {
|
|||
SelectableMixin<StatefulWidget> get forward =>
|
||||
forwardKey.currentState as SelectableMixin;
|
||||
|
||||
Offset get baseOffset {
|
||||
final parentBox = containerKey.currentContext?.findRenderObject();
|
||||
final childBox = forwardKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && childBox is RenderBox) {
|
||||
return childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
||||
Offset baseOffset({
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (shiftWithBaseOffset) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -24,7 +28,7 @@ mixin DefaultSelectableMixin {
|
|||
if (parentBox is RenderBox && childBox is RenderBox) {
|
||||
final offset = childBox.localToGlobal(Offset.zero, ancestor: parentBox);
|
||||
final size = parentBox.size;
|
||||
return offset & (size - offset as Size);
|
||||
return Offset.zero & (size - offset as Size);
|
||||
}
|
||||
return Rect.zero;
|
||||
}
|
||||
|
|
@ -32,19 +36,42 @@ mixin DefaultSelectableMixin {
|
|||
Position getPositionInOffset(Offset start) =>
|
||||
forward.getPositionInOffset(start);
|
||||
|
||||
Rect? getCursorRectInPosition(Position position) =>
|
||||
forward.getCursorRectInPosition(position)?.shift(baseOffset);
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
forward.getCursorRectInPosition(position)?.shift(
|
||||
baseOffset(
|
||||
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||
),
|
||||
);
|
||||
|
||||
List<Rect> getRectsInSelection(Selection selection) => forward
|
||||
.getRectsInSelection(selection)
|
||||
.map((rect) => rect.shift(baseOffset))
|
||||
.toList(growable: false);
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
forward
|
||||
.getRectsInSelection(selection)
|
||||
.map(
|
||||
(rect) => rect.shift(
|
||||
baseOffset(
|
||||
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
|
||||
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||
forward.getSelectionInRange(start, end);
|
||||
|
||||
Offset localToGlobal(Offset offset) =>
|
||||
forward.localToGlobal(offset) - baseOffset;
|
||||
Offset localToGlobal(
|
||||
Offset offset, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
forward.localToGlobal(offset) -
|
||||
baseOffset(
|
||||
shiftWithBaseOffset: shiftWithBaseOffset,
|
||||
);
|
||||
|
||||
Selection? getWordBoundaryInOffset(Offset offset) =>
|
||||
forward.getWordBoundaryInOffset(offset);
|
||||
|
|
|
|||
|
|
@ -164,6 +164,17 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
|||
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) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
|
|
@ -188,12 +199,18 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
|||
Position getPositionInOffset(Offset start) => end();
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final parentBox = context.findRenderObject();
|
||||
final tableBox = tableKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && tableBox is RenderBox) {
|
||||
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];
|
||||
|
|
@ -213,7 +230,11 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
|||
CursorStyle get cursorStyle => CursorStyle.cover;
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||
Offset localToGlobal(
|
||||
Offset offset, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
_renderBox.localToGlobal(offset);
|
||||
|
||||
@override
|
||||
Rect getBlockRect() {
|
||||
|
|
@ -221,7 +242,10 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
|||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final size = _renderBox.size;
|
||||
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