Skip to content

Commit

Permalink
[SuperEditor][Android] Honor handle builders (Resolves #1934) (#2272)
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre authored and matthew-carroll committed Nov 19, 2024
1 parent 691d15b commit 82e0c98
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 47 deletions.
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

0 comments on commit 82e0c98

Please sign in to comment.