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.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.PlatformSupport.UnitTests");
}); });
Target RunRenderTests => _ => _ Target RunRenderTests => _ => _

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

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

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

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

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

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

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

@ -1,17 +1,31 @@
using System; using System;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Input.Raw;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public interface IPointerDevice : IInputDevice public interface IPointerDevice : IInputDevice
{ {
/// <inheritdoc cref="IPointer.Captured" />
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
IInputElement? Captured { get; } IInputElement? Captured { get; }
/// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
void Capture(IInputElement? control); void Capture(IInputElement? control);
/// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
[Obsolete("Use PointerEventArgs.GetPosition")] [Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo); 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, RoutedEvent = routedEvent,
Device = this, Device = this,
Key = keyInput.Key, Key = keyInput.Key,
KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element, Source = element,
}; };

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

@ -21,27 +21,17 @@ namespace Avalonia.Input
private readonly Pointer _pointer; private readonly Pointer _pointer;
private bool _disposed; private bool _disposed;
private PixelPoint? _position; private PixelPoint? _position;
private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null) public MouseDevice(Pointer? pointer = null)
{ {
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); _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")] [Obsolete("Use IPointer instead")]
public IInputElement? Captured => _pointer.Captured; public IInputElement? Captured => _pointer.Captured;
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use events instead")] [Obsolete("Use events instead")]
public PixelPoint Position public PixelPoint Position
{ {
@ -49,15 +39,7 @@ namespace Avalonia.Input
protected set => _position = value; protected set => _position = value;
} }
/// <summary> [Obsolete("Use IPointer instead")]
/// 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>
public void Capture(IInputElement? control) public void Capture(IInputElement? control)
{ {
_pointer.Capture(control); _pointer.Capture(control);
@ -90,39 +72,6 @@ namespace Avalonia.Input
ProcessRawEvent(margs); 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) int ButtonCount(PointerPointProperties props)
{ {
var rv = 0; var rv = 0;
@ -138,7 +87,7 @@ namespace Avalonia.Input
rv++; rv++;
return rv; return rv;
} }
private void ProcessRawEvent(RawPointerEventArgs e) private void ProcessRawEvent(RawPointerEventArgs e)
{ {
e = e ?? throw new ArgumentNullException(nameof(e)); e = e ?? throw new ArgumentNullException(nameof(e));
@ -147,15 +96,14 @@ namespace Avalonia.Input
if(mouse._disposed) if(mouse._disposed)
return; return;
if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return;
_position = e.Root.PointToScreen(e.Position); _position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e); var props = CreateProperties(e);
var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type) switch (e.Type)
{ {
case RawPointerEventType.LeaveWindow: case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); case RawPointerEventType.NonClientLeftButtonDown:
LeaveWindow();
break; break;
case RawPointerEventType.LeftButtonDown: case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown: case RawPointerEventType.RightButtonDown:
@ -163,10 +111,9 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down: case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1) 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 else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
props, keyModifiers);
break; break;
case RawPointerEventType.LeftButtonUp: case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp: case RawPointerEventType.RightButtonUp:
@ -174,82 +121,50 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up: case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0) 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 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; break;
case RawPointerEventType.Move: 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; break;
case RawPointerEventType.Wheel: 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; break;
case RawPointerEventType.Magnify: 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; break;
case RawPointerEventType.Rotate: 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; break;
case RawPointerEventType.Swipe: 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; break;
} }
} }
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, private void LeaveWindow()
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
_position = null; _position = null;
ClearPointerOver(this, timestamp, root, properties, inputModifiers);
} }
PointerPointProperties CreateProperties(RawPointerEventArgs args) PointerPointProperties CreateProperties(RawPointerEventArgs args)
{ {
return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind());
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);
} }
private MouseButton _lastMouseDownButton;
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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); _pointer.Capture(source);
var source = GetSource(hit);
if (source != null) if (source != null)
{ {
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>(); var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -275,23 +190,14 @@ namespace Avalonia.Input
return false; return false;
} }
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints) PointerPointProperties properties, KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints,
IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
IInputElement? source; var source = _pointer.Captured ?? hitTest;
if (_pointer.Captured == null)
{
source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers);
}
else
{
SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers);
source = _pointer.Captured;
}
if (source is object) if (source is object)
{ {
@ -306,13 +212,12 @@ namespace Avalonia.Input
} }
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
var source = GetSource(hit);
if (source is not null) if (source is not null)
{ {
@ -329,13 +234,12 @@ namespace Avalonia.Input
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, PointerPointProperties props,
Vector delta, KeyModifiers inputModifiers) Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
var source = GetSource(hit);
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. // 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. // 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, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _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, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _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, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _pointer, root, p, timestamp, props, inputModifiers, delta);
@ -418,154 +319,27 @@ namespace Avalonia.Input
return false; return false;
} }
private IInteractive? GetSource(IVisual? hit) public void Dispose()
{
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)
{ {
foreach (IInputElement el in element.VisualChildren) _disposed = true;
{ _pointer?.Dispose();
if (el.IsPointerOver)
{
ClearChildrenPointerOver(e, el, true);
break;
}
}
if(clearRoot)
{
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
}
} }
private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, [Obsolete]
PointerPointProperties properties, public void TopLevelClosed(IInputRoot root)
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); // no-op
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;
} }
private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, [Obsolete]
PointerPointProperties properties, public void SceneInvalidated(IInputRoot root, Rect rect)
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); // no-op
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;
}
} }
public void Dispose() public IPointer? TryGetPointer(RawPointerEventArgs ev)
{ {
_disposed = true; return _pointer;
_pointer?.Dispose();
} }
} }
} }

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

@ -63,6 +63,8 @@ namespace Avalonia.Input
} }
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
} }
public IPointer Pointer { get; } 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; Location = location;
Data = data; Data = data;
Effects = effects; Effects = effects;
KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); KeyModifiers = modifiers.ToKeyModifiers();
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers; Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete #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 /// only valid for Move and TouchUpdate
/// </summary> /// </summary>
public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; } public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; }
internal IInputElement? InputHitTestResult { get; set; }
} }
public struct RawPointerPoint public struct RawPointerPoint

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

@ -3,24 +3,26 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
/// Handles raw touch events /// Handles raw touch events
/// </summary>
/// <remarks> /// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one /// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks> /// </remarks>
/// </summary> public class TouchDevice : IPointerDevice, IDisposable
public class TouchDevice : IInputDevice, IDisposable
{ {
private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>(); private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
private bool _disposed; private bool _disposed;
private int _clickCount; private int _clickCount;
private Rect _lastClickRect; private Rect _lastClickRect;
private ulong _lastClickTime; private ulong _lastClickTime;
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => private Pointer? _lastPointer;
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{ {
@ -30,6 +32,10 @@ namespace Avalonia.Input
return rv; return rv;
} }
void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
public void ProcessRawEvent(RawInputEventArgs ev) public void ProcessRawEvent(RawInputEventArgs ev)
{ {
if (ev.Handled || _disposed) if (ev.Handled || _disposed)
@ -39,15 +45,18 @@ namespace Avalonia.Input
{ {
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
return; return;
var hit = args.Root.InputHitTest(args.Position); var hit = args.InputHitTestResult;
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0); PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit); pointer.Capture(hit);
} }
_lastPointer = pointer;
var target = pointer.Captured ?? args.Root; var target = pointer.Captured ?? args.Root;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();
if (args.Type == RawPointerEventType.TouchBegin) if (args.Type == RawPointerEventType.TouchBegin)
{ {
if (_pointers.Count > 1) if (_pointers.Count > 1)
@ -73,9 +82,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerPressedEventArgs(target, pointer, target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp, args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
PointerUpdateKind.LeftButtonPressed), keyModifier, _clickCount));
GetKeyModifiers(args.InputModifiers), _clickCount));
} }
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
@ -85,10 +93,10 @@ namespace Avalonia.Input
{ {
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp, args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false), new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
PointerUpdateKind.LeftButtonReleased), keyModifier, MouseButton.Left));
GetKeyModifiers(args.InputModifiers), MouseButton.Left));
} }
_lastPointer = null;
} }
if (args.Type == RawPointerEventType.TouchCancel) if (args.Type == RawPointerEventType.TouchCancel)
@ -96,18 +104,16 @@ namespace Avalonia.Input
_pointers.Remove(args.TouchPointId); _pointers.Remove(args.TouchPointId);
using (pointer) using (pointer)
pointer.Capture(null); pointer.Capture(null);
_lastPointer = null;
} }
if (args.Type == RawPointerEventType.TouchUpdate) if (args.Type == RawPointerEventType.TouchUpdate)
{ {
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); keyModifier, args.IntermediatePoints));
} }
} }
public void Dispose() public void Dispose()
@ -121,5 +127,12 @@ namespace Avalonia.Input
p.Dispose(); 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; 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[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') && (s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') && (s[2] == 'b' || s[2] == 'B') &&
@ -175,7 +178,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') && (s[2] == 'l' || s[2] == 'L') &&
@ -185,7 +188,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') && (s[2] == 'v' || s[2] == 'V') &&
@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings // At this point all parsing uses strings
var str = s.ToString(); 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[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') && (s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') && (s[2] == 'b' || s[2] == 'B') &&
@ -238,7 +244,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') && (s[2] == 'l' || s[2] == 'L') &&
@ -248,7 +254,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') && (s[2] == 'v' || s[2] == 'V') &&
@ -271,6 +277,9 @@ namespace Avalonia.Media
return false; 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) private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{ {
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color) static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color); 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) private static bool TryParseCssFormat(string s, out Color color)
{ {
bool prefixMatched = false;
color = default; color = default;
if (s is null) if (s is null)
@ -342,27 +356,35 @@ namespace Avalonia.Media
return false; return false;
} }
if (workingString.Length > 6 && if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) && workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); 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.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');
if (components.Length == 3) // RGB if (components.Length == 3) // RGB
{ {
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && if (InternalTryParseByte(components[0], out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && InternalTryParseByte(components[1], out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue)) InternalTryParseByte(components[2], out byte blue))
{ {
color = new Color(0xFF, red, green, blue); color = new Color(0xFF, red, green, blue);
return true; return true;
@ -370,18 +392,45 @@ namespace Avalonia.Media
} }
else if (components.Length == 4) // RGBA else if (components.Length == 4) // RGBA
{ {
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && if (InternalTryParseByte(components[0], out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && InternalTryParseByte(components[1], out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) && InternalTryParseByte(components[2], out byte blue) &&
TryInternalParse(components[3], out double alpha)) 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; 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 // 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 // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);

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

@ -12,6 +12,7 @@ namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model. /// Defines a color using the hue/saturation/lightness (HSL) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary> /// </summary>
#if !BUILDTASK #if !BUILDTASK
public public
@ -98,24 +99,53 @@ namespace Avalonia.Media
} }
/// <summary> /// <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> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; } public double A { get; }
/// <summary> /// <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. /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary> /// </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; } public double H { get; }
/// <summary> /// <summary>
/// Gets the Saturation component in the range from 0..1. /// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary> /// </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; } public double S { get; }
/// <summary> /// <summary>
/// Gets the Lightness component in the range from 0..1. /// Gets the Lightness component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black.</item>
/// <item>1 is fully white.</item>
/// </list>
/// </remarks>
public double L { get; } public double L { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor) public static bool TryParse(string s, out HslColor hslColor)
{ {
bool prefixMatched = false;
hslColor = default; hslColor = default;
if (s is null) if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false; 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.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); 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.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');

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

@ -12,6 +12,7 @@ namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a color using the hue/saturation/value (HSV) model. /// Defines a color using the hue/saturation/value (HSV) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary> /// </summary>
#if !BUILDTASK #if !BUILDTASK
public public
@ -98,24 +99,53 @@ namespace Avalonia.Media
} }
/// <summary> /// <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> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; } public double A { get; }
/// <summary> /// <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. /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary> /// </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; } public double H { get; }
/// <summary> /// <summary>
/// Gets the Saturation component in the range from 0..1. /// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary> /// </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; } public double S { get; }
/// <summary> /// <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> /// </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; } public double V { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HsvColor hsvColor) public static bool TryParse(string s, out HsvColor hsvColor)
{ {
bool prefixMatched = false;
hsvColor = default; hsvColor = default;
if (s is null) if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false; 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.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); 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.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');

15
src/Avalonia.Base/RelativePoint.cs

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

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

@ -95,6 +95,9 @@
<Compile Include="../Avalonia.Controls/GridLength.cs"> <Compile Include="../Avalonia.Controls/GridLength.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link> <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile> </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 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\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.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> /// <summary>
/// Displays data in a customizable grid. /// Displays data in a customizable grid.
/// </summary> /// </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")] [PseudoClasses(":invalid", ":empty-rows", ":empty-columns")]
public partial class DataGrid : TemplatedControl public partial class DataGrid : TemplatedControl
{ {

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

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

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

@ -21,6 +21,11 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row. /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row.
/// </summary> /// </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")] [PseudoClasses(":selected", ":editing", ":invalid")]
public class DataGridRow : TemplatedControl public class DataGridRow : TemplatedControl
{ {

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

@ -14,6 +14,12 @@ using System.Reactive.Linq;
namespace Avalonia.Controls 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")] [PseudoClasses(":pressed", ":current", ":expanded")]
public class DataGridRowGroupHeader : TemplatedControl public class DataGridRowGroupHeader : TemplatedControl
{ {

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

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

32
src/Avalonia.Controls/ComboBox.cs

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

1
src/Avalonia.Controls/ItemsControl.cs

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

2
src/Avalonia.Controls/StackPanel.cs

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

25
src/Avalonia.Controls/TopLevel.cs

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

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

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

8
src/Avalonia.PlatformSupport/AssetLoader.cs

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

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

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

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

@ -6,7 +6,15 @@ using Avalonia.Utilities;
namespace Avalonia.PlatformSupport.Internal; 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) public AssemblyDescriptor(Assembly assembly)
{ {

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

@ -5,11 +5,16 @@ using System.Reflection;
namespace Avalonia.PlatformSupport.Internal; 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) if (name == null)
throw new ArgumentNullException(nameof(name)); 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; 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)) if (type.Equals(types.GridLength))
{ {
try 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 IXamlConstructor MatrixFullConstructor { get; }
public IXamlType CornerRadius { get; } public IXamlType CornerRadius { get; }
public IXamlConstructor CornerRadiusFullConstructor { get; } public IXamlConstructor CornerRadiusFullConstructor { get; }
public IXamlType RelativeUnit { get; }
public IXamlType RelativePoint { get; }
public IXamlConstructor RelativePointFullConstructor { get; }
public IXamlType GridLength { get; } public IXamlType GridLength { get; }
public IXamlConstructor GridLengthConstructorValueType { get; } public IXamlConstructor GridLengthConstructorValueType { get; }
public IXamlType Color { get; } public IXamlType Color { get; }
@ -175,6 +178,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
(Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6); (Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6);
(CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4); (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"); GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength");
GridLengthConstructorValueType = GridLength.GetConstructor(new List<IXamlType> { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") }); GridLengthConstructorValueType = GridLength.GetConstructor(new List<IXamlType> { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") });
Color = cfg.TypeSystem.GetType("Avalonia.Media.Color"); 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.Media;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq; using Moq;
using Xunit; using Xunit;
@ -34,160 +33,6 @@ namespace Avalonia.Base.UnitTests.Input
} }
#pragma warning restore CS0618 // Type or member is obsolete #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] [Fact]
public void GetPosition_Should_Respect_Control_RenderTransform() 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()) private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
{ {
inputManager.ProcessInput(new RawPointerEventArgs( inputManager.ProcessInput(new RawPointerEventArgs(
@ -238,15 +72,6 @@ namespace Avalonia.Base.UnitTests.Input
RawInputModifiers.None)); 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) private IDisposable TestApplication(IRenderer renderer)
{ {
return UnitTestApplication.Start( 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, 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 (Hue wraps to zero)
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(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)), 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)), 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, 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 (Hue wraps to zero)
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(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)), 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)), 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("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)), 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, 0.9)", new Color(230, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 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 // HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)), 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); 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] [Fact]
public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control() 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() public void Impl_Input_Should_Pass_Input_To_InputManager()
{ {
var inputManagerMock = new Mock<IInputManager>(); var inputManagerMock = new Mock<IInputManager>();
inputManagerMock.DefaultValue = DefaultValue.Mock;
inputManagerMock.SetupAllProperties();
var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object); var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object);
using (UnitTestApplication.Start(services)) 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] [Fact]
public void Close_Should_Dispose_LayoutManager() public void Close_Should_Dispose_LayoutManager()
{ {

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

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

Loading…
Cancel
Save