Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions RadialActions.Tests/PieSelectionControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Windows.Input;

namespace RadialActions.Tests;

public class PieSelectionControllerTests
{
private static readonly PieSelectionController.Item[] DirectionalItems =
[
new(0, -60),
new(1, 20),
new(2, 100),
new(3, 170),
];

[Theory]
[InlineData(Key.Up, 0)]
[InlineData(Key.Right, 1)]
[InlineData(Key.Down, 2)]
[InlineData(Key.Left, 3)]
public void HandleArrowKey_FirstSelectionChoosesClosestSlice(Key key, int expectedIndex)
{
var controller = new PieSelectionController();

controller.HandleArrowKey(key, DirectionalItems);

Assert.Equal(expectedIndex, controller.SelectedIndex);
}

[Theory]
[InlineData(Key.Left)]
[InlineData(Key.Up)]
public void HandleArrowKey_LeftAndUpWrapBackward(Key key)
{
var controller = new PieSelectionController();
PieSelectionController.Item[] items =
[
new(0, -10),
new(1, 100),
new(2, 170),
];

controller.HandleArrowKey(Key.Right, items);
controller.HandleArrowKey(key, items);

Assert.Equal(2, controller.SelectedIndex);
}

[Theory]
[InlineData(Key.Right)]
[InlineData(Key.Down)]
public void HandleArrowKey_RightAndDownWrapForward(Key key)
{
var controller = new PieSelectionController();
PieSelectionController.Item[] items =
[
new(0, -10),
new(1, 100),
new(2, 170),
];

controller.HandleArrowKey(Key.Left, items);
controller.HandleArrowKey(key, items);

Assert.Equal(0, controller.SelectedIndex);
}

[Fact]
public void EnsureSelectionIsValid_ResetsWhenSelectedIndexIsMissing()
{
var controller = new PieSelectionController();
PieSelectionController.Item[] originalItems =
[
new(0, -10),
new(1, 100),
new(2, 170),
];
PieSelectionController.Item[] updatedItems =
[
new(0, -10),
new(2, 170),
];

controller.HandleArrowKey(Key.Down, originalItems);
controller.EnsureSelectionIsValid(updatedItems);

Assert.Equal(PieSelectionController.NoSelection, controller.SelectedIndex);
}
}
140 changes: 140 additions & 0 deletions RadialActions/Pie/PieAnimationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace RadialActions;

internal sealed class PieAnimationService
{
public void ApplyBrushColor(
SolidColorBrush brush,
Color color,
bool animate,
Duration duration,
IEasingFunction easingFunction)
{
if (animate)
{
AnimateBrushColor(brush, color, duration, easingFunction);
return;
}

brush.BeginAnimation(SolidColorBrush.ColorProperty, null);
brush.Color = color;
}

public void AnimateBrushColor(
SolidColorBrush brush,
Color toColor,
Duration duration,
IEasingFunction easingFunction)
{
if (IsReducedMotionEnabled())
{
brush.BeginAnimation(SolidColorBrush.ColorProperty, null);
brush.Color = toColor;
return;
}

var colorAnimation = new ColorAnimation
{
To = toColor,
Duration = duration,
EasingFunction = easingFunction,
};

brush.BeginAnimation(SolidColorBrush.ColorProperty, colorAnimation, HandoffBehavior.SnapshotAndReplace);
}

public void AnimateOpacity(
UIElement element,
double toOpacity,
Duration duration,
IEasingFunction easingFunction,
Action onCompleted = null)
{
if (IsReducedMotionEnabled())
{
element.BeginAnimation(UIElement.OpacityProperty, null);
element.Opacity = toOpacity;
onCompleted?.Invoke();
return;
}

var opacityAnimation = new DoubleAnimation
{
To = toOpacity,
Duration = duration,
EasingFunction = easingFunction,
};

if (onCompleted != null)
{
opacityAnimation.Completed += (_, _) => onCompleted();
}

element.BeginAnimation(UIElement.OpacityProperty, opacityAnimation, HandoffBehavior.SnapshotAndReplace);
}

public void AnimateClickDown(UIElement target, Duration duration, IEasingFunction easingFunction)
{
if (target.RenderTransform is not ScaleTransform scaleTransform)
{
scaleTransform = new ScaleTransform(1, 1);
target.RenderTransform = scaleTransform;
target.RenderTransformOrigin = new Point(0.5, 0.5);
}

if (IsReducedMotionEnabled())
{
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, null);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, null);
scaleTransform.ScaleX = 0.95;
scaleTransform.ScaleY = 0.95;
return;
}

var scaleAnimation = new DoubleAnimation
{
To = 0.95,
Duration = duration,
EasingFunction = easingFunction,
FillBehavior = FillBehavior.HoldEnd,
};

scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
}

public void AnimateClickUp(UIElement target, Duration duration, IEasingFunction easingFunction)
{
if (target.RenderTransform is not ScaleTransform scaleTransform)
{
return;
}

if (IsReducedMotionEnabled())
{
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, null);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, null);
scaleTransform.ScaleX = 1;
scaleTransform.ScaleY = 1;
return;
}

var scaleAnimation = new DoubleAnimation
{
To = 1,
Duration = duration,
EasingFunction = easingFunction,
};

scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
}

public static bool IsReducedMotionEnabled()
{
return !SystemParameters.ClientAreaAnimation;
}
}
12 changes: 12 additions & 0 deletions RadialActions/Pie/PieCenterVisual.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Windows.Controls;
using System.Windows.Media;

namespace RadialActions;

internal sealed class PieCenterVisual
{
public required Grid Target { get; init; }
public required SolidColorBrush FillBrush { get; init; }
public required SolidColorBrush StrokeBrush { get; init; }
public required TextBlock Icon { get; init; }
}
Loading
Loading