Browse Source

Merge branch 'master' into characterBufferReference

pull/9765/head^2
Max Katz 4 years ago
committed by GitHub
parent
commit
bf2cb5efe0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      NOTICE.md
  2. 2
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs
  3. 43
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  4. 375
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  5. 121
      src/Avalonia.Base/Input/Gestures.cs
  6. 51
      src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs
  7. 16
      src/Avalonia.Base/Input/InputElement.cs
  8. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  9. 2
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  10. 19
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  11. 53
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  12. 11
      src/Avalonia.Controls/ContentControl.cs
  13. 13
      src/Avalonia.Controls/Control.cs
  14. 7
      src/Avalonia.Controls/TransitioningContentControl.cs
  15. 4
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  16. 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  17. 240
      tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs
  18. 2
      tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
  19. 66
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
  20. 908
      tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json
  21. 2
      tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package.json
  22. 73
      tests/Avalonia.RenderTests/Controls/AdornerTests.cs
  23. 9
      tests/Avalonia.RenderTests/TestBase.cs
  24. BIN
      tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png
  25. BIN
      tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png

31
NOTICE.md

@ -303,3 +303,34 @@ https://github.com/chromium/chromium
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Flutter
https://github.com/flutter/flutter
//Copyright 2014 The Flutter Authors. All rights reserved.
//Redistribution and use in source and binary forms, with or without modification,
//are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
//LOSS OF USE, DATA, OR PROFITS;
//OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
//ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs

@ -38,7 +38,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return null;
}
var eventTime = (ulong)DateTime.Now.Millisecond;
var eventTime = (ulong)e.EventTime;
var inputRoot = _view.InputRoot;
var actionMasked = e.ActionMasked;
var modifiers = GetModifiers(e.MetaState, e.ButtonState);

