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

[SuperEditor][Android] Fix auto-scrolling start region (Resolves #2000) #2036

Merged
merged 2 commits into from
May 26, 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 @@ -1387,6 +1387,18 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
@visibleForTesting
bool get wantsToDisplayMagnifier => _controlsController!.shouldShowMagnifier.value;

/// Returns the `RenderBox` for the scrolling viewport.
///
/// If this widget has an ancestor `Scrollable`, then the returned
/// `RenderBox` belongs to that ancestor `Scrollable`.
///
/// If this widget doesn't have an ancestor `Scrollable`, then this
/// widget includes a `ScrollView` and this `State`'s render object
/// is the viewport `RenderBox`.
RenderBox get viewportBox =>
(context.findAncestorScrollableWithVerticalScroll?.context.findRenderObject() ?? context.findRenderObject())
as RenderBox;

void _onSelectionChange() {
final selection = widget.selection.value;
if (selection == null) {
Expand Down Expand Up @@ -1546,11 +1558,12 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
documentLayout.getDocumentOffsetFromAncestorOffset(_dragHandleSelectionGlobalFocalPoint.value!),
)!;

final centerOfContentInContentSpace = documentLayout.getRectForPosition(nearestPosition)!.center;

// Move the magnifier focal point to match the drag x-offset, but always remain focused on the vertical
// center of the line.
final centerOfContentAtNearestPosition = documentLayout.getAncestorOffsetFromDocumentOffset(
documentLayout.getRectForPosition(nearestPosition)!.center,
);
final centerOfContentAtNearestPosition =
documentLayout.getAncestorOffsetFromDocumentOffset(centerOfContentInContentSpace);
_magnifierFocalPoint.value = Offset(
_magnifierFocalPoint.value!.dx + dragDx,
centerOfContentAtNearestPosition.dy,
Expand Down Expand Up @@ -1580,10 +1593,17 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
// Update the auto-scroll focal point so that the viewport scrolls if we're
// close to the boundary.
widget.dragHandleAutoScroller.value?.updateAutoScrollHandleMonitoring(
dragEndInViewport: centerOfContentAtNearestPosition,
dragEndInViewport: _contentOffsetInViewport(centerOfContentInContentSpace),
);
}

/// Converts the [offset] in content space to an offset in the viewport space.
Offset _contentOffsetInViewport(Offset offset) {
final documentLayout = widget.getDocumentLayout();
final globalOffset = documentLayout.getGlobalOffsetFromDocumentOffset(offset);
return viewportBox.globalToLocal(globalOffset);
}

@override
Widget build(BuildContext context) {
return OverlayPortal(
Expand Down
48 changes: 48 additions & 0 deletions super_editor/test/super_editor/supereditor_scrolling_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,54 @@ void main() {
);
});

testWidgetsOnMobile('starts auto-scrolling when dragging near the top', (tester) async {
final scrollController = ScrollController();

// Pump an editor with an appbar above the editor so we make sure that
// auto-scroll starts when the user dragged near the top of the editor,
// not at the top of the screen.
await tester
.createDocument()
.withLongTextContent()
.withScrollController(scrollController)
.withEditorSize(Size(300, 300))
.autoFocus(true)
.withAppBar(100.0)
.pump();

// Scroll all the way to the bottom.
scrollController.jumpTo(scrollController.position.maxScrollExtent);
await tester.pumpAndSettle();

// Place the caret at approximately at the middle of the first visible line.
await tester.tapAt(tester.getTopLeft(find.byType(SuperEditor)) + Offset(150, 20));
await tester.pump(kDoubleTapTimeout);

final scrollOffsetBeforeDrag = scrollController.offset;

// Drag the handle a bit to the top.
final dragGesture = await tester.startGesture(tester.getCenter(
SuperEditorInspector.findMobileCaretDragHandle(),
));
await dragGesture.moveBy(Offset(0, -20));
await tester.pump();

// Pump some frames to let the auto-scroll kick in.
for (int i = 0; i < 60; i += 1) {
await tester.pump();
}

// Release the gesture.
await dragGesture.up();
await tester.pump();

// Ensure the editor scrolled up.
expect(scrollController.offset, lessThan(scrollOffsetBeforeDrag));

// Let the long-press timer resolve.
await tester.pump(kLongPressTimeout);
});

testWidgetsOnDesktop("auto-scrolls to caret position", (tester) async {
const windowSize = Size(800, 600);
tester.view.physicalSize = windowSize;
Expand Down
20 changes: 20 additions & 0 deletions super_editor/test/super_editor/supereditor_test_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ class TestSuperEditorConfigurator {
return this;
}

/// Configures the [SuperEditor] to display an [AppBar] with the given height above the [SuperEditor].
///
/// If [withCustomWidgetTreeBuilder] is used, this setting is ignored.
TestSuperEditorConfigurator withAppBar(double height) {
_config.appBarHeight = height;
return this;
}

/// Configures the [SuperEditor] to use the given [scrollController]
TestSuperEditorConfigurator withScrollController(ScrollController? scrollController) {
_config.scrollController = scrollController;
Expand Down Expand Up @@ -438,6 +446,17 @@ class TestSuperEditorConfigurator {
// Flutter to pick the web shortcuts.
shortcuts: defaultFlutterShortcuts,
home: Scaffold(
appBar: _config.appBarHeight != null
? PreferredSize(
preferredSize: ui.Size(double.infinity, _config.appBarHeight!),
child: SafeArea(
child: SizedBox(
height: _config.appBarHeight!,
child: ColoredBox(color: Colors.yellow),
),
),
)
: null,
body: superEditor,
),
debugShowCheckedModeBanner: false,
Expand Down Expand Up @@ -674,6 +693,7 @@ class SuperEditorTestConfiguration {
final plugins = <SuperEditorPlugin>{};

WidgetTreeBuilder? widgetTreeBuilder;
double? appBarHeight;
}

/// Must return a widget tree containing the given [superEditor]
Expand Down
Loading