OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / KeyframeEditorUiTests.cs

140 lines · csharp ref:
using System;
using System.Linq;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.VisualTree;

using IFSEngine.Animation;
using IFSEngine.Model;

using Xunit;

namespace IFSRenderer.AvaloniaShell.Tests;

/// <summary>
/// The keyframe type dropdown, as a user reaches it.
/// </summary>
/// <remarks>
/// Half of what was reported - "I got no control over it" - was never a missing feature. The
/// interpolation controls existed, hidden until a keyframe is selected, and on Windows they are
/// behind a right-click on the dope diamond. So the test that matters is not that the model
/// supports a type, but that SELECTING A KEYFRAME PUTS THE CONTROL ON SCREEN.
/// </remarks>
public class KeyframeEditorUiTests
{
    private static (AnimationPanel Panel, Channel Channel, CurveCanvas Curve) Panel()
    {
        var fractal = new IFS();
        foreach ((double t, double v) in new[] { (3.0, 0.0), (5.0, 1.0), (7.0, 1.0), (9.0, 2.0) })
            fractal.Dopesheet.AddOrUpdateChannel("Focus", "Camera.FocusDistance",
                TimeOnly.FromTimeSpan(TimeSpan.FromSeconds(t)), v);

        var panel = new AnimationPanel();
        var window = new Window { Width = 1000, Height = 600, Content = panel };
        window.Show();
        panel.SetFractal(fractal);
        window.Measure(new Size(1000, 600));
        window.Arrange(new Rect(0, 0, 1000, 600));

        CurveCanvas curve = panel.GetVisualDescendants().OfType<CurveCanvas>().Single();
        return (panel, fractal.Dopesheet.Channels["Camera.FocusDistance"], curve);
    }

    private static ComboBox TypeCombo(AnimationPanel panel) =>
        panel.GetVisualDescendants().OfType<ComboBox>()
            .Single(c => c.ItemsSource is not null
                         && c.ItemsSource.Cast<object>().Any(i => i.ToString() == "Bezier"));

    [AvaloniaFact]
    public void TheKeyframeEditorIsHiddenUntilAKeyframeIsSelected()
    {
        (AnimationPanel panel, Channel channel, CurveCanvas curve) = Panel();
        ComboBox combo = TypeCombo(panel);

        Assert.Null(curve.SelectedKeyframe);
        Assert.False(combo.IsEffectivelyVisible);

        curve.BeginDrag(channel.Keyframes[1], new Point(0, 0));
        curve.EndDrag();
        Relayout(panel);

        Assert.True(combo.IsEffectivelyVisible,
            "selecting a keyframe did not reveal the type dropdown");
    }

    /// <summary>Visibility changes only take effect once the tree has been measured again.</summary>
    private static void Relayout(AnimationPanel panel)
    {
        Window window = panel.GetVisualRoot() as Window;
        window!.Measure(new Size(1000, 600));
        window.Arrange(new Rect(0, 0, 1000, 600));
    }

    [AvaloniaFact]
    public void TheDropdownOffersBlendersPresetsAndSetsTheSelectedKeyframesType()
    {
        (AnimationPanel panel, Channel channel, CurveCanvas curve) = Panel();
        Keyframe key = channel.Keyframes[1];

        curve.BeginDrag(key, new Point(0, 0));
        curve.EndDrag();

        ComboBox combo = TypeCombo(panel);
        string[] labels = combo.ItemsSource!.Cast<object>().Select(i => i.ToString()).ToArray();

        // Blender's own menu, in its own order.
        Assert.Equal(
            [
                "Constant", "Linear", "Bezier",
                "Sinusoidal", "Quadratic", "Cubic", "Quartic", "Quintic", "Exponential", "Circular",
                "Back", "Bounce", "Elastic",
                "Legacy",
            ],
            labels);

        // A keyed value starts on Blender's default, which is what stops the hold wobbling.
        Assert.Equal(KeyframeType.Bezier, key.Type);

        combo.SelectedIndex = Array.IndexOf(labels, "Constant");

        Assert.Equal(KeyframeType.Constant, key.Type);
        // ...and the legacy mode went with it, so upstream still reads something sane.
        Assert.Equal(IFSEngine.Animation.InterpolationMode.Constant, key.InterpolationMode);
    }

    /// <summary>
    /// The dropdown reaches the evaluator, not just the model: picking a type changes what the
    /// channel is worth between its keyframes.
    /// </summary>
    /// <remarks>
    /// Measured on the 3s..5s RAMP rather than the 5s..7s hold, because a hold between two
    /// equal keys is 1.0 under Linear and under Bezier alike - it would not tell the two apart,
    /// which is the entire point of the fix.
    /// </remarks>
    [AvaloniaFact]
    public void ChangingTheTypeChangesWhatTheChannelEvaluatesTo()
    {
        (AnimationPanel panel, Channel channel, CurveCanvas curve) = Panel();
        Keyframe ramp = channel.Keyframes[0];   // t=3, value 0, running up to t=5, value 1

        curve.BeginDrag(ramp, new Point(0, 0));
        curve.EndDrag();

        ComboBox combo = TypeCombo(panel);
        string[] labels = combo.ItemsSource!.Cast<object>().Select(i => i.ToString()).ToArray();

        combo.SelectedIndex = Array.IndexOf(labels, "Linear");
        Assert.Equal(0.5, channel.EvaluateAt(4), 9);

        combo.SelectedIndex = Array.IndexOf(labels, "Constant");
        Assert.Equal(0.0, channel.EvaluateAt(4), 9);

        // Legacy hands the segment back to InterpolationMode, whatever that now says.
        combo.SelectedIndex = Array.IndexOf(labels, "Legacy");
        Assert.Equal(KeyframeType.Legacy, ramp.Type);
        Assert.Equal(0.0, channel.EvaluateAt(4), 9);
    }
}