43
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -17,7 +17,9 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canVerticallyScroll;
private int _gestureId;
private int _scrollStartDistance = 30;
private Point _pointerPressedPoint;
private VelocityTracker? _velocityTracker;
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
@ -91,7 +93,7 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = e.GetPosition((Visual?)_target);
_trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target);
}
}
@ -111,6 +113,13 @@ namespace Avalonia.Input.GestureRecognizers
_scrolling = true;
if (_scrolling)
{
_velocityTracker = new VelocityTracker();
// Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
_trackedRootPoint = new Point(
_trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance),
_trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance));
_actions!.Capture(e.Pointer, this);
}
}
@ -118,14 +127,11 @@ namespace Avalonia.Input.GestureRecognizers
if (_scrolling)
{
var vector = _trackedRootPoint - rootPoint;
var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ?
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
TimeSpan.Zero;
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
_lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
@ -150,12 +156,14 @@ namespace Avalonia.Input.GestureRecognizers
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer == _tracking && _scrolling)
{
_inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
e.Handled = true;
if (_inertia == default
|| e.Timestamp == 0
@ -183,9 +191,18 @@ namespace Avalonia.Input.GestureRecognizers
var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
// EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd)
{
EndGesture();
return false;

375
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@ -0,0 +1,375 @@
// Code in this file is derived from
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart
using System;
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Input.GestureRecognizers
{
// Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'?
internal readonly record struct Velocity(Vector PixelsPerSecond)
{
public Velocity ClampMagnitude(double minValue, double maxValue)
{
Debug.Assert(minValue >= 0.0);
Debug.Assert(maxValue >= 0.0 && maxValue >= minValue);
double valueSquared = PixelsPerSecond.SquaredLength;
if (valueSquared > maxValue * maxValue)
{
double length = PixelsPerSecond.Length;
return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * maxValue : Vector.Zero);
// preventing double.NaN in Vector PixelsPerSecond is important -- if a NaN eventually gets into a
// ScrollGestureEventArgs it results in runtime errors.
}
if (valueSquared < minValue * minValue)
{
double length = PixelsPerSecond.Length;
return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * minValue : Vector.Zero);
}
return this;
}
}
/// A two dimensional velocity estimate.
///
/// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
/// estimate's [confidence] measures how well the velocity tracker's position
/// data fit a straight line, [duration] is the time that elapsed between the
/// first and last position sample used to compute the velocity, and [offset]
/// is similarly the difference between the first and last positions.
///
/// See also:
///
/// * [VelocityTracker], which computes [VelocityEstimate]s.
/// * [Velocity], which encapsulates (just) a velocity vector and provides some
/// useful velocity operations.
internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset);
internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time);
/// Computes a pointer's velocity based on data from [PointerMoveEvent]s.
///
/// The input data is provided by calling [addPosition]. Adding data is cheap.
///
/// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will
/// compute the velocity based on the data added so far. Only call these when
/// you need to use the velocity, as they are comparatively expensive.
///
/// The quality of the velocity estimation will be better if more data points
/// have been received.
internal class VelocityTracker
{
private const int AssumePointerMoveStoppedMilliseconds = 40;
private const int HistorySize = 20;
private const int HorizonMilliseconds = 100;
private const int MinSampleSize = 3;
private const double MinFlingVelocity = 50.0; // Logical pixels / second
private const double MaxFlingVelocity = 8000.0;
private readonly PointAtTime[] _samples = new PointAtTime[HistorySize];
private int _index = 0;
/// <summary>
/// Adds a position as the given time to the tracker.
/// </summary>
/// <param name="time"></param>
/// <param name="position"></param>
public void AddPosition(TimeSpan time, Vector position)
{
_index++;
if (_index == HistorySize)
{
_index = 0;
}
_samples[_index] = new PointAtTime(true, position, time);
}
/// Returns an estimate of the velocity of the object being tracked by the
/// tracker given the current information available to the tracker.
///
/// Information is added using [addPosition].
///
/// Returns null if there is no data on which to base an estimate.
protected virtual VelocityEstimate? GetVelocityEstimate()
{
Span<double> x = stackalloc double[HistorySize];
Span<double> y = stackalloc double[HistorySize];
Span<double> w = stackalloc double[HistorySize];
Span<double> time = stackalloc double[HistorySize];
int sampleCount = 0;
int index = _index;
var newestSample = _samples[index];
if (!newestSample.Valid)
{
return null;
}
var previousSample = newestSample;
var oldestSample = newestSample;
// Starting with the most recent PointAtTime sample, iterate backwards while
// the samples represent continuous motion.
do
{
var sample = _samples[index];
if (!sample.Valid)
{
break;
}
double age = (newestSample.Time - sample.Time).TotalMilliseconds;
double delta = Math.Abs((sample.Time - previousSample.Time).TotalMilliseconds);
previousSample = sample;
if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds)
{
break;
}
oldestSample = sample;
var position = sample.Point;
x[sampleCount] = position.X;
y[sampleCount] = position.Y;
w[sampleCount] = 1.0;
time[sampleCount] = -age;
index = (index == 0 ? HistorySize : index) - 1;
sampleCount++;
} while (sampleCount < HistorySize);
if (sampleCount >= MinSampleSize)
{
var xFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), x.Slice(0, sampleCount), w.Slice(0, sampleCount));
if (xFit != null)
{
var yFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), y.Slice(0, sampleCount), w.Slice(0, sampleCount));
if (yFit != null)
{
return new VelocityEstimate( // convert from pixels/ms to pixels/s
PixelsPerSecond: new Vector(xFit.Coefficients[1] * 1000, yFit.Coefficients[1] * 1000),
Confidence: xFit.Confidence * yFit.Confidence,
Duration: newestSample.Time - oldestSample.Time,
Offset: newestSample.Point - oldestSample.Point
);
}
}
}
// We're unable to make a velocity estimate but we did have at least one
// valid pointer position.
return new VelocityEstimate(
PixelsPerSecond: Vector.Zero,
Confidence: 1.0,
Duration: newestSample.Time - oldestSample.Time,
Offset: newestSample.Point - oldestSample.Point
);
}
/// <summary>
/// Computes the velocity of the pointer at the time of the last
/// provided data point.
///
/// This can be expensive. Only call this when you need the velocity.
///
/// Returns [Velocity.zero] if there is no data from which to compute an
/// estimate or if the estimated velocity is zero.///
/// </summary>
/// <returns></returns>
internal Velocity GetVelocity()
{
var estimate = GetVelocityEstimate();
if (estimate == null || estimate.PixelsPerSecond.IsDefault)
{
return new Velocity(Vector.Zero);
}
return new Velocity(estimate.PixelsPerSecond);
}
internal virtual Velocity GetFlingVelocity()
{
return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity);
}
}
/// An nth degree polynomial fit to a dataset.
internal class PolynomialFit
{
/// Creates a polynomial fit of the given degree.
///
/// There are n + 1 coefficients in a fit of degree n.
internal PolynomialFit(int degree)
{
Coefficients = new double[degree + 1];
}
/// The polynomial coefficients of the fit.
public double[] Coefficients { get; }
/// An indicator of the quality of the fit.
///
/// Larger values indicate greater quality.
public double Confidence { get; set; }
}
internal class LeastSquaresSolver
{
private const double PrecisionErrorTolerance = 1e-10;
/// <summary>
/// Fits a polynomial of the given degree to the data points.
/// When there is not enough data to fit a curve null is returned.
/// </summary>
public static PolynomialFit? Solve(int degree, ReadOnlySpan<double> x, ReadOnlySpan<double> y, ReadOnlySpan<double> w)
{
if (degree > x.Length)
{
// Not enough data to fit a curve.
return null;
}
PolynomialFit result = new PolynomialFit(degree);
// Shorthands for the purpose of notation equivalence to original C++ code.
int m = x.Length;
int n = degree + 1;
// Expand the X vector to a matrix A, pre-multiplied by the weights.
_Matrix a = new _Matrix(m, stackalloc double[n * m]);
for (int h = 0; h < m; h += 1)
{
a[0, h] = w[h];
for (int i = 1; i < n; i += 1)
{
a[i, h] = a[i - 1, h] * x[h];
}
}
// Apply the Gram-Schmidt process to A to obtain its QR decomposition.
// Orthonormal basis, column-major order Vector.
_Matrix q = new _Matrix(m, stackalloc double[n * m]);
// Upper triangular matrix, row-major order.
_Matrix r = new _Matrix(n, stackalloc double[n * n]);
for (int j = 0; j < n; j += 1)
{
for (int h = 0; h < m; h += 1)
{
q[j, h] = a[j, h];
}
for (int i = 0; i < j; i += 1)
{
double dot = Multiply(q.GetRow(j), q.GetRow(i));
for (int h = 0; h < m; h += 1)
{
q[j, h] = q[j, h] - dot * q[i, h];
}
}
double norm = Norm(q.GetRow(j));
if (norm < PrecisionErrorTolerance)
{
// Vectors are linearly dependent or zero so no solution.
return null;
}
double inverseNorm = 1.0 / norm;
for (int h = 0; h < m; h += 1)
{
q[j, h] = q[j, h] * inverseNorm;
}
for (int i = 0; i < n; i += 1)
{
r[j, i] = i < j ? 0.0 : Multiply(q.GetRow(j), a.GetRow(i));
}
}
// Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
// We just work from bottom-right to top-left calculating B's coefficients.
// "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe.
Span<double> wy = stackalloc double[m];
for (int h = 0; h < m; h += 1)
{
wy[h] = y[h] * w[h];
}
for (int i = n - 1; i >= 0; i -= 1)
{
result.Coefficients[i] = Multiply(q.GetRow(i), wy);
for (int j = n - 1; j > i; j -= 1)
{
result.Coefficients[i] -= r[i, j] * result.Coefficients[j];
}
result.Coefficients[i] /= r[i, i];
}
// Calculate the coefficient of determination (confidence) as:
// 1 - (sumSquaredError / sumSquaredTotal)
// ...where sumSquaredError is the residual sum of squares (variance of the
// error), and sumSquaredTotal is the total sum of squares (variance of the
// data) where each has been weighted.
double yMean = 0.0;
for (int h = 0; h < m; h += 1)
{
yMean += y[h];
}
yMean /= m;
double sumSquaredError = 0.0;
double sumSquaredTotal = 0.0;
for (int h = 0; h < m; h += 1)
{
double term = 1.0;
double err = y[h] - result.Coefficients[0];
for (int i = 1; i < n; i += 1)
{
term *= x[h];
err -= term * result.Coefficients[i];
}
sumSquaredError += w[h] * w[h] * err * err;
double v = y[h] - yMean;
sumSquaredTotal += w[h] * w[h] * v * v;
}
result.Confidence = sumSquaredTotal <= PrecisionErrorTolerance ? 1.0 :
1.0 - (sumSquaredError / sumSquaredTotal);
return result;
}
private static double Multiply(Span<double> v1, Span<double> v2)
{
double result = 0.0;
for (int i = 0; i < v1.Length; i += 1)
{
result += v1[i] * v2[i];
}
return result;
}
private static double Norm(Span<double> v)
{
return Math.Sqrt(Multiply(v, v));
}
private readonly ref struct _Matrix
{
private readonly int _columns;
private readonly Span<double> _elements;
internal _Matrix(int cols, Span<double> elements)
{
_columns = cols;
_elements = elements;
}
public double this[int row, int col]
{
get => _elements[row * _columns + col];
set => _elements[row * _columns + col] = value;
}
public Span<double> GetRow(int row) => _elements.Slice(row * _columns, _columns);
}
}
}

121
src/Avalonia.Base/Input/Gestures.cs

@ -1,6 +1,8 @@
using System;
using System.Threading;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -8,6 +10,21 @@ namespace Avalonia.Input
public static class Gestures
{
private static bool s_isDoubleTapped = false;
private static bool s_isHolding;
private static CancellationTokenSource? s_holdCancellationToken;
/// <summary>
/// Defines the IsHoldingEnabled attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsHoldingEnabledProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>("IsHoldingEnabled", typeof(Gestures), true);
/// <summary>
/// Defines the IsHoldWithMouseEnabled attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsHoldWithMouseEnabledProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>("IsHoldWithMouseEnabled", typeof(Gestures), false);
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = RoutedEvent.Register<TappedEventArgs>(
"Tapped",
RoutingStrategies.Bubble,
@ -45,6 +62,7 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint;
private static IPointer? s_lastPointer;
public static readonly RoutedEvent<PinchEventArgs> PinchEvent =
RoutedEvent.Register<PinchEventArgs>(
@ -58,14 +76,40 @@ namespace Avalonia.Input
RoutedEvent.Register<PullGestureEventArgs>(
"PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
/// <summary>
/// Occurs when a user performs a press and hold gesture (with a single touch, mouse, or pen/stylus contact).
/// </summary>
public static readonly RoutedEvent<HoldingRoutedEventArgs> HoldingEvent =
RoutedEvent.Register<HoldingRoutedEventArgs>(
"Holding", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
RoutedEvent.Register<PullGestureEndedEventArgs>(
"PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
public static bool GetIsHoldingEnabled(StyledElement element)
{
return element.GetValue(IsHoldingEnabledProperty);
}
public static void SetIsHoldingEnabled(StyledElement element, bool value)
{
element.SetValue(IsHoldingEnabledProperty, value);
}
public static bool GetIsHoldWithMouseEnabled(StyledElement element)
{
return element.GetValue(IsHoldWithMouseEnabledProperty);
}
public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value)
{
element.SetValue(IsHoldWithMouseEnabledProperty, value);
}
static Gestures()
{
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved);
}
public static void AddTappedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
@ -110,11 +154,42 @@ namespace Avalonia.Input
var e = (PointerPressedEventArgs)ev;
var visual = (Visual)ev.Source;
if(s_lastPointer != null)
{
if(s_isHolding && ev.Source is Interactive i)
{
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer.Type));
}
s_holdCancellationToken?.Cancel();
s_holdCancellationToken?.Dispose();
s_holdCancellationToken = null;
s_lastPointer = null;
}
s_isHolding = false;
if (e.ClickCount % 2 == 1)
{
s_isDoubleTapped = false;
s_lastPress.SetTarget(ev.Source);
s_lastPointer = e.Pointer;
s_lastPressPoint = e.GetPosition((Visual)ev.Source);
s_holdCancellationToken = new CancellationTokenSource();
var token = s_holdCancellationToken.Token;
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
if (settings != null)
{
DispatcherTimer.RunOnce(() =>
{
if (!token.IsCancellationRequested && e.Source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i)))
{
s_isHolding = true;
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_lastPointer.Type));
}
}, settings.HoldWaitDuration);
}
}
else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{
@ -148,7 +223,12 @@ namespace Avalonia.Input
if (tapRect.ContainsExclusive(point.Position))
{
if (e.InitialPressMouseButton == MouseButton.Right)
if(s_isHolding)
{
s_isHolding = false;
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_lastPointer!.Type));
}
else if (e.InitialPressMouseButton == MouseButton.Right)
{
i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e));
}
@ -160,6 +240,45 @@ namespace Avalonia.Input
}
}
}
s_holdCancellationToken?.Cancel();
s_holdCancellationToken?.Dispose();
s_holdCancellationToken = null;
s_lastPointer = null;
}
}
private static void PointerMoved(RoutedEventArgs ev)
{
if (ev.Route == RoutingStrategies.Bubble)
{
var e = (PointerEventArgs)ev;
if (s_lastPress.TryGetTarget(out var target))
{
if (e.Pointer == s_lastPointer)
{
var point = e.GetCurrentPoint((Visual)target);
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4);
var tapRect = new Rect(s_lastPressPoint, new Size())
.Inflate(new Thickness(tapSize.Width, tapSize.Height));
if (tapRect.ContainsExclusive(point.Position))
{
return;
}
if (s_isHolding && ev.Source is Interactive i)
{
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer!.Type));
}
}
}
s_holdCancellationToken?.Cancel();
s_holdCancellationToken?.Dispose();
s_holdCancellationToken = null;
s_isHolding = false;
}
}
}

