using System; using System.Collections.Generic; using RSG; using Unity.UIWidgets.animation; using Unity.UIWidgets.foundation; using Unity.UIWidgets.scheduler; using Unity.UIWidgets.ui; namespace Unity.UIWidgets.widgets { public abstract class OverlayRoute : Route { readonly List _overlayEntries = new List(); public OverlayRoute( RouteSettings settings = null ) : base(settings) { } public override List overlayEntries { get { return _overlayEntries; } } protected virtual bool finishedWhenPopped { get { return true; } } public abstract ICollection createOverlayEntries(); protected internal override void install(OverlayEntry insertionPoint) { D.assert(_overlayEntries.isEmpty()); _overlayEntries.AddRange(createOverlayEntries()); navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); base.install(insertionPoint); } protected internal override bool didPop(object result) { var returnValue = base.didPop(result); D.assert(returnValue); if (finishedWhenPopped) { navigator.finalizeRoute(this); } return returnValue; } protected internal override void dispose() { foreach (var entry in _overlayEntries) { entry.remove(); } _overlayEntries.Clear(); base.dispose(); } } public abstract class TransitionRoute : OverlayRoute { public TransitionRoute( RouteSettings settings = null ) : base(settings) { } public IPromise completed { get { return _transitionCompleter; } } internal readonly Promise _transitionCompleter = new Promise(); public virtual TimeSpan transitionDuration { get; } public virtual bool opaque { get; } protected override bool finishedWhenPopped { get { return controller.status == AnimationStatus.dismissed; } } public virtual Animation animation { get { return _animation; } } internal Animation _animation; public AnimationController controller { get { return _controller; } } internal AnimationController _controller; public virtual AnimationController createAnimationController() { D.assert(_transitionCompleter.CurState == PromiseState.Pending, () => $"Cannot reuse a {GetType()} after disposing it."); TimeSpan duration = transitionDuration; D.assert(duration >= TimeSpan.Zero); return new AnimationController( duration: duration, debugLabel: debugLabel, vsync: navigator ); } public virtual Animation createAnimation() { D.assert(_transitionCompleter.CurState == PromiseState.Pending, () => $"Cannot reuse a {GetType()} after disposing it."); D.assert(_controller != null); return _controller.view; } object _result; internal void _handleStatusChanged(AnimationStatus status) { switch (status) { case AnimationStatus.completed: if (overlayEntries.isNotEmpty()) { overlayEntries.first().opaque = opaque; } break; case AnimationStatus.forward: case AnimationStatus.reverse: if (overlayEntries.isNotEmpty()) { overlayEntries.first().opaque = false; } break; case AnimationStatus.dismissed: // We might still be an active route if a subclass is controlling the // the transition and hits the dismissed status. For example, the iOS // back gesture drives this animation to the dismissed status before // popping the navigator. if (!isActive) { navigator.finalizeRoute(this); D.assert(overlayEntries.isEmpty()); } break; } changedInternalState(); } public virtual Animation secondaryAnimation { get { return _secondaryAnimation; } } readonly ProxyAnimation _secondaryAnimation = new ProxyAnimation(Animations.kAlwaysDismissedAnimation); protected internal override void install(OverlayEntry insertionPoint) { D.assert(!_transitionCompleter.isCompleted, () => $"Cannot install a {GetType()} after disposing it."); _controller = createAnimationController(); D.assert(_controller != null, () => $"{GetType()}.createAnimationController() returned null."); _animation = createAnimation(); D.assert(_animation != null, () => $"{GetType()}.createAnimation() returned null."); base.install(insertionPoint); } protected internal override TickerFuture didPush() { D.assert(_controller != null, () => $"{GetType()}.didPush called before calling install() or after calling dispose()."); D.assert(!_transitionCompleter.isCompleted, () => $"Cannot reuse a {GetType()} after disposing it."); _animation.addStatusListener(_handleStatusChanged); return _controller.forward(); } protected internal override void didReplace(Route oldRoute) { D.assert(_controller != null, () => $"{GetType()}.didReplace called before calling install() or after calling dispose()."); D.assert(!_transitionCompleter.isCompleted, () => $"Cannot reuse a {GetType()} after disposing it."); if (oldRoute is TransitionRoute route) { _controller.setValue(route._controller.value); } _animation.addStatusListener(_handleStatusChanged); base.didReplace(oldRoute); } protected internal override bool didPop(object result) { D.assert(_controller != null, () => $"{GetType()}.didPop called before calling install() or after calling dispose()."); D.assert(!_transitionCompleter.isCompleted, () => $"Cannot reuse a {GetType()} after disposing it."); _result = result; _controller.reverse(); return base.didPop(result); } protected internal override void didPopNext(Route nextRoute) { D.assert(_controller != null, () => $"{GetType()}.didPopNext called before calling install() or after calling dispose()."); D.assert(!_transitionCompleter.isCompleted, () => $"Cannot reuse a {GetType()} after disposing it."); _updateSecondaryAnimation(nextRoute); base.didPopNext(nextRoute); } protected internal override void didChangeNext(Route nextRoute) { D.assert(_controller != null, () => $"{GetType()}.didChangeNext called before calling install() or after calling dispose()."); D.assert(!_transitionCompleter.isCompleted, () => $"Cannot reuse a {GetType()} after disposing it."); _updateSecondaryAnimation(nextRoute); base.didChangeNext(nextRoute); } void _updateSecondaryAnimation(Route nextRoute) { if (nextRoute is TransitionRoute && canTransitionTo((TransitionRoute) nextRoute) && ((TransitionRoute) nextRoute).canTransitionFrom(this)) { Animation current = _secondaryAnimation.parent; if (current != null) { if (current is TrainHoppingAnimation) { TrainHoppingAnimation newAnimation = null; newAnimation = new TrainHoppingAnimation( ((TrainHoppingAnimation) current).currentTrain, ((TransitionRoute) nextRoute)._animation, onSwitchedTrain: () => { D.assert(_secondaryAnimation.parent == newAnimation); D.assert(newAnimation.currentTrain == ((TransitionRoute) nextRoute)._animation); _secondaryAnimation.parent = newAnimation.currentTrain; newAnimation.dispose(); } ); _secondaryAnimation.parent = newAnimation; ((TrainHoppingAnimation) current).dispose(); } else { _secondaryAnimation.parent = new TrainHoppingAnimation(current, ((TransitionRoute) nextRoute)._animation); } } else { _secondaryAnimation.parent = ((TransitionRoute) nextRoute)._animation; } } else { _secondaryAnimation.parent = Animations.kAlwaysDismissedAnimation; } } public virtual bool canTransitionTo(TransitionRoute nextRoute) { return true; } public virtual bool canTransitionFrom(TransitionRoute previousRoute) { return true; } protected internal override void dispose() { D.assert(!_transitionCompleter.isCompleted, () => $"Cannot dispose a {GetType()} twice."); _controller?.dispose(); _transitionCompleter.Resolve(_result); base.dispose(); } public string debugLabel { get { return $"{GetType()}"; } } public override string ToString() { return $"{GetType()}(animation: {_controller}"; } } public class LocalHistoryEntry { public LocalHistoryEntry(VoidCallback onRemove = null) { this.onRemove = onRemove; } public readonly VoidCallback onRemove; internal LocalHistoryRoute _owner; public void remove() { _owner.removeLocalHistoryEntry(this); D.assert(_owner == null); } internal void _notifyRemoved() { onRemove?.Invoke(); } } public interface LocalHistoryRoute { void addLocalHistoryEntry(LocalHistoryEntry entry); void removeLocalHistoryEntry(LocalHistoryEntry entry); Route route { get; } } // todo make it to mixin public abstract class LocalHistoryRouteTransitionRoute : TransitionRoute, LocalHistoryRoute { List _localHistory; protected LocalHistoryRouteTransitionRoute(RouteSettings settings = null) : base(settings: settings) { } public void addLocalHistoryEntry(LocalHistoryEntry entry) { D.assert(entry._owner == null); entry._owner = this; _localHistory = _localHistory ?? new List(); var wasEmpty = _localHistory.isEmpty(); _localHistory.Add(entry); if (wasEmpty) { changedInternalState(); } } public void removeLocalHistoryEntry(LocalHistoryEntry entry) { D.assert(entry != null); D.assert(entry._owner == this); D.assert(_localHistory.Contains(entry)); _localHistory.Remove(entry); entry._owner = null; entry._notifyRemoved(); if (_localHistory.isEmpty()) { changedInternalState(); } } public override IPromise willPop() { if (willHandlePopInternally) { return Promise.Resolved(RoutePopDisposition.pop); } return base.willPop(); } protected internal override bool didPop(object result) { if (_localHistory != null && _localHistory.isNotEmpty()) { var entry = _localHistory.removeLast(); D.assert(entry._owner == this); entry._owner = null; entry._notifyRemoved(); if (_localHistory.isEmpty()) { changedInternalState(); } return false; } return base.didPop(result); } public override bool willHandlePopInternally { get { return _localHistory != null && _localHistory.isNotEmpty(); } } public Route route { get { return this; } } } public class _ModalScopeStatus : InheritedWidget { public _ModalScopeStatus(Key key = null, bool isCurrent = false, bool canPop = false, Route route = null, Widget child = null) : base(key: key, child: child) { D.assert(route != null); D.assert(child != null); this.isCurrent = isCurrent; this.canPop = canPop; this.route = route; } public readonly bool isCurrent; public readonly bool canPop; public readonly Route route; public override bool updateShouldNotify(InheritedWidget oldWidget) { return isCurrent != ((_ModalScopeStatus) oldWidget).isCurrent || canPop != ((_ModalScopeStatus) oldWidget).canPop || route != ((_ModalScopeStatus) oldWidget).route; } public override void debugFillProperties(DiagnosticPropertiesBuilder description) { base.debugFillProperties(description); description.add(new FlagProperty("isCurrent", value: isCurrent, ifTrue: "active", ifFalse: "inactive")); description.add(new FlagProperty("canPop", value: canPop, ifTrue: "can pop")); } } public class _ModalScope : StatefulWidget { public _ModalScope(Key key = null, ModalRoute route = null) : base(key) { this.route = route; } public readonly ModalRoute route; public override State createState() { return new _ModalScopeState(); } } public class _ModalScopeState : State<_ModalScope> { Widget _page; Listenable _listenable; public override void initState() { base.initState(); var animations = new List { }; if (widget.route.animation != null) { animations.Add(widget.route.animation); } if (widget.route.secondaryAnimation != null) { animations.Add(widget.route.secondaryAnimation); } _listenable = ListenableUtils.merge(animations); } public override void didUpdateWidget(StatefulWidget oldWidget) { base.didUpdateWidget(oldWidget); D.assert(widget.route == ((_ModalScope) oldWidget).route); } public override void didChangeDependencies() { base.didChangeDependencies(); _page = null; } internal void _forceRebuildPage() { setState(() => { _page = null; }); } internal void _routeSetState(VoidCallback fn) { setState(fn); } public override Widget build(BuildContext context) { _page = _page ?? new RepaintBoundary( key: widget.route._subtreeKey, // immutable child: new Builder( builder: (BuildContext _context) => widget.route.buildPage( _context, widget.route.animation, widget.route.secondaryAnimation )) ); return new _ModalScopeStatus( route: widget.route, isCurrent: widget.route.isCurrent, canPop: widget.route.canPop, child: new Offstage( offstage: widget.route.offstage, child: new PageStorage( bucket: widget.route._storageBucket, child: new FocusScope( node: widget.route.focusScopeNode, child: new RepaintBoundary( child: new AnimatedBuilder( animation: _listenable, // immutable builder: (BuildContext _context, Widget child) => widget.route.buildTransitions( _context, widget.route.animation, widget.route.secondaryAnimation, new IgnorePointer( ignoring: widget.route.animation?.status == AnimationStatus.reverse, child: child ) ), child: _page ) ) ) ) ) ); } } public abstract class ModalRoute : LocalHistoryRouteTransitionRoute { protected ModalRoute() {} protected ModalRoute(RouteSettings settings) : base(settings) { } public static Color _kTransparent = new Color(0x00000000); public static ModalRoute of(BuildContext context) { _ModalScopeStatus widget = (_ModalScopeStatus) context.inheritFromWidgetOfExactType(typeof(_ModalScopeStatus)); return (ModalRoute) widget?.route; } protected virtual void setState(VoidCallback fn) { if (_scopeKey.currentState != null) { _scopeKey.currentState._routeSetState(fn); } else { fn(); } } public RoutePredicate withName(string name) { return (Route route) => !route.willHandlePopInternally && route is ModalRoute && route.settings.name == name; } public abstract Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation); public virtual Widget buildTransitions( BuildContext context, Animation animation, Animation secondaryAnimation, Widget child ) { return child; } public readonly FocusScopeNode focusScopeNode = new FocusScopeNode(); protected internal override void install(OverlayEntry insertionPoint) { base.install(insertionPoint); _animationProxy = new ProxyAnimation(base.animation); _secondaryAnimationProxy = new ProxyAnimation(base.secondaryAnimation); } protected internal override TickerFuture didPush() { navigator.focusScopeNode.setFirstFocus(focusScopeNode); return base.didPush(); } protected internal override void dispose() { focusScopeNode.detach(); base.dispose(); } public virtual bool barrierDismissible { get; } public virtual Color barrierColor { get; } public virtual bool maintainState { get; } public bool offstage { get { return _offstage; } set { if (_offstage == value) { return; } setState(() => { _offstage = value; }); _animationProxy.parent = _offstage ? Animations.kAlwaysCompleteAnimation : base.animation; _secondaryAnimationProxy.parent = _offstage ? Animations.kAlwaysDismissedAnimation : base.secondaryAnimation; } } bool _offstage = false; public BuildContext subtreeContext { get { return _subtreeKey.currentContext; } } public override Animation animation { get { return _animationProxy; } } ProxyAnimation _animationProxy; public override Animation secondaryAnimation { get { return _secondaryAnimationProxy; } } ProxyAnimation _secondaryAnimationProxy; readonly List _willPopCallbacks = new List(); public override IPromise willPop() { _ModalScopeState scope = _scopeKey.currentState; D.assert(scope != null); var callbacks = new List(_willPopCallbacks); Promise result = new Promise(); Action fn = null; fn = (int index) => { if (index < callbacks.Count) { callbacks[index]().Then((pop) => { if (!pop) { result.Resolve(RoutePopDisposition.doNotPop); } else { fn(index + 1); } }); } else { base.willPop().Then((pop) => result.Resolve(pop)); } }; fn(0); return result; } public void addScopedWillPopCallback(WillPopCallback callback) { D.assert(_scopeKey.currentState != null, () => "Tried to add a willPop callback to a route that is not currently in the tree."); _willPopCallbacks.Add(callback); } public void removeScopedWillPopCallback(WillPopCallback callback) { D.assert(_scopeKey.currentState != null, () => "Tried to remove a willPop callback from a route that is not currently in the tree."); _willPopCallbacks.Remove(callback); } public bool hasScopedWillPopCallback { get { return _willPopCallbacks.isNotEmpty(); } } protected internal override void didChangePrevious(Route previousRoute) { base.didChangePrevious(previousRoute); changedInternalState(); } protected internal override void changedInternalState() { base.changedInternalState(); setState(() => { }); _modalBarrier.markNeedsBuild(); } protected internal override void changedExternalState() { base.changedExternalState(); _scopeKey.currentState?._forceRebuildPage(); } public bool canPop { get { return !isFirst || willHandlePopInternally; } } readonly GlobalKey<_ModalScopeState> _scopeKey = new LabeledGlobalKey<_ModalScopeState>(); internal readonly GlobalKey _subtreeKey = new LabeledGlobalKey<_ModalScopeState>(); internal readonly PageStorageBucket _storageBucket = new PageStorageBucket(); static readonly Animatable _easeCurveTween = new CurveTween(curve: Curves.ease); OverlayEntry _modalBarrier; Widget _buildModalBarrier(BuildContext context) { Widget barrier; if (barrierColor != null && !offstage) { // changedInternalState is called if these update D.assert(barrierColor != _kTransparent); Animation color = new ColorTween( begin: _kTransparent, end: barrierColor // changedInternalState is called if this updates ).chain(_easeCurveTween).animate(animation); barrier = new AnimatedModalBarrier( color: color, dismissible: barrierDismissible ); } else { barrier = new ModalBarrier( dismissible: barrierDismissible ); } return new IgnorePointer( ignoring: animation.status == AnimationStatus.reverse || animation.status == AnimationStatus.dismissed, child: barrier ); } Widget _modalScopeCache; Widget _buildModalScope(BuildContext context) { return _modalScopeCache = _modalScopeCache ?? new _ModalScope( key: _scopeKey, route: this // _ModalScope calls buildTransitions() and buildChild(), defined above ); } public override ICollection createOverlayEntries() { _modalBarrier = new OverlayEntry(builder: _buildModalBarrier); var content = new OverlayEntry( builder: _buildModalScope, maintainState: maintainState ); return new List {_modalBarrier, content}; } public override string ToString() { return $"{GetType()}({settings}, animation: {_animation})"; } } public abstract class PopupRoute : ModalRoute { protected PopupRoute( RouteSettings settings = null ) : base(settings: settings) { } public override bool opaque { get { return false; } } public override bool maintainState { get { return true; } } } public class RouteObserve : NavigatorObserver where R : Route { readonly Dictionary> _listeners = new Dictionary>(); public void subscribe(RouteAware routeAware, R route) { D.assert(routeAware != null); D.assert(route != null); HashSet subscribers = _listeners.putIfAbsent(route, () => new HashSet()); if (subscribers.Add(routeAware)) { routeAware.didPush(); } } public void unsubscribe(RouteAware routeAware) { D.assert(routeAware != null); foreach (R route in _listeners.Keys) { HashSet subscribers = _listeners[route]; subscribers?.Remove(routeAware); } } public override void didPop(Route route, Route previousRoute) { if (route is R && previousRoute is R) { var previousSubscribers = _listeners.getOrDefault((R) previousRoute); if (previousSubscribers != null) { foreach (RouteAware routeAware in previousSubscribers) { routeAware.didPopNext(); } } var subscribers = _listeners.getOrDefault((R) route); if (subscribers != null) { foreach (RouteAware routeAware in subscribers) { routeAware.didPop(); } } } } public override void didPush(Route route, Route previousRoute) { if (route is R && previousRoute is R) { var previousSubscribers = _listeners.getOrDefault((R) previousRoute); if (previousSubscribers != null) { foreach (RouteAware routeAware in previousSubscribers) { routeAware.didPushNext(); } } } } } public interface RouteAware { void didPopNext(); void didPush(); void didPop(); void didPushNext(); } class _DialogRoute : PopupRoute { internal _DialogRoute(RoutePageBuilder pageBuilder = null, bool barrierDismissible = true, Color barrierColor = null, TimeSpan? transitionDuration = null, RouteTransitionsBuilder transitionBuilder = null, RouteSettings setting = null) : base(settings: setting) { _pageBuilder = pageBuilder; this.barrierDismissible = barrierDismissible; this.barrierColor = barrierColor ?? new Color(0x80000000); this.transitionDuration = transitionDuration ?? TimeSpan.FromMilliseconds(200); _transitionBuilder = transitionBuilder; } readonly RoutePageBuilder _pageBuilder; public override bool barrierDismissible { get; } public override Color barrierColor { get; } public override TimeSpan transitionDuration { get; } readonly RouteTransitionsBuilder _transitionBuilder; public override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return _pageBuilder(context, animation, secondaryAnimation); } public override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { if (_transitionBuilder == null) { return new FadeTransition( opacity: new CurvedAnimation( parent: animation, curve: Curves.linear ), child: child); } return _transitionBuilder(context, animation, secondaryAnimation, child); } } public static class DialogUtils { public static IPromise showGeneralDialog( BuildContext context = null, RoutePageBuilder pageBuilder = null, bool barrierDismissible = false, Color barrierColor = null, TimeSpan? transitionDuration = null, RouteTransitionsBuilder transitionBuilder = null ) { D.assert(pageBuilder != null); return Navigator.of(context, rootNavigator: true).push(new _DialogRoute( pageBuilder: pageBuilder, barrierDismissible: barrierDismissible, barrierColor: barrierColor, transitionDuration: transitionDuration, transitionBuilder: transitionBuilder )); } } public delegate Widget RoutePageBuilder(BuildContext context, Animation animation, Animation secondaryAnimation); public delegate Widget RouteTransitionsBuilder(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child); }