Skip to content

Commit

Permalink
[SuperEditor][SuperTextField][iOS] Fix focal point for native toolbar (
Browse files Browse the repository at this point in the history
…Resolves #2345) (#2428)
  • Loading branch information
angelosilvestre authored and matthew-carroll committed Dec 20, 2024
1 parent f9a350d commit 762ca29
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class _NativeIosContextMenuFeatureDemoState extends State<NativeIosContextMenuFe
Key mobileToolbarKey,
LeaderLink focalPoint,
) {
if (_editor.composer.selection == null) {
return const SizedBox();
}

return iOSSystemPopoverEditorToolbarWithFallbackBuilder(
context,
mobileToolbarKey,
Expand Down
9 changes: 1 addition & 8 deletions super_editor/lib/src/default_editor/super_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -923,16 +923,9 @@ Widget iOSSystemPopoverEditorToolbarWithFallbackBuilder(
return const SizedBox();
}

if (focalPoint.offset == null || focalPoint.leaderSize == null) {
// It's unclear when/why this might happen. But there seem to be some
// cases, such as placing a caret in an empty document and tapping again
// to show the toolbar.
return const SizedBox();
}

if (IOSSystemContextMenu.isSupported(context)) {
return IOSSystemContextMenu(
anchor: focalPoint.offset! & focalPoint.leaderSize!,
leaderLink: focalPoint,
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';

/// Displays the iOS system context menu on top of the Flutter view.
///
Expand Down Expand Up @@ -33,12 +35,13 @@ class IOSSystemContextMenu extends StatefulWidget {

const IOSSystemContextMenu({
super.key,
required this.anchor,
required this.leaderLink,
this.onSystemHide,
});

/// The [Rect] that the context menu should point to.
final Rect anchor;
/// A [LeaderLink] attached to the widget that determines the position
/// of the system context menu.
final LeaderLink leaderLink;

/// Called when the system hides this context menu.
///
Expand All @@ -62,23 +65,41 @@ class _IOSSystemContextMenuState extends State<IOSSystemContextMenu> {
_systemContextMenuController = SystemContextMenuController(
onSystemHide: widget.onSystemHide,
);
_systemContextMenuController.show(widget.anchor);
widget.leaderLink.addListener(_onLeaderChanged);
onNextFrame((_) => _positionSystemMenu());
}

@override
void didUpdateWidget(IOSSystemContextMenu oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.anchor != oldWidget.anchor) {
_systemContextMenuController.show(widget.anchor);
if (widget.leaderLink != oldWidget.leaderLink) {
oldWidget.leaderLink.removeListener(_onLeaderChanged);
widget.leaderLink.addListener(_onLeaderChanged);
onNextFrame((_) => _positionSystemMenu());
}
}

@override
void dispose() {
widget.leaderLink.removeListener(_onLeaderChanged);
_systemContextMenuController.dispose();
super.dispose();
}

void _onLeaderChanged() {
if (widget.leaderLink.offset == null || widget.leaderLink.leaderSize == null) {
return;
}

onNextFrame((_) {
_positionSystemMenu();
});
}

void _positionSystemMenu() {
_systemContextMenuController.show(widget.leaderLink.offset! & widget.leaderLink.leaderSize!);
}

@override
Widget build(BuildContext context) {
assert(IOSSystemContextMenu.isSupported(context));
Expand Down
76 changes: 74 additions & 2 deletions super_editor/lib/src/super_textfield/ios/ios_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,8 @@ typedef IOSPopoverToolbarBuilder = Widget Function(BuildContext, IOSEditingOverl
/// iOS is recent enough, otherwise builds [defaultIosPopoverToolbarBuilder].
Widget iOSSystemPopoverTextFieldToolbarWithFallback(BuildContext context, IOSEditingOverlayController controller) {
if (IOSSystemContextMenu.isSupported(context)) {
return IOSSystemContextMenu(
anchor: controller.toolbarFocalPoint.offset! & controller.toolbarFocalPoint.leaderSize!,
return IOSSuperTextFieldSystemContextMenu(
controller: controller,
);
}

Expand Down Expand Up @@ -752,3 +752,75 @@ Widget defaultIosPopoverToolbarBuilder(BuildContext context, IOSEditingOverlayCo
},
);
}

class IOSSuperTextFieldSystemContextMenu extends StatefulWidget {
const IOSSuperTextFieldSystemContextMenu({
super.key,
required this.controller,
});

final IOSEditingOverlayController controller;

@override
State<IOSSuperTextFieldSystemContextMenu> createState() => _IOSSuperTextFieldSystemContextMenuState();
}

class _IOSSuperTextFieldSystemContextMenuState extends State<IOSSuperTextFieldSystemContextMenu> {
late final SystemContextMenuController _systemContextMenuController;

@override
void initState() {
super.initState();
_systemContextMenuController = SystemContextMenuController();
widget.controller.addListener(_onControllerChanged);
onNextFrame((_) {
_positionSystemMenu();
});
}

@override
void didUpdateWidget(covariant IOSSuperTextFieldSystemContextMenu oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller.removeListener(_onControllerChanged);
widget.controller.addListener(_onControllerChanged);
}
onNextFrame((_) {
_positionSystemMenu();
});
}

@override
void dispose() {
widget.controller.removeListener(_onControllerChanged);
_systemContextMenuController.dispose();
super.dispose();
}

void _onControllerChanged() {
onNextFrame((_) {
_positionSystemMenu();
});
}

void _positionSystemMenu() {
// The size reported by the controller's toolbarFocalPoint is one frame behind. Query the information
// overlayController instead.
final topAnchor = widget.controller.overlayController.toolbarTopAnchor;
final bottomAnchor = widget.controller.overlayController.toolbarTopAnchor;

if (topAnchor == null || bottomAnchor == null) {
// We don't expect the toolbar builder to be called without having the anchors
// defined. But, since these properties are nullable, we account for that.
return;
}

_systemContextMenuController.show(Rect.fromLTRB(topAnchor.dx, topAnchor.dy, bottomAnchor.dx, bottomAnchor.dy));
}

@override
Widget build(BuildContext context) {
assert(IOSSystemContextMenu.isSupported(context));
return const SizedBox.shrink();
}
}

0 comments on commit 762ca29

Please sign in to comment.