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

Cherry Pick: [SuperEditor][Android] Honor handle builders (Resolves #1934) #2414

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 @@ -1336,11 +1336,39 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
final _dragHandleSelectionGlobalFocalPoint = ValueNotifier<Offset?>(null);
final _magnifierFocalPoint = ValueNotifier<Offset?>(null);

late final DocumentHandleGestureDelegate _collapsedHandleGestureDelegate;
late final DocumentHandleGestureDelegate _upstreamHandleGesturesDelegate;
late final DocumentHandleGestureDelegate _downstreamHandleGesturesDelegate;

@override
void initState() {
super.initState();
_overlayController.show();
widget.selection.addListener(_onSelectionChange);
_collapsedHandleGestureDelegate = DocumentHandleGestureDelegate(
onTap: _toggleToolbarOnCollapsedHandleTap,
onPanStart: (details) => _onHandlePanStart(details, HandleType.collapsed),
onPanUpdate: _onHandlePanUpdate,
onPanEnd: (details) => _onHandlePanEnd(details, HandleType.collapsed),
);
_upstreamHandleGesturesDelegate = DocumentHandleGestureDelegate(
onTap: () {
// Register tap down to win gesture arena ASAP.
},
onPanStart: (details) => _onHandlePanStart(details, HandleType.upstream),
onPanUpdate: _onHandlePanUpdate,
onPanEnd: (details) => _onHandlePanEnd(details, HandleType.upstream),
onPanCancel: () => _onHandlePanCancel(HandleType.upstream),
);
_downstreamHandleGesturesDelegate = DocumentHandleGestureDelegate(
onTap: () {
// Register tap down to win gesture arena ASAP.
},
onPanStart: (details) => _onHandlePanStart(details, HandleType.downstream),
onPanUpdate: _onHandlePanUpdate,
onPanEnd: (details) => _onHandlePanEnd(details, HandleType.downstream),
onPanCancel: () => _onHandlePanCancel(HandleType.downstream),
);
}

@override
Expand Down Expand Up @@ -1675,6 +1703,16 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
return const SizedBox();
}

if (_controlsController!.collapsedHandleBuilder != null) {
return _controlsController!.collapsedHandleBuilder!(
context,
handleKey: DocumentKeys.androidCaretHandle,
focalPoint: _controlsController!.collapsedHandleFocalPoint,
shouldShow: shouldShow,
gestureDelegate: _collapsedHandleGestureDelegate,
);
}

// Note: If we pass this widget as the `child` property, it causes repeated starts and stops
// of the pan gesture. By building it here, pan events work as expected.
return Follower.withOffset(
Expand All @@ -1699,11 +1737,11 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
onTapDown: (_) {
// Register tap down to win gesture arena ASAP.
},
onTap: _toggleToolbarOnCollapsedHandleTap,
onPanStart: (details) => _onHandlePanStart(details, HandleType.collapsed),
onPanUpdate: _onHandlePanUpdate,
onPanEnd: (details) => _onHandlePanEnd(details, HandleType.collapsed),
onPanCancel: () => _onHandlePanCancel(HandleType.collapsed),
onTap: _collapsedHandleGestureDelegate.onTap,
onPanStart: _collapsedHandleGestureDelegate.onPanStart,
onPanUpdate: _collapsedHandleGestureDelegate.onPanUpdate,
onPanEnd: _collapsedHandleGestureDelegate.onPanEnd,
onPanCancel: _collapsedHandleGestureDelegate.onPanCancel,
dragStartBehavior: DragStartBehavior.down,
child: AndroidSelectionHandle(
key: DocumentKeys.androidCaretHandle,
Expand All @@ -1719,6 +1757,26 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
}

List<Widget> _buildExpandedHandles() {
if (_controlsController!.expandedHandlesBuilder != null) {
return [
ValueListenableBuilder(
valueListenable: _controlsController!.shouldShowExpandedHandles,
builder: (context, shouldShow, child) {
return _controlsController!.expandedHandlesBuilder!(
context,
upstreamHandleKey: DocumentKeys.upstreamHandle,
upstreamFocalPoint: _controlsController!.upstreamHandleFocalPoint,
upstreamGestureDelegate: _upstreamHandleGesturesDelegate,
downstreamHandleKey: DocumentKeys.downstreamHandle,
downstreamFocalPoint: _controlsController!.downstreamHandleFocalPoint,
downstreamGestureDelegate: _downstreamHandleGesturesDelegate,
shouldShow: shouldShow,
);
},
)
];
}

return [
ValueListenableBuilder(
valueListenable: _controlsController!.shouldShowExpandedHandles,
Expand All @@ -1735,13 +1793,11 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
offset:
-AndroidSelectionHandle.defaultTouchRegionExpansion.topRight * MediaQuery.devicePixelRatioOf(context),
child: GestureDetector(
onTapDown: (_) {
// Register tap down to win gesture arena ASAP.
},
onPanStart: (details) => _onHandlePanStart(details, HandleType.upstream),
onPanUpdate: _onHandlePanUpdate,
onPanEnd: (details) => _onHandlePanEnd(details, HandleType.upstream),
onPanCancel: () => _onHandlePanCancel(HandleType.upstream),
onTapDown: _upstreamHandleGesturesDelegate.onTapDown,
onPanStart: _upstreamHandleGesturesDelegate.onPanStart,
onPanUpdate: _upstreamHandleGesturesDelegate.onPanUpdate,
onPanEnd: _upstreamHandleGesturesDelegate.onPanEnd,
onPanCancel: _upstreamHandleGesturesDelegate.onPanCancel,
dragStartBehavior: DragStartBehavior.down,
child: AndroidSelectionHandle(
key: DocumentKeys.upstreamHandle,
Expand All @@ -1767,13 +1823,11 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
offset:
-AndroidSelectionHandle.defaultTouchRegionExpansion.topLeft * MediaQuery.devicePixelRatioOf(context),
child: GestureDetector(
onTapDown: (_) {
// Register tap down to win gesture arena ASAP.
},
onPanStart: (details) => _onHandlePanStart(details, HandleType.downstream),
onPanUpdate: _onHandlePanUpdate,
onPanEnd: (details) => _onHandlePanEnd(details, HandleType.downstream),
onPanCancel: () => _onHandlePanCancel(HandleType.downstream),
onTapDown: _downstreamHandleGesturesDelegate.onTapDown,
onPanStart: _downstreamHandleGesturesDelegate.onPanStart,
onPanUpdate: _downstreamHandleGesturesDelegate.onPanUpdate,
onPanEnd: _downstreamHandleGesturesDelegate.onPanEnd,
onPanCancel: _downstreamHandleGesturesDelegate.onPanCancel,
dragStartBehavior: DragStartBehavior.down,
child: AndroidSelectionHandle(
key: DocumentKeys.downstreamHandle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,12 @@ class AndroidTextFieldDragHandleSelectionStrategy {
final didFocalPointStayInSameNode = nearestPositionNodeIndex == previousNearestPositionNodeIndex;

final didFocalPointMoveDownstream = didFocalPointMoveToDownstreamNode ||
(didFocalPointStayInSameNode && nearestPositionTextOffset > previousNearestPositionTextOffset);
(didFocalPointStayInSameNode && nearestPositionTextOffset > previousNearestPositionTextOffset) ||
(didFocalPointStayInSameNode && details.delta.dx > 0);

final didFocalPointMoveUpstream = didFocalPointMoveToUpstreamNode ||
(didFocalPointStayInSameNode && nearestPositionTextOffset < previousNearestPositionTextOffset);
(didFocalPointStayInSameNode && nearestPositionTextOffset < previousNearestPositionTextOffset) ||
(didFocalPointStayInSameNode && details.delta.dx < 0);

_lastFocalPosition = nearestPosition;

Expand Down
159 changes: 133 additions & 26 deletions super_editor/lib/src/infrastructure/platforms/mobile_documents.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,45 @@ class DocumentKeys {
/// Builds a full-screen collapsed drag handle display, with the handle positioned near the [focalPoint],
/// and with the handle attached to the given [handleKey].
///
/// The [handleKey] is used to find the handle in the widget tree for various purposes,
/// e.g., within tests to verify the presence or absence of the handle.
///
/// The [handleKey] must be attached to the handle, not the top-level widget returned
/// from this builder, because the [handleKey] might be used to verify the size and location
/// of the handle. For example:
/// Implementers of this builder have the following responsibilities:
/// * Attach the [handleKey] to the widget that renders the handle.
/// * Wrap the handle widget with a `Follower` and attach the `focalPoint` to the `Follower`.
/// * Wrap the handle widget with a `GestureDetector` and attach the provided [gestureDelegate] callbacks to the `GestureDetector`.
/// * When [shouldShow] is `false`, hide the handle and ensure that no gestures are handled.
///
/// ```dart
/// Widget buildCollapsedHandle(context, handleKey, focalPoint) {
/// return Follower(
/// Widget buildCollapsedHandle(BuildContext context, {
/// required LeaderLink focalPoint,
/// required DocumentHandleGestureDelegate gestureDelegate,
/// required Key handleKey,
/// required bool shouldShow,
/// }) {
/// if (!shouldShow) {
/// return const SizedBox();
/// }
/// return Follower.withOffset(
/// offset: Offset.zero,
/// link: focalPoint,
/// child: CollapsedHandle(
/// key: handleKey,
/// child: GestureDetector(
/// onTap: gestureDelegate.onTap,
/// onPanStart: gestureDelegate.onPanStart,
/// onPanUpdate: gestureDelegate.onPanUpdate,
/// onPanEnd: gestureDelegate.onPanEnd,
/// onPanCancel: gestureDelegate.onPanCancel,
/// child: CollapsedHandle(
/// key: handleKey,
/// ),
/// ),
/// );
/// }
/// ```
typedef DocumentCollapsedHandleBuilder = Widget Function(BuildContext, Key handleKey, LeaderLink focalPoint);
typedef DocumentCollapsedHandleBuilder = Widget Function(
BuildContext, {
required Key handleKey,
required LeaderLink focalPoint,
required DocumentHandleGestureDelegate gestureDelegate,
required bool shouldShow,
});

/// Builds a full-screen display of a set of expanded drag handles, with the handles positioned near the
/// [upstreamFocalPoint] and [downstreamFocalPoint], respectively, and with the handles attached to the
Expand All @@ -46,33 +67,119 @@ typedef DocumentCollapsedHandleBuilder = Widget Function(BuildContext, Key handl
/// The [upstreamHandleKey] and [downstreamHandleKey] are used to find the handles in the widget tree for
/// various purposes, e.g., within tests to verify the presence or absence of the handles.
///
/// Implementers of this builder have the following responsibilities:
/// * Attach the [upstreamHandleKey] to the widget that renders the upstream handle and [downstreamHandleKey]
/// to the downstream handle.
/// * Wrap each handle widget with a `Follower`, attaching the [downstreamFocalPoint] to the downstream handle `Follower`
/// and [upstreamFocalPoint] to the upstream handle `Follower`.
/// * Wrap each handle widget with a `GestureDetector`, attaching the provided [upstreamGestureDelegate] callbacks to
/// the upstream handle `GestureDetector` and the [downstreamGestureDelegate] callbacks to the downstream
/// handle `GestureDetector`.
/// * When [shouldShow] is `false`, hide the handle and ensure that no gestures are handled.
///
/// The handle keys must be attached to the handles, not the top-level widget returned
/// from this builder, because the handle keys might be used to verify the size and location
/// of the handles. For example:
///
/// ```dart
/// Widget buildCollapsedHandle(context, upstreamHandleKey, upstreamFocalPoint, downstreamHandleKey, downstreamFocalPoint) {
/// Widget buildExpandedHandles(BuildContext context, {
/// required LeaderLink downstreamFocalPoint,
/// required DocumentHandleGestureDelegate downstreamGestureDelegate,
/// required Key downstreamHandleKey,
/// required LeaderLink upstreamFocalPoint,
/// required DocumentHandleGestureDelegate upstreamGestureDelegate,
/// required Key upstreamHandleKey,
/// required bool shouldShow,
/// }) {
/// if (!shouldShow) {
/// return const SizedBox();
/// }
/// return Stack(
/// children: [
/// Follower(
/// link: upstreamFocalPoint,
/// child: UpstreamHandle(key: upstreamHandleKey),
/// Follower.withOffset(
/// offset: Offset.zero,
/// link: upstreamFocalPoint,
/// child: GestureDetector(
/// onTapDown: upstreamGestureDelegate.onTapDown,
/// onPanStart: upstreamGestureDelegate.onPanStart,
/// onPanUpdate: upstreamGestureDelegate.onPanUpdate,
/// onPanEnd: upstreamGestureDelegate.onPanEnd,
/// onPanCancel: upstreamGestureDelegate.onPanCancel,
/// child: UpstreamHandle(key: upstreamHandleKey),
/// ),
/// ),
/// Follower(
/// link: downstreamFocalPoint,
/// child: DownstreamHandle(key: downstreamHandleKey),
/// Follower.withOffset(
/// offset: Offset.zero,
/// link: downstreamFocalPoint,
/// child: GestureDetector(
/// onTapDown: downstreamGestureDelegate.onTapDown,
/// onPanStart: downstreamGestureDelegate.onPanStart,
/// onPanUpdate: downstreamGestureDelegate.onPanUpdate,
/// onPanEnd: downstreamGestureDelegate.onPanEnd,
/// onPanCancel: downstreamGestureDelegate.onPanCancel,
/// child: DownstreamHandle(key: downstreamHandleKey),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
typedef DocumentExpandedHandlesBuilder = Widget Function(
BuildContext,
Key upstreamHandleKey,
LeaderLink upstreamFocalPoint,
Key downstreamHandleKey,
LeaderLink downstreamFocalPoint,
);
BuildContext, {
required Key upstreamHandleKey,
required LeaderLink upstreamFocalPoint,
required DocumentHandleGestureDelegate upstreamGestureDelegate,
required Key downstreamHandleKey,
required LeaderLink downstreamFocalPoint,
required DocumentHandleGestureDelegate downstreamGestureDelegate,
required bool shouldShow,
});

/// Delegate for handling gestures on a document handle.
///
/// These callbacks are intended to make it easier for developers to customize
/// the drag handles, without having to re-implement the gesture logic. For
/// example, implementers can wrap the handle in a `GestureDetector`:
///
/// ```dart
/// Widget buildCollapsedHandle(BuildContext context, {
/// required LeaderLink focalPoint,
/// required DocumentHandleGestureDelegate gestureDelegate,
/// required Key handleKey,
/// required bool shouldShow,
/// }) {
/// return Follower(
/// link: focalPoint,
/// child: GestureDetector(
/// onTap: gestureDelegate.onTap,
/// onPanStart: gestureDelegate.onPanStart,
/// onPanUpdate: gestureDelegate.onPanUpdate,
/// onPanEnd: gestureDelegate.onPanEnd,
/// onPanCancel: gestureDelegate.onPanCancel,
/// child: CollapsedHandle(
/// key: handleKey,
/// ),
/// ),
/// );
/// }
/// ```
class DocumentHandleGestureDelegate {
DocumentHandleGestureDelegate({
this.onTapDown,
this.onTap,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
});

final GestureTapDownCallback? onTapDown;
final GestureTapCallback? onTap;
final GestureDragStartCallback? onPanStart;
final GestureDragUpdateCallback? onPanUpdate;
final GestureDragEndCallback? onPanEnd;
final GestureDragCancelCallback? onPanCancel;
}

/// Builds a full-screen floating toolbar display, with the toolbar positioned near the
/// [focalPoint], and with the toolbar attached to the given [mobileToolbarKey].
Expand Down Expand Up @@ -422,8 +529,8 @@ class DragHandleAutoScroller {
// at the top edge of the scrollable, so we can't scroll further up.
if (currentScrollOffset > 0.0) {
// Jump to the position where the offset sits at the leading boundary.
scrollPosition.jumpTo((
currentScrollOffset + (offsetInViewport.dy - _dragAutoScrollBoundary.leading).clamp(min, max)),
scrollPosition.jumpTo(
(currentScrollOffset + (offsetInViewport.dy - _dragAutoScrollBoundary.leading).clamp(min, max)),
);
}
} else if (offsetInViewport.dy > _getViewportBox().size.height - _dragAutoScrollBoundary.trailing) {
Expand Down
Loading
Loading