ソースを参照

feat: use custom follower

Vincent Chan 2 年 前
コミット
14c1959d63

+ 84 - 0
frontend/app_flowy/packages/appflowy_popover/lib/follower.dart

@@ -0,0 +1,84 @@
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+
+class PopoverCompositedTransformFollower extends CompositedTransformFollower {
+  const PopoverCompositedTransformFollower({
+    super.key,
+    required super.link,
+    super.showWhenUnlinked = true,
+    super.offset = Offset.zero,
+    super.targetAnchor = Alignment.topLeft,
+    super.followerAnchor = Alignment.topLeft,
+    super.child,
+  });
+
+  @override
+  PopoverRenderFollowerLayer createRenderObject(BuildContext context) {
+    final screenSize = MediaQuery.of(context).size;
+    return PopoverRenderFollowerLayer(
+      screenSize: screenSize,
+      link: link,
+      showWhenUnlinked: showWhenUnlinked,
+      offset: offset,
+      leaderAnchor: targetAnchor,
+      followerAnchor: followerAnchor,
+    );
+  }
+
+  @override
+  void updateRenderObject(
+      BuildContext context, PopoverRenderFollowerLayer renderObject) {
+    final screenSize = MediaQuery.of(context).size;
+    renderObject
+      ..screenSize = screenSize
+      ..link = link
+      ..showWhenUnlinked = showWhenUnlinked
+      ..offset = offset
+      ..leaderAnchor = targetAnchor
+      ..followerAnchor = followerAnchor;
+  }
+}
+
+class PopoverRenderFollowerLayer extends RenderFollowerLayer {
+  Size screenSize;
+
+  PopoverRenderFollowerLayer({
+    required super.link,
+    super.showWhenUnlinked = true,
+    super.offset = Offset.zero,
+    super.leaderAnchor = Alignment.topLeft,
+    super.followerAnchor = Alignment.topLeft,
+    super.child,
+    required this.screenSize,
+  });
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    super.paint(context, offset);
+
+    final global = localToGlobal(offset);
+
+    if (link.leader == null) {
+      return;
+    }
+
+    if (link.leader!.offset.dx + link.leaderSize!.width + size.width >
+        screenSize.width) {
+      debugPrint("over flow");
+    }
+    debugPrint(
+        "right: ${link.leader!.offset.dx + link.leaderSize!.width + size.width}, screen with: ${screenSize.width}");
+    // debugPrint(
+    //     "offset: $offset, global: $global, link: ${link.leader?.offset}, link size: ${link.leaderSize}");
+    // debugPrint("follower size: ${this.size}, screen size: ${this.screenSize}");
+  }
+}
+
+class EdgeFollowerLayer extends FollowerLayer {
+  EdgeFollowerLayer({
+    required super.link,
+    super.showWhenUnlinked = true,
+    super.unlinkedOffset = Offset.zero,
+    super.linkedOffset = Offset.zero,
+  });
+}

+ 339 - 0
frontend/app_flowy/packages/appflowy_popover/lib/layout.dart

