-
Notifications
You must be signed in to change notification settings - Fork 857
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feature] : quill add magnifier #2026
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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; | ||
|
@@ -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, | ||
|
@@ -252,6 +260,9 @@ class EditorTextSelectionOverlay { | |
selection: _selection, | ||
selectionControls: selectionCtrls, | ||
position: position, | ||
onHandleDragStart: _onHandleDragStart, | ||
onHandleDragUpdate: _onHandleDragUpdate, | ||
onHandleDragEnd: _onHandleDragEnd, | ||
dragStartBehavior: dragStartBehavior, | ||
)); | ||
} | ||
|
@@ -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) => | ||
|
@@ -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, | ||
), | ||
); | ||
} | ||
demoYang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
void _showMagnifier(MagnifierInfo initialMagnifierInfo) { | ||
// 隐藏toolbar | ||
if (toolbar != null) { | ||
hideToolbar(); | ||
} | ||
// 更新 magnifierInfo | ||
demoYang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain the difference between |
||
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({ | ||
|
@@ -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, | ||
}); | ||
|
||
|
@@ -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; | ||
|
@@ -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); | ||
|
@@ -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() { | ||
|
@@ -579,6 +724,7 @@ class _TextSelectionHandleOverlayState | |
dragStartBehavior: widget.dragStartBehavior, | ||
onPanStart: _handleDragStart, | ||
onPanUpdate: _handleDragUpdate, | ||
onPanEnd: _handleDragEnd, | ||
onTap: _handleTap, | ||
child: Padding( | ||
padding: EdgeInsets.only( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -839,6 +839,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); | ||
} | ||
Comment on lines
+842
to
+845
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this change was introduced? Does it solve a problem that is introduced with this feature? If yes then it shouldn't affect users that don't use this feature. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @demoYang This issue is not solved. |
||
return TextRange( | ||
start: getPositionForOffset( | ||
Offset(lineBoxes.first.left, lineDy), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this change. It might be there for a reason.
assert
will be only invoked in development and removed in production.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@demoYang Can you explain why this has been removed? I do plan on reverting the change, it shouldn't cause any issues at least for the release mode. Having an unknown error is better than unexpected behavior that is difficult to trace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted in #2338
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assert was removed to workaround the assertion error, which is something we will avoid in future PRs.