51
src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs

@ -0,0 +1,51 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class HoldingRoutedEventArgs : RoutedEventArgs
{
/// <summary>
/// Gets the state of the <see cref="Gestures.HoldingEvent"/> event.
/// </summary>
public HoldingState HoldingState { get; }
/// <summary>
/// Gets the location of the touch, mouse, or pen/stylus contact.
/// </summary>
public Point Position { get; }
/// <summary>
/// Gets the pointer type of the input source.
/// </summary>
public PointerType PointerType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HoldingRoutedEventArgs"/> class.
/// </summary>
public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent)
{
HoldingState = holdingState;
Position = position;
PointerType = pointerType;
}
}
public enum HoldingState
{
/// <summary>
/// A single contact has been detected and a time threshold is crossed without the contact being lifted, another contact detected, or another gesture started.
/// </summary>
Started,
/// <summary>
/// The single contact is lifted.
/// </summary>
Completed,
/// <summary>
/// An additional contact is detected or a subsequent gesture (such as a slide) is detected.
/// </summary>
Cancelled,
}
}

16
src/Avalonia.Base/Input/InputElement.cs

@ -188,11 +188,16 @@ namespace Avalonia.Input
/// </summary>
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = Gestures.TappedEvent;
/// <summary>
/// Defines the <see cref="Holding"/> event.
/// </summary>
public static readonly RoutedEvent<HoldingRoutedEventArgs> HoldingEvent = Gestures.HoldingEvent;
/// <summary>
/// Defines the <see cref="DoubleTapped"/> event.
/// </summary>
public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isKeyboardFocusWithin;
@ -352,6 +357,15 @@ namespace Avalonia.Input
add { AddHandler(TappedEvent, value); }
remove { RemoveHandler(TappedEvent, value); }
}
/// <summary>
/// Occurs when a hold gesture occurs on the control.
/// </summary>
public event EventHandler<HoldingRoutedEventArgs>? Holding
{
add { AddHandler(HoldingEvent, value); }
remove { RemoveHandler(HoldingEvent, value); }
}
/// <summary>
/// Occurs when a double-tap gesture occurs on the control.

