Skip to content

Commit

Permalink
[feature] : quill add magnifier (#2026)
Browse files Browse the repository at this point in the history
* [feature] : quill add magnifier

* [feature] : fix ci issue

* [feature] : fix ci issue

* [feature] : desktop not show magnifier

* [feature] : empty line case exception

---------

Co-authored-by: xuyang <xuyang@qimao.com>
  • Loading branch information
demoYang and xuyang authored Jul 17, 2024
1 parent 0dbe4c4 commit af691e6
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import 'package:flutter/material.dart'
AdaptiveTextSelectionToolbar,
PointerDownEvent,
TextCapitalization,
TextInputAction;
TextInputAction,
TextMagnifierConfiguration;
import 'package:flutter/widgets.dart'
show
Action,
Expand Down Expand Up @@ -87,6 +88,7 @@ class QuillRawEditorConfigurations extends Equatable {
this.onScribbleActivated,
this.scribbleAreaInsets,
this.readOnlyMouseCursor = SystemMouseCursors.text,
this.magnifierConfiguration,
});

/// Controls the document being edited.
Expand Down Expand Up @@ -337,6 +339,8 @@ class QuillRawEditorConfigurations extends Equatable {
/// Optional insets for the scribble area.
final EdgeInsets? scribbleAreaInsets;

final TextMagnifierConfiguration? magnifierConfiguration;

@override
List<Object?> get props => [
readOnly,
Expand Down
26 changes: 25 additions & 1 deletion lib/src/widgets/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import 'dart:math' as math;

import 'package:flutter/cupertino.dart'
show CupertinoTheme, cupertinoTextSelectionControls;
import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/foundation.dart'
show ValueListenable, defaultTargetPlatform;
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
Expand Down Expand Up @@ -297,6 +298,7 @@ class QuillEditorState extends State<QuillEditor>
onScribbleActivated: configurations.onScribbleActivated,
scribbleAreaInsets: configurations.scribbleAreaInsets,
readOnlyMouseCursor: configurations.readOnlyMouseCursor,
magnifierConfiguration: configurations.magnifierConfiguration,
),
),
),
Expand Down Expand Up @@ -423,6 +425,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
SelectionChangedCause.longPress,
);
}
editor?.updateMagnifier(details.globalPosition);
}

bool _isPositionSelected(TapUpDetails details) {
Expand Down Expand Up @@ -562,6 +565,8 @@ class _QuillEditorSelectionGestureDetectorBuilder
Feedback.forLongPress(_state.context);
}
}

_showMagnifierIfSupportedByPlatform(details.globalPosition);
}

@override
Expand All @@ -580,8 +585,27 @@ class _QuillEditorSelectionGestureDetectorBuilder
}
}
}
_hideMagnifierIfSupportedByPlatform();
super.onSingleLongTapEnd(details);
}

void _showMagnifierIfSupportedByPlatform(Offset positionToShow) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editor?.showMagnifier(positionToShow);
default:
}
}

void _hideMagnifierIfSupportedByPlatform() {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editor?.hideMagnifier();
default:
}
}
}

