Skip to content
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

Merged
merged 5 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -86,6 +87,7 @@ class QuillRawEditorConfigurations extends Equatable {
this.onScribbleActivated,
this.scribbleAreaInsets,
this.readOnlyMouseCursor = SystemMouseCursors.text,
this.magnifierConfiguration,
});

/// Controls the document being edited.
Expand Down Expand Up @@ -334,6 +336,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 @@ -292,6 +293,7 @@ class QuillEditorState extends State<QuillEditor>
onScribbleActivated: configurations.onScribbleActivated,
scribbleAreaInsets: configurations.scribbleAreaInsets,
readOnlyMouseCursor: configurations.readOnlyMouseCursor,
magnifierConfiguration: configurations.magnifierConfiguration,
),
),
),
Expand Down Expand Up @@ -418,6 +420,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
SelectionChangedCause.longPress,
);
}
editor?.updateMagnifier(details.globalPosition);
}

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

_showMagnifierIfSupportedByPlatform(details.globalPosition);
}

@override
Expand All @@ -575,8 +580,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);
Copy link
Collaborator

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.

Copy link
Collaborator

@EchoEllet EchoEllet Sep 19, 2024

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in #2338

Copy link
Collaborator

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.

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,
),
);
}
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(
Copy link
Collaborator

@EchoEllet EchoEllet Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the difference between updateMagnifier and _updateMagnifier? Is the private one is for internal use? More details are needed

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 @@ -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
Copy link
Collaborator

@EchoEllet EchoEllet Sep 19, 2024

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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),
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