2
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -26,5 +26,7 @@ namespace Avalonia.Platform
};
}
public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
}
}

2
src/Avalonia.Base/Platform/IPlatformSettings.cs

@ -27,5 +27,7 @@ namespace Avalonia.Platform
/// tap gesture.
/// </summary>
TimeSpan GetDoubleTapTime(PointerType type);
TimeSpan HoldWaitDuration { get; set; }
}
}

19
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -42,11 +42,18 @@ namespace Avalonia.Rendering.Composition.Server
Root!.RenderedVisuals++;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
canvas.Transform = Matrix.Identity;
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
}
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
@ -67,6 +74,8 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip();
if (AdornedVisual != null)
canvas.PopClip();
if(Opacity != 1)
canvas.PopOpacity();
}
@ -155,8 +164,12 @@ namespace Avalonia.Rendering.Composition.Server
_clipSizeDirty = false;
}
_combinedTransformedClipBounds =
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? new Rect(Root!.Size);
_combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);

53
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
@ -27,6 +28,37 @@ public record struct BatchStreamSegment<TData>
public int ElementCount { get; set; }
}
// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm,
// see https://github.com/dotnet/runtime/issues/80068
static unsafe class UnalignedMemoryHelper
{
public static T ReadUnaligned<T>(byte* src) where T : unmanaged
{
#if NET6_0_OR_GREATER
Unsafe.SkipInit<T>(out var rv);
#else
T rv;
#endif
UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf<T>());
return rv;
}
public static void WriteUnaligned<T>(byte* dst, T value) where T : unmanaged
{
UnalignedMemcpy(dst, (byte*)&value, Unsafe.SizeOf<T>());
}
[MethodImpl(MethodImplOptions.NoInlining)]
static unsafe void UnalignedMemcpy(byte* dst, byte* src, int count)
{
for (var c = 0; c < count; c++)
{
dst[c] = src[c];
}
}
}
internal class BatchStreamWriter : IDisposable
{
private readonly BatchStreamData _output;
@ -74,7 +106,15 @@ internal class BatchStreamWriter : IDisposable
var size = Unsafe.SizeOf<T>();
if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize)
NextDataSegment();
Unsafe.WriteUnaligned<T>((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item);
var ptr = (byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount;
// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32,
// see https://github.com/dotnet/runtime/issues/80068
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
UnalignedMemoryHelper.WriteUnaligned(ptr, item);
else
Unsafe.WriteUnaligned(ptr, item);
_currentDataSegment.ElementCount += size;
}
@ -125,7 +165,16 @@ internal class BatchStreamReader : IDisposable
if (_memoryOffset + size > _currentDataSegment.ElementCount)
throw new InvalidOperationException("Attempted to read more memory then left in the current segment");
var rv = Unsafe.ReadUnaligned<T>((byte*)_currentDataSegment.Data + _memoryOffset);
var ptr = (byte*)_currentDataSegment.Data + _memoryOffset;
T rv;
// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32,
// see https://github.com/dotnet/runtime/issues/80068
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
rv = UnalignedMemoryHelper.ReadUnaligned<T>(ptr);
else
rv = Unsafe.ReadUnaligned<T>(ptr);
_memoryOffset += size;
if (_memoryOffset == _currentDataSegment.ElementCount)
{

11
src/Avalonia.Controls/ContentControl.cs

@ -116,14 +116,19 @@ namespace Avalonia.Controls
return false;
}
private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
protected virtual void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
UpdateLogicalTree(e.OldValue, e.NewValue);
}
protected void UpdateLogicalTree(object? toRemove, object? toAdd)
{
if (toRemove is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
if (toAdd is ILogical newChild)
{
LogicalChildren.Add(newChild);
}

13
src/Avalonia.Controls/Control.cs

@ -366,16 +366,29 @@ namespace Avalonia.Controls
{
base.OnAttachedToVisualTreeCore(e);
AddHandler(Gestures.HoldingEvent, OnHoldEvent);
InitializeIfNeeded();
ScheduleOnLoadedCore();
}
private void OnHoldEvent(object? sender, HoldingRoutedEventArgs e)
{
if(e.HoldingState == HoldingState.Started)
{
// Trigger ContentRequest when hold has started
RaiseEvent(new ContextRequestedEventArgs());
}
}
/// <inheritdoc/>
protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTreeCore(e);
RemoveHandler(Gestures.HoldingEvent, OnHoldEvent);
OnUnloadedCore();
}

7
src/Avalonia.Controls/TransitioningContentControl.cs

@ -71,6 +71,11 @@ public class TransitioningContentControl : ContentControl
}
}
protected override void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
// We do nothing becuse we should not remove old Content until the animation is over
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
@ -89,6 +94,8 @@ public class TransitioningContentControl : ContentControl
if (PageTransition != null)
await PageTransition.Start(this, null, true, localToken);
UpdateLogicalTree(CurrentContent, content);
if (localToken.IsCancellationRequested)
{
return;

4
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -50,7 +50,8 @@
Opacity="0"
Fill="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}"
Stretch="Uniform"
VerticalAlignment="Center" />
VerticalAlignment="Center"
FlowDirection="LeftToRight" />
</Panel>
</Viewbox>
</Grid>
@ -146,7 +147,6 @@
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
<!-- Checked PointerOver State -->

2
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -258,6 +258,8 @@ namespace Avalonia.Win32
public bool CurrentThreadIsLoopThread => _uiThread == Thread.CurrentThread;
public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
public event Action<DispatcherPriority?> Signaled;
public event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;

240
tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs

@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Input
@ -169,6 +173,242 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(raised);
}
[Fact]
public void Hold_Should_Be_Raised_After_Hold_Duration()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
iSettingsMock.Setup(x => x.GetTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
HoldingState holding = HoldingState.Cancelled;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => holding = e.HoldingState);
_mouse.Down(border);
Assert.False(holding != HoldingState.Cancelled);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
Assert.True(holding == HoldingState.Started);
_mouse.Up(border);
Assert.True(holding == HoldingState.Completed);
}
[Fact]
public void Hold_Should_Not_Raised_When_Pointer_Released_Before_Timer()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Started);
_mouse.Down(border);
Assert.False(raised);
_mouse.Up(border);
Assert.False(raised);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
Assert.False(raised);
}
[Fact]
public void Hold_Should_Not_Raised_When_Pointer_Is_Moved_Before_Timer()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed);
_mouse.Down(border);
Assert.False(raised);
_mouse.Move(border, position: new Point(20, 20));
Assert.False(raised);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
Assert.False(raised);
}
[Fact]
public void Hold_Should_Be_Cancelled_When_Second_Contact_Is_Detected()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var cancelled = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
_mouse.Down(border);
Assert.False(cancelled);
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
var secondMouse = new MouseTestHelper();
secondMouse.Down(border);
Assert.True(cancelled);
}
[Fact]
public void Hold_Should_Be_Cancelled_When_Pointer_Moves_Too_Far()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
iSettingsMock.Setup(x => x.GetTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var cancelled = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
_mouse.Down(border);
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
_mouse.Move(border, position: new Point(3, 3));
Assert.False(cancelled);
_mouse.Move(border, position: new Point(20, 20));
Assert.True(cancelled);
}
[Fact]
public void Hold_Should_Not_Be_Raised_For_Multiple_Contacts()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed);
var secondMouse = new MouseTestHelper();
_mouse.Down(border, MouseButton.Left);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
secondMouse.Down(border, MouseButton.Left);
Assert.False(raised);
}
private static IPlatformThreadingInterface CreatePlatformThreadingInterface(Action<(TimeSpan, Action)> callback)
{
var threadingInterface = new Mock<IPlatformThreadingInterface>();
threadingInterface.SetupGet(p => p.CurrentThreadIsLoopThread).Returns(true);
threadingInterface.Setup(p => p
.StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
.Callback<DispatcherPriority, TimeSpan, Action>((_, t, a) => callback((t, a)));
return threadingInterface.Object;
}
private static void AddHandlers(
Decorator decorator,
Border border,

2
tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

@ -207,7 +207,7 @@ namespace Avalonia.Input.UnitTests
private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan())
{
var unitTestApp = UnitTestApplication.Start(
new TestServices(inputManager: new InputManager()));
new TestServices(inputManager: new InputManager(), threadingInterface: Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true)));
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny<PointerType>())).Returns(doubleClickTime);
iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));

66
tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs

@ -0,0 +1,66 @@
using System;
using Avalonia.LogicalTree;
using Avalonia.UnitTests;
using Xunit;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
namespace Avalonia.Controls.UnitTests
{
public class TransitioningContentControlTests
{
[Fact]
public void Old_Content_Shuold_Be_Removed__From_Logical_Tree_After_Out_Animation()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var testTransition = new TestTransition();
var target = new TransitioningContentControl();
target.PageTransition = testTransition;
var root = new TestRoot() { Child = target };
var oldControl = new Control();
var newControl = new Control();
target.Content = oldControl;
Threading.Dispatcher.UIThread.RunJobs();
Assert.Equal(target, oldControl.GetLogicalParent());
Assert.Equal(null, newControl.GetLogicalParent());
testTransition.BeginTransition += isFrom =>
{
// Old out
if (isFrom)
{
Assert.Equal(target, oldControl.GetLogicalParent());
Assert.Equal(null, newControl.GetLogicalParent());
}
// New in
else
{
Assert.Equal(null, oldControl.GetLogicalParent());
Assert.Equal(target, newControl.GetLogicalParent());
}
};
target.Content = newControl;
Threading.Dispatcher.UIThread.RunJobs();
}
}
}
public class TestTransition : IPageTransition
{
public event Action<bool> BeginTransition;
public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
bool isFrom = from != null && to == null;
BeginTransition?.Invoke(isFrom);
return Task.CompletedTask;
}
}
}

