import 'dart:math'; import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:pixes/foundation/app.dart'; const double _kBackGestureWidth = 20.0; const int _kMaxDroppedSwipePageForwardAnimationTime = 800; const int _kMaxPageBackAnimationTime = 300; const double _kMinFlingVelocity = 1.0; class AppPageRoute extends PageRoute with _AppRouteTransitionMixin { /// Construct a MaterialPageRoute whose contents are defined by [builder]. AppPageRoute({ required this.builder, super.settings, this.maintainState = true, super.fullscreenDialog, super.allowSnapshotting = true, super.barrierDismissible = false, this.enableIOSGesture = true, this.preventRebuild = true, }) { assert(opaque); } /// Builds the primary contents of the route. final WidgetBuilder builder; @override Widget buildContent(BuildContext context) { return builder(context); } @override final bool maintainState; @override String get debugLabel => '${super.debugLabel}(${settings.name})'; @override final bool enableIOSGesture; @override final bool preventRebuild; static void updateBackButton() { Future.delayed(const Duration(milliseconds: 300), () { StateController.findOrNull(tag: "back_button")?.update(); }); } } mixin _AppRouteTransitionMixin on PageRoute { /// Builds the primary contents of the route. @protected Widget buildContent(BuildContext context); @override Duration get transitionDuration => const Duration(milliseconds: 300); @override Color? get barrierColor => null; @override String? get barrierLabel => null; @override bool canTransitionTo(TransitionRoute nextRoute) { // Don't perform outgoing animation if the next route is a fullscreen dialog. return nextRoute is PageRoute && !nextRoute.fullscreenDialog; } bool get enableIOSGesture; bool get preventRebuild; Widget? _child; @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { Widget result; if (preventRebuild) { result = _child ?? (_child = buildContent(context)); } else { result = buildContent(context); } return Semantics( scopesRoute: true, explicitChildNodes: true, child: result, ); } static bool _isPopGestureEnabled(PageRoute route) { if (route.isFirst || route.willHandlePopInternally || route.popDisposition == RoutePopDisposition.doNotPop || route.fullscreenDialog || route.animation!.status != AnimationStatus.completed || route.secondaryAnimation!.status != AnimationStatus.dismissed || route.navigator!.userGestureInProgress) { return false; } return true; } @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return DrillInPageTransition( animation: CurvedAnimation( parent: animation, curve: FluentTheme.of(context).animationCurve, ), child: enableIOSGesture && App.isIOS ? IOSBackGestureDetector( gestureWidth: _kBackGestureWidth, enabledCallback: () => _isPopGestureEnabled(this), onStartPopGesture: () => _startPopGesture(this), child: child) : child, ); } IOSBackGestureController _startPopGesture(PageRoute route) { return IOSBackGestureController(route.controller!, route.navigator!); } } class IOSBackGestureController { final AnimationController controller; final NavigatorState navigator; IOSBackGestureController(this.controller, this.navigator) { navigator.didStartUserGesture(); } void dragEnd(double velocity) { const Curve animationCurve = Curves.fastLinearToSlowEaseIn; final bool animateForward; if (velocity.abs() >= _kMinFlingVelocity) { animateForward = velocity <= 0; } else { animateForward = controller.value > 0.5; } if (animateForward) { final droppedPageForwardAnimationTime = min( lerpDouble( _kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)! .floor(), _kMaxPageBackAnimationTime, ); controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve); } else { navigator.pop(); if (controller.isAnimating) { final droppedPageBackAnimationTime = lerpDouble( 0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)! .floor(); controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve); } } if (controller.isAnimating) { late AnimationStatusListener animationStatusCallback; animationStatusCallback = (status) { navigator.didStopUserGesture(); controller.removeStatusListener(animationStatusCallback); }; controller.addStatusListener(animationStatusCallback); } else { navigator.didStopUserGesture(); } } void dragUpdate(double delta) { controller.value -= delta; } } class IOSBackGestureDetector extends StatefulWidget { const IOSBackGestureDetector( {required this.enabledCallback, required this.child, required this.gestureWidth, required this.onStartPopGesture, super.key}); final double gestureWidth; final bool Function() enabledCallback; final IOSBackGestureController Function() onStartPopGesture; final Widget child; @override State createState() => _IOSBackGestureDetectorState(); } class _IOSBackGestureDetectorState extends State { IOSBackGestureController? _backGestureController; late HorizontalDragGestureRecognizer _recognizer; @override void dispose() { _recognizer.dispose(); super.dispose(); } @override void initState() { super.initState(); _recognizer = HorizontalDragGestureRecognizer(debugOwner: this) ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _handleDragCancel; } @override Widget build(BuildContext context) { var dragAreaWidth = Directionality.of(context) == TextDirection.ltr ? MediaQuery.of(context).padding.left : MediaQuery.of(context).padding.right; dragAreaWidth = max(dragAreaWidth, widget.gestureWidth); return Stack( fit: StackFit.passthrough, children: [ widget.child, Positioned( width: dragAreaWidth, top: 0.0, bottom: 0.0, left: 0, child: Listener( onPointerDown: _handlePointerDown, behavior: HitTestBehavior.translucent, ), ), ], ); } void _handlePointerDown(PointerDownEvent event) { if (widget.enabledCallback()) _recognizer.addPointer(event); } void _handleDragCancel() { assert(mounted); _backGestureController?.dragEnd(0.0); _backGestureController = null; } double _convertToLogical(double value) { switch (Directionality.of(context)) { case TextDirection.rtl: return -value; case TextDirection.ltr: return value; } } void _handleDragEnd(DragEndDetails details) { assert(mounted); assert(_backGestureController != null); _backGestureController!.dragEnd(_convertToLogical( details.velocity.pixelsPerSecond.dx / context.size!.width)); _backGestureController = null; } void _handleDragStart(DragStartDetails details) { assert(mounted); assert(_backGestureController == null); _backGestureController = widget.onStartPopGesture(); } void _handleDragUpdate(DragUpdateDetails details) { assert(mounted); assert(_backGestureController != null); _backGestureController!.dragUpdate( _convertToLogical(details.primaryDelta! / context.size!.width)); } } const _kSideBarWidth = 420.0; class SideBarRoute extends PopupRoute { SideBarRoute(this.child); final Widget child; @override Color? get barrierColor => Colors.transparent; @override bool get barrierDismissible => true; @override String? get barrierLabel => "side bar"; @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return Align( alignment: Alignment.centerRight, child: Stack( children: [ Positioned( right: 0, top: 0, bottom: 0, child: Container( decoration: BoxDecoration( color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.98), borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4)) ), constraints: BoxConstraints(maxWidth: min(_kSideBarWidth, MediaQuery.of(context).size.width)), width: double.infinity, child: child, ), ) ], ), ); } @override Duration get transitionDuration => const Duration(milliseconds: 200); static bool _isPopGestureEnabled(PopupRoute route) { if (route.isFirst || route.willHandlePopInternally || route.popDisposition == RoutePopDisposition.doNotPop || route.animation!.status != AnimationStatus.completed || route.secondaryAnimation!.status != AnimationStatus.dismissed || route.navigator!.userGestureInProgress) { return false; } return true; } bool get enableIOSGesture => true; @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { var offset = Tween(begin: const Offset(1, 0), end: const Offset(0, 0)); return SlideTransition( position: offset.animate(CurvedAnimation( parent: animation, curve: Curves.fastOutSlowIn, )), child: enableIOSGesture ? IOSBackGestureDetector( gestureWidth: _kBackGestureWidth, enabledCallback: () => _isPopGestureEnabled(this), onStartPopGesture: () => _startPopGesture(this), child: child) : child, ); } IOSBackGestureController _startPopGesture(PopupRoute route) { return IOSBackGestureController(route.controller!, route.navigator!); } }