@@ -0,0 +1,339 @@
+import 'dart:math' as math;
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import './popover.dart';
+
+class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
+  PopoverLink link;
+  PopoverDirection direction;
+
+  PopoverLayoutDelegate({
+    required this.link,
+    required this.direction,
+  });
+
+  @override
+  bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {
+    if (direction != oldDelegate.direction) {
+      return true;
+    }
+
+    if (link != oldDelegate.link) {
+      return true;
+    }
+
+    if (link.leaderOffset != oldDelegate.link.leaderOffset) {
+      return true;
+    }
+
+    if (link.leaderSize != oldDelegate.link.leaderSize) {
+      return true;
+    }
+
+    return false;
+  }
+
+  @override
+  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
+    return constraints.loosen();
+    // assert(link.leaderSize != null);
+    // // if (link.leaderSize == null) {
+    // //   return constraints.loosen();
+    // // }
+    // final anchorRect = Rect.fromLTWH(
+    //   link.leaderOffset!.dx,
+    //   link.leaderOffset!.dy,
+    //   link.leaderSize!.width,
+    //   link.leaderSize!.height,
+    // );
+    // BoxConstraints childConstraints;
+    // switch (direction) {
+    //   case PopoverDirection.topLeft:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.left,
+    //       anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.topRight:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth - anchorRect.right,
+    //       anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.bottomLeft:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.left,
+    //       constraints.maxHeight - anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.bottomRight:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth - anchorRect.right,
+    //       constraints.maxHeight - anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.center:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth,
+    //       constraints.maxHeight,
+    //     ));
+    //     break;
+    //   case PopoverDirection.topWithLeftAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth - anchorRect.left,
+    //       anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.topWithCenterAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth,
+    //       anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.topWithRightAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.right,
+    //       anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.rightWithTopAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth - anchorRect.right,
+    //       constraints.maxHeight - anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.rightWithCenterAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth - anchorRect.right,
+    //       constraints.maxHeight,
+    //     ));
+    //     break;
+    //   case PopoverDirection.rightWithBottomAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth - anchorRect.right,
+    //       anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.bottomWithLeftAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.left,
+    //       constraints.maxHeight - anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.bottomWithCenterAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       constraints.maxWidth,
+    //       constraints.maxHeight - anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.bottomWithRightAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.right,
+    //       constraints.maxHeight - anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.leftWithTopAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.left,
+    //       constraints.maxHeight - anchorRect.top,
+    //     ));
+    //     break;
+    //   case PopoverDirection.leftWithCenterAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.left,
+    //       constraints.maxHeight,
+    //     ));
+    //     break;
+    //   case PopoverDirection.leftWithBottomAligned:
+    //     childConstraints = BoxConstraints.loose(Size(
+    //       anchorRect.left,
+    //       anchorRect.bottom,
+    //     ));
+    //     break;
+    //   case PopoverDirection.custom:
+    //     childConstraints = constraints.loosen();
+    //     break;
+    //   default:
+    //     throw UnimplementedError();
+    // }
+    // return childConstraints;
+  }
+
+  @override
+  Offset getPositionForChild(Size size, Size childSize) {
+    if (link.leaderSize == null) {
+      return Offset.zero;
+    }
+    final anchorRect = Rect.fromLTWH(
+      link.leaderOffset!.dx,
+      link.leaderOffset!.dy,
+      link.leaderSize!.width,
+      link.leaderSize!.height,
+    );
+    Offset position;
+    switch (direction) {
+      case PopoverDirection.topLeft:
+        position = Offset(
+          anchorRect.left - childSize.width,
+          anchorRect.top - childSize.height,
+        );
+        break;
+      case PopoverDirection.topRight:
+        position = Offset(
+          anchorRect.right,
+          anchorRect.top - childSize.height,
+        );
+        break;
+      case PopoverDirection.bottomLeft:
+        position = Offset(
+          anchorRect.left - childSize.width,
+          anchorRect.bottom,
+        );
+        break;
+      case PopoverDirection.bottomRight:
+        position = Offset(
+          anchorRect.right,
+          anchorRect.bottom,
+        );
+        break;
+      case PopoverDirection.center:
+        position = anchorRect.center;
+        break;
+      case PopoverDirection.topWithLeftAligned:
+        position = Offset(
+          anchorRect.left,
+          anchorRect.top - childSize.height,
+        );
+        break;
+      case PopoverDirection.topWithCenterAligned:
+        position = Offset(
+          anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
+          anchorRect.top - childSize.height,
+        );
+        break;
+      case PopoverDirection.topWithRightAligned:
+        position = Offset(
+          anchorRect.right - childSize.width,
+          anchorRect.top - childSize.height,
+        );
+        break;
+      case PopoverDirection.rightWithTopAligned:
+        position = Offset(anchorRect.right, anchorRect.top);
+        break;
+      case PopoverDirection.rightWithCenterAligned:
+        position = Offset(
+          anchorRect.right,
+          anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
+        );
+        break;
+      case PopoverDirection.rightWithBottomAligned:
+        position = Offset(
+          anchorRect.right,
+          anchorRect.bottom - childSize.height,
+        );
+        break;
+      case PopoverDirection.bottomWithLeftAligned:
+        position = Offset(
+          anchorRect.left,
+          anchorRect.bottom,
+        );
+        break;
+      case PopoverDirection.bottomWithCenterAligned:
+        position = Offset(
+          anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
+          anchorRect.bottom,
+        );
+        break;
+      case PopoverDirection.bottomWithRightAligned:
+        position = Offset(
+          anchorRect.right - childSize.width,
+          anchorRect.bottom,
+        );
+        break;
+      case PopoverDirection.leftWithTopAligned:
+        position = Offset(
+          anchorRect.left - childSize.width,
+          anchorRect.top,
+        );
+        break;
+      case PopoverDirection.leftWithCenterAligned:
+        position = Offset(
+          anchorRect.left - childSize.width,
+          anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
+        );
+        break;
+      case PopoverDirection.leftWithBottomAligned:
+        position = Offset(
+          anchorRect.left - childSize.width,
+          anchorRect.bottom - childSize.height,
+        );
+        break;
+      default:
+        throw UnimplementedError();
+    }
+    return Offset(
+      math.max(0.0, math.min(size.width - childSize.width, position.dx)),
+      math.max(0.0, math.min(size.height - childSize.height, position.dy)),
+    );
+  }
+}
+
+class PopoverTarget extends SingleChildRenderObjectWidget {
+  final PopoverLink link;
+  const PopoverTarget({
+    super.key,
+    super.child,
+    required this.link,
+  });
+
+  @override
+  PopoverTargetRenderBox createRenderObject(BuildContext context) {
+    return PopoverTargetRenderBox(
+      link: link,
+    );
+  }
+
+  @override
+  void updateRenderObject(
+      BuildContext context, PopoverTargetRenderBox renderObject) {
+    renderObject.link = link;
+  }
+}
+
+class PopoverTargetRenderBox extends RenderProxyBox {
+  PopoverLink link;
+  PopoverTargetRenderBox({required this.link, RenderBox? child}) : super(child);
+
+  @override
+  bool get alwaysNeedsCompositing => true;
+
+  @override
+  void performLayout() {
+    super.performLayout();
+    link.leaderSize = size;
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    link.leaderOffset = localToGlobal(Offset.zero);
+    super.paint(context, offset);
+  }
+
+  @override
+  void detach() {
+    link.leaderOffset = null;
+    link.leaderSize = null;
+    super.detach();
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<PopoverLink>('link', link));
+  }
+}
+
+class PopoverLink {
+  Offset? leaderOffset;
+  Size? leaderSize;
+}

+ 41 - 11
frontend/app_flowy/packages/appflowy_popover/lib/popover.dart

@@ -1,6 +1,8 @@
+import 'package:appflowy_popover/layout.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import './follower.dart';
 
 class PopoverMutex {
   PopoverState? state;
@@ -23,6 +25,31 @@ class PopoverTriggerActionFlags {
   static int hover = 0x02;
 }
 
+enum PopoverDirection {
+  // Corner aligned with a corner of the SourceWidget
+  topLeft,
+  topRight,
+  bottomLeft,
+  bottomRight,
+  center,
+
+  // Edge aligned with a edge of the SourceWidget
+  topWithLeftAligned,
+  topWithCenterAligned,
+  topWithRightAligned,
+  rightWithTopAligned,
+  rightWithCenterAligned,
+  rightWithBottomAligned,
+  bottomWithLeftAligned,
+  bottomWithCenterAligned,
+  bottomWithRightAligned,
+  leftWithTopAligned,
+  leftWithCenterAligned,
+  leftWithBottomAligned,
+
+  custom,
+}
+
 class Popover extends StatefulWidget {
   final Widget child;
   final PopoverController? controller;
@@ -33,6 +60,7 @@ class Popover extends StatefulWidget {
   final Widget Function(BuildContext context) popupBuilder;
   final int triggerActions;
   final PopoverMutex? mutex;
+  final PopoverDirection direction;
   final void Function()? onClose;
 
   const Popover({
@@ -45,6 +73,7 @@ class Popover extends StatefulWidget {
     this.targetAnchor = Alignment.topLeft,
     this.followerAnchor = Alignment.topLeft,
     this.triggerActions = 0,
+    this.direction = PopoverDirection.rightWithTopAligned,
     this.mutex,
     this.onClose,
   }) : super(key: key);
@@ -54,7 +83,7 @@ class Popover extends StatefulWidget {
 }
 
 class PopoverState extends State<Popover> {
-  final LayerLink layerLink = LayerLink();
+  final PopoverLink popoverLink = PopoverLink();
   OverlayEntry? _overlayEntry;
   bool hasMask = true;
 
@@ -95,14 +124,15 @@ class PopoverState extends State<Popover> {
         ));
       }
 
-      children.add(CompositedTransformFollower(
-        link: layerLink,
-        showWhenUnlinked: false,
-        offset: widget.offset ?? Offset.zero,
-        targetAnchor: widget.targetAnchor,
-        followerAnchor: widget.followerAnchor,
-        child: widget.popupBuilder(context),
-      ));
+      children.add(
+        CustomSingleChildLayout(
+          delegate: PopoverLayoutDelegate(
+            direction: widget.direction,
+            link: popoverLink,
+          ),
+          child: widget.popupBuilder(context),
+        ),
+      );
 
       return Stack(children: children);
     });
@@ -150,8 +180,8 @@ class PopoverState extends State<Popover> {
 
   @override
   Widget build(BuildContext context) {
-    return CompositedTransformTarget(
-      link: layerLink,
+    return PopoverTarget(
+      link: popoverLink,
       child: MouseRegion(
         onEnter: _handleTargetPointerEnter,
         child: Listener(