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

Rework interaction gesture handling #2322

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

knopp
Copy link
Contributor

@knopp knopp commented Sep 16, 2024

This PR reworks gesture handling inside interactors with the goal of super_editor gesture detectors playing nicely with other detectors. With this PR it is no longer necessary for interactor to drive scrolling and it is now possible to have working dismissable rows in the editor.

The gesture recognizer configuration on mobile has been changed so that the document contents is sandwiched between pan and tap recognizers, i.e.

  • EagerPanGestureRecognizer (above document)
  • Document contents
  • TapSequenceGestureRecognizer (below document)

EagerPanGestureRecognizer always wins, but it gets only accepted conditionally (if user drags handles or long press is in progress).
TapSequenceGestureRecognzer is below document so that it doesn't interfere with checkboxes or buttons inside document.

@knopp knopp changed the title WIP Rework gesture detection Rework interaction gesture handling Sep 16, 2024
@@ -749,7 +749,7 @@ void main() {

await tester //
.createDocument()
.withSingleParagraph()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By default Flutter disables overscroll of contents is too short.

@matthew-carroll
Copy link
Contributor

@angelosilvestre can you give this a first pass?

Copy link
Collaborator

@angelosilvestre angelosilvestre left a comment

Choose a reason for hiding this comment

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

I left a few comments, but it looks good in general.

@@ -1287,7 +1200,31 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
@override
Widget build(BuildContext context) {
final gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
return RawGestureDetector(
final layerAbove = RawGestureDetector(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a comment explaining why we need two layers of gesture detectors so it's clear for future readers?

@@ -1046,28 +978,23 @@ class _ReadOnlyAndroidDocumentTouchInteractorState extends State<ReadOnlyAndroid
@override
Widget build(BuildContext context) {
final gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
return OverlayPortal(
final layerAbove = OverlayPortal(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here about adding a comment explaining why we need the layers.

}
}

final gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
return RawGestureDetector(
behavior: HitTestBehavior.opaque,
final layerAbove = RawGestureDetector(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here about adding a comment explaining why we need the layers.

Comment on lines +55 to +56
required this.child,
required this.fillViewport,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please place child as the last parameter.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please adjust this, as Angelo mentioned. Additionally, please move fillViewport above showDebugPaint, which is typically the last property before child.

// The user wasn't dragging over a selection. Do nothing.
_dragMode = null;
break;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we end up removing DragMode.scroll and DragMode.waitingForScrollDirection, can we simplify this switch to the following code?

if (_dragMode != null) {
  // The user was dragging a selection change in some way, either with handles
  // or with a long-press. Finish that interaction.
  _onDragSelectionEnd();
} 

}
}

final gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
return RawGestureDetector(
behavior: HitTestBehavior.opaque,
final layerAbove = RawGestureDetector(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here about adding a comment explaining the need of two layers of gesture detectors.

@@ -1511,6 +1378,7 @@ class SuperEditorIosToolbarOverlayManagerState extends State<SuperEditorIosToolb

@override
Widget build(BuildContext context) {
final ancestorScrollable = context.findAncestorScrollableWithVerticalScroll;
Copy link
Collaborator

Choose a reason for hiding this comment

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

It doesn't seem we are using this variable.

@knopp
Copy link
Contributor Author

knopp commented Sep 30, 2024

@angelosilvestre, can you give this another go?

Copy link
Collaborator

@angelosilvestre angelosilvestre left a comment

Choose a reason for hiding this comment

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

It looks good to me. @matthew-carroll should take a look now.

Copy link
Contributor

@matthew-carroll matthew-carroll left a comment

Choose a reason for hiding this comment

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

I left a number of code organization and documentation comments. Since we have a lot of duplication in the gesture area of the codebase, most of the comments apply to a number of similar locations in this PR. For each comment, please check all the changes in this PR for other places where the same comment applies.

Comment on lines +55 to +56
required this.child,
required this.fillViewport,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please adjust this, as Angelo mentioned. Additionally, please move fillViewport above showDebugPaint, which is typically the last property before child.

@@ -69,12 +71,14 @@ class DocumentMouseInteractor extends StatefulWidget {
/// Auto-scrolling delegate.
final AutoScrollController autoScroller;

final bool fillViewport;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please always include a Dart Doc for non-standard properties. What does this do? When should someone use it? When should they not use it? Dart Docs are our instructions for all downstream users of the package.

Please double check the rest of this PR for any other missing Dart Docs, too.

child: _buildCursorStyle(
child: _buildGestureInput(
child: widget.child ?? const SizedBox(),
return SliverHybridStack(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need a SliverHybridStack here?

@@ -619,6 +598,10 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
return viewportBox.globalToLocal(globalOffset);
}

final _interactor = GlobalKey();
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move this property up to the top with all the other properties, and please add a Dart Doc to get interactorBox.

return SliverHybridStack(
fillViewport: widget.fillViewport,
children: [
layerBelow,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please try not to assign intermediate widgets. Either place all of them here in this tree, or breakout private build methods to keep the tree size tractable, e.g., _buildBottomGestureLayer() and _buildTopGestureLayer().

_buildMagnifierFocalPoint(),
],
),
);
final layerBelow = RawGestureDetector(
Copy link
Contributor

Choose a reason for hiding this comment

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

Same note here about creating intermediate widgets in a build method.

child,
],
);
return widget.gestureBuilder(context, child: child);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure where this method is called, but now that all the content of this method has been replaced by a single line, is there any reason not to call that builder directly from wherever this method is being called?

@@ -17,6 +17,8 @@ class EagerPanGestureRecognizer extends DragGestureRecognizer {
super.allowedButtonsFilter,
});

bool Function()? canAccept;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a Dart Doc. What does this do? When should it be used? How should it be used?

),
);
final layerBelow = RawGestureDetector(
Copy link
Contributor

Choose a reason for hiding this comment

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

Same note about creating intermediate widgets.

@@ -378,6 +379,9 @@ void main() {
const DocumentPosition(nodeId: "1", nodePosition: TextNodePosition(offset: 34)),
);
await tester.pumpAndSettle();
// Wait a bit to ensure that the interactor recognizer doesn't consider this a double
// tap.
await tester.pump(kDoubleTapTimeout + const Duration(seconds: 1));
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this truly meant to be Duration(seconds: 1) or was it supposed to be Duration(milliseconds: 1)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants