diff --git a/src/Files.App/Views/ModernShellPage.xaml b/src/Files.App/Views/ModernShellPage.xaml
index 76db10f8d7f6..66d20c3e2efb 100644
--- a/src/Files.App/Views/ModernShellPage.xaml
+++ b/src/Files.App/Views/ModernShellPage.xaml
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Files.App.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:wct="using:CommunityToolkit.WinUI.UI"
xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters"
x:Name="RootPage"
KeyboardAcceleratorPlacementMode="Hidden"
@@ -51,12 +52,52 @@
Modifiers="Menu" />
-
+
+
+
+
+
+
+
+
+
diff --git a/src/Files.App/Views/ModernShellPage.xaml.cs b/src/Files.App/Views/ModernShellPage.xaml.cs
index 8b37cf7628bb..8c86a8774500 100644
--- a/src/Files.App/Views/ModernShellPage.xaml.cs
+++ b/src/Files.App/Views/ModernShellPage.xaml.cs
@@ -33,6 +33,8 @@ public sealed partial class ModernShellPage : BaseShellPage
protected override Frame ItemDisplay => ItemDisplayFrame;
+ private NavigationInteractionTracker _navigationInteractionTracker;
+
public Thickness CurrentInstanceBorderThickness
{
get => (Thickness)GetValue(CurrentInstanceBorderThicknessProperty);
@@ -56,8 +58,10 @@ public ModernShellPage() : base(new CurrentInstanceViewModel())
FilesystemViewModel.GitDirectoryUpdated += FilesystemViewModel_GitDirectoryUpdated;
ToolbarViewModel.PathControlDisplayText = "Home".GetLocalizedResource();
-
ToolbarViewModel.RefreshWidgetsRequested += ModernShellPage_RefreshWidgetsRequested;
+
+ _navigationInteractionTracker = new NavigationInteractionTracker(this, BackIcon, ForwardIcon);
+ _navigationInteractionTracker.NavigationRequested += OverscrollNavigationRequested;
}
private void ModernShellPage_RefreshWidgetsRequested(object sender, EventArgs e)
@@ -179,6 +183,8 @@ private async void ItemDisplayFrame_Navigated(object sender, NavigationEventArgs
if (parameters.IsLayoutSwitch)
FilesystemViewModel_DirectoryInfoUpdated(sender, EventArgs.Empty);
+ _navigationInteractionTracker.CanNavigateBackward = CanNavigateBackward;
+ _navigationInteractionTracker.CanNavigateForward = CanNavigateForward;
}
private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
@@ -225,6 +231,20 @@ private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, Keybo
}
}
+ private void OverscrollNavigationRequested(object? sender, OverscrollNavigationEventArgs e)
+ {
+ switch (e)
+ {
+ case OverscrollNavigationEventArgs.Forward:
+ Forward_Click();
+ break;
+
+ case OverscrollNavigationEventArgs.Back:
+ Back_Click();
+ break;
+ }
+ }
+
public override void Back_Click()
{
ToolbarViewModel.CanGoBack = false;
@@ -289,6 +309,8 @@ public override void Up_Click()
public override void Dispose()
{
ToolbarViewModel.RefreshWidgetsRequested -= ModernShellPage_RefreshWidgetsRequested;
+ _navigationInteractionTracker.NavigationRequested -= OverscrollNavigationRequested;
+ _navigationInteractionTracker.Dispose();
base.Dispose();
}
diff --git a/src/Files.App/Views/NavigationInteractionTracker.cs b/src/Files.App/Views/NavigationInteractionTracker.cs
new file mode 100644
index 000000000000..e8c0fad6faf3
--- /dev/null
+++ b/src/Files.App/Views/NavigationInteractionTracker.cs
@@ -0,0 +1,261 @@
+// Copyright (c) 2023 Files Community
+// Licensed under the MIT License. See the LICENSE.
+
+using Microsoft.UI.Composition;
+using Microsoft.UI.Composition.Interactions;
+using Microsoft.UI.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Hosting;
+using Microsoft.UI.Xaml.Input;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+
+namespace Files.App.Views
+{
+ internal class NavigationInteractionTracker : IDisposable
+ {
+ public bool CanNavigateForward
+ {
+ get
+ {
+ _props.TryGetBoolean(nameof(CanNavigateForward), out bool val);
+ return val;
+ }
+ set
+ {
+ _props.InsertBoolean(nameof(CanNavigateForward), value);
+ _tracker.MaxPosition = new(value ? 96f : 0f);
+ }
+ }
+
+ public bool CanNavigateBackward
+ {
+ get
+ {
+ _props.TryGetBoolean(nameof(CanNavigateBackward), out bool val);
+ return val;
+ }
+ set
+ {
+ _props.InsertBoolean(nameof(CanNavigateBackward), value);
+ _tracker.MinPosition = new(value ? -96f : 0f);
+ }
+ }
+
+ private UIElement _rootElement;
+ private UIElement _backIcon;
+ private UIElement _forwardIcon;
+
+ private PointerEventHandler _pointerPressedHandler;
+
+ private Visual _rootVisual;
+ private Visual _backVisual;
+ private Visual _forwardVisual;
+
+ private InteractionTracker _tracker;
+ private VisualInteractionSource _source;
+ private InteractionTrackerOwner _trackerOwner;
+ private CompositionPropertySet _props;
+
+ public event EventHandler? NavigationRequested;
+
+ private bool _disposed;
+
+ public NavigationInteractionTracker(UIElement rootElement, UIElement backIcon, UIElement forwardIcon)
+ {
+ _rootElement = rootElement;
+ _backIcon = backIcon;
+ _forwardIcon = forwardIcon;
+
+ ElementCompositionPreview.SetIsTranslationEnabled(_backIcon, true);
+ ElementCompositionPreview.SetIsTranslationEnabled(_forwardIcon, true);
+ _rootVisual = ElementCompositionPreview.GetElementVisual(_rootElement);
+ _backVisual = ElementCompositionPreview.GetElementVisual(_backIcon);
+ _forwardVisual = ElementCompositionPreview.GetElementVisual(_forwardIcon);
+
+ SetupInteractionTracker();
+
+ _props = _rootVisual.Compositor.CreatePropertySet();
+ CanNavigateBackward = false;
+ CanNavigateForward = false;
+
+ SetupAnimations();
+
+ _pointerPressedHandler = new(PointerPressed);
+ _rootElement.AddHandler(UIElement.PointerPressedEvent, _pointerPressedHandler, true);
+ }
+
+ [MemberNotNull(nameof(_tracker), nameof(_source), nameof(_trackerOwner))]
+ private void SetupInteractionTracker()
+ {
+ var compositor = _rootVisual.Compositor;
+
+ _trackerOwner = new(this);
+ _tracker = InteractionTracker.CreateWithOwner(compositor, _trackerOwner);
+ _tracker.MinPosition = new Vector3(-96f);
+ _tracker.MaxPosition = new Vector3(96f);
+
+ _source = VisualInteractionSource.Create(_rootVisual);
+ _source.ManipulationRedirectionMode = VisualInteractionSourceRedirectionMode.CapableTouchpadOnly;
+ _source.PositionXSourceMode = InteractionSourceMode.EnabledWithoutInertia;
+ _source.PositionXChainingMode = InteractionChainingMode.Always;
+ _source.PositionYSourceMode = InteractionSourceMode.Disabled;
+ _tracker.InteractionSources.Add(_source);
+ }
+
+ private void SetupAnimations()
+ {
+ var compositor = _rootVisual.Compositor;
+
+ var backResistance = CreateResistanceCondition(-96f, 0f);
+ var forwardResistance = CreateResistanceCondition(0f, 96f);
+ List conditionalValues = new() { backResistance, forwardResistance };
+ _source.ConfigureDeltaPositionXModifiers(conditionalValues);
+
+ var backAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, -96, 0) * 2) - 48");
+ backAnim.SetReferenceParameter("tracker", _tracker);
+ backAnim.SetReferenceParameter("props", _props);
+ _backVisual.StartAnimation("Translation.X", backAnim);
+
+ var forwardAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, 0, 96) * 2) + 48");
+ forwardAnim.SetReferenceParameter("tracker", _tracker);
+ forwardAnim.SetReferenceParameter("props", _props);
+ _forwardVisual.StartAnimation("Translation.X", forwardAnim);
+ }
+
+ private void PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch)
+ {
+ _source.TryRedirectForManipulation(e.GetCurrentPoint(_rootElement));
+ }
+ }
+
+ private CompositionConditionalValue CreateResistanceCondition(float minValue, float maxValue)
+ {
+ var compositor = _rootVisual.Compositor;
+
+ var resistance = CompositionConditionalValue.Create(compositor);
+ var resistanceCondition = compositor.CreateExpressionAnimation($"tracker.Position.X > {minValue} && tracker.Position.X < {maxValue}");
+ resistanceCondition.SetReferenceParameter("tracker", _tracker);
+ var resistanceValue = compositor.CreateExpressionAnimation($"source.DeltaPosition.X * (1 - sqrt(1 - square((tracker.Position.X / {minValue + maxValue}) - 1)))");
+ resistanceValue.SetReferenceParameter("source", _source);
+ resistanceValue.SetReferenceParameter("tracker", _tracker);
+ resistance.Condition = resistanceCondition;
+ resistance.Value = resistanceValue;
+
+ return resistance;
+ }
+
+ ~NavigationInteractionTracker()
+ {
+ Dispose();
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _rootElement.RemoveHandler(UIElement.PointerPressedEvent, _pointerPressedHandler);
+ _backVisual.StopAnimation("Translation.X");
+ _forwardVisual.StopAnimation("Translation.X");
+ _tracker.Dispose();
+ _source.Dispose();
+ _props.Dispose();
+
+ _disposed = true;
+ GC.SuppressFinalize(this);
+ }
+
+ private class InteractionTrackerOwner : IInteractionTrackerOwner
+ {
+ private NavigationInteractionTracker _parent;
+ private bool _shouldBounceBack;
+ private bool _shouldAnimate = true;
+ private Vector3KeyFrameAnimation _scaleAnimation;
+ private SpringVector3NaturalMotionAnimation _returnAnimation;
+
+ public InteractionTrackerOwner(NavigationInteractionTracker parent)
+ {
+ _parent = parent;
+
+ var compositor = _parent._rootVisual.Compositor;
+ _scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
+ _scaleAnimation.InsertKeyFrame(0.5f, new(1.3f));
+ _scaleAnimation.InsertKeyFrame(1f, new(1f));
+ _scaleAnimation.Duration = TimeSpan.FromMilliseconds(275);
+
+ _returnAnimation = compositor.CreateSpringVector3Animation();
+ _returnAnimation.FinalValue = new(0f);
+ _returnAnimation.DampingRatio = 1f;
+ }
+
+ public void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args)
+ {
+ if (!_shouldBounceBack)
+ return;
+
+ if (Math.Abs(sender.Position.X) > 64)
+ {
+ _parent._tracker.TryUpdatePosition(new(0f));
+
+ EventHandler? navEvent = _parent.NavigationRequested;
+ if (navEvent is not null)
+ {
+ if (sender.Position.X > 0 && _parent.CanNavigateForward)
+ {
+ navEvent(_parent, OverscrollNavigationEventArgs.Forward);
+ }
+ else if (sender.Position.X < 0 && _parent.CanNavigateBackward)
+ {
+ navEvent(_parent, OverscrollNavigationEventArgs.Back);
+ }
+ }
+ }
+ else
+ {
+ _parent._tracker.TryUpdatePositionWithAnimation(_returnAnimation);
+ }
+ _shouldBounceBack = false;
+ _shouldAnimate = true;
+ }
+
+ public void InteractingStateEntered(InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args)
+ {
+ _shouldBounceBack = true;
+ }
+
+ public void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args)
+ {
+ if (!_shouldAnimate)
+ return;
+
+ if (args.Position.X <= -64)
+ {
+ _parent._backVisual.StartAnimation("Scale", _scaleAnimation);
+ _shouldAnimate = false;
+ }
+ else if (args.Position.X >= 64)
+ {
+ _parent._forwardVisual.StartAnimation("Scale", _scaleAnimation);
+ _shouldAnimate = false;
+ }
+
+ }
+
+ // required to implement IInteractionTrackerOwner
+ public void CustomAnimationStateEntered(InteractionTracker sender, InteractionTrackerCustomAnimationStateEnteredArgs args) { }
+ public void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) { }
+ public void RequestIgnored(InteractionTracker sender, InteractionTrackerRequestIgnoredArgs args) { }
+ }
+ }
+
+ public enum OverscrollNavigationEventArgs
+ {
+ Back,
+ Forward
+ }
+}