/// Signature for the callback that reports when the user changes the selection
Expand Down
152 changes: 149 additions & 3 deletions lib/src/widgets/others/text_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class EditorTextSelectionOverlay {
this.onSelectionHandleTapped,
this.dragStartBehavior = DragStartBehavior.start,
this.handlesVisible = false,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
}) {
// Clipboard status is only checked on first instance of
// ClipboardStatusNotifier
Expand Down Expand Up @@ -183,6 +184,13 @@ class EditorTextSelectionOverlay {

TextSelection get _selection => value.selection;

final MagnifierController _magnifierController = MagnifierController();

final TextMagnifierConfiguration magnifierConfiguration;

final ValueNotifier<MagnifierInfo> _magnifierInfo =
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);

void setHandlesVisible(bool visible) {
if (handlesVisible == visible) {
return;
Expand Down Expand Up @@ -237,7 +245,7 @@ class EditorTextSelectionOverlay {
BuildContext context, _TextSelectionHandlePosition position) {
if (_selection.isCollapsed &&
position == _TextSelectionHandlePosition.end) {
return Container();
return const SizedBox.shrink();
}
return Visibility(
visible: handlesVisible,
Expand All @@ -252,6 +260,9 @@ class EditorTextSelectionOverlay {
selection: _selection,
selectionControls: selectionCtrls,
position: position,
onHandleDragStart: _onHandleDragStart,
onHandleDragUpdate: _onHandleDragUpdate,
onHandleDragEnd: _onHandleDragEnd,
dragStartBehavior: dragStartBehavior,
));
}
Expand Down Expand Up @@ -341,11 +352,12 @@ class EditorTextSelectionOverlay {
/// Final cleanup.
void dispose() {
hide();
_magnifierInfo.dispose();
}

/// Builds the handles by inserting them into the [context]'s overlay.
void showHandles() {
assert(_handles == null);
if (_handles != null) return;
_handles = <OverlayEntry>[
OverlayEntry(
builder: (context) =>
Expand All @@ -366,8 +378,123 @@ class EditorTextSelectionOverlay {
void updateForScroll() {
markNeedsBuild();
}

void _onHandleDragStart(DragStartDetails details, TextPosition position) {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) return;
showMagnifier(position, details.globalPosition, renderObject);
}

void _onHandleDragUpdate(DragUpdateDetails details, TextPosition position) {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) return;
updateMagnifier(position, details.globalPosition, renderObject);
}

void _onHandleDragEnd(DragEndDetails details) {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) return;
hideMagnifier();
}

void showMagnifier(
TextPosition position, Offset offset, RenderEditor editor) {
_showMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: offset,
renderEditable: editor,
),
);
}

void _showMagnifier(MagnifierInfo initialMagnifierInfo) {
// 隐藏toolbar
if (toolbar != null) {
hideToolbar();
}
// 更新 magnifierInfo
_magnifierInfo.value = initialMagnifierInfo;

final builtMagnifier = magnifierConfiguration.magnifierBuilder(
context,
_magnifierController,
_magnifierInfo,
);

if (builtMagnifier == null) return;

_magnifierController.show(
context: context,
below: magnifierConfiguration.shouldDisplayHandlesInMagnifier
? null
: _handles![0],
builder: (_) => builtMagnifier,
);
}

void updateMagnifier(
TextPosition position, Offset offset, RenderEditor editor) {
_updateMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: offset,
renderEditable: editor,
),
);
}

void _updateMagnifier(MagnifierInfo magnifierInfo) {
if (_magnifierController.overlayEntry == null) {
return;
}
_magnifierInfo.value = magnifierInfo;
}

void hideMagnifier() {
if (_magnifierController.overlayEntry == null) {
return;
}
_magnifierController.hide();
}

// build magnifier info
MagnifierInfo _buildMagnifier(
{required RenderEditor renderEditable,
required Offset globalGesturePosition,
required TextPosition currentTextPosition}) {
final globalRenderEditableTopLeft =
renderEditable.localToGlobal(Offset.zero);
final localCaretRect =
renderEditable.getLocalRectForCaret(currentTextPosition);

final lineAtOffset = renderEditable.getLineAtOffset(currentTextPosition);
final positionAtEndOfLine = TextPosition(
offset: lineAtOffset.extentOffset,
affinity: TextAffinity.upstream,
);

// Default affinity is downstream.
final positionAtBeginningOfLine = TextPosition(
offset: lineAtOffset.baseOffset,
);

final lineBoundaries = Rect.fromPoints(
renderEditable.getLocalRectForCaret(positionAtBeginningOfLine).topCenter,
renderEditable.getLocalRectForCaret(positionAtEndOfLine).bottomCenter,
);

return MagnifierInfo(
fieldBounds: globalRenderEditableTopLeft & renderEditable.size,
globalGesturePosition: globalGesturePosition,
caretRect: localCaretRect.shift(globalRenderEditableTopLeft),
currentLineBoundaries: lineBoundaries.shift(globalRenderEditableTopLeft),
);
}
}

typedef DargHandleCallback<T> = void Function(T details, TextPosition position);

/// This widget represents a single draggable text selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget {
const _TextSelectionHandleOverlay({
Expand All @@ -379,6 +506,9 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
required this.onSelectionHandleChanged,
required this.onSelectionHandleTapped,
required this.selectionControls,
required this.onHandleDragStart,
required this.onHandleDragUpdate,
required this.onHandleDragEnd,
this.dragStartBehavior = DragStartBehavior.start,
});

Expand All @@ -388,6 +518,9 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
final LayerLink endHandleLayerLink;
final RenderEditor renderObject;
final ValueChanged<TextSelection?> onSelectionHandleChanged;
final DargHandleCallback<DragStartDetails>? onHandleDragStart;
final DargHandleCallback<DragUpdateDetails>? onHandleDragUpdate;
final ValueChanged<DragEndDetails> onHandleDragEnd;
final VoidCallback? onSelectionHandleTapped;
final TextSelectionControls selectionControls;
final DragStartBehavior dragStartBehavior;
Expand Down Expand Up @@ -453,15 +586,18 @@ class _TextSelectionHandleOverlayState
}

void _handleDragStart(DragStartDetails details) {
if (!widget.renderObject.attached) return;
final textPosition = widget.position == _TextSelectionHandlePosition.start
? widget.selection.base
: widget.selection.extent;
final lineHeight = widget.renderObject.preferredLineHeight(textPosition);
final handleSize = widget.selectionControls.getHandleSize(lineHeight);
_dragPosition = details.globalPosition + Offset(0, -handleSize.height);
widget.onHandleDragStart?.call(details, textPosition);
}

void _handleDragUpdate(DragUpdateDetails details) {
if (!widget.renderObject.attached) return;
_dragPosition += details.delta;
final position =
widget.renderObject.getPositionForOffset(details.globalPosition);
Expand Down Expand Up @@ -497,8 +633,17 @@ class _TextSelectionHandleOverlayState
if (newSelection.baseOffset >= newSelection.extentOffset) {
return; // don't allow order swapping.
}

widget.onSelectionHandleChanged(newSelection);
if (widget.position == _TextSelectionHandlePosition.start) {
widget.onHandleDragUpdate?.call(details, newSelection.base);
} else if (widget.position == _TextSelectionHandlePosition.end) {
widget.onHandleDragUpdate?.call(details, newSelection.extent);
}
}

void _handleDragEnd(DragEndDetails details) {
if (!widget.renderObject.attached) return;
widget.onHandleDragEnd.call(details);
}

void _handleTap() {
Expand Down Expand Up @@ -579,6 +724,7 @@ class _TextSelectionHandleOverlayState
dragStartBehavior: widget.dragStartBehavior,
onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate,
onPanEnd: _handleDragEnd,
onTap: _handleTap,
child: Padding(
padding: EdgeInsets.only(
Expand Down
4 changes: 4 additions & 0 deletions lib/src/widgets/quill/text_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,10 @@ class RenderEditableTextLine extends RenderEditableBox {
_getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1))
.where((element) => element.top < lineDy && element.bottom > lineDy)
.toList(growable: false);
if (lineBoxes.isEmpty) {
// Empty line, line box is empty
return TextRange.collapsed(position.offset);
}
return TextRange(
start: getPositionForOffset(
Offset(lineBoxes.first.left, lineDy),
Expand Down
8 changes: 8 additions & 0 deletions lib/src/widgets/raw_editor/raw_editor.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:ui';

import 'package:flutter/widgets.dart'
show
AnimationController,
Expand Down Expand Up @@ -84,4 +86,10 @@ abstract class EditorState extends State<QuillRawEditor>
bool showToolbar();

void requestKeyboard();

void showMagnifier(Offset positionToShow);

void updateMagnifier(Offset positionToShow);

void hideMagnifier();
}
Loading

0 comments on commit af691e6

Please sign in to comment.