feat: support remote selections (#753)
* chore: delete deprecated code * feat: support remote seleciton * feat: support selection flag
This commit is contained in:
parent
a571f2bc9d
commit
b927ec0685
20 changed files with 424 additions and 210 deletions
|
|
@ -5,6 +5,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:example/pages/collab_editor.dart';
|
||||
import 'package:example/pages/collab_selection_editor.dart';
|
||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||
import 'package:example/pages/editor.dart';
|
||||
import 'package:example/pages/editor_list.dart';
|
||||
|
|
@ -168,6 +169,14 @@ class _HomePageState extends State<HomePage> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
_buildListTile(context, 'Collab Selection', () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CollabSelectionEditor(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
_buildListTile(context, 'Custom Theme', () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
141
example/lib/pages/collab_selection_editor.dart
Normal file
141
example/lib/pages/collab_selection_editor.dart
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CollabSelectionEditor extends StatefulWidget {
|
||||
const CollabSelectionEditor({super.key});
|
||||
|
||||
@override
|
||||
State<CollabSelectionEditor> createState() => _CollabSelectionEditorState();
|
||||
}
|
||||
|
||||
class _CollabSelectionEditorState extends State<CollabSelectionEditor> {
|
||||
late final Future<String> future;
|
||||
|
||||
EditorState? editorState;
|
||||
|
||||
late final List<RemoteSelection> remoteSelections = [
|
||||
RemoteSelection(
|
||||
id: '1',
|
||||
selection: Selection(
|
||||
start: Position(path: [0], offset: 0),
|
||||
end: Position(path: [0], offset: 5),
|
||||
),
|
||||
selectionColor: Colors.red,
|
||||
cursorColor: Colors.red,
|
||||
builder: _buildSelectionFlag,
|
||||
),
|
||||
// collapsed selection
|
||||
RemoteSelection(
|
||||
id: '2',
|
||||
selection: Selection.collapsed(
|
||||
Position(path: [1], offset: 3),
|
||||
),
|
||||
selectionColor: Colors.yellow,
|
||||
cursorColor: Colors.yellow,
|
||||
builder: _buildSelectionFlag,
|
||||
),
|
||||
// multi-line selection
|
||||
RemoteSelection(
|
||||
id: '3',
|
||||
selection: Selection(
|
||||
start: Position(path: [2], offset: 0),
|
||||
end: Position(path: [3], offset: 5),
|
||||
),
|
||||
selectionColor: Colors.green,
|
||||
cursorColor: Colors.green,
|
||||
builder: _buildSelectionFlag,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
future = PlatformExtension.isDesktopOrWeb
|
||||
? rootBundle.loadString('assets/example.json')
|
||||
: rootBundle.loadString('assets/mobile_example.json').then((value) {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
editorState?.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FutureBuilder<String>(
|
||||
future: future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
editorState?.dispose();
|
||||
editorState ??= EditorState(
|
||||
document: Document.fromJson(jsonDecode(snapshot.data!)),
|
||||
);
|
||||
return AppFlowyEditor(
|
||||
editorState: editorState!,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
editorState?.remoteSelections.value = [...remoteSelections];
|
||||
},
|
||||
child: const Text('Add Selection'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectionFlag(
|
||||
BuildContext context,
|
||||
RemoteSelection remoteSelection,
|
||||
Rect rect,
|
||||
) {
|
||||
final selection = remoteSelection.selection;
|
||||
if (selection.isCollapsed) {
|
||||
return Positioned(
|
||||
top: rect.top - 5.0,
|
||||
left: rect.right,
|
||||
child: const Text(
|
||||
'Lucas.Xu',
|
||||
style: TextStyle(
|
||||
backgroundColor: Colors.yellow,
|
||||
color: Colors.black,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: rect.top - 5.0,
|
||||
left: rect.left,
|
||||
child: const Text(
|
||||
'Guest',
|
||||
style: TextStyle(
|
||||
backgroundColor: Colors.yellow,
|
||||
color: Colors.black,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ class BlockSelectionContainer extends StatelessWidget {
|
|||
required this.node,
|
||||
required this.delegate,
|
||||
required this.listenable,
|
||||
this.remoteSelection,
|
||||
this.cursorColor = Colors.black,
|
||||
this.selectionColor = Colors.blue,
|
||||
this.blockColor = Colors.blue,
|
||||
|
|
@ -24,6 +25,9 @@ class BlockSelectionContainer extends StatelessWidget {
|
|||
// get the selection from the listenable
|
||||
final ValueListenable<Selection?> listenable;
|
||||
|
||||
// remote selection
|
||||
final ValueListenable<List<RemoteSelection>>? remoteSelection;
|
||||
|
||||
// the color of the cursor
|
||||
final Color cursorColor;
|
||||
|
||||
|
|
@ -50,6 +54,17 @@ class BlockSelectionContainer extends StatelessWidget {
|
|||
? AlignmentDirectional.topStart
|
||||
: AlignmentDirectional.topEnd,
|
||||
children: [
|
||||
if (remoteSelection != null)
|
||||
RemoteBlockSelectionsArea(
|
||||
node: node,
|
||||
delegate: delegate,
|
||||
remoteSelections: remoteSelection!,
|
||||
supportTypes: supportTypes
|
||||
.where(
|
||||
(element) => element != BlockSelectionType.cursor,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
// block selection or selection area
|
||||
BlockSelectionArea(
|
||||
node: node,
|
||||
|
|
@ -66,6 +81,16 @@ class BlockSelectionContainer extends StatelessWidget {
|
|||
),
|
||||
child,
|
||||
// cursor
|
||||
// remote cursor
|
||||
if (supportTypes.contains(BlockSelectionType.cursor) &&
|
||||
remoteSelection != null)
|
||||
RemoteBlockSelectionsArea(
|
||||
node: node,
|
||||
delegate: delegate,
|
||||
remoteSelections: remoteSelection!,
|
||||
supportTypes: const [BlockSelectionType.cursor],
|
||||
),
|
||||
// local cursor
|
||||
if (supportTypes.contains(BlockSelectionType.cursor))
|
||||
BlockSelectionArea(
|
||||
node: node,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
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';
|
||||
|
||||
final _deepEqual = const DeepCollectionEquality().equals;
|
||||
|
||||
class RemoteBlockSelectionsArea extends StatelessWidget {
|
||||
const RemoteBlockSelectionsArea({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.delegate,
|
||||
required this.remoteSelections,
|
||||
this.supportTypes = const [
|
||||
BlockSelectionType.cursor,
|
||||
BlockSelectionType.selection,
|
||||
],
|
||||
});
|
||||
|
||||
// get the cursor rect or selection rects from the delegate
|
||||
final SelectableMixin delegate;
|
||||
|
||||
final ValueListenable<List<RemoteSelection>> remoteSelections;
|
||||
|
||||
// the node of the block
|
||||
final Node node;
|
||||
|
||||
final List<BlockSelectionType> supportTypes;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: remoteSelections,
|
||||
builder: (_, value, child) {
|
||||
child ??= const SizedBox.shrink();
|
||||
final selections = value
|
||||
.where((e) => node.inSelection(e.selection.normalized))
|
||||
.toList();
|
||||
if (selections.isEmpty) {
|
||||
return child;
|
||||
}
|
||||
return Positioned.fill(
|
||||
child: Stack(
|
||||
children: selections
|
||||
.map(
|
||||
(e) => RemoteBlockSelectionArea(
|
||||
node: node,
|
||||
delegate: delegate,
|
||||
remoteSelection: e,
|
||||
supportTypes: supportTypes,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [RemoteBlockSelectionArea] is a widget that renders the selection area or the cursor of a block from remote.
|
||||
class RemoteBlockSelectionArea extends StatefulWidget {
|
||||
const RemoteBlockSelectionArea({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.delegate,
|
||||
required this.remoteSelection,
|
||||
this.supportTypes = const [
|
||||
BlockSelectionType.cursor,
|
||||
BlockSelectionType.selection,
|
||||
],
|
||||
});
|
||||
|
||||
// get the cursor rect or selection rects from the delegate
|
||||
final SelectableMixin delegate;
|
||||
|
||||
final RemoteSelection remoteSelection;
|
||||
|
||||
// the node of the block
|
||||
final Node node;
|
||||
|
||||
final List<BlockSelectionType> supportTypes;
|
||||
|
||||
@override
|
||||
State<RemoteBlockSelectionArea> createState() =>
|
||||
_RemoteBlockSelectionAreaState();
|
||||
}
|
||||
|
||||
class _RemoteBlockSelectionAreaState extends State<RemoteBlockSelectionArea> {
|
||||
// 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) {
|
||||
const child = SizedBox.shrink();
|
||||
final selection = widget.remoteSelection.selection;
|
||||
if (selection.isCollapsed) {
|
||||
// show the cursor when the selection is collapsed
|
||||
if (!widget.supportTypes.contains(BlockSelectionType.cursor) ||
|
||||
prevCursorRect == null) {
|
||||
return child;
|
||||
}
|
||||
const shouldBlink = false;
|
||||
final cursor = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Cursor(
|
||||
rect: prevCursorRect!,
|
||||
shouldBlink: shouldBlink,
|
||||
cursorStyle: widget.delegate.cursorStyle,
|
||||
color: widget.remoteSelection.cursorColor,
|
||||
),
|
||||
widget.remoteSelection.builder?.call(
|
||||
context,
|
||||
widget.remoteSelection,
|
||||
prevCursorRect!,
|
||||
) ??
|
||||
child,
|
||||
],
|
||||
);
|
||||
return cursor;
|
||||
} else {
|
||||
// show the selection area when the selection is not collapsed
|
||||
if (!widget.supportTypes.contains(BlockSelectionType.selection) ||
|
||||
prevSelectionRects == null ||
|
||||
prevSelectionRects!.isEmpty ||
|
||||
(prevSelectionRects!.length == 1 &&
|
||||
prevSelectionRects!.first.width == 0)) {
|
||||
return child;
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
SelectionAreaPaint(
|
||||
rects: prevSelectionRects!,
|
||||
selectionColor: widget.remoteSelection.selectionColor,
|
||||
),
|
||||
if (selection.start.path.equals(widget.node.path))
|
||||
widget.remoteSelection.builder?.call(
|
||||
context,
|
||||
widget.remoteSelection,
|
||||
prevSelectionRects!.first,
|
||||
) ??
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSelectionIfNeeded() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final selection = widget.remoteSelection.selection.normalized;
|
||||
final path = widget.node.path;
|
||||
|
||||
// the current path is in the selection
|
||||
if (path.inSelection(selection)) {
|
||||
if (widget.supportTypes.contains(BlockSelectionType.cursor) &&
|
||||
selection.isCollapsed) {
|
||||
final rect = widget.delegate.getCursorRectInPosition(selection.start);
|
||||
if (rect != prevCursorRect) {
|
||||
setState(() {
|
||||
prevCursorRect = rect;
|
||||
prevBlockRect = null;
|
||||
prevSelectionRects = null;
|
||||
});
|
||||
}
|
||||
} else if (widget.supportTypes.contains(BlockSelectionType.selection)) {
|
||||
final rects = widget.delegate.getRectsInSelection(selection);
|
||||
if (!_deepEqual(rects, prevSelectionRects)) {
|
||||
setState(() {
|
||||
prevSelectionRects = rects;
|
||||
prevCursorRect = null;
|
||||
prevBlockRect = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (prevBlockRect != null ||
|
||||
prevSelectionRects != null ||
|
||||
prevCursorRect != null) {
|
||||
setState(() {
|
||||
prevBlockRect = null;
|
||||
prevSelectionRects = null;
|
||||
prevCursorRect = null;
|
||||
});
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_updateSelectionIfNeeded();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RemoteSelection {
|
||||
const RemoteSelection({
|
||||
required this.id,
|
||||
required this.selection,
|
||||
required this.selectionColor,
|
||||
required this.cursorColor,
|
||||
this.builder,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final Selection selection;
|
||||
final Color selectionColor;
|
||||
final Color cursorColor;
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
RemoteSelection selection,
|
||||
Rect rect,
|
||||
)? builder;
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ 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/selection/remote_block_selection_area.dart';
|
||||
export 'base_component/selection/remote_selection.dart';
|
||||
export 'base_component/text_direction_mixin.dart';
|
||||
export 'base_component/text_style_configuration.dart';
|
||||
export 'base_component/widget/full_screen_overlay_entry.dart';
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ class _BulletedListBlockComponentWidgetState
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ class _DividerBlockComponentWidgetState
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ class _HeadingBlockComponentWidgetState
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
@ -196,6 +197,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
child: child!,
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ class _NumberedListBlockComponentWidgetState
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ class _ParagraphBlockComponentWidgetState
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
|||
return BlockSelectionContainer(
|
||||
delegate: widget.delegate,
|
||||
listenable: widget.editorState.selectionNotifier,
|
||||
remoteSelection: widget.editorState.remoteSelections,
|
||||
node: widget.node,
|
||||
cursorColor: widget.cursorColor,
|
||||
selectionColor: widget.selectionColor,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ class _TodoListBlockComponentWidgetState
|
|||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
remoteSelection: editorState.remoteSelections,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ class EditorState {
|
|||
/// The selection of the editor.
|
||||
Selection? get selection => selectionNotifier.value;
|
||||
|
||||
/// Remote selection is the selection from other users.
|
||||
final PropertyValueNotifier<List<RemoteSelection>> remoteSelections =
|
||||
PropertyValueNotifier<List<RemoteSelection>>([]);
|
||||
|
||||
/// Sets the selection of the editor.
|
||||
set selection(Selection? value) {
|
||||
// clear the toggled style when the selection is changed.
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CursorWidget extends StatefulWidget {
|
||||
const CursorWidget({
|
||||
super.key,
|
||||
required this.layerLink,
|
||||
required this.rect,
|
||||
required this.color,
|
||||
this.blinkingInterval = 0.5,
|
||||
this.shouldBlink = true,
|
||||
this.cursorStyle = CursorStyle.verticalLine,
|
||||
});
|
||||
|
||||
final double blinkingInterval; // milliseconds
|
||||
final bool shouldBlink;
|
||||
final CursorStyle cursorStyle;
|
||||
final Color color;
|
||||
final Rect rect;
|
||||
final LayerLink layerLink;
|
||||
|
||||
@override
|
||||
State<CursorWidget> createState() => CursorWidgetState();
|
||||
}
|
||||
|
||||
class CursorWidgetState extends State<CursorWidget> {
|
||||
bool showCursor = true;
|
||||
late Timer timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
timer = _initTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Timer _initTimer() {
|
||||
return Timer.periodic(
|
||||
Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()),
|
||||
(timer) => setState(() => showCursor = !showCursor),
|
||||
);
|
||||
}
|
||||
|
||||
/// force the cursor widget to show for a while
|
||||
void show() {
|
||||
setState(() {
|
||||
showCursor = true;
|
||||
});
|
||||
timer.cancel();
|
||||
timer = _initTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fromRect(
|
||||
rect: widget.rect,
|
||||
child: CompositedTransformFollower(
|
||||
link: widget.layerLink,
|
||||
offset: widget.rect.topCenter,
|
||||
showWhenUnlinked: false,
|
||||
// Ignore the gestures in cursor
|
||||
// to solve the problem that cursor area cannot be selected.
|
||||
child: IgnorePointer(
|
||||
child: _buildCursor(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCursor(BuildContext context) {
|
||||
var color = widget.color;
|
||||
if (widget.shouldBlink && !showCursor) {
|
||||
color = Colors.transparent;
|
||||
}
|
||||
switch (widget.cursorStyle) {
|
||||
case CursorStyle.verticalLine:
|
||||
return Container(
|
||||
color: color,
|
||||
);
|
||||
case CursorStyle.borderLine:
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color, width: 2),
|
||||
),
|
||||
);
|
||||
case CursorStyle.cover:
|
||||
final size = widget.rect.size;
|
||||
return Container(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
color: color.withOpacity(0.2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class SelectionWidget extends StatefulWidget {
|
||||
const SelectionWidget({
|
||||
super.key,
|
||||
required this.layerLink,
|
||||
required this.rect,
|
||||
required this.color,
|
||||
this.decoration,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final Rect rect;
|
||||
final LayerLink layerLink;
|
||||
final BoxDecoration? decoration;
|
||||
|
||||
@override
|
||||
State<SelectionWidget> createState() => _SelectionWidgetState();
|
||||
}
|
||||
|
||||
class _SelectionWidgetState extends State<SelectionWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fromRect(
|
||||
rect: widget.rect,
|
||||
child: CompositedTransformFollower(
|
||||
link: widget.layerLink,
|
||||
offset: widget.rect.topLeft,
|
||||
showWhenUnlinked: false,
|
||||
// Ignore the gestures in selection overlays
|
||||
// to solve the problem that selection areas cannot overlap.
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
color: widget.decoration == null ? widget.color : null,
|
||||
decoration: widget.decoration,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/selection/cursor_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../test_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('CursorWidget', () {
|
||||
testWidgets('can render verticalLine', (tester) async {
|
||||
final node = Node(type: 'paragraph');
|
||||
await tester.buildAndPump(
|
||||
Stack(
|
||||
children: [
|
||||
CursorWidget(
|
||||
layerLink: node.layerLink,
|
||||
rect: const Rect.fromLTWH(0, 0, 50, 50),
|
||||
color: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CursorWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('can render borderLine', (tester) async {
|
||||
final node = Node(type: 'paragraph');
|
||||
await tester.buildAndPump(
|
||||
Stack(
|
||||
children: [
|
||||
CursorWidget(
|
||||
cursorStyle: CursorStyle.borderLine,
|
||||
layerLink: node.layerLink,
|
||||
rect: const Rect.fromLTWH(0, 0, 50, 50),
|
||||
color: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CursorWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('can render cover', (tester) async {
|
||||
final node = Node(type: 'paragraph');
|
||||
await tester.buildAndPump(
|
||||
Stack(
|
||||
children: [
|
||||
CursorWidget(
|
||||
cursorStyle: CursorStyle.cover,
|
||||
layerLink: node.layerLink,
|
||||
rect: const Rect.fromLTWH(0, 0, 50, 50),
|
||||
color: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CursorWidget), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue