Browse Source

Merge branch 'master' into master

pull/7935/head
Steve 4 years ago
committed by GitHub
parent
commit
ea3d4fc7e2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      nukebuild/Build.cs
  2. 3
      src/Avalonia.Base/Input/IInputRoot.cs
  3. 6
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  4. 2
      src/Avalonia.Base/Input/IMouseDevice.cs
  5. 16
      src/Avalonia.Base/Input/IPointerDevice.cs
  6. 2
      src/Avalonia.Base/Input/KeyboardDevice.cs
  7. 328
      src/Avalonia.Base/Input/MouseDevice.cs
  8. 2
      src/Avalonia.Base/Input/PointerEventArgs.cs
  9. 209
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  10. 2
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  11. 27
      src/Avalonia.Base/Input/Raw/RawInputHelpers.cs
  12. 2
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  13. 47
      src/Avalonia.Base/Input/TouchDevice.cs
  14. 83
      src/Avalonia.Base/Media/Color.cs
  15. 55
      src/Avalonia.Base/Media/HslColor.cs
  16. 55
      src/Avalonia.Base/Media/HsvColor.cs
  17. 15
      src/Avalonia.Base/RelativePoint.cs
  18. 3
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  19. 10
      src/Avalonia.Build.Tasks/Properties/launchSettings.json
  20. 8
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  21. 1
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  22. 5
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  23. 6
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  24. 1
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  25. 32
      src/Avalonia.Controls/ComboBox.cs
  26. 1
      src/Avalonia.Controls/ItemsControl.cs
  27. 2
      src/Avalonia.Controls/StackPanel.cs
  28. 25
      src/Avalonia.Controls/TopLevel.cs
  29. 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  30. 8
      src/Avalonia.PlatformSupport/AssetLoader.cs
  31. 2
      src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
  32. 10
      src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs
  33. 11
      src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs
  34. 23
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  35. 7
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  36. 175
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  37. 534
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  38. 24
      tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
  39. 75
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  40. 21
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  41. 6
      tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs

1
nukebuild/Build.cs

@ -221,6 +221,7 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.PlatformSupport.UnitTests");
});
Target RunRenderTests => _ => _

3
src/Avalonia.Base/Input/IInputRoot.cs

@ -1,5 +1,3 @@
using JetBrains.Annotations;
namespace Avalonia.Input
{
/// <summary>
@ -30,7 +28,6 @@ namespace Avalonia.Input
/// <summary>
/// Gets associated mouse device
/// </summary>
[CanBeNull]
IMouseDevice? MouseDevice { get; }
}
}

6
src/Avalonia.Base/Input/IKeyboardDevice.cs

@ -50,12 +50,6 @@ namespace Avalonia.Input
KeyboardMask = Alt | Control | Shift | Meta
}
internal static class KeyModifiersUtils
{
public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
}
public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
{
IInputElement? FocusedElement { get; }

2
src/Avalonia.Base/Input/IMouseDevice.cs

@ -13,8 +13,10 @@ namespace Avalonia.Input
[Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
[Obsolete]
void TopLevelClosed(IInputRoot root);
[Obsolete]
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

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

@ -1,17 +1,31 @@
using System;
using Avalonia.VisualTree;
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
public interface IPointerDevice : IInputDevice
{
/// <inheritdoc cref="IPointer.Captured" />
[Obsolete("Use IPointer")]
IInputElement? Captured { get; }
/// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
[Obsolete("Use IPointer")]
void Capture(IInputElement? control);
/// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
[Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo);
/// <summary>
/// Gets a pointer for specific event args.
/// </summary>
/// <remarks>
/// If pointer doesn't exist or wasn't yet created this method will return null.
/// </remarks>
/// <param name="ev">Raw pointer event args associated with the pointer.</param>
/// <returns>The pointer.</returns>
IPointer? TryGetPointer(RawPointerEventArgs ev);
}
}

2
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -188,7 +188,7 @@ namespace Avalonia.Input
RoutedEvent = routedEvent,
Device = this,
Key = keyInput.Key,
KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers),
KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element,
};

328
src/Avalonia.Base/Input/MouseDevice.cs

@ -21,27 +21,17 @@ namespace Avalonia.Input
private readonly Pointer _pointer;
private bool _disposed;
private PixelPoint? _position;
private PixelPoint? _position;
private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null)
{
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
/// <summary>
/// Gets the control that is currently capturing by the mouse, if any.
/// </summary>
/// <remarks>
/// When an element captures the mouse, it receives mouse input whether the cursor is
/// within the control's bounds or not. To set the mouse capture, call the
/// <see cref="Capture"/> method.
/// </remarks>
[Obsolete("Use IPointer instead")]
public IInputElement? Captured => _pointer.Captured;
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use events instead")]
public PixelPoint Position
{
@ -49,15 +39,7 @@ namespace Avalonia.Input
protected set => _position = value;
}
/// <summary>
/// Captures mouse input to the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <remarks>
/// When an element captures the mouse, it receives mouse input whether the cursor is
/// within the control's bounds or not. The current mouse capture control is exposed
/// by the <see cref="Captured"/> property.
/// </remarks>
[Obsolete("Use IPointer instead")]
public void Capture(IInputElement? control)
{
_pointer.Capture(control);
@ -90,39 +72,6 @@ namespace Avalonia.Input
ProcessRawEvent(margs);
}
public void TopLevelClosed(IInputRoot root)
{
ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None);
}
public void SceneInvalidated(IInputRoot root, Rect rect)
{
// Pointer is outside of the target area
if (_position == null )
{
if (root.PointerOverElement != null)
ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None);
return;
}
var clientPoint = root.PointToClient(_position.Value);
if (rect.Contains(clientPoint))
{
if (_pointer.Captured == null)
{
SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint,
PointerPointProperties.None, KeyModifiers.None);
}
else
{
SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured,
PointerPointProperties.None, KeyModifiers.None);
}
}
}
int ButtonCount(PointerPointProperties props)
{
var rv = 0;
@ -138,7 +87,7 @@ namespace Avalonia.Input
rv++;
return rv;
}
private void ProcessRawEvent(RawPointerEventArgs e)
{
e = e ?? throw new ArgumentNullException(nameof(e));
@ -147,15 +96,14 @@ namespace Avalonia.Input
if(mouse._disposed)
return;
if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return;
_position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers);
var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type)
{
case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers);
case RawPointerEventType.NonClientLeftButtonDown:
LeaveWindow();
break;
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
@ -163,10 +111,9 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, keyModifiers);
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp:
@ -174,82 +121,50 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
else
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Move:
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
break;
case RawPointerEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers);
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Magnify:
e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Rotate:
e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Swipe:
e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
}
}
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties,
KeyModifiers inputModifiers)
private void LeaveWindow()
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
_position = null;
ClearPointerOver(this, timestamp, root, properties, inputModifiers);
}
PointerPointProperties CreateProperties(RawPointerEventArgs args)
{
var kind = PointerUpdateKind.Other;
if (args.Type == RawPointerEventType.LeftButtonDown)
kind = PointerUpdateKind.LeftButtonPressed;
if (args.Type == RawPointerEventType.MiddleButtonDown)
kind = PointerUpdateKind.MiddleButtonPressed;
if (args.Type == RawPointerEventType.RightButtonDown)
kind = PointerUpdateKind.RightButtonPressed;
if (args.Type == RawPointerEventType.XButton1Down)
kind = PointerUpdateKind.XButton1Pressed;
if (args.Type == RawPointerEventType.XButton2Down)
kind = PointerUpdateKind.XButton2Pressed;
if (args.Type == RawPointerEventType.LeftButtonUp)
kind = PointerUpdateKind.LeftButtonReleased;
if (args.Type == RawPointerEventType.MiddleButtonUp)
kind = PointerUpdateKind.MiddleButtonReleased;
if (args.Type == RawPointerEventType.RightButtonUp)
kind = PointerUpdateKind.RightButtonReleased;
if (args.Type == RawPointerEventType.XButton1Up)
kind = PointerUpdateKind.XButton1Released;
if (args.Type == RawPointerEventType.XButton2Up)
kind = PointerUpdateKind.XButton2Released;
return new PointerPointProperties(args.InputModifiers, kind);
return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind());
}
private MouseButton _lastMouseDownButton;
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties,
KeyModifiers inputModifiers)
KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
var source = _pointer.Captured ?? root.InputHitTest(p);
if (hit != null)
if (source != null)
{
_pointer.Capture(hit);
var source = GetSource(hit);
_pointer.Capture(source);
if (source != null)
{
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -275,23 +190,14 @@ namespace Avalonia.Input
return false;
}
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints)
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties properties, KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints,
IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
IInputElement? source;
if (_pointer.Captured == null)
{
source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers);
}
else
{
SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers);
source = _pointer.Captured;
}
var source = _pointer.Captured ?? hitTest;
if (source is object)
{
@ -306,13 +212,12 @@ namespace Avalonia.Input
}
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
KeyModifiers inputModifiers)
KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
var source = GetSource(hit);
var source = _pointer.Captured ?? hitTest;
if (source is not null)
{
@ -329,13 +234,12 @@ namespace Avalonia.Input
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props,
Vector delta, KeyModifiers inputModifiers)
Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
var source = GetSource(hit);
var source = _pointer.Captured ?? hitTest;
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform.
// If Shift-Key is pressed and X is close to 0 we swap the Vector.
@ -356,16 +260,15 @@ namespace Avalonia.Input
}
private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
var source = _pointer.Captured ?? hitTest;
if (hit != null)
if (source != null)
{
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
@ -377,16 +280,15 @@ namespace Avalonia.Input
}
private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
var source = _pointer.Captured ?? hitTest;
if (hit != null)
if (source != null)
{
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
@ -398,16 +300,15 @@ namespace Avalonia.Input
}
private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
var source = _pointer.Captured ?? hitTest;
if (hit != null)
if (source != null)
{
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
@ -418,154 +319,27 @@ namespace Avalonia.Input
return false;
}
private IInteractive? GetSource(IVisual? hit)
{
if (hit is null)
return null;
return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
}
private IInputElement? HitTest(IInputElement root, Point p)
{
root = root ?? throw new ArgumentNullException(nameof(root));
return _pointer.Captured ?? root.InputHitTest(p);
}
PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source,
PointerPointProperties properties,
KeyModifiers inputModifiers)
{
return new PointerEventArgs(ev, source, _pointer, null, default,
timestamp, properties, inputModifiers);
}
private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root,
PointerPointProperties properties,
KeyModifiers inputModifiers)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var element = root.PointerOverElement;
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers);
if (element!=null && !element.IsAttachedToVisualTree)
{
// element has been removed from visual tree so do top down cleanup
if (root.IsPointerOver)
ClearChildrenPointerOver(e, root,true);
}
while (element != null)
{
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
element = (IInputElement?)element.VisualParent;
}
root.PointerOverElement = null;
}
private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot)
public void Dispose()
{
foreach (IInputElement el in element.VisualChildren)
{
if (el.IsPointerOver)
{
ClearChildrenPointerOver(e, el, true);
break;
}
}
if(clearRoot)
{
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
}
_disposed = true;
_pointer?.Dispose();
}
private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties properties,
KeyModifiers inputModifiers)
[Obsolete]
public void TopLevelClosed(IInputRoot root)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var element = root.InputHitTest(p);
if (element != root.PointerOverElement)
{
if (element != null)
{
SetPointerOver(device, timestamp, root, element, properties, inputModifiers);
}
else
{
ClearPointerOver(device, timestamp, root, properties, inputModifiers);
}
}
return element;
// no-op
}
private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element,
PointerPointProperties properties,
KeyModifiers inputModifiers)
[Obsolete]
public void SceneInvalidated(IInputRoot root, Rect rect)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
element = element ?? throw new ArgumentNullException(nameof(element));
IInputElement? branch = null;
IInputElement? el = element;
while (el != null)
{
if (el.IsPointerOver)
{
branch = el;
break;
}
el = (IInputElement?)el.VisualParent;
}
el = root.PointerOverElement;
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers);
if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e,branch,false);
}
while (el != null && el != branch)
{
e.Source = el;
e.Handled = false;
el.RaiseEvent(e);
el = (IInputElement?)el.VisualParent;
}
el = root.PointerOverElement = element;
e.RoutedEvent = InputElement.PointerEnterEvent;
while (el != null && el != branch)
{
e.Source = el;
e.Handled = false;
el.RaiseEvent(e);
el = (IInputElement?)el.VisualParent;
}
// no-op
}
public void Dispose()
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
_disposed = true;
_pointer?.Dispose();
return _pointer;
}
}
}

2
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -63,6 +63,8 @@ namespace Avalonia.Input
}
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
}
public IPointer Pointer { get; }

209
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -0,0 +1,209 @@
using System;
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
{
private IPointerDevice? _lastActivePointerDevice;
private (IPointer pointer, PixelPoint position)? _lastPointer;
private readonly IInputRoot _inputRoot;
public PointerOverPreProcessor(IInputRoot inputRoot)
{
_inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
}
public void OnCompleted()
{
ClearPointerOver();
}
public void OnError(Exception error)
{
}
public void OnNext(RawInputEventArgs value)
{
if (value is RawPointerEventArgs args
&& args.Root == _inputRoot
&& value.Device is IPointerDevice pointerDevice)
{
if (pointerDevice != _lastActivePointerDevice)
{
ClearPointerOver();
// Set last active device before processing input, because ClearPointerOver might be called and clear last device.
_lastActivePointerDevice = pointerDevice;
}
if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
&& _lastPointer is (var lastPointer, var lastPosition))
{
_lastPointer = null;
ClearPointerOver(lastPointer, args.Root, 0, args.Root.PointToClient(lastPosition),
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
args.InputModifiers.ToKeyModifiers());
}
else if (pointerDevice.TryGetPointer(args) is IPointer pointer
&& pointer.Type != PointerType.Touch)
{
var element = pointer.Captured ?? args.InputHitTestResult;
SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position,
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
args.InputModifiers.ToKeyModifiers());
}
}
}
public void SceneInvalidated(Rect dirtyRect)
{
if (_lastPointer is (var pointer, var position))
{
var clientPoint = _inputRoot.PointToClient(position);
if (dirtyRect.Contains(clientPoint))
{
SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
else if (!_inputRoot.Bounds.Contains(clientPoint))
{
ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None);
}
}
}
private void ClearPointerOver()
{
if (_lastPointer is (var pointer, var _))
{
ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None);
}
_lastPointer = null;
_lastActivePointerDevice = null;
}
private void ClearPointerOver(IPointer pointer, IInputRoot root,
ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
{
var element = root.PointerOverElement;
if (element is null)
{
return;
}
// Do not pass rootVisual, when we have unknown (negative) position,
// so GetPosition won't return invalid values.
var hasPosition = position.X >= 0 && position.Y >= 0;
var e = new PointerEventArgs(InputElement.PointerLeaveEvent, element, pointer,
hasPosition ? root : null, hasPosition ? position : default,
timestamp, properties, inputModifiers);
if (element != null && !element.IsAttachedToVisualTree)
{
// element has been removed from visual tree so do top down cleanup
if (root.IsPointerOver)
{
ClearChildrenPointerOver(e, root, true);
}
}
while (element != null)
{
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
element = (IInputElement?)element.VisualParent;
}
root.PointerOverElement = null;
_lastActivePointerDevice = null;
_lastPointer = null;
}
private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
{
foreach (IInputElement el in element.VisualChildren)
{
if (el.IsPointerOver)
{
ClearChildrenPointerOver(e, el, true);
break;
}
}
if (clearRoot)
{
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
}
}
private void SetPointerOver(IPointer pointer, IInputRoot root, IInputElement? element,
ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
{
var pointerOverElement = root.PointerOverElement;
if (element != pointerOverElement)
{
if (element != null)
{
SetPointerOverToElement(pointer, root, element, timestamp, position, properties, inputModifiers);
}
else
{
ClearPointerOver(pointer, root, timestamp, position, properties, inputModifiers);
}
}
}
private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
{
IInputElement? branch = null;
IInputElement? el = element;
while (el != null)
{
if (el.IsPointerOver)
{
branch = el;
break;
}
el = (IInputElement?)el.VisualParent;
}
el = root.PointerOverElement;
var e = new PointerEventArgs(InputElement.PointerLeaveEvent, el, pointer, root, position,
timestamp, properties, inputModifiers);
if (el != null && branch != null && !el.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e, branch, false);
}
while (el != null && el != branch)
{
e.Source = el;
e.Handled = false;
el.RaiseEvent(e);
el = (IInputElement?)el.VisualParent;
}
el = root.PointerOverElement = element;
_lastPointer = (pointer, root.PointToScreen(position));
e.RoutedEvent = InputElement.PointerEnterEvent;
while (el != null && el != branch)
{
e.Source = el;
e.Handled = false;
el.RaiseEvent(e);
el = (IInputElement?)el.VisualParent;
}
}
}
}

2
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@ -20,7 +20,7 @@ namespace Avalonia.Input.Raw
Location = location;
Data = data;
Effects = effects;
KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers);
KeyModifiers = modifiers.ToKeyModifiers();
#pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete

27
src/Avalonia.Base/Input/Raw/RawInputHelpers.cs

@ -0,0 +1,27 @@
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
internal static class RawInputHelpers
{
public static KeyModifiers ToKeyModifiers(this RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
public static PointerUpdateKind ToUpdateKind(this RawPointerEventType type) => type switch
{
RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed,
RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased,
RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed,
RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased,
RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed,
RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased,
RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed,
RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released,
RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed,
RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released,
RawPointerEventType.TouchBegin => PointerUpdateKind.LeftButtonPressed,
RawPointerEventType.TouchEnd => PointerUpdateKind.LeftButtonReleased,
_ => PointerUpdateKind.Other
};
}
}

2
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@ -120,6 +120,8 @@ namespace Avalonia.Input.Raw
/// only valid for Move and TouchUpdate
/// </summary>
public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; }
internal IInputElement? InputHitTestResult { get; set; }
}
public struct RawPointerPoint

47
src/Avalonia.Base/Input/TouchDevice.cs

@ -3,24 +3,26 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
/// <summary>
/// Handles raw touch events
/// </summary>
/// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks>
/// </summary>
public class TouchDevice : IInputDevice, IDisposable
public class TouchDevice : IPointerDevice, IDisposable
{
private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
private bool _disposed;
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
private Pointer? _lastPointer;
IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{
@ -30,6 +32,10 @@ namespace Avalonia.Input
return rv;
}
void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
public void ProcessRawEvent(RawInputEventArgs ev)
{
if (ev.Handled || _disposed)
@ -39,15 +45,18 @@ namespace Avalonia.Input
{
if (args.Type == RawPointerEventType.TouchEnd)
return;
var hit = args.Root.InputHitTest(args.Position);
var hit = args.InputHitTestResult;
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
_lastPointer = pointer;
var target = pointer.Captured ?? args.Root;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();
if (args.Type == RawPointerEventType.TouchBegin)
{
if (_pointers.Count > 1)
@ -73,9 +82,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true),
PointerUpdateKind.LeftButtonPressed),
GetKeyModifiers(args.InputModifiers), _clickCount));
new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
keyModifier, _clickCount));
}
if (args.Type == RawPointerEventType.TouchEnd)
@ -85,10 +93,10 @@ namespace Avalonia.Input
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false),
PointerUpdateKind.LeftButtonReleased),
GetKeyModifiers(args.InputModifiers), MouseButton.Left));
new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
keyModifier, MouseButton.Left));
}
_lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchCancel)
@ -96,18 +104,16 @@ namespace Avalonia.Input
_pointers.Remove(args.TouchPointId);
using (pointer)
pointer.Capture(null);
_lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchUpdate)
{
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other),
GetKeyModifiers(args.InputModifiers), args.IntermediatePoints));
new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
keyModifier, args.IntermediatePoints));
}
}
public void Dispose()
@ -121,5 +127,12 @@ namespace Avalonia.Input
p.Dispose();
}
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return ev is RawTouchEventArgs args
&& _pointers.TryGetValue(args.TouchPointId, out var pointer)
? pointer
: null;
}
}
}

83
src/Avalonia.Base/Media/Color.cs

@ -166,7 +166,10 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
// Note: The length checks are also an important optimization.
// The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@ -175,7 +178,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@ -185,7 +188,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings
var str = s.ToString();
if (s.Length > 5 &&
// Note: The length checks are also an important optimization.
// The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@ -238,7 +244,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@ -248,7 +254,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@ -271,6 +277,9 @@ namespace Avalonia.Media
return false;
}
/// <summary>
/// Parses the given span of characters representing a hex color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
/// <summary>
/// Parses the given string representing a CSS color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseCssFormat(string s, out Color color)
{
bool prefixMatched = false;
color = default;
if (s is null)
@ -342,27 +356,35 @@ namespace Avalonia.Media
return false;
}
if (workingString.Length > 6 &&
if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
}
if (workingString.Length > 5 &&
if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
}
string[] components = workingString.Split(',');
if (components.Length == 3) // RGB
{
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue))
if (InternalTryParseByte(components[0], out byte red) &&
InternalTryParseByte(components[1], out byte green) &&
InternalTryParseByte(components[2], out byte blue))
{
color = new Color(0xFF, red, green, blue);
return true;
@ -370,18 +392,45 @@ namespace Avalonia.Media
}
else if (components.Length == 4) // RGBA
{
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) &&
TryInternalParse(components[3], out double alpha))
if (InternalTryParseByte(components[0], out byte red) &&
InternalTryParseByte(components[1], out byte green) &&
InternalTryParseByte(components[2], out byte blue) &&
InternalTryParseDouble(components[3], out double alpha))
{
color = new Color((byte)(alpha * 255), red, green, blue);
color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue);
return true;
}
}
// Local function to specially parse a byte value with an optional percentage sign
bool InternalTryParseByte(string inString, out byte outByte)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outByte = (byte)Math.Round((percentage / 100.0) * 255.0);
return result;
}
else
{
return byte.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outByte);
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
bool InternalTryParseDouble(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);

55
src/Avalonia.Base/Media/HslColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{
/// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary>
#if !BUILDTASK
public
@ -98,24 +99,53 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1.
/// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; }
/// <summary>
/// Gets the Hue component in the range from 0..360.
/// Gets the Hue component in the range from 0..360 (degrees).
/// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; }
/// <summary>
/// Gets the Saturation component in the range from 0..1.
/// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; }
/// <summary>
/// Gets the Lightness component in the range from 0..1.
/// Gets the Lightness component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black.</item>
/// <item>1 is fully white.</item>
/// </list>
/// </remarks>
public double L { get; }
/// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor)
{
bool prefixMatched = false;
hslColor = default;
if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
if (workingString.Length > 6 &&
// Note: The length checks are also an important optimization.
// The shortest possible format is "hsl(0,0,0)", Length = 10.
if (workingString.Length >= 11 &&
workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
}
if (workingString.Length > 5 &&
if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
}
string[] components = workingString.Split(',');

55
src/Avalonia.Base/Media/HsvColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{
/// <summary>
/// Defines a color using the hue/saturation/value (HSV) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary>
#if !BUILDTASK
public
@ -98,24 +99,53 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1.
/// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; }
/// <summary>
/// Gets the Hue component in the range from 0..360.
/// Gets the Hue component in the range from 0..360 (degrees).
/// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; }
/// <summary>
/// Gets the Saturation component in the range from 0..1.
/// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; }
/// <summary>
/// Gets the Value component in the range from 0..1.
/// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black and shows no color.</item>
/// <item>1 is the brightest and shows full color.</item>
/// </list>
/// </remarks>
public double V { get; }
/// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HsvColor hsvColor)
{
bool prefixMatched = false;
hsvColor = default;
if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
if (workingString.Length > 6 &&
// Note: The length checks are also an important optimization.
// The shortest possible format is "hsv(0,0,0)", Length = 10.
if (workingString.Length >= 11 &&
workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
}
if (workingString.Length > 5 &&
if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
}
string[] components = workingString.Split(',');

15
src/Avalonia.Base/RelativePoint.cs

@ -1,7 +1,8 @@
using System;
using System.Globalization;
#if !BUILDTASK
using Avalonia.Animation.Animators;
#endif
using Avalonia.Utilities;
namespace Avalonia
@ -10,7 +11,10 @@ namespace Avalonia
/// Defines the reference point units of an <see cref="RelativePoint"/> or
/// <see cref="RelativeRect"/>.
/// </summary>
public enum RelativeUnit
#if !BUILDTASK
public
#endif
enum RelativeUnit
{
/// <summary>
/// The point is expressed as a fraction of the containing element's size.
@ -26,7 +30,10 @@ namespace Avalonia
/// <summary>
/// Defines a point that may be defined relative to a containing element.
/// </summary>
public readonly struct RelativePoint : IEquatable<RelativePoint>
#if !BUILDTASK
public
#endif
readonly struct RelativePoint : IEquatable<RelativePoint>
{
/// <summary>
/// A point at the top left of the containing element.
@ -49,7 +56,9 @@ namespace Avalonia
static RelativePoint()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<RelativePointAnimator>(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>

3
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -95,6 +95,9 @@
<Compile Include="../Avalonia.Controls/GridLength.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Base/RelativePoint.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />

10
src/Avalonia.Build.Tasks/Properties/launchSettings.json

@ -0,0 +1,10 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Compile Sandbox": {
"commandName": "Project",
"executablePath": "$(SolutionDir)\\src\\Avalonia.Build.Tasks\\bin\\Debug\\net6.0\\Avalonia.Build.Tasks.exe",
"commandLineArgs": "$(SolutionDir)\\samples\\Sandbox\\obj\\Debug\\net6.0\\Avalonia\\original.dll $(SolutionDir)\\samples\\Sandbox\\bin\\Debug\\net6.0\\Sandbox.dll.refs $(SolutionDir)\\out.dll"
}
}
}

8
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -32,6 +32,14 @@ namespace Avalonia.Controls
/// <summary>
/// Displays data in a customizable grid.
/// </summary>
[TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(IVisual))]
[TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))]
[TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))]
[TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))]
[TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))]
[TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))]
[TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))]
[TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))]
[PseudoClasses(":invalid", ":empty-rows", ":empty-columns")]
public partial class DataGrid : TemplatedControl
{

1
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -13,6 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell.
/// </summary>
[TemplatePart(DATAGRIDCELL_elementRightGridLine, typeof(Rectangle))]
[PseudoClasses(":selected", ":current", ":edited", ":invalid")]
public class DataGridCell : ContentControl
{

5
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -21,6 +21,11 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row.
/// </summary>
[TemplatePart(DATAGRIDROW_elementBottomGridLine, typeof(Rectangle))]
[TemplatePart(DATAGRIDROW_elementCells, typeof(DataGridCellsPresenter))]
[TemplatePart(DATAGRIDROW_elementDetails, typeof(DataGridDetailsPresenter))]
[TemplatePart(DATAGRIDROW_elementRoot, typeof(Panel))]
[TemplatePart(DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))]
[PseudoClasses(":selected", ":editing", ":invalid")]
public class DataGridRow : TemplatedControl
{

6
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -14,6 +14,12 @@ using System.Reactive.Linq;
namespace Avalonia.Controls
{
[TemplatePart(DATAGRIDROWGROUPHEADER_expanderButton, typeof(ToggleButton))]
[TemplatePart(DATAGRIDROWGROUPHEADER_indentSpacer, typeof(Control))]
[TemplatePart(DATAGRIDROWGROUPHEADER_itemCountElement, typeof(TextBlock))]
[TemplatePart(DATAGRIDROWGROUPHEADER_propertyNameElement, typeof(TextBlock))]
[TemplatePart(DataGridRow.DATAGRIDROW_elementRoot, typeof(Panel))]
[TemplatePart(DataGridRow.DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))]
[PseudoClasses(":pressed", ":current", ":expanded")]
public class DataGridRowGroupHeader : TemplatedControl
{

1
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@ -13,6 +13,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header.
/// </summary>
[TemplatePart(DATAGRIDROWHEADER_elementRootName, typeof(Control))]
[PseudoClasses(":invalid", ":selected", ":editing", ":current")]
public class DataGridRowHeader : ContentControl
{

32
src/Avalonia.Controls/ComboBox.cs

@ -453,42 +453,18 @@ namespace Avalonia.Controls
private void SelectNext()
{
int next = SelectedIndex + 1;
if (next >= ItemCount)
if (ItemCount >= 1)
{
if (WrapSelection == true)
{
next = 0;
}
else
{
return;
}
MoveSelection(NavigationDirection.Next, WrapSelection);
}
SelectedIndex = next;
}
private void SelectPrev()
{
int prev = SelectedIndex - 1;
if (prev < 0)
if (ItemCount >= 1)
{
if (WrapSelection == true)
{
prev = ItemCount - 1;
}
else
{
return;
}
MoveSelection(NavigationDirection.Previous, WrapSelection);
}
SelectedIndex = prev;
}
}
}

1
src/Avalonia.Controls/ItemsControl.cs

@ -508,7 +508,6 @@ namespace Avalonia.Controls
do
{
result = container.GetControl(direction, c, wrap);
from = from ?? result;
if (result != null &&
result.Focusable &&

2
src/Avalonia.Controls/StackPanel.cs

@ -123,7 +123,7 @@ namespace Avalonia.Controls
index = Children.Count - 1;
break;
case NavigationDirection.Next:
if (index != -1) ++index;
++index;
break;
case NavigationDirection.Previous:
if (index != -1) --index;

25
src/Avalonia.Controls/TopLevel.cs

@ -15,7 +15,6 @@ using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls
{
@ -87,6 +86,8 @@ namespace Avalonia.Controls
private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler;
private readonly IPlatformRenderInterface? _renderInterface;
private readonly IGlobalStyles? _globalStyles;
private readonly PointerOverPreProcessor? _pointerOverPreProcessor;
private readonly IDisposable? _pointerOverPreProcessorSubscription;
private Size _clientSize;
private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel;
@ -195,6 +196,9 @@ namespace Avalonia.Controls
}
impl.LostFocus += PlatformImpl_LostFocus;
_pointerOverPreProcessor = new PointerOverPreProcessor(this);
_pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor);
}
/// <summary>
@ -283,9 +287,7 @@ namespace Avalonia.Controls
/// </summary>
IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!;
/// <summary>
/// Gets or sets the input element that the pointer is currently over.
/// </summary>
/// <inheritdoc/>
IInputElement? IInputRoot.PointerOverElement
{
get { return GetValue(PointerOverElementProperty); }
@ -378,10 +380,12 @@ namespace Avalonia.Controls
Renderer?.Dispose();
Renderer = null!;
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
_pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose();
PlatformImpl = null;
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
@ -515,12 +519,17 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private void HandleInput(RawInputEventArgs e)
{
if (e is RawPointerEventArgs pointerArgs)
{
pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
}
_inputManager?.ProcessInput(e);
}
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
{
(this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect);
_pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect);
}
void PlatformImpl_LostFocus()

5
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels
.Subscribe(e =>
{
PointerOverRoot = e.Root;
PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault();
});
PointerOverElement = e.Root.InputHitTest(e.Position);
});
#nullable restore
}
Console = new ConsoleViewModel(UpdateConsoleContext);

8
src/Avalonia.PlatformSupport/AssetLoader.cs

@ -14,14 +14,14 @@ namespace Avalonia.PlatformSupport
/// </summary>
public class AssetLoader : IAssetLoader
{
private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new();
private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver();
private AssemblyDescriptor? _defaultResmAssembly;
/// <remarks>
/// Introduced for tests.
/// </remarks>
internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) =>
internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) =>
s_assemblyDescriptorResolver = resolver;
/// <summary>
@ -182,13 +182,13 @@ namespace Avalonia.PlatformSupport
throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri));
}
private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
private (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
{
var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority);
return (asm, uri.GetUnescapeAbsolutePath());
}
private AssemblyDescriptor? GetAssembly(Uri? uri)
private IAssemblyDescriptor? GetAssembly(Uri? uri)
{
if (uri != null)
{

2
src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj

@ -19,6 +19,6 @@
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>
</Project>

10
src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs

@ -6,7 +6,15 @@ using Avalonia.Utilities;
namespace Avalonia.PlatformSupport.Internal;
internal class AssemblyDescriptor
internal interface IAssemblyDescriptor
{
Assembly Assembly { get; }
Dictionary<string, IAssetDescriptor>? Resources { get; }
Dictionary<string, IAssetDescriptor>? AvaloniaResources { get; }
string? Name { get; }
}
internal class AssemblyDescriptor : IAssemblyDescriptor
{
public AssemblyDescriptor(Assembly assembly)
{

11
src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs

@ -5,11 +5,16 @@ using System.Reflection;
namespace Avalonia.PlatformSupport.Internal;
internal class AssemblyDescriptorResolver
internal interface IAssemblyDescriptorResolver
{
private readonly Dictionary<string, AssemblyDescriptor> _assemblyNameCache = new();
IAssemblyDescriptor GetAssembly(string name);
}
internal class AssemblyDescriptorResolver: IAssemblyDescriptorResolver
{
private readonly Dictionary<string, IAssemblyDescriptor> _assemblyNameCache = new();
public AssemblyDescriptor GetAssembly(string name)
public IAssemblyDescriptor GetAssembly(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));

23
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -160,6 +160,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return true;
}
if (type.Equals(types.RelativePoint))
{
try
{
var relativePoint = RelativePoint.Parse(text);
var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false);
result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List<IXamlAstValueNode>
{
new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X),
new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.Y),
new XamlConstantNode(node, types.RelativeUnit, (int) relativePoint.Unit),
});
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a relative point", node);
}
}
if (type.Equals(types.GridLength))
{
try

7
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -71,6 +71,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlConstructor MatrixFullConstructor { get; }
public IXamlType CornerRadius { get; }
public IXamlConstructor CornerRadiusFullConstructor { get; }
public IXamlType RelativeUnit { get; }
public IXamlType RelativePoint { get; }
public IXamlConstructor RelativePointFullConstructor { get; }
public IXamlType GridLength { get; }
public IXamlConstructor GridLengthConstructorValueType { get; }
public IXamlType Color { get; }
@ -175,6 +178,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
(Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6);
(CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4);
RelativeUnit = cfg.TypeSystem.GetType("Avalonia.RelativeUnit");
RelativePoint = cfg.TypeSystem.GetType("Avalonia.RelativePoint");
RelativePointFullConstructor = RelativePoint.GetConstructor(new List<IXamlType> { XamlIlTypes.Double, XamlIlTypes.Double, RelativeUnit });
GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength");
GridLengthConstructorValueType = GridLength.GetConstructor(new List<IXamlType> { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") });
Color = cfg.TypeSystem.GetType("Avalonia.Media.Color");

175
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@ -6,7 +6,6 @@ using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
@ -34,160 +33,6 @@ namespace Avalonia.Base.UnitTests.Input
}
#pragma warning restore CS0618 // Type or member is obsolete
[Fact]
public void MouseMove_Should_Update_IsPointerOver()
{
var renderer = new Mock<IRenderer>();
using (TestApplication(renderer.Object))
{
var inputManager = InputManager.Instance;
Canvas canvas;
Border border;
Decorator decorator;
var root = new TestRoot
{
MouseDevice = new MouseDevice(),
Renderer = renderer.Object,
Child = new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
}
};
SetHit(renderer, decorator);
SendMouseMove(inputManager, root);
Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
SetHit(renderer, canvas);
SendMouseMove(inputManager, root);
Assert.False(decorator.IsPointerOver);
Assert.False(border.IsPointerOver);
Assert.True(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}
}
[Fact]
public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
{
var renderer = new Mock<IRenderer>();
using (TestApplication(renderer.Object))
{
var inputManager = InputManager.Instance;
Canvas canvas;
Border border;
Decorator decorator;
var root = new TestRoot
{
MouseDevice = new MouseDevice(),
Renderer = renderer.Object,
Child = new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
}
};
SetHit(renderer, canvas);
SendMouseMove(inputManager, root);
Assert.False(decorator.IsPointerOver);
Assert.False(border.IsPointerOver);
Assert.True(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
// Ensure that e.Handled is reset between controls.
decorator.PointerEnter += (s, e) => e.Handled = true;
SetHit(renderer, decorator);
SendMouseMove(inputManager, root);
Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}
}
[Fact]
public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order()
{
var renderer = new Mock<IRenderer>();
var result = new List<(object, string)>();
void HandleEvent(object sender, PointerEventArgs e)
{
result.Add((sender, e.RoutedEvent.Name));
}
using (TestApplication(renderer.Object))
{
var inputManager = InputManager.Instance;
Canvas canvas;
Border border;
Decorator decorator;
var root = new TestRoot
{
MouseDevice = new MouseDevice(),
Renderer = renderer.Object,
Child = new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
}
};
SetHit(renderer, canvas);
SendMouseMove(inputManager, root);
AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator);
SetHit(renderer, decorator);
SendMouseMove(inputManager, root);
Assert.Equal(
new[]
{
((object)canvas, "PointerLeave"),
((object)decorator, "PointerEnter"),
((object)border, "PointerEnter"),
},
result);
}
}
[Fact]
public void GetPosition_Should_Respect_Control_RenderTransform()
{
@ -216,17 +61,6 @@ namespace Avalonia.Base.UnitTests.Input
}
}
private void AddEnterLeaveHandlers(
EventHandler<PointerEventArgs> handler,
params IControl[] controls)
{
foreach (var c in controls)
{
c.PointerEnter += handler;
c.PointerLeave += handler;
}
}
private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
{
inputManager.ProcessInput(new RawPointerEventArgs(
@ -238,15 +72,6 @@ namespace Avalonia.Base.UnitTests.Input
RawInputModifiers.None));
}
private void SetHit(Mock<IRenderer> renderer, IControl hit)
{
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(new[] { hit });
renderer.Setup(x => x.HitTestFirst(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit);
}
private IDisposable TestApplication(IRenderer renderer)
{
return UnitTestApplication.Start(

534
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@ -0,0 +1,534 @@
#nullable enable
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Input
{
public class PointerOverTests
{
// https://github.com/AvaloniaUI/Avalonia/issues/2821
[Fact]
public void Close_Should_Remove_PointerOver()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object);
Canvas canvas;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
});
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.True(canvas.IsPointerOver);
impl.Object.Closed!();
Assert.False(canvas.IsPointerOver);
}
[Fact]
public void MouseMove_Should_Update_IsPointerOver()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object);
Canvas canvas;
Border border;
Decorator decorator;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
});
SetHit(renderer, decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.False(decorator.IsPointerOver);
Assert.False(border.IsPointerOver);
Assert.True(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}
[Fact]
public void TouchMove_Should_Not_Set_IsPointerOver()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object;
var impl = CreateTopLevelImplMock(renderer.Object);
Canvas canvas;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
});
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.False(canvas.IsPointerOver);
Assert.False(root.IsPointerOver);
}
[Fact]
public void HitTest_Should_Be_Ignored_If_Element_Captured()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var pointer = new Mock<IPointer>();
var device = CreatePointerDeviceMock(pointer.Object).Object;
var impl = CreateTopLevelImplMock(renderer.Object);
Canvas canvas;
Border border;
Decorator decorator;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
});
SetHit(renderer, canvas);
pointer.SetupGet(p => p.Captured).Returns(decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}
[Fact]
public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object);
Canvas canvas;
Border border;
Decorator decorator;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
});
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.False(decorator.IsPointerOver);
Assert.False(border.IsPointerOver);
Assert.True(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
// Ensure that e.Handled is reset between controls.
root.PointerMoved += (s, e) => e.Handled = true;
decorator.PointerEnter += (s, e) => e.Handled = true;
SetHit(renderer, decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}
[Fact]
public void Pointer_Enter_Move_Leave_Should_Be_Followed()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string)>();
void HandleEvent(object? sender, PointerEventArgs e)
{
result.Add((sender, e.RoutedEvent!.Name));
}
Canvas canvas;
Border border;
Decorator decorator;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
});
AddEnterLeaveHandlers(HandleEvent, canvas, decorator);
// Enter decorator
SetHit(renderer, decorator);
SetMove(deviceMock, root, decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
// Leave decorator
SetHit(renderer, canvas);
SetMove(deviceMock, root, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
Assert.Equal(
new[]
{
((object?)decorator, "PointerEnter"),
(decorator, "PointerMove"),
(decorator, "PointerLeave"),
(canvas, "PointerEnter"),
(canvas, "PointerMove")
},
result);
}
[Fact]
public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string)>();
void HandleEvent(object? sender, PointerEventArgs e)
{
result.Add((sender, e.RoutedEvent!.Name));
}
Canvas canvas;
Border border;
Decorator decorator;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
});
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator);
SetHit(renderer, decorator);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
Assert.Equal(
new[]
{
((object?)canvas, "PointerLeave"),
(decorator, "PointerEnter"),
(border, "PointerEnter"),
},
result);
}
// https://github.com/AvaloniaUI/Avalonia/issues/7896
[Fact]
public void PointerEnter_Leave_Should_Set_Correct_Position()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var expectedPosition = new Point(15, 15);
var renderer = new Mock<IRenderer>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string, Point)>();
void HandleEvent(object? sender, PointerEventArgs e)
{
result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
}
Canvas canvas;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
});
AddEnterLeaveHandlers(HandleEvent, root, canvas);
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition));
SetHit(renderer, null);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition));
Assert.Equal(
new[]
{
((object?)canvas, "PointerEnter", expectedPosition),
(root, "PointerEnter", expectedPosition),
(canvas, "PointerLeave", expectedPosition),
(root, "PointerLeave", expectedPosition)
},
result);
}
[Fact]
public void Render_Invalidation_Should_Affect_PointerOver()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var invalidateRect = new Rect(0, 0, 15, 15);
Canvas canvas;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
});
// Let input know about latest device.
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
Assert.True(canvas.IsPointerOver);
SetHit(renderer, canvas);
renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect));
Assert.True(canvas.IsPointerOver);
// Raise SceneInvalidated again, but now hide element from the hittest.
SetHit(renderer, null);
renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect));
Assert.False(canvas.IsPointerOver);
}
// https://github.com/AvaloniaUI/Avalonia/issues/7748
[Fact]
public void LeaveWindow_Should_Reset_PointerOver()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var lastClientPosition = new Point(1, 5);
var invalidateRect = new Rect(0, 0, 15, 15);
var result = new List<(object?, string, Point)>();
void HandleEvent(object? sender, PointerEventArgs e)
{
result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
}
Canvas canvas;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
});
AddEnterLeaveHandlers(HandleEvent, root, canvas);
// Init pointer over.
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition));
Assert.True(canvas.IsPointerOver);
// Send LeaveWindow.
impl.Object.Input!(new RawPointerEventArgs(deviceMock.Object, 0, root, RawPointerEventType.LeaveWindow, new Point(), default));
Assert.False(canvas.IsPointerOver);
Assert.Equal(
new[]
{
((object?)canvas, "PointerEnter", lastClientPosition),
(root, "PointerEnter", lastClientPosition),
(canvas, "PointerLeave", lastClientPosition),
(root, "PointerLeave", lastClientPosition),
},
result);
}
private static void AddEnterLeaveHandlers(
EventHandler<PointerEventArgs> handler,
params IInputElement[] controls)
{
foreach (var c in controls)
{
c.PointerEnter += handler;
c.PointerLeave += handler;
c.PointerMoved += handler;
}
}
private static void SetHit(Mock<IRenderer> renderer, IControl? hit)
{
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit is null ? Array.Empty<IControl>() : new[] { hit });
renderer.Setup(x => x.HitTestFirst(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit);
}
private static void SetMove(Mock<IPointerDevice> deviceMock, IInputRoot root, IInputElement element)
{
deviceMock.Setup(d => d.ProcessRawEvent(It.IsAny<RawPointerEventArgs>()))
.Callback(() => element.RaiseEvent(CreatePointerMovedArgs(root, element)));
}
private static Mock<IWindowImpl> CreateTopLevelImplMock(IRenderer renderer)
{
var impl = new Mock<IWindowImpl>();
impl.DefaultValue = DefaultValue.Mock;
impl.SetupAllProperties();
impl.SetupGet(r => r.RenderScaling).Returns(1);
impl.Setup(r => r.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer);
impl.Setup(r => r.PointToScreen(It.IsAny<Point>())).Returns<Point>(p => new PixelPoint((int)p.X, (int)p.Y));
impl.Setup(r => r.PointToClient(It.IsAny<PixelPoint>())).Returns<PixelPoint>(p => new Point(p.X, p.Y));
return impl;
}
private static IInputRoot CreateInputRoot(IWindowImpl impl, IControl child)
{
var root = new Window(impl)
{
Width = 100,
Height = 100,
Content = child,
Template = new FuncControlTemplate<Window>((w, _) => new ContentPresenter
{
Content = w.Content
})
};
root.Show();
return root;
}
private static IInputRoot CreateInputRoot(IRenderer renderer, IControl child)
{
return CreateInputRoot(CreateTopLevelImplMock(renderer).Object, child);
}
private static RawPointerEventArgs CreateRawPointerMovedArgs(
IPointerDevice pointerDevice,
IInputRoot root,
Point? positition = null)
{
return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move,
positition ?? default, default);
}
private static PointerEventArgs CreatePointerMovedArgs(
IInputRoot root, IInputElement? source, Point? positition = null)
{
return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock<IPointer>().Object, root,
positition ?? default, default, PointerPointProperties.None, KeyModifiers.None);
}
private static Mock<IPointerDevice> CreatePointerDeviceMock(
IPointer? pointer = null,
PointerType pointerType = PointerType.Mouse)
{
if (pointer is null)
{
var pointerMock = new Mock<IPointer>();
pointerMock.SetupGet(p => p.Type).Returns(pointerType);
pointer = pointerMock.Object;
}
var pointerDevice = new Mock<IPointerDevice>();
pointerDevice.Setup(d => d.TryGetPointer(It.IsAny<RawPointerEventArgs>()))
.Returns(pointer);
return pointerDevice;
}
}
}

24
tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

@ -216,8 +216,8 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("hsl(-1000, -1000, -1000)", new HslColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsl(-1000, -1000%, -1000%)", new HslColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsl(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsl(300, 80%, 20%)", new HslColor(1.0, 300, 0.8, 0.2)),
@ -262,8 +262,8 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("hsv(-1000, -1000, -1000)", new HsvColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsv(-1000, -1000%, -1000%)", new HsvColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsv(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsv(300, 80%, 20%)", new HsvColor(1.0, 300, 0.8, 0.2)),
@ -303,8 +303,20 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(229, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(230, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 90%)", new Color(230, 100, 30, 45)),
Tuple.Create("rgb(255,0,0)", new Color(255, 255, 0, 0)),
Tuple.Create("rgb(0,255,0)", new Color(255, 0, 255, 0)),
Tuple.Create("rgb(0,0,255)", new Color(255, 0, 0, 255)),
Tuple.Create("rgb(100%, 0, 0)", new Color(255, 255, 0, 0)),
Tuple.Create("rgb(0, 100%, 0)", new Color(255, 0, 255, 0)),
Tuple.Create("rgb(0, 0, 100%)", new Color(255, 0, 0, 255)),
Tuple.Create("rgba(0, 0, 100%, 50%)", new Color(128, 0, 0, 255)),
Tuple.Create("rgba(50%, 10%, 80%, 50%)", new Color(128, 128, 26, 204)),
Tuple.Create("rgba(50%, 10%, 80%, 0.5)", new Color(128, 128, 26, 204)),
// HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)),

75
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -36,6 +36,81 @@ namespace Avalonia.Controls.UnitTests
Assert.False(target.IsDropDownOpen);
}
[Fact]
public void WrapSelection_Should_Work()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var items = new[]
{
new ComboBoxItem() { Content = "bla" },
new ComboBoxItem() { Content = "dd" },
new ComboBoxItem() { Content = "sdf", IsEnabled = false }
};
var target = new ComboBox
{
Items = items,
Template = GetTemplate(),
WrapSelection = true
};
var root = new TestRoot(target);
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.Focus();
Assert.Equal(target.SelectedIndex, -1);
Assert.True(target.IsFocused);
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Up,
});
Assert.Equal(target.SelectedIndex, 1);
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Down,
});
Assert.Equal(target.SelectedIndex, 0);
}
}
[Fact]
public void Focuses_Next_Item_On_Key_Down()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var items = new[]
{
new ComboBoxItem() { Content = "bla" },
new ComboBoxItem() { Content = "dd", IsEnabled = false },
new ComboBoxItem() { Content = "sdf" }
};
var target = new ComboBox
{
Items = items,
Template = GetTemplate()
};
var root = new TestRoot(target);
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.Focus();
Assert.Equal(target.SelectedIndex, -1);
Assert.True(target.IsFocused);
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Down,
});
Assert.Equal(target.SelectedIndex, 0);
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Down,
});
Assert.Equal(target.SelectedIndex, 2);
}
}
[Fact]
public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control()
{

21
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -193,6 +193,9 @@ namespace Avalonia.Controls.UnitTests
public void Impl_Input_Should_Pass_Input_To_InputManager()
{
var inputManagerMock = new Mock<IInputManager>();
inputManagerMock.DefaultValue = DefaultValue.Mock;
inputManagerMock.SetupAllProperties();
var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object);
using (UnitTestApplication.Start(services))
@ -249,24 +252,6 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Close_Should_Notify_MouseDevice()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var impl = new Mock<ITopLevelImpl>();
var mouseDevice = new Mock<IMouseDevice>();
impl.SetupAllProperties();
impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object);
var target = new TestTopLevel(impl.Object);
impl.Object.Closed();
mouseDevice.Verify(x => x.TopLevelClosed(target));
}
}
[Fact]
public void Close_Should_Dispose_LayoutManager()
{

6
tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs

@ -16,7 +16,7 @@ public class AssetLoaderTests
static AssetLoaderTests()
{
var resolver = Mock.Of<AssemblyDescriptorResolver>();
var resolver = Mock.Of<IAssemblyDescriptorResolver>();
var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor);
@ -49,13 +49,13 @@ public class AssetLoaderTests
Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName);
}
private static AssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
{
var assembly = Mock.Of<MockAssembly>();
Mock.Get(assembly).Setup(x => x.GetName()).Returns(new AssemblyName(assemblyName));
Mock.Get(assembly).Setup(x => x.FullName).Returns(assemblyName);
var descriptor = Mock.Of<AssemblyDescriptor>();
var descriptor = Mock.Of<IAssemblyDescriptor>();
Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly);
return descriptor;
}

Loading…
Cancel
Save