908
tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json

File diff suppressed because it is too large

2
tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package.json

@ -16,7 +16,7 @@
"@types/mocha": "8.0.3",
"@types/react": "^16.3.14",
"chai": "^4.2.0",
"mocha": "^8.1.3",
"mocha": "^10.2.0",
"moq.ts": "^6.4.0",
"nyc": "^15.1.0",
"react": "^16.3.2",

73
tests/Avalonia.RenderTests/Controls/AdornerTests.cs

@ -0,0 +1,73 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#else
namespace Avalonia.Direct2D1.RenderTests.Controls;
#endif
public class AdornerTests : TestBase
{
public AdornerTests()
: base(@"Controls\Adorner")
{
}
[Fact]
public async Task Focus_Adorner_Is_Properly_Clipped()
{
Border adorned;
var tree = new Decorator
{
Child = new VisualLayerManager
{
Child = new Border
{
Background = Brushes.Red,
Padding = new Thickness(10, 50, 10,10),
Child = new Border()
{
Background = Brushes.White,
ClipToBounds = true,
Padding = new Thickness(0, -30, 0, 0),
Child = adorned = new Border
{
Background = Brushes.Green,
VerticalAlignment = VerticalAlignment.Top,
Height = 100,
Width = 50
}
}
}
},
Width = 200,
Height = 200
};
var adorner = new Border
{
BorderThickness = new Thickness(2),
BorderBrush = Brushes.Black
};
var size = new Size(tree.Width, tree.Height);
tree.Measure(size);
tree.Arrange(new Rect(size));
adorned.AttachedToVisualTree += delegate
{
AdornerLayer.SetAdornedElement(adorner, adorned);
AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner);
};
tree.Measure(size);
tree.Arrange(new Rect(size));
await RenderToFile(tree);
CompareImages(skipImmediate: true, skipDeferred: true);
}
}

9
tests/Avalonia.RenderTests/TestBase.cs

@ -156,7 +156,8 @@ namespace Avalonia.Direct2D1.RenderTests
public ILockedFramebuffer Lock() => _bitmap.Lock();
}
protected void CompareImages([CallerMemberName] string testName = "")
protected void CompareImages([CallerMemberName] string testName = "",
bool skipImmediate = false, bool skipDeferred = false, bool skipCompositor = false)
{
var expectedPath = Path.Combine(OutputPath, testName + ".expected.png");
var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
@ -172,17 +173,17 @@ namespace Avalonia.Direct2D1.RenderTests
var deferredError = CompareImages(deferred, expected);
var compositedError = CompareImages(composited, expected);
if (immediateError > 0.022)
if (immediateError > 0.022 && !skipImmediate)
{
Assert.True(false, immediatePath + ": Error = " + immediateError);
}
if (deferredError > 0.022)
if (deferredError > 0.022 && !skipDeferred)
{
Assert.True(false, deferredPath + ": Error = " + deferredError);
}
if (compositedError > 0.022)
if (compositedError > 0.022 && !skipCompositor)
{
Assert.True(false, compositedPath + ": Error = " + compositedError);
}

BIN
tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

BIN
tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Loading…
Cancel
Save