Browse Source

Merge branch 'master' into wpfgridimport

pull/2563/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
51c7ab375c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      dirs.proj
  2. 1
      native/Avalonia.Native/inc/avalonia-native.h
  3. 10
      native/Avalonia.Native/src/OSX/cursor.h
  4. 5
      native/Avalonia.Native/src/OSX/cursor.mm
  5. 10
      native/Avalonia.Native/src/OSX/window.mm
  6. 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  7. 6
      samples/ControlCatalog.NetCore/Program.cs
  8. 1
      samples/ControlCatalog/MainView.xaml
  9. 99
      samples/ControlCatalog/Pages/PointersPage.cs
  10. 3
      samples/RenderDemo/MainWindow.xaml
  11. 49
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  12. 16
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  13. 2
      src/Avalonia.Base/Utilities/MathUtilities.cs
  14. 7
      src/Avalonia.Controls/Button.cs
  15. 13
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  16. 13
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  17. 32
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  18. 18
      src/Avalonia.Controls/MenuItem.cs
  19. 4
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  20. 22
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  21. 69
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  22. 4
      src/Avalonia.Controls/Primitives/Popup.cs
  23. 3
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  24. 18
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  25. 18
      src/Avalonia.Controls/TabControl.cs
  26. 18
      src/Avalonia.Controls/TextBox.cs
  27. 196
      src/Avalonia.Controls/WrapPanel.cs
  28. 1
      src/Avalonia.Input/Cursors.cs
  29. 127
      src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
  30. 23
      src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
  31. 183
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  32. 18
      src/Avalonia.Input/Gestures.cs
  33. 5
      src/Avalonia.Input/IMouseDevice.cs
  34. 18
      src/Avalonia.Input/IPointer.cs
  35. 8
      src/Avalonia.Input/IPointerDevice.cs
  36. 39
      src/Avalonia.Input/InputElement.cs
  37. 221
      src/Avalonia.Input/MouseDevice.cs
  38. 67
      src/Avalonia.Input/Pointer.cs
  39. 108
      src/Avalonia.Input/PointerEventArgs.cs
  40. 45
      src/Avalonia.Input/PointerPoint.cs
  41. 12
      src/Avalonia.Input/PointerWheelEventArgs.cs
  42. 1
      src/Avalonia.Input/Properties/AssemblyInfo.cs
  43. 4
      src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs
  44. 15
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  45. 15
      src/Avalonia.Input/Raw/RawTouchEventArgs.cs
  46. 29
      src/Avalonia.Input/ScrollGestureEventArgs.cs
  47. 74
      src/Avalonia.Input/TouchDevice.cs
  48. 2
      src/Avalonia.Native/WindowImplBase.cs
  49. 18
      src/Avalonia.OpenGL/AngleOptions.cs
  50. 71
      src/Avalonia.OpenGL/EglDisplay.cs
  51. 33
      src/Avalonia.OpenGL/EglGlPlatformSurface.cs
  52. 43
      src/Avalonia.OpenGL/EglInterface.cs
  53. 7
      src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs
  54. 5
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  55. 55
      src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
  56. 143
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  57. 75
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  58. 80
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  59. 10
      src/Avalonia.Themes.Default/Accents/BaseDark.xaml
  60. 10
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  61. 6
      src/Avalonia.Themes.Default/NotificationCard.xaml
  62. 11
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  63. 8
      src/Avalonia.Visuals/Animation/CrossFade.cs
  64. 5
      src/Avalonia.Visuals/Platform/IRenderTarget.cs
  65. 5
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  66. 14
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  67. 32
      src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs
  68. 24
      src/Avalonia.X11/X11CursorFactory.cs
  69. 4
      src/Avalonia.X11/X11Platform.cs
  70. 26
      src/Avalonia.X11/X11Window.cs
  71. 69
      src/Avalonia.X11/XI2Manager.cs
  72. 9
      src/Avalonia.X11/XIStructs.cs
  73. 3
      src/Avalonia.X11/XLib.cs
  74. 3
      src/Gtk/Avalonia.Gtk3/CursorFactory.cs
  75. 1
      src/Gtk/Avalonia.Gtk3/GdkCursor.cs
  76. 18
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  77. 18
      src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
  78. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
  79. 4
      src/Skia/Avalonia.Skia/GlRenderTarget.cs
  80. 3
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  81. 24
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  82. 1
      src/Windows/Avalonia.Win32/CursorFactory.cs
  83. 72
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  84. 7
      src/Windows/Avalonia.Win32/Win32Platform.cs
  85. 104
      src/Windows/Avalonia.Win32/WindowImpl.cs
  86. 1
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  87. 16
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  88. 106
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  89. 14
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  90. 43
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  91. 9
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  92. 41
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
  93. 111
      tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
  94. 54
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  95. 50
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  96. 15
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  97. 89
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  98. 8
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
  99. 2
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  100. 44
      tests/Avalonia.Interactivity.UnitTests/GestureTests.cs

5
dirs.proj

@ -8,10 +8,11 @@
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/XamlIl/**/*.*proj" />
</ItemGroup>
<!-- Disabled on CI because of ancient MSBuild project format -->
<ItemGroup>
<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">
<ProjectReference Remove="src/Android/**/*.*proj" />
<ProjectReference Remove="samples/ControlCatalog.Android/ControlCatalog.Android.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\iOS') Or $([MSBuild]::IsOsPlatform('Windows')) ">
<ProjectReference Remove="src/iOS/**/*.*proj" />
<ProjectReference Remove="samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj" />
</ItemGroup>

1
native/Avalonia.Native/inc/avalonia-native.h

@ -144,6 +144,7 @@ enum AvnStandardCursorType
CursorDragMove,
CursorDragCopy,
CursorDragLink,
CursorNone
};
enum AvnWindowEdge

10
native/Avalonia.Native/src/OSX/cursor.h

@ -11,18 +11,24 @@ class Cursor : public ComSingleObject<IAvnCursor, &IID_IAvnCursor>
{
private:
NSCursor * _native;
bool _isHidden;
public:
FORWARD_IUNKNOWN()
Cursor(NSCursor * cursor)
Cursor(NSCursor * cursor, bool isHidden = false)
{
_native = cursor;
_isHidden = isHidden;
}
NSCursor* GetNative()
{
return _native;
}
bool IsHidden ()
{
return _isHidden;
}
};
extern std::map<AvnStandardCursorType, Cursor*> s_cursorMap;

5
native/Avalonia.Native/src/OSX/cursor.mm

@ -21,6 +21,7 @@ class CursorFactory : public ComSingleObject<IAvnCursorFactory, &IID_IAvnCursorF
Cursor* resizeRightCursor = new Cursor([NSCursor resizeRightCursor]);
Cursor* resizeWestEastCursor = new Cursor([NSCursor resizeLeftRightCursor]);
Cursor* operationNotAllowedCursor = new Cursor([NSCursor operationNotAllowedCursor]);
Cursor* noCursor = new Cursor([NSCursor arrowCursor], true);
std::map<AvnStandardCursorType, Cursor*> s_cursorMap =
{
@ -46,11 +47,13 @@ class CursorFactory : public ComSingleObject<IAvnCursorFactory, &IID_IAvnCursorF
{ CursorIbeam, IBeamCursor },
{ CursorLeftSide, resizeLeftCursor },
{ CursorRightSide, resizeRightCursor },
{ CursorNo, operationNotAllowedCursor }
{ CursorNo, operationNotAllowedCursor },
{ CursorNone, noCursor }
};
public:
FORWARD_IUNKNOWN()
virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) override
{
*retOut = s_cursorMap[cursorType];

10
native/Avalonia.Native/src/OSX/window.mm

@ -353,6 +353,16 @@ public:
Cursor* avnCursor = dynamic_cast<Cursor*>(cursor);
this->cursor = avnCursor->GetNative();
UpdateCursor();
if(avnCursor->IsHidden())
{
[NSCursor hide];
}
else
{
[NSCursor unhide];
}
return S_OK;
}
}

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -11,6 +11,7 @@
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2019013001"/>
</ItemGroup>

6
samples/ControlCatalog.NetCore/Program.cs

@ -46,6 +46,12 @@ namespace ControlCatalog.NetCore
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new X11PlatformOptions {EnableMultiTouch = true})
.With(new Win32PlatformOptions
{
EnableMultitouch = true,
AllowEglInitialization = true
})
.UseSkia()
.UseReactiveUI();

1
samples/ControlCatalog/MainView.xaml

@ -31,6 +31,7 @@
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>

99
samples/ControlCatalog/Pages/PointersPage.cs

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace ControlCatalog.Pages
{
public class PointersPage : Control
{
class PointerInfo
{
public Point Point { get; set; }
public Color Color { get; set; }
}
private static Color[] AllColors = new[]
{
Colors.Aqua,
Colors.Beige,
Colors.Chartreuse,
Colors.Coral,
Colors.Fuchsia,
Colors.Crimson,
Colors.Lavender,
Colors.Orange,
Colors.Orchid,
Colors.ForestGreen,
Colors.SteelBlue,
Colors.PapayaWhip,
Colors.PaleVioletRed,
Colors.Goldenrod,
Colors.Maroon,
Colors.Moccasin,
Colors.Navy,
Colors.Wheat,
Colors.Violet,
Colors.Sienna,
Colors.Indigo,
Colors.Honeydew
};
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointersPage()
{
ClipToBounds = true;
}
void UpdatePointer(PointerEventArgs e)
{
if (!_pointers.TryGetValue(e.Pointer, out var info))
{
if (e.RoutedEvent == PointerMovedEvent)
return;
var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray();
var color = colors[new Random().Next(0, colors.Length - 1)];
_pointers[e.Pointer] = info = new PointerInfo {Color = color};
}
info.Point = e.GetPosition(this);
InvalidateVisual();
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
UpdatePointer(e);
e.Pointer.Capture(this);
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
UpdatePointer(e);
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
_pointers.Remove(e.Pointer);
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size));
foreach (var pt in _pointers.Values)
{
var brush = new ImmutableSolidColorBrush(pt.Color);
context.DrawGeometry(brush, null, new EllipseGeometry(new Rect(pt.Point.X - 75, pt.Point.Y - 75,
150, 150)));
}
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -38,6 +38,9 @@
<TabItem Header="SkCanvas">
<pages:CustomSkiaPage/>
</TabItem>
<TabItem Header="RenderTargetBitmap">
<pages:RenderTargetBitmapPage/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

49
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -0,0 +1,49 @@
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Avalonia.Visuals.Media.Imaging;
namespace RenderDemo.Pages
{
public class RenderTargetBitmapPage : Control
{
private RenderTargetBitmap _bitmap;
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap = new RenderTargetBitmap(new PixelSize(200, 200), new Vector(96, 96));
base.OnAttachedToLogicalTree(e);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap.Dispose();
_bitmap = null;
base.OnDetachedFromLogicalTree(e);
}
readonly Stopwatch _st = Stopwatch.StartNew();
public override void Render(DrawingContext context)
{
using (var ctxi = _bitmap.CreateDrawingContext(null))
using(var ctx = new DrawingContext(ctxi, false))
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100)))
{
ctxi.Clear(default);
ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
}
context.DrawImage(_bitmap, 1,
new Rect(0, 0, 200, 200),
new Rect(0, 0, 200, 200));
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
base.Render(context);
}
}
}

16
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@ -33,7 +33,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return null;
}
RawMouseEventType? mouseEventType = null;
RawPointerEventType? mouseEventType = null;
var eventTime = DateTime.Now;
//Basic touch support
switch (e.Action)
@ -42,17 +42,17 @@ namespace Avalonia.Android.Platform.Specific.Helpers
//may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
{
mouseEventType = RawMouseEventType.Move;
mouseEventType = RawPointerEventType.Move;
}
break;
case MotionEventActions.Down:
mouseEventType = RawMouseEventType.LeftButtonDown;
mouseEventType = RawPointerEventType.LeftButtonDown;
break;
case MotionEventActions.Up:
mouseEventType = RawMouseEventType.LeftButtonUp;
mouseEventType = RawPointerEventType.LeftButtonUp;
break;
}
@ -75,14 +75,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers
//we need to generate mouse move before first mouse down event
//as this is the way buttons are working every time
//otherwise there is a problem sometimes
if (mouseEventType == RawMouseEventType.LeftButtonDown)
if (mouseEventType == RawPointerEventType.LeftButtonDown)
{
var me = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
RawMouseEventType.Move, _point, InputModifiers.None);
var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
RawPointerEventType.Move, _point, InputModifiers.None);
_view.Input(me);
}
var mouseEvent = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
mouseEventType.Value, _point, InputModifiers.LeftMouseButton);
_view.Input(mouseEvent);

2
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -170,4 +170,4 @@ namespace Avalonia.Utilities
}
}
}
}
}

7
src/Avalonia.Controls/Button.cs

@ -252,7 +252,6 @@ namespace Avalonia.Controls
if (e.MouseButton == MouseButton.Left)
{
e.Device.Capture(this);
IsPressed = true;
e.Handled = true;
@ -270,7 +269,6 @@ namespace Avalonia.Controls
if (IsPressed && e.MouseButton == MouseButton.Left)
{
e.Device.Capture(null);
IsPressed = false;
e.Handled = true;
@ -282,6 +280,11 @@ namespace Avalonia.Controls
}
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
IsPressed = false;
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
base.UpdateDataValidation(property, status);

13
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
CalendarLeftMouseButtonUp?.Invoke(this, e);
}
/// <summary>
/// We need to simulate the MouseLeftButtonUp event for the
/// CalendarButton that stays in Pressed state after MouseCapture is
/// released since there is no actual MouseLeftButtonUp event for the
/// release.
/// </summary>
/// <param name="e">Event arguments.</param>
internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
{
e.Handled = false;
base.OnPointerReleased(e);
}
}
}

13
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
CalendarDayButtonMouseUp?.Invoke(this, e);
}
/// <summary>
/// We need to simulate the MouseLeftButtonUp event for the
/// CalendarDayButton that stays in Pressed state after MouseCapture is
/// released since there is no actual MouseLeftButtonUp event for the
/// release.
/// </summary>
/// <param name="e">Event arguments.</param>
internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
{
e.Handled = false;
base.OnPointerReleased(e);
}
}
}

32
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -934,22 +934,6 @@ namespace Avalonia.Controls.Primitives
// The button is in Pressed state. Change the state to normal.
if (e.Device.Captured == b)
e.Device.Capture(null);
// null check is added for unit tests
if (_downEventArg != null)
{
var arg =
new PointerReleasedEventArgs()
{
Device = _downEventArg.Device,
MouseButton = _downEventArg.MouseButton,
Handled = _downEventArg.Handled,
InputModifiers = _downEventArg.InputModifiers,
Route = _downEventArg.Route,
Source = _downEventArg.Source
};
b.SendMouseLeftButtonUp(arg);
}
_lastCalendarDayButton = b;
}
}
@ -1221,21 +1205,7 @@ namespace Avalonia.Controls.Primitives
if (e.Device.Captured == b)
e.Device.Capture(null);
//b.ReleaseMouseCapture();
if (_downEventArgYearView != null)
{
var args =
new PointerReleasedEventArgs()
{
Device = _downEventArgYearView.Device,
MouseButton = _downEventArgYearView.MouseButton,
Handled = _downEventArgYearView.Handled,
InputModifiers = _downEventArgYearView.InputModifiers,
Route = _downEventArgYearView.Route,
Source = _downEventArgYearView.Source
};
b.SendMouseLeftButtonUp(args);
}
_lastCalendarButton = b;
}
}

18
src/Avalonia.Controls/MenuItem.cs

@ -337,12 +337,9 @@ namespace Avalonia.Controls
{
base.OnPointerEnter(e);
RaiseEvent(new PointerEventArgs
{
Device = e.Device,
RoutedEvent = PointerEnterItemEvent,
Source = this,
});
var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
e.Timestamp, point.Properties, e.InputModifiers));
}
/// <inheritdoc/>
@ -350,12 +347,9 @@ namespace Avalonia.Controls
{
base.OnPointerLeave(e);
RaiseEvent(new PointerEventArgs
{
Device = e.Device,
RoutedEvent = PointerLeaveItemEvent,
Source = this,
});
var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
e.Timestamp, point.Properties, e.InputModifiers));
}
/// <summary>

4
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -373,9 +373,9 @@ namespace Avalonia.Controls.Platform
protected internal virtual void RawInput(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
var mouse = e as RawPointerEventArgs;
if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Menu.Close();
}

22
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@ -43,7 +43,7 @@ namespace Avalonia.Platform
_lastPosition = default(Point);
_allowedEffects = allowedEffects;
using (_inputManager.PreProcess.OfType<RawMouseEventArgs>().Subscribe(ProcessMouseEvents))
using (_inputManager.PreProcess.OfType<RawPointerEventArgs>().Subscribe(ProcessMouseEvents))
{
using (_inputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(ProcessKeyEvents))
{
@ -153,7 +153,7 @@ namespace Avalonia.Platform
}
}
private void ProcessMouseEvents(RawMouseEventArgs e)
private void ProcessMouseEvents(RawPointerEventArgs e)
{
if (!_initialInputModifiers.HasValue)
_initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS;
@ -174,22 +174,22 @@ namespace Avalonia.Platform
switch (e.Type)
{
case RawMouseEventType.LeftButtonDown:
case RawMouseEventType.RightButtonDown:
case RawMouseEventType.MiddleButtonDown:
case RawMouseEventType.NonClientLeftButtonDown:
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
case RawPointerEventType.NonClientLeftButtonDown:
CancelDragging();
e.Handled = true;
return;
case RawMouseEventType.LeaveWindow:
case RawPointerEventType.LeaveWindow:
RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break;
case RawMouseEventType.LeftButtonUp:
case RawPointerEventType.LeftButtonUp:
CheckDraggingAccepted(InputModifiers.LeftMouseButton); break;
case RawMouseEventType.MiddleButtonUp:
case RawPointerEventType.MiddleButtonUp:
CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break;
case RawMouseEventType.RightButtonUp:
case RawPointerEventType.RightButtonUp:
CheckDraggingAccepted(InputModifiers.RightMouseButton); break;
case RawMouseEventType.Move:
case RawPointerEventType.Move:
var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS;
if (_initialInputModifiers.Value != mods)
{

69
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters
private Vector _offset;
private IDisposable _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
{
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
{
var scrollable = Child as ILogicalScrollable;
bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
double x = Offset.X;
double y = Offset.Y;
Vector delta = default;
if (isLogical)
_activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta);
delta += e.Delta;
if (Extent.Height > Viewport.Height)
{
double dy;
if (isLogical)
{
var logicalUnits = delta.Y / LogicalScrollItemSize;
delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
dy = logicalUnits * scrollable.ScrollSize.Height;
}
else
dy = delta.Y;
y += dy;
y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
}
if (Extent.Width > Viewport.Width)
{
double dx;
if (isLogical)
{
var logicalUnits = delta.X / LogicalScrollItemSize;
delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
dx = logicalUnits * scrollable.ScrollSize.Width;
}
else
dx = delta.X;
x += dx;
x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width);
}
if (isLogical)
{
if (_activeLogicalGestureScrolls == null)
_activeLogicalGestureScrolls = new Dictionary<int, Vector>();
_activeLogicalGestureScrolls[e.Id] = delta;
}
Offset = new Vector(x, y);
e.Handled = true;
}
}
private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e)
=> _activeLogicalGestureScrolls?.Remove(e.Id);
/// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{

4
src/Avalonia.Controls/Primitives/Popup.cs

@ -421,9 +421,9 @@ namespace Avalonia.Controls.Primitives
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Close();
}

3
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -54,7 +54,8 @@ namespace Avalonia.Controls.Primitives
nameof(SelectedIndex),
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v,
unsetValue: -1);
unsetValue: -1,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.

18
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -39,21 +39,21 @@ namespace Avalonia.Controls.Remote.Server
KeyboardDevice = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
}
private static RawMouseEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
private static RawPointerEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
{
switch (button)
{
case Avalonia.Remote.Protocol.Input.MouseButton.Left:
return pressed ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp;
return pressed ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
case Avalonia.Remote.Protocol.Input.MouseButton.Middle:
return pressed ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp;
return pressed ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
case Avalonia.Remote.Protocol.Input.MouseButton.Right:
return pressed ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp;
return pressed ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
default:
return RawMouseEventType.Move;
return RawPointerEventType.Move;
}
}
@ -166,11 +166,11 @@ namespace Avalonia.Controls.Remote.Server
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot,
RawMouseEventType.Move,
RawPointerEventType.Move,
new Point(pointer.X, pointer.Y),
GetAvaloniaInputModifiers(pointer.Modifiers)));
}, DispatcherPriority.Input);
@ -179,7 +179,7 @@ namespace Avalonia.Controls.Remote.Server
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot,
@ -192,7 +192,7 @@ namespace Avalonia.Controls.Remote.Server
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot,

18
src/Avalonia.Controls/TabControl.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -166,10 +168,24 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse)
{
var container = GetContainerFromEventSource(e.Source);
if (container != null
&& container.GetVisualsAt(e.GetPosition(container))
.Any(c => container == c || container.IsVisualAncestorOf(c)))
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
}
}
}

18
src/Avalonia.Controls/TextBox.cs

@ -214,9 +214,9 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0);
CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0);
SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
{
@ -677,11 +677,15 @@ namespace Avalonia.Controls
}
}
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0);
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text);
private int CoerceCaretIndex(int value, int length)
private int CoerceCaretIndex(int value, string text)
{
var text = Text;
if (text == null)
{
return 0;
}
var length = text.Length;
if (value < 0)
{
@ -691,7 +695,7 @@ namespace Avalonia.Controls
{
return length;
}
else if (value > 0 && text[value - 1] == '\r' && text[value] == '\n')
else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n')
{
return value + 1;
}

196
src/Avalonia.Controls/WrapPanel.cs

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using Avalonia.Input;
using Avalonia.Utilities;
using static System.Math;
@ -92,109 +93,127 @@ namespace Avalonia.Controls
}
}
private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size);
private UVSize CreateUVSize() => new UVSize(Orientation);
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = CreateUVSize();
var lineSize = CreateUVSize();
var uvAvailableSize = CreateUVSize(availableSize);
var curLineSize = new UVSize(Orientation);
var panelSize = new UVSize(Orientation);
var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height);
foreach (var child in Children)
var childConstraint = new Size(constraint.Width, constraint.Height);
for (int i = 0, count = Children.Count; i < count; i++)
{
child.Measure(availableSize);
var childSize = CreateUVSize(child.DesiredSize);
if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line
var child = Children[i];
if (child == null) continue;
//Flow passes its own constrint to children
child.Measure(childConstraint);
//this is the size of the child in UV space
var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line
{
lineSize.U += childSize.U;
lineSize.V = Max(lineSize.V, childSize.V);
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line
{
panelSize.U = Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(Orientation);
}
}
else // moving to next line
else //continue to accumulate a line
{
desiredSize.U = Max(lineSize.U, uvAvailableSize.U);
desiredSize.V += lineSize.V;
lineSize = childSize;
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
}
}
// last element
desiredSize.U = Max(lineSize.U, desiredSize.U);
desiredSize.V += lineSize.V;
return desiredSize.ToSize();
//the last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
//go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
int firstInLine = 0;
double accumulatedV = 0;
var uvFinalSize = CreateUVSize(finalSize);
var lineSize = CreateUVSize();
int firstChildInLineIndex = 0;
for (int index = 0; index < Children.Count; index++)
UVSize curLineSize = new UVSize(Orientation);
UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
for (int i = 0; i < Children.Count; i++)
{
var child = Children[index];
var childSize = CreateUVSize(child.DesiredSize);
if (lineSize.U + childSize.U <= uvFinalSize.U) // same line
var child = Children[i];
if (child == null) continue;
var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line
{
lineSize.U += childSize.U;
lineSize.V = Max(lineSize.V, childSize.V);
arrangeLine(accumulatedV, curLineSize.V, firstInLine, i);
accumulatedV += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line
{
//switch to next line which only contain one element
arrangeLine(accumulatedV, sz.V, i, ++i);
accumulatedV += sz.V;
curLineSize = new UVSize(Orientation);
}
firstInLine = i;
}
else // moving to next line
else //continue to accumulate a line
{
var controlsInLine = GetControlsBetween(firstChildInLineIndex, index);
ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
accumulatedV += lineSize.V;
lineSize = childSize;
firstChildInLineIndex = index;
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
}
}
if (firstChildInLineIndex < Children.Count)
//arrange the last line, if any
if (firstInLine < Children.Count)
{
var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count);
ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count);
}
return finalSize;
}
private IEnumerable<IControl> GetControlsBetween(int first, int last)
{
return Children.Skip(first).Take(last - first);
return finalSize;
}
private void ArrangeLine(double v, double lineV, IEnumerable<IControl> controls)
private void arrangeLine(double v, double lineV, int start, int end)
{
double u = 0;
bool isHorizontal = (Orientation == Orientation.Horizontal);
foreach (var child in controls)
for (int i = start; i < end; i++)
{
var childSize = CreateUVSize(child.DesiredSize);
var x = isHorizontal ? u : v;
var y = isHorizontal ? v : u;
var width = isHorizontal ? childSize.U : lineV;
var height = isHorizontal ? lineV : childSize.U;
child.Arrange(new Rect(x, y, width, height));
u += childSize.U;
var child = Children[i];
if (child != null)
{
UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
double layoutSlotU = childSize.U;
child.Arrange(new Rect(
(isHorizontal ? u : v),
(isHorizontal ? v : u),
(isHorizontal ? layoutSlotU : lineV),
(isHorizontal ? lineV : layoutSlotU)));
u += layoutSlotU;
}
}
}
/// <summary>
/// Used to not not write separate code for horizontal and vertical orientation.
/// U is direction in line. (x if orientation is horizontal)
/// V is direction of lines. (y if orientation is horizontal)
/// </summary>
[DebuggerDisplay("U = {U} V = {V}")]
private struct UVSize
{
private readonly Orientation _orientation;
internal double U;
internal double V;
private UVSize(Orientation orientation, double width, double height)
internal UVSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
@ -202,52 +221,25 @@ namespace Avalonia.Controls
Height = height;
}
internal UVSize(Orientation orientation, Size size)
: this(orientation, size.Width, size.Height)
{
}
internal UVSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
private double Width
internal double U;
internal double V;
private Orientation _orientation;
internal double Width
{
get { return (_orientation == Orientation.Horizontal ? U : V); }
set
{
if (_orientation == Orientation.Horizontal)
{
U = value;
}
else
{
V = value;
}
}
set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
}
private double Height
internal double Height
{
get { return (_orientation == Orientation.Horizontal ? V : U); }
set
{
if (_orientation == Orientation.Horizontal)
{
V = value;
}
else
{
U = value;
}
}
}
public Size ToSize()
{
return new Size(Width, Height);
set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
}
}
}

1
src/Avalonia.Input/Cursors.cs

@ -38,6 +38,7 @@ namespace Avalonia.Input
DragMove,
DragCopy,
DragLink,
None,
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
// We might enable them later, preferably, by loading pixmax direclty from theme with fallback image

127
src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs

@ -0,0 +1,127 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Input.GestureRecognizers
{
public class GestureRecognizerCollection : IReadOnlyCollection<IGestureRecognizer>, IGestureRecognizerActionsDispatcher
{
private readonly IInputElement _inputElement;
private List<IGestureRecognizer> _recognizers;
private Dictionary<IPointer, IGestureRecognizer> _pointerGrabs;
public GestureRecognizerCollection(IInputElement inputElement)
{
_inputElement = inputElement;
}
public void Add(IGestureRecognizer recognizer)
{
if (_recognizers == null)
{
// We initialize the collection when the first recognizer is added
_recognizers = new List<IGestureRecognizer>();
_pointerGrabs = new Dictionary<IPointer, IGestureRecognizer>();
}
_recognizers.Add(recognizer);
recognizer.Initialize(_inputElement, this);
// Hacks to make bindings work
if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical)
{
logical.SetParent(logicalParent);
if (recognizer is IStyleable styleableRecognizer
&& _inputElement is IStyleable styleableParent)
styleableRecognizer.Bind(StyledElement.TemplatedParentProperty,
styleableParent.GetObservable(StyledElement.TemplatedParentProperty));
}
}
static readonly List<IGestureRecognizer> s_Empty = new List<IGestureRecognizer>();
public IEnumerator<IGestureRecognizer> GetEnumerator()
=> _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int Count => _recognizers?.Count ?? 0;
internal bool HandlePointerPressed(PointerPressedEventArgs e)
{
if (_recognizers == null)
return false;
foreach (var r in _recognizers)
{
if(e.Handled)
break;
r.PointerPressed(e);
}
return e.Handled;
}
internal bool HandlePointerReleased(PointerReleasedEventArgs e)
{
if (_recognizers == null)
return false;
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture))
{
capture.PointerReleased(e);
}
else
foreach (var r in _recognizers)
{
if (e.Handled)
break;
r.PointerReleased(e);
}
return e.Handled;
}
internal bool HandlePointerMoved(PointerEventArgs e)
{
if (_recognizers == null)
return false;
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture))
{
capture.PointerMoved(e);
}
else
foreach (var r in _recognizers)
{
if (e.Handled)
break;
r.PointerMoved(e);
}
return e.Handled;
}
internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e)
{
if (_recognizers == null)
return;
_pointerGrabs.Remove(e.Pointer);
foreach (var r in _recognizers)
{
if(e.Handled)
break;
r.PointerCaptureLost(e);
}
}
void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer)
{
pointer.Capture(_inputElement);
_pointerGrabs[pointer] = recognizer;
}
}
}

23
src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs

@ -0,0 +1,23 @@
namespace Avalonia.Input.GestureRecognizers
{
public interface IGestureRecognizer
{
void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions);
void PointerPressed(PointerPressedEventArgs e);
void PointerReleased(PointerReleasedEventArgs e);
void PointerMoved(PointerEventArgs e);
void PointerCaptureLost(PointerCaptureLostEventArgs e);
}
public interface IGestureRecognizerActionsDispatcher
{
void Capture(IPointer pointer, IGestureRecognizer recognizer);
}
public enum GestureRecognizerResult
{
None,
Capture,
ReleaseCapture
}
}

183
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -0,0 +1,183 @@
using System;
using System.Diagnostics;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers
{
public class ScrollGestureRecognizer
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
IGestureRecognizer
{
private bool _scrolling;
private Point _trackedRootPoint;
private IPointer _tracking;
private IInputElement _target;
private IGestureRecognizerActionsDispatcher _actions;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private int _gestureId;
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll,
(o, v) => o.CanHorizontallyScroll = v);
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanVerticallyScroll
{
get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{
_target = target;
_actions = actions;
}
public void PointerPressed(PointerPressedEventArgs e)
{
if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch)
{
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();;
_trackedRootPoint = e.GetPosition(null);
}
}
// Arbitrary chosen value, probably need to move that to platform settings or something
private const double ScrollStartDistance = 30;
// Pixels per second speed that is considered to be the stop of inertiall scroll
private const double InertialScrollSpeedEnd = 5;
public void PointerMoved(PointerEventArgs e)
{
if (e.Pointer == _tracking)
{
var rootPoint = e.GetPosition(null);
if (!_scrolling)
{
if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
_scrolling = true;
if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance)
_scrolling = true;
if (_scrolling)
{
_actions.Capture(e.Pointer, this);
}
}
if (_scrolling)
{
var vector = _trackedRootPoint - rootPoint;
var elapsed = _lastMoveTimestamp.HasValue ?
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
TimeSpan.Zero;
_lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
}
}
public void PointerCaptureLost(PointerCaptureLostEventArgs e)
{
if (e.Pointer == _tracking) EndGesture();
}
void EndGesture()
{
_tracking = null;
if (_scrolling)
{
_inertia = default;
_scrolling = false;
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0;
_lastMoveTimestamp = null;
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer == _tracking && _scrolling)
{
e.Handled = true;
if (_inertia == default
|| e.Timestamp == 0
|| _lastMoveTimestamp == 0
|| e.Timestamp - _lastMoveTimestamp > 200)
EndGesture();
else
{
var savedGestureId = _gestureId;
var st = Stopwatch.StartNew();
var lastTime = TimeSpan.Zero;
DispatcherTimer.Run(() =>
{
// Another gesture has started, finish the current one
if (_gestureId != savedGestureId)
{
return false;
}
var elapsedSinceLastTick = st.Elapsed - lastTime;
lastTime = st.Elapsed;
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
return true;
}, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background);
}
}
}
}
}

18
src/Avalonia.Input/Gestures.cs

@ -18,6 +18,14 @@ namespace Avalonia.Input
RoutingStrategies.Bubble,
typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEndedEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
private static WeakReference s_lastPress;
static Gestures()
@ -38,7 +46,10 @@ namespace Avalonia.Input
}
else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source)
{
e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
if (!ev.Handled)
{
e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
}
}
}
}
@ -51,7 +62,10 @@ namespace Avalonia.Input
if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source)
{
((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent));
if (!ev.Handled)
{
((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent));
}
}
}
}

5
src/Avalonia.Input/IMouseDevice.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Input
{
/// <summary>
@ -11,6 +13,9 @@ namespace Avalonia.Input
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

18
src/Avalonia.Input/IPointer.cs

@ -0,0 +1,18 @@
namespace Avalonia.Input
{
public interface IPointer
{
int Id { get; }
void Capture(IInputElement control);
IInputElement Captured { get; }
PointerType Type { get; }
bool IsPrimary { get; }
}
public enum PointerType
{
Mouse,
Touch
}
}

8
src/Avalonia.Input/IPointerDevice.cs

@ -1,18 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public interface IPointerDevice : IInputDevice
{
[Obsolete("Use IPointer")]
IInputElement Captured { get; }
[Obsolete("Use IPointer")]
void Capture(IInputElement control);
[Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo);
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

39
src/Avalonia.Input/InputElement.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -127,6 +128,14 @@ namespace Avalonia.Input
RoutedEvent.Register<InputElement, PointerReleasedEventArgs>(
"PointerReleased",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerCaptureLost"/> routed event.
/// </summary>
public static readonly RoutedEvent<PointerCaptureLostEventArgs> PointerCaptureLostEvent =
RoutedEvent.Register<InputElement, PointerCaptureLostEventArgs>(
"PointerCaptureLost",
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="PointerWheelChanged"/> event.
@ -148,6 +157,7 @@ namespace Avalonia.Input
private bool _isFocused;
private bool _isPointerOver;
private GestureRecognizerCollection _gestureRecognizers;
/// <summary>
/// Initializes static members of the <see cref="InputElement"/> class.
@ -166,6 +176,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler<InputElement>(x => x.OnPointerMoved);
PointerPressedEvent.AddClassHandler<InputElement>(x => x.OnPointerPressed);
PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased);
PointerCaptureLostEvent.AddClassHandler<InputElement>(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged);
PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled");
@ -263,6 +274,16 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); }
}
/// <summary>
/// Occurs when the control or its child control loses the pointer capture for any reason,
/// event will not be triggered for a parent control if capture was transferred to another child of that parent control
/// </summary>
public event EventHandler<PointerCaptureLostEventArgs> PointerCaptureLost
{
add => AddHandler(PointerCaptureLostEvent, value);
remove => RemoveHandler(PointerCaptureLostEvent, value);
}
/// <summary>
/// Occurs when the mouse wheen is scrolled over the control.
/// </summary>
@ -370,6 +391,9 @@ namespace Avalonia.Input
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
public GestureRecognizerCollection GestureRecognizers
=> _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
/// <summary>
/// Focuses the control.
/// </summary>
@ -460,6 +484,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerMoved(PointerEventArgs e)
{
if (_gestureRecognizers?.HandlePointerMoved(e) == true)
e.Handled = true;
}
/// <summary>
@ -468,6 +494,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{
if (_gestureRecognizers?.HandlePointerPressed(e) == true)
e.Handled = true;
}
/// <summary>
@ -476,6 +504,17 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerReleased(PointerReleasedEventArgs e)
{
if (_gestureRecognizers?.HandlePointerReleased(e) == true)
e.Handled = true;
}
/// <summary>
/// Called before the <see cref="PointerCaptureLost"/> event occurs.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_gestureRecognizers?.HandlePointerCaptureLost(e);
}
/// <summary>

221
src/Avalonia.Input/MouseDevice.cs

@ -19,9 +19,9 @@ namespace Avalonia.Input
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
private IInputElement _captured;
private IDisposable _capturedSubscription;
private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
/// <summary>
/// Gets the control that is currently capturing by the mouse, if any.
/// </summary>
@ -30,27 +30,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. To set the mouse capture, call the
/// <see cref="Capture"/> method.
/// </remarks>
public IInputElement Captured
{
get => _captured;
protected set
{
_capturedSubscription?.Dispose();
_capturedSubscription = null;
[Obsolete("Use IPointer instead")]
public IInputElement Captured => _pointer.Captured;
if (value != null)
{
_capturedSubscription = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => value.DetachedFromVisualTree += x,
x => value.DetachedFromVisualTree -= x)
.Take(1)
.Subscribe(_ => Captured = null);
}
_captured = value;
}
}
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
@ -71,8 +53,7 @@ namespace Avalonia.Input
/// </remarks>
public virtual void Capture(IInputElement control)
{
// TODO: Check visibility and enabled state before setting capture.
Captured = control;
_pointer.Capture(control);
}
/// <summary>
@ -96,7 +77,7 @@ namespace Avalonia.Input
public void ProcessRawEvent(RawInputEventArgs e)
{
if (!e.Handled && e is RawMouseEventArgs margs)
if (!e.Handled && e is RawPointerEventArgs margs)
ProcessRawEvent(margs);
}
@ -106,66 +87,100 @@ namespace Avalonia.Input
if (rect.Contains(clientPoint))
{
if (Captured == null)
if (_pointer.Captured == null)
{
SetPointerOver(this, root, clientPoint, InputModifiers.None);
SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None);
}
else
{
SetPointerOver(this, root, Captured, InputModifiers.None);
SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None);
}
}
}
private void ProcessRawEvent(RawMouseEventArgs e)
int ButtonCount(PointerPointProperties props)
{
var rv = 0;
if (props.IsLeftButtonPressed)
rv++;
if (props.IsMiddleButtonPressed)
rv++;
if (props.IsRightButtonPressed)
rv++;
return rv;
}
private void ProcessRawEvent(RawPointerEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
var mouse = (IMouseDevice)e.Device;
Position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
switch (e.Type)
{
case RawMouseEventType.LeaveWindow:
LeaveWindow(mouse, e.Root, e.InputModifiers);
case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers);
break;
case RawMouseEventType.LeftButtonDown:
case RawMouseEventType.RightButtonDown:
case RawMouseEventType.MiddleButtonDown:
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
e.Type == RawMouseEventType.LeftButtonDown
? MouseButton.Left
: e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle,
e.InputModifiers);
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, e.InputModifiers);
break;
case RawMouseEventType.LeftButtonUp:
case RawMouseEventType.RightButtonUp:
case RawMouseEventType.MiddleButtonUp:
e.Handled = MouseUp(mouse, e.Root, e.Position,
e.Type == RawMouseEventType.LeftButtonUp
? MouseButton.Left
: e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle,
e.InputModifiers);
case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp:
case RawPointerEventType.MiddleButtonUp:
if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
else
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
break;
case RawMouseEventType.Move:
e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers);
case RawPointerEventType.Move:
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
break;
case RawMouseEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
case RawPointerEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
break;
}
}
private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers)
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
ClearPointerOver(this, root, inputModifiers);
ClearPointerOver(this, timestamp, root, inputModifiers);
}
PointerPointProperties CreateProperties(RawPointerEventArgs args)
{
var rv = new PointerPointProperties(args.InputModifiers);
if (args.Type == RawPointerEventType.LeftButtonDown)
rv.IsLeftButtonPressed = true;
if (args.Type == RawPointerEventType.MiddleButtonDown)
rv.IsMiddleButtonPressed = true;
if (args.Type == RawPointerEventType.RightButtonDown)
rv.IsRightButtonPressed = true;
if (args.Type == RawPointerEventType.LeftButtonUp)
rv.IsLeftButtonPressed = false;
if (args.Type == RawPointerEventType.MiddleButtonUp)
rv.IsMiddleButtonPressed = false;
if (args.Type == RawPointerEventType.RightButtonUp)
rv.IsRightButtonPressed = false;
return rv;
}
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers)
private MouseButton _lastMouseDownButton;
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties,
InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -174,8 +189,8 @@ namespace Avalonia.Input
if (hit != null)
{
IInteractive source = GetSource(hit);
_pointer.Capture(hit);
var source = GetSource(hit);
if (source != null)
{
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -190,17 +205,8 @@ namespace Avalonia.Input
_lastClickTime = timestamp;
_lastClickRect = new Rect(p, new Size())
.Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
var e = new PointerPressedEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerPressedEvent,
Source = source,
ClickCount = _clickCount,
MouseButton = button,
InputModifiers = inputModifiers
};
_lastMouseDownButton = properties.GetObsoleteMouseButton();
var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount);
source.RaiseEvent(e);
return e.Handled;
}
@ -209,36 +215,33 @@ namespace Avalonia.Input
return false;
}
private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
IInputElement source;
if (Captured == null)
if (_pointer.Captured == null)
{
source = SetPointerOver(this, root, p, inputModifiers);
source = SetPointerOver(this, timestamp, root, p, inputModifiers);
}
else
{
SetPointerOver(this, root, Captured, inputModifiers);
source = Captured;
SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers);
source = _pointer.Captured;
}
var e = new PointerEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerMovedEvent,
Source = source,
InputModifiers = inputModifiers
};
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers);
source?.RaiseEvent(e);
return e.Handled;
}
private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers)
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -248,23 +251,20 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerReleasedEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerReleasedEvent,
Source = source,
MouseButton = button,
InputModifiers = inputModifiers
};
var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers,
_lastMouseDownButton);
source?.RaiseEvent(e);
_pointer.Capture(null);
return e.Handled;
}
return false;
}
private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers)
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props,
Vector delta, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -274,14 +274,7 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerWheelEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerWheelChangedEvent,
Source = source,
Delta = delta,
InputModifiers = inputModifiers
};
var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
@ -294,7 +287,7 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(hit != null);
return Captured ??
return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
}
@ -303,21 +296,22 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(root != null);
return Captured ?? root.InputHitTest(p);
return _pointer.Captured ?? root.InputHitTest(p);
}
private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers)
PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers)
{
return new PointerEventArgs(ev, source, _pointer, null, default,
timestamp, new PointerPointProperties(inputModifiers), inputModifiers);
}
private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
var element = root.PointerOverElement;
var e = new PointerEventArgs
{
RoutedEvent = InputElement.PointerLeaveEvent,
Device = device,
InputModifiers = inputModifiers
};
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers);
if (element!=null && !element.IsAttachedToVisualTree)
{
@ -354,7 +348,7 @@ namespace Avalonia.Input
}
}
private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -365,18 +359,18 @@ namespace Avalonia.Input
{
if (element != null)
{
SetPointerOver(device, root, element, inputModifiers);
SetPointerOver(device, timestamp, root, element, inputModifiers);
}
else
{
ClearPointerOver(device, root, inputModifiers);
ClearPointerOver(device, timestamp, root, inputModifiers);
}
}
return element;
}
private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -384,7 +378,6 @@ namespace Avalonia.Input
IInputElement branch = null;
var e = new PointerEventArgs { Device = device, InputModifiers = inputModifiers };
var el = element;
while (el != null)
@ -398,8 +391,8 @@ namespace Avalonia.Input
}
el = root.PointerOverElement;
e.RoutedEvent = InputElement.PointerLeaveEvent;
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers);
if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e,branch,false);

67
src/Avalonia.Input/Pointer.cs

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public class Pointer : IPointer, IDisposable
{
private static int s_NextFreePointerId = 1000;
public static int GetNextFreeId() => s_NextFreePointerId++;
public Pointer(int id, PointerType type, bool isPrimary)
{
Id = id;
Type = type;
IsPrimary = isPrimary;
}
public int Id { get; }
IInputElement FindCommonParent(IInputElement control1, IInputElement control2)
{
if (control1 == null || control2 == null)
return null;
var seen = new HashSet<IInputElement>(control1.GetSelfAndVisualAncestors().OfType<IInputElement>());
return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().FirstOrDefault(seen.Contains);
}
public void Capture(IInputElement control)
{
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
var oldCapture = control;
Captured = control;
if (oldCapture != null)
{
var commonParent = FindCommonParent(control, oldCapture);
foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType<IInputElement>())
{
if (notifyTarget == commonParent)
break;
notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this));
}
}
if (Captured != null)
Captured.DetachedFromVisualTree += OnCaptureDetached;
}
IInputElement GetNextCapture(IVisual parent) =>
parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
{
Capture(GetNextCapture(e.Parent));
}
public IInputElement Captured { get; private set; }
public PointerType Type { get; }
public bool IsPrimary { get; }
public void Dispose() => Capture(null);
}
}

108
src/Avalonia.Input/PointerEventArgs.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -8,25 +10,69 @@ namespace Avalonia.Input
{
public class PointerEventArgs : RoutedEventArgs
{
public PointerEventArgs()
{
private readonly IVisual _rootVisual;
private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties;
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
InputModifiers modifiers)
: base(routedEvent)
{
Source = source;
_rootVisual = rootVisual;
_rootVisualPosition = rootVisualPosition;
_properties = properties;
Pointer = pointer;
Timestamp = timestamp;
InputModifiers = modifiers;
}
public PointerEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
class EmulatedDevice : IPointerDevice
{
private readonly PointerEventArgs _ev;
public EmulatedDevice(PointerEventArgs ev)
{
_ev = ev;
}
public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
public IInputElement Captured => _ev.Pointer.Captured;
public void Capture(IInputElement control)
{
_ev.Pointer.Capture(control);
}
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
}
public IPointerDevice Device { get; set; }
public IPointer Pointer { get; }
public ulong Timestamp { get; }
private IPointerDevice _device;
public InputModifiers InputModifiers { get; set; }
[Obsolete("Use Pointer to get pointer-specific information")]
public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
public InputModifiers InputModifiers { get; }
public Point GetPosition(IVisual relativeTo)
{
return Device.GetPosition(relativeTo);
if (_rootVisual == null)
return default;
if (relativeTo == null)
return _rootVisualPosition;
return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default;
}
public PointerPoint GetPointerPoint(IVisual relativeTo)
=> new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
}
public enum MouseButton
@ -39,32 +85,52 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
public PointerPressedEventArgs()
: base(InputElement.PointerPressedEvent)
{
}
private readonly int _obsoleteClickCount;
public PointerPressedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
public PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
InputModifiers modifiers,
int obsoleteClickCount = 1)
: base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
_obsoleteClickCount = obsoleteClickCount;
}
public int ClickCount { get; set; }
public MouseButton MouseButton { get; set; }
[Obsolete("Use DoubleTapped or DoubleRightTapped event instead")]
public int ClickCount => _obsoleteClickCount;
[Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton();
}
public class PointerReleasedEventArgs : PointerEventArgs
{
public PointerReleasedEventArgs()
: base(InputElement.PointerReleasedEvent)
public PointerReleasedEventArgs(
IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton)
: base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
MouseButton = obsoleteMouseButton;
}
public PointerReleasedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
[Obsolete()]
public MouseButton MouseButton { get; private set; }
}
public class PointerCaptureLostEventArgs : RoutedEventArgs
{
public IPointer Pointer { get; }
public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;
}
public MouseButton MouseButton { get; set; }
}
}

45
src/Avalonia.Input/PointerPoint.cs

@ -0,0 +1,45 @@
namespace Avalonia.Input
{
public sealed class PointerPoint
{
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
{
Pointer = pointer;
Position = position;
Properties = properties;
}
public IPointer Pointer { get; }
public PointerPointProperties Properties { get; }
public Point Position { get; }
}
public sealed class PointerPointProperties
{
public bool IsLeftButtonPressed { get; set; }
public bool IsMiddleButtonPressed { get; set; }
public bool IsRightButtonPressed { get; set; }
public PointerPointProperties()
{
}
public PointerPointProperties(InputModifiers modifiers)
{
IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton);
}
public MouseButton GetObsoleteMouseButton()
{
if (IsLeftButtonPressed)
return MouseButton.Left;
if (IsMiddleButtonPressed)
return MouseButton.Middle;
if (IsRightButtonPressed)
return MouseButton.Right;
return MouseButton.None;
}
}
}

12
src/Avalonia.Input/PointerWheelEventArgs.cs

@ -1,10 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public class PointerWheelEventArgs : PointerEventArgs
{
public Vector Delta { get; set; }
public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, InputModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
Delta = delta;
}
}
}

1
src/Avalonia.Input/Properties/AssemblyInfo.cs

@ -5,3 +5,4 @@ using System.Reflection;
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")]

4
src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs

@ -4,7 +4,7 @@
namespace Avalonia.Input.Raw
{
public class RawMouseWheelEventArgs : RawMouseEventArgs
public class RawMouseWheelEventArgs : RawPointerEventArgs
{
public RawMouseWheelEventArgs(
IInputDevice device,
@ -12,7 +12,7 @@ namespace Avalonia.Input.Raw
IInputRoot root,
Point position,
Vector delta, InputModifiers inputModifiers)
: base(device, timestamp, root, RawMouseEventType.Wheel, position, inputModifiers)
: base(device, timestamp, root, RawPointerEventType.Wheel, position, inputModifiers)
{
Delta = delta;
}

15
src/Avalonia.Input/Raw/RawMouseEventArgs.cs → src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -5,7 +5,7 @@ using System;
namespace Avalonia.Input.Raw
{
public enum RawMouseEventType
public enum RawPointerEventType
{
LeaveWindow,
LeftButtonDown,
@ -17,15 +17,18 @@ namespace Avalonia.Input.Raw
Move,
Wheel,
NonClientLeftButtonDown,
TouchBegin,
TouchUpdate,
TouchEnd
}
/// <summary>
/// A raw mouse event.
/// </summary>
public class RawMouseEventArgs : RawInputEventArgs
public class RawPointerEventArgs : RawInputEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="RawMouseEventArgs"/> class.
/// Initializes a new instance of the <see cref="RawPointerEventArgs"/> class.
/// </summary>
/// <param name="device">The associated device.</param>
/// <param name="timestamp">The event timestamp.</param>
@ -33,11 +36,11 @@ namespace Avalonia.Input.Raw
/// <param name="type">The type of the event.</param>
/// <param name="position">The mouse position, in client DIPs.</param>
/// <param name="inputModifiers">The input modifiers.</param>
public RawMouseEventArgs(
public RawPointerEventArgs(
IInputDevice device,
ulong timestamp,
IInputRoot root,
RawMouseEventType type,
RawPointerEventType type,
Point position,
InputModifiers inputModifiers)
: base(device, timestamp)
@ -64,7 +67,7 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the type of the event.
/// </summary>
public RawMouseEventType Type { get; private set; }
public RawPointerEventType Type { get; private set; }
/// <summary>
/// Gets the input modifiers.

15
src/Avalonia.Input/Raw/RawTouchEventArgs.cs

@ -0,0 +1,15 @@
namespace Avalonia.Input.Raw
{
public class RawTouchEventArgs : RawPointerEventArgs
{
public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root,
RawPointerEventType type, Point position, InputModifiers inputModifiers,
long touchPointId)
: base(device, timestamp, root, type, position, inputModifiers)
{
TouchPointId = touchPointId;
}
public long TouchPointId { get; set; }
}
}

29
src/Avalonia.Input/ScrollGestureEventArgs.cs

@ -0,0 +1,29 @@
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class ScrollGestureEventArgs : RoutedEventArgs
{
public int Id { get; }
public Vector Delta { get; }
private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++;
public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{
Id = id;
Delta = delta;
}
}
public class ScrollGestureEndedEventArgs : RoutedEventArgs
{
public int Id { get; }
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{
Id = id;
}
}
}

74
src/Avalonia.Input/TouchDevice.cs

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
/// <summary>
/// Handles raw touch events
/// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks>
/// </summary>
public class TouchDevice : IInputDevice
{
Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
static InputModifiers GetModifiers(InputModifiers modifiers, bool left)
{
var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^
InputModifiers.RightMouseButton;
modifiers &= mask;
if (left)
modifiers |= InputModifiers.LeftMouseButton;
return modifiers;
}
public void ProcessRawEvent(RawInputEventArgs ev)
{
var args = (RawTouchEventArgs)ev;
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
{
if (args.Type == RawPointerEventType.TouchEnd)
return;
var hit = args.Root.InputHitTest(args.Position);
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
var target = pointer.Captured ?? args.Root;
if (args.Type == RawPointerEventType.TouchBegin)
{
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)),
GetModifiers(args.InputModifiers, false)));
}
if (args.Type == RawPointerEventType.TouchEnd)
{
_pointers.Remove(args.TouchPointId);
using (pointer)
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false)),
GetModifiers(args.InputModifiers, pointer.IsPrimary),
pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
}
}
if (args.Type == RawPointerEventType.TouchUpdate)
{
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
}
}
}
}

2
src/Avalonia.Native/WindowImplBase.cs

@ -226,7 +226,7 @@ namespace Avalonia.Native
break;
default:
Input?.Invoke(new RawMouseEventArgs(_mouse, timeStamp, _inputRoot, (RawMouseEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers));
Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers));
break;
}
}

18
src/Avalonia.OpenGL/AngleOptions.cs

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Avalonia.OpenGL
{
public class AngleOptions
{
public enum PlatformApi
{
DirectX9,
DirectX11
}
public List<PlatformApi> AllowedPlatformApis = new List<PlatformApi>
{
PlatformApi.DirectX9
};
}
}

71
src/Avalonia.OpenGL/EglDisplay.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Platform.Interop;
using static Avalonia.OpenGL.EglConsts;
@ -13,21 +14,42 @@ namespace Avalonia.OpenGL
private readonly int[] _contextAttributes;
public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
public EglDisplay(EglInterface egl)
{
_egl = egl;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _egl.GetPlatformDisplayEXT != null)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
foreach (var dapi in new[] {EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE})
if (_egl.GetPlatformDisplayEXT == null)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
var allowedApis = AvaloniaLocator.Current.GetService<AngleOptions>()?.AllowedPlatformApis
?? new List<AngleOptions.PlatformApi> {AngleOptions.PlatformApi.DirectX9};
foreach (var platformApi in allowedApis)
{
int dapi;
if (platformApi == AngleOptions.PlatformApi.DirectX9)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE;
else if (platformApi == AngleOptions.PlatformApi.DirectX11)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE;
else
continue;
_display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[]
{
EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE
});
if(_display != IntPtr.Zero)
if (_display != IntPtr.Zero)
{
_angleApi = platformApi;
break;
}
}
if (_display == IntPtr.Zero)
throw new OpenGlException("Unable to create ANGLE display");
}
if (_display == IntPtr.Zero)
@ -64,29 +86,35 @@ namespace Avalonia.OpenGL
if (!_egl.BindApi(cfg.Api))
continue;
var attribs = new[]
foreach(var stencilSize in new[]{8, 1, 0})
foreach (var depthSize in new []{8, 1, 0})
{
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, 8,
EGL_DEPTH_SIZE, 8,
EGL_NONE
};
if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs))
continue;
if (numConfigs == 0)
continue;
_contextAttributes = cfg.Attributes;
Type = cfg.Type;
var attribs = new[]
{
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, stencilSize,
EGL_DEPTH_SIZE, depthSize,
EGL_NONE
};
if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs))
continue;
if (numConfigs == 0)
continue;
_contextAttributes = cfg.Attributes;
Type = cfg.Type;
}
}
if (_contextAttributes == null)
throw new OpenGlException("No suitable EGL config was found");
GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b));
}
@ -97,6 +125,7 @@ namespace Avalonia.OpenGL
public GlDisplayType Type { get; }
public GlInterface GlInterface { get; }
public EglInterface EglInterface => _egl;
public IGlContext CreateContext(IGlContext share)
{
var shareCtx = (EglContext)share;

33
src/Avalonia.OpenGL/EglGlPlatformSurface.cs

@ -26,31 +26,44 @@ namespace Avalonia.OpenGL
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{
var glSurface = _display.CreateWindowSurface(_info.Handle);
return new RenderTarget(_context, glSurface, _info);
return new RenderTarget(_display, _context, glSurface, _info);
}
class RenderTarget : IGlPlatformSurfaceRenderTarget
class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo
{
private readonly EglDisplay _display;
private readonly EglContext _context;
private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private PixelSize _initialSize;
public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info)
public RenderTarget(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info)
{
_display = display;
_context = context;
_glSurface = glSurface;
_info = info;
_initialSize = info.Size;
}
public void Dispose() => _glSurface.Dispose();
public bool IsCorrupted => _initialSize != _info.Size;
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var l = _context.Lock();
try
{
if (IsCorrupted)
throw new RenderTargetCorruptedException();
_context.MakeCurrent(_glSurface);
return new Session(_context, _glSurface, _info, l);
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative();
return new Session(_display, _context, _glSurface, _info, l);
}
catch
{
@ -61,15 +74,19 @@ namespace Avalonia.OpenGL
class Session : IGlPlatformSurfaceRenderingSession
{
private readonly IGlContext _context;
private readonly EglContext _context;
private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglDisplay _display;
private IDisposable _lock;
public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
public Session(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock)
{
_context = context;
_display = display;
_glSurface = glSurface;
_info = info;
_lock = @lock;
@ -78,7 +95,11 @@ namespace Avalonia.OpenGL
public void Dispose()
{
_context.Display.GlInterface.Flush();
_display.EglInterface.WaitGL();
_glSurface.SwapBuffers();
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative();
_context.Display.ClearContext();
_lock.Dispose();
}

43
src/Avalonia.OpenGL/EglInterface.cs

@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
@ -15,13 +16,28 @@ namespace Avalonia.OpenGL
{
}
[DllImport("libegl.dll", CharSet = CharSet.Ansi)]
static extern IntPtr eglGetProcAddress(string proc);
static Func<string, bool, IntPtr> Load()
{
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem;
if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android)
return Load("libEGL.so.1");
if (os == OperatingSystemType.WinNT)
return Load(@"libegl.dll");
{
var disp = eglGetProcAddress("eglGetPlatformDisplayEXT");
if (disp == IntPtr.Zero)
throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point");
return (name, optional) =>
{
var r = eglGetProcAddress(name);
if (r == IntPtr.Zero && !optional)
throw new OpenGlException($"Entry point {r} is not found");
return r;
};
}
throw new PlatformNotSupportedException();
}
@ -91,6 +107,31 @@ namespace Avalonia.OpenGL
[GlEntryPoint("eglGetConfigAttrib")]
public EglGetConfigAttrib GetConfigAttrib { get; }
public delegate bool EglWaitGL();
[GlEntryPoint("eglWaitGL")]
public EglWaitGL WaitGL { get; }
public delegate bool EglWaitClient();
[GlEntryPoint("eglWaitClient")]
public EglWaitGL WaitClient { get; }
public delegate bool EglWaitNative();
[GlEntryPoint("eglWaitNative")]
public EglWaitGL WaitNative { get; }
public delegate IntPtr EglQueryString(IntPtr display, int i);
[GlEntryPoint("eglQueryString")]
public EglQueryString QueryStringNative { get; }
public string QueryString(IntPtr display, int i)
{
var rv = QueryStringNative(display, i);
if (rv == IntPtr.Zero)
return null;
return Marshal.PtrToStringAnsi(rv);
}
// ReSharper restore UnassignedGetOnlyAutoProperty
}
}

7
src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs

@ -6,4 +6,9 @@ namespace Avalonia.OpenGL
{
IGlPlatformSurfaceRenderingSession BeginDraw();
}
}
public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget
{
bool IsCorrupted { get; }
}
}

5
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI
return builder.AfterSetup(_ =>
{
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.Register(
() => new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
});
}
}

55
src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs

@ -0,0 +1,55 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Templates;
using ReactiveUI;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls
/// that don't have DataTemplates, and assigns a default DataTemplate that
/// loads the View associated with each ViewModel.
/// </summary>
public class AutoDataTemplateBindingHook : IPropertyBindingHook
{
private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate<object>(x =>
{
var control = new ViewModelViewHost();
var context = control.GetObservable(Control.DataContextProperty);
control.Bind(ViewModelViewHost.ViewModelProperty, context);
control.HorizontalContentAlignment = HorizontalAlignment.Stretch;
control.VerticalContentAlignment = VerticalAlignment.Stretch;
return control;
},
true);
/// <inheritdoc/>
public bool ExecuteHook(
object source, object target,
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
Func<IObservedChange<object, object>[]> getCurrentViewProperties,
BindingDirection direction)
{
var viewProperties = getCurrentViewProperties();
var lastViewProperty = viewProperties.LastOrDefault();
var itemsControl = lastViewProperty?.Sender as ItemsControl;
if (itemsControl == null)
return true;
var propertyName = viewProperties.Last().GetPropertyName();
if (propertyName != "Items" &&
propertyName != "ItemsSource")
return true;
if (itemsControl.ItemTemplate != null)
return true;
itemsControl.ItemTemplate = DefaultItemTemplate;
return true;
}
}
}

143
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI
/// ReactiveUI routing documentation website</see> for more info.
/// </para>
/// </remarks>
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger
{
/// <summary>
/// The router dependency property.
/// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
/// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </summary>
@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
/// <exception cref="Exception">
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
private void NavigateToViewModel(IRoutableViewModel viewModel)
private void NavigateToViewModel(object viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null, falling back to default content.");
UpdateContent(DefaultContent);
this.Log().Info("ViewModel is null. Falling back to default content.");
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(viewModel);
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?");
var viewInstance = viewLocator.ResolveView(viewModel);
if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {view} with autowired {viewModel}.");
view.ViewModel = viewModel;
if (view is IStyledElement styled)
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel;
UpdateContent(view);
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration)
{
return new Avalonia.Animation.Animation
{
Duration = duration,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = from
}
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = to
}
},
Cue = new Cue(1d)
}
}
};
Content = viewInstance;
}
}
}
}

75
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@ -0,0 +1,75 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// A ContentControl that animates the transition when its content is changed.
/// </summary>
public class TransitioningContentControl : ContentControl, IStyleable
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
/// </summary>
public static readonly AvaloniaProperty<IPageTransition> PageTransitionProperty =
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition),
new CrossFade(TimeSpan.FromSeconds(0.5)));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent));
/// <summary>
/// Gets or sets the animation played when content appears and disappears.
/// </summary>
public IPageTransition PageTransition
{
get => GetValue(PageTransitionProperty);
set => SetValue(PageTransitionProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the content with animation.
/// </summary>
public new object Content
{
get => base.Content;
set => UpdateContentWithTransition(value);
}
/// <summary>
/// TransitioningContentControl uses the default ContentControl
/// template from Avalonia default theme.
/// </summary>
Type IStyleable.StyleKey => typeof(ContentControl);
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="content">New content to set.</param>
private async void UpdateContentWithTransition(object content)
{
if (PageTransition != null)
await PageTransition.Start(this, null, true);
base.Content = content;
if (PageTransition != null)
await PageTransition.Start(null, this, true);
}
}
}

80
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -0,0 +1,80 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using ReactiveUI;
using Splat;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// This content control will automatically load the View associated with
/// the ViewModel property and display it. This control is very useful
/// inside a DataTemplate to display the View associated with a ViewModel.
/// </summary>
public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel));
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary>
public ViewModelViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.ViewModel)
.Subscribe(NavigateToViewModel)
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the ViewModel to display.
/// </summary>
public object ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>
/// Gets or sets the view locator.
/// </summary>
public IViewLocator ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null. Falling back to default content.");
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel);
if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel;
Content = viewInstance;
}
}
}

10
src/Avalonia.Themes.Default/Accents/BaseDark.xaml

@ -46,11 +46,11 @@
<SolidColorBrush x:Key="ErrorBrush" Color="{DynamicResource ErrorColor}"></SolidColorBrush>
<SolidColorBrush x:Key="ErrorLowBrush" Color="{DynamicResource ErrorLowColor}"></SolidColorBrush>
<SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="#444444"/>
<SolidColorBrush x:Key="NotificationCardInformationBackgroundBrush" Color="Teal"/>
<SolidColorBrush x:Key="NotificationCardSuccessBackgroundBrush" Color="LimeGreen"/>
<SolidColorBrush x:Key="NotificationCardWarningBackgroundBrush" Color="Orange"/>
<SolidColorBrush x:Key="NotificationCardErrorBackgroundBrush" Color="OrangeRed"/>
<SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="#444444" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardInformationBackgroundBrush" Color="#007ACC" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardSuccessBackgroundBrush" Color="#1F9E45" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardWarningBackgroundBrush" Color="#FDB328" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardErrorBackgroundBrush" Color="#BD202C" Opacity="0.75"/>
<Thickness x:Key="ThemeBorderThickness">1,1,1,1</Thickness>
<sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>

10
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@ -46,11 +46,11 @@
<SolidColorBrush x:Key="ErrorBrush" Color="{DynamicResource ErrorColor}"></SolidColorBrush>
<SolidColorBrush x:Key="ErrorLowBrush" Color="{DynamicResource ErrorLowColor}"></SolidColorBrush>
<SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="#444444"/>
<SolidColorBrush x:Key="NotificationCardInformationBackgroundBrush" Color="Teal"/>
<SolidColorBrush x:Key="NotificationCardSuccessBackgroundBrush" Color="LimeGreen"/>
<SolidColorBrush x:Key="NotificationCardWarningBackgroundBrush" Color="Orange"/>
<SolidColorBrush x:Key="NotificationCardErrorBackgroundBrush" Color="OrangeRed"/>
<SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="#444444" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardInformationBackgroundBrush" Color="#007ACC" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardSuccessBackgroundBrush" Color="#1F9E45" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardWarningBackgroundBrush" Color="#FDB328" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardErrorBackgroundBrush" Color="#BD202C" Opacity="0.75"/>
<Thickness x:Key="ThemeBorderThickness">1</Thickness>
<sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>

6
src/Avalonia.Themes.Default/NotificationCard.xaml

@ -13,7 +13,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Margin="8,8,0,0">
<ContentControl MinHeight="150" Content="{TemplateBinding Content}" />
<ContentControl Name="PART_Content" Content="{TemplateBinding Content}" />
</Border>
</LayoutTransformControl>
</ControlTemplate>
@ -40,6 +40,10 @@
</Style.Animations>
</Style>
<Style Selector="NotificationCard/template/ ContentControl#PART_Content">
<Setter Property="MinHeight" Value="150" />
</Style>
<Style Selector="NotificationCard[IsClosing=true] /template/ LayoutTransformControl#PART_LayoutTransformControl">
<Setter Property="RenderTransformOrigin" Value="50%,0%"/>
<Style.Animations>

11
src/Avalonia.Themes.Default/ScrollViewer.xaml

@ -12,7 +12,14 @@
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/>
Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
@ -32,4 +39,4 @@
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Style>

8
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -14,8 +14,8 @@ namespace Avalonia.Animation
/// </summary>
public class CrossFade : IPageTransition
{
private Animation _fadeOutAnimation;
private Animation _fadeInAnimation;
private readonly Animation _fadeOutAnimation;
private readonly Animation _fadeInAnimation;
/// <summary>
/// Initializes a new instance of the <see cref="CrossFade"/> class.
@ -61,10 +61,10 @@ namespace Avalonia.Animation
new Setter
{
Property = Visual.OpacityProperty,
Value = 0d
Value = 1d
}
},
Cue = new Cue(0d)
Cue = new Cue(1d)
}
}

5
src/Avalonia.Visuals/Platform/IRenderTarget.cs

@ -23,4 +23,9 @@ namespace Avalonia.Platform
/// </param>
IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer);
}
public interface IRenderTargetWithCorruptionInfo : IRenderTarget
{
bool IsCorrupted { get; }
}
}

5
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -245,6 +245,11 @@ namespace Avalonia.Rendering
{
if (context != null)
return context;
if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
{
RenderTarget.Dispose();
RenderTarget = null;
}
if (RenderTarget == null)
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
return context = RenderTarget.CreateDrawingContext(this);

14
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@ -7,11 +7,25 @@ namespace Avalonia.Rendering
public class ManagedDeferredRendererLock : IDeferredRendererLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to lock the target surface or window
/// </summary>
/// <returns>IDisposable if succeeded to obtain the lock</returns>
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock));
return null;
}
/// <summary>
/// Enters a waiting lock, only use from platform code, not from the renderer
/// </summary>
public IDisposable Lock()
{
Monitor.Enter(_lock);
return Disposable.Create(() => Monitor.Exit(_lock));
}
}
}

32
src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs

@ -0,0 +1,32 @@
using System;
using System.Diagnostics;
using System.Reactive.Disposables;
using Avalonia.Threading;
namespace Avalonia.Rendering
{
/// <summary>
/// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms
/// </summary>
public class UiThreadRenderTimer : DefaultRenderTimer
{
public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
bool cancelled = false;
var st = Stopwatch.StartNew();
DispatcherTimer.Run(() =>
{
if (cancelled)
return false;
tick(st.Elapsed);
return !cancelled;
}, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.Render);
return Disposable.Create(() => cancelled = true);
}
}
}

24
src/Avalonia.X11/X11CursorFactory.cs

@ -8,6 +8,8 @@ namespace Avalonia.X11
{
class X11CursorFactory : IStandardCursorFactory
{
private static IntPtr _nullCursor;
private readonly IntPtr _display;
private Dictionary<CursorFontShape, IntPtr> _cursors;
@ -42,16 +44,34 @@ namespace Avalonia.X11
public X11CursorFactory(IntPtr display)
{
_display = display;
_nullCursor = GetNullCursor(display);
_cursors = Enum.GetValues(typeof(CursorFontShape)).Cast<CursorFontShape>()
.ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id));
}
public IPlatformHandle GetCursor(StandardCursorType cursorType)
{
var handle = s_mapping.TryGetValue(cursorType, out var shape)
IntPtr handle;
if (cursorType == StandardCursorType.None)
{
handle = _nullCursor;
}
else
{
handle = s_mapping.TryGetValue(cursorType, out var shape)
? _cursors[shape]
: _cursors[CursorFontShape.XC_top_left_arrow];
}
return new PlatformHandle(handle, "XCURSOR");
}
private static IntPtr GetNullCursor(IntPtr display)
{
XColor color = new XColor();
byte[] data = new byte[] { 0 };
IntPtr window = XLib.XRootWindow(display, 0);
IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1);
return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0);
}
}
}

4
src/Avalonia.X11/X11Platform.cs

@ -28,6 +28,7 @@ namespace Avalonia.X11
public X11PlatformOptions Options { get; private set; }
public void Initialize(X11PlatformOptions options)
{
Options = options;
XInitThreads();
Display = XOpenDisplay(IntPtr.Zero);
DeferredDisplay = XOpenDisplay(IntPtr.Zero);
@ -66,7 +67,7 @@ namespace Avalonia.X11
GlxGlPlatformFeature.TryInitialize(Info);
}
Options = options;
}
public IntPtr DeferredDisplay { get; set; }
@ -96,6 +97,7 @@ namespace Avalonia
public bool UseEGL { get; set; }
public bool UseGpu { get; set; } = true;
public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication";
public bool? EnableMultiTouch { get; set; }
}
public static class AvaloniaX11PlatformExtensions
{

26
src/Avalonia.X11/X11Window.cs

@ -314,9 +314,9 @@ namespace Avalonia.X11
else if (ev.type == XEventName.FocusOut)
Deactivated?.Invoke();
else if (ev.type == XEventName.MotionNotify)
MouseEvent(RawMouseEventType.Move, ref ev, ev.MotionEvent.state);
MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state);
else if (ev.type == XEventName.LeaveNotify)
MouseEvent(RawMouseEventType.LeaveWindow, ref ev, ev.CrossingEvent.state);
MouseEvent(RawPointerEventType.LeaveWindow, ref ev, ev.CrossingEvent.state);
else if (ev.type == XEventName.PropertyNotify)
{
OnPropertyChange(ev.PropertyEvent.atom, ev.PropertyEvent.state == 0);
@ -326,9 +326,9 @@ namespace Avalonia.X11
if (ActivateTransientChildIfNeeded())
return;
if (ev.ButtonEvent.button < 4)
MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonDown
: ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonDown
: RawMouseEventType.RightButtonDown, ref ev, ev.ButtonEvent.state);
MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown
: ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown
: RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state);
else
{
var delta = ev.ButtonEvent.button == 4
@ -347,9 +347,9 @@ namespace Avalonia.X11
else if (ev.type == XEventName.ButtonRelease)
{
if (ev.ButtonEvent.button < 4)
MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonUp
: ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonUp
: RawMouseEventType.RightButtonUp, ref ev, ev.ButtonEvent.state);
MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp
: ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp
: RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state);
}
else if (ev.type == XEventName.ConfigureNotify)
{
@ -577,7 +577,7 @@ namespace Avalonia.X11
public void ScheduleInput(RawInputEventArgs args)
{
if (args is RawMouseEventArgs mouse)
if (args is RawPointerEventArgs mouse)
mouse.Position = mouse.Position / Scaling;
if (args is RawDragEvent drag)
drag.Location = drag.Location / Scaling;
@ -598,13 +598,13 @@ namespace Avalonia.X11
}
}
void MouseEvent(RawMouseEventType type, ref XEvent ev, XModifierMask mods)
void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods)
{
var mev = new RawMouseEventArgs(
var mev = new RawPointerEventArgs(
_mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
if(type == RawMouseEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawMouseEventArgs ma)
if (ma.Type == RawMouseEventType.Move)
if(type == RawPointerEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawPointerEventArgs ma)
if (ma.Type == RawPointerEventType.Move)
{
_lastEvent.Event = mev;
return;

69
src/Avalonia.X11/XI2Manager.cs

@ -11,6 +11,7 @@ namespace Avalonia.X11
unsafe class XI2Manager
{
private X11Info _x11;
private bool _multitouch;
private Dictionary<IntPtr, IXI2Client> _clients = new Dictionary<IntPtr, IXI2Client>();
class DeviceInfo
{
@ -77,11 +78,14 @@ namespace Avalonia.X11
private PointerDeviceInfo _pointerDevice;
private AvaloniaX11Platform _platform;
private readonly TouchDevice _touchDevice = new TouchDevice();
public bool Init(AvaloniaX11Platform platform)
{
_platform = platform;
_x11 = platform.Info;
_multitouch = platform.Options?.EnableMultiTouch ?? false;
var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
for (var c = 0; c < num; c++)
@ -121,16 +125,23 @@ namespace Avalonia.X11
public XEventMask AddWindow(IntPtr xid, IXI2Client window)
{
_clients[xid] = window;
XiSelectEvents(_x11.Display, xid, new Dictionary<int, List<XiEventType>>
var events = new List<XiEventType>
{
[_pointerDevice.Id] = new List<XiEventType>()
XiEventType.XI_Motion,
XiEventType.XI_ButtonPress,
XiEventType.XI_ButtonRelease
};
if (_multitouch)
events.AddRange(new[]
{
XiEventType.XI_Motion,
XiEventType.XI_ButtonPress,
XiEventType.XI_ButtonRelease,
}
});
XiEventType.XI_TouchBegin,
XiEventType.XI_TouchUpdate,
XiEventType.XI_TouchEnd
});
XiSelectEvents(_x11.Display, xid,
new Dictionary<int, List<XiEventType>> {[_pointerDevice.Id] = events});
// We are taking over mouse input handling from here
return XEventMask.PointerMotionMask
@ -154,8 +165,9 @@ namespace Avalonia.X11
_pointerDevice.Update(changed->Classes, changed->NumClasses);
}
//TODO: this should only be used for non-touch devices
if (xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion)
if ((xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion)
|| (xev->evtype>=XiEventType.XI_TouchBegin&&xev->evtype<=XiEventType.XI_TouchEnd))
{
var dev = (XIDeviceEvent*)xev;
if (_clients.TryGetValue(dev->EventWindow, out var client))
@ -165,6 +177,23 @@ namespace Avalonia.X11
void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev)
{
if (ev.Type == XiEventType.XI_TouchBegin
|| ev.Type == XiEventType.XI_TouchUpdate
|| ev.Type == XiEventType.XI_TouchEnd)
{
var type = ev.Type == XiEventType.XI_TouchBegin ?
RawPointerEventType.TouchBegin :
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
if (_multitouch && ev.Emulated)
return;
if (ev.Type == XiEventType.XI_Motion)
{
Vector scrollDelta = default;
@ -194,23 +223,23 @@ namespace Avalonia.X11
client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev))
client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
RawMouseEventType.Move, ev.Position, ev.Modifiers));
client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease)
{
var down = ev.Type == XiEventType.XI_ButtonPress;
var type =
ev.Button == 1 ? (down ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp)
: ev.Button == 2 ? (down ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp)
: ev.Button == 3 ? (down ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp)
: (RawMouseEventType?)null;
ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp)
: ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp)
: ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
: (RawPointerEventType?)null;
if (type.HasValue)
client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers));
}
_pointerDevice.UpdateValuators(ev.Valuators);
}
}
@ -222,6 +251,8 @@ namespace Avalonia.X11
public ulong Timestamp { get; }
public Point Position { get; }
public int Button { get; set; }
public int Detail { get; set; }
public bool Emulated { get; set; }
public Dictionary<int, double> Valuators { get; }
public ParsedDeviceEvent(XIDeviceEvent* ev)
{
@ -258,6 +289,8 @@ namespace Avalonia.X11
Valuators[c] = *values++;
if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease)
Button = ev->detail;
Detail = ev->detail;
Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated);
}
}

9
src/Avalonia.X11/XIStructs.cs

@ -230,13 +230,20 @@ namespace Avalonia.X11
public double root_y;
public double event_x;
public double event_y;
public int flags;
public XiDeviceEventFlags flags;
public XIButtonState buttons;
public XIValuatorState valuators;
public XIModifierState mods;
public XIModifierState group;
}
[Flags]
public enum XiDeviceEventFlags : int
{
None = 0,
XIPointerEmulated = (1 << 16)
}
[StructLayout(LayoutKind.Sequential)]
unsafe struct XIEvent
{

3
src/Avalonia.X11/XLib.cs

@ -321,6 +321,9 @@ namespace Avalonia.X11
public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask,
ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot);
[DllImport(libX11)]
public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height);
[DllImport(libX11)]
public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width,
int height, IntPtr fg, IntPtr bg, int depth);

3
src/Gtk/Avalonia.Gtk3/CursorFactory.cs

@ -12,6 +12,7 @@ namespace Avalonia.Gtk3
private static readonly Dictionary<StandardCursorType, object> CursorTypeMapping = new Dictionary
<StandardCursorType, object>
{
{StandardCursorType.None, CursorType.Blank},
{StandardCursorType.AppStarting, CursorType.Watch},
{StandardCursorType.Arrow, CursorType.LeftPtr},
{StandardCursorType.Cross, CursorType.Cross},
@ -80,4 +81,4 @@ namespace Avalonia.Gtk3
return rv;
}
}
}
}

1
src/Gtk/Avalonia.Gtk3/GdkCursor.cs

@ -2,6 +2,7 @@
{
enum GdkCursorType
{
Blank = -2,
CursorIsPixmap = -1,
XCursor = 0,
Arrow = 2,

18
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@ -145,17 +145,17 @@ namespace Avalonia.Gtk3
private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata)
{
var evnt = (GdkEventButton*)ev;
var e = new RawMouseEventArgs(
var e = new RawPointerEventArgs(
Gtk3Platform.Mouse,
evnt->time,
_inputRoot,
evnt->type == GdkEventType.ButtonRelease
? evnt->button == 1
? RawMouseEventType.LeftButtonUp
: evnt->button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp
? RawPointerEventType.LeftButtonUp
: evnt->button == 3 ? RawPointerEventType.RightButtonUp : RawPointerEventType.MiddleButtonUp
: evnt->button == 1
? RawMouseEventType.LeftButtonDown
: evnt->button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown,
? RawPointerEventType.LeftButtonDown
: evnt->button == 3 ? RawPointerEventType.RightButtonDown : RawPointerEventType.MiddleButtonDown,
new Point(evnt->x, evnt->y), GetModifierKeys(evnt->state));
OnInput(e);
return true;
@ -179,11 +179,11 @@ namespace Avalonia.Gtk3
var evnt = (GdkEventMotion*)ev;
var position = new Point(evnt->x, evnt->y);
Native.GdkEventRequestMotions(ev);
var e = new RawMouseEventArgs(
var e = new RawPointerEventArgs(
Gtk3Platform.Mouse,
evnt->time,
_inputRoot,
RawMouseEventType.Move,
RawPointerEventType.Move,
position, GetModifierKeys(evnt->state));
OnInput(e);
@ -237,10 +237,10 @@ namespace Avalonia.Gtk3
{
var evnt = (GdkEventCrossing*) pev;
var position = new Point(evnt->x, evnt->y);
OnInput(new RawMouseEventArgs(Gtk3Platform.Mouse,
OnInput(new RawPointerEventArgs(Gtk3Platform.Mouse,
evnt->time,
_inputRoot,
RawMouseEventType.Move,
RawPointerEventType.Move,
position, GetModifierKeys(evnt->state)));
return true;
}

18
src/Linux/Avalonia.LinuxFramebuffer/Mice.cs

@ -76,9 +76,9 @@ namespace Avalonia.LinuxFramebuffer
_y = Math.Min(_height, Math.Max(0, _y + ev.value));
else
return;
Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice,
Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
LinuxFramebufferPlatform.Timestamp,
LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y),
LinuxFramebufferPlatform.TopLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
InputModifiers.None));
}
if (ev.type ==(int) EvType.EV_ABS)
@ -89,24 +89,24 @@ namespace Avalonia.LinuxFramebuffer
_y = TranslateAxis(device.AbsY.Value, ev.value, _height);
else
return;
Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice,
Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
LinuxFramebufferPlatform.Timestamp,
LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y),
LinuxFramebufferPlatform.TopLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
InputModifiers.None));
}
if (ev.type == (short) EvType.EV_KEY)
{
RawMouseEventType? type = null;
RawPointerEventType? type = null;
if (ev.code == (ushort) EvKey.BTN_LEFT)
type = ev.value == 1 ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp;
type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
if (ev.code == (ushort)EvKey.BTN_RIGHT)
type = ev.value == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp;
type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
if (ev.code == (ushort) EvKey.BTN_MIDDLE)
type = ev.value == 1 ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp;
type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
if (!type.HasValue)
return;
Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice,
Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
LinuxFramebufferPlatform.Timestamp,
LinuxFramebufferPlatform.TopLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers)));
}

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

@ -1 +1 @@
Subproject commit 1e3ffc315401f0b2eb96a0e79b25c2fc19a80d78
Subproject commit a73c5234831267b23160e01a9fbc83be633f69fc

4
src/Skia/Avalonia.Skia/GlRenderTarget.cs

@ -8,7 +8,7 @@ using static Avalonia.OpenGL.GlConsts;
namespace Avalonia.Skia
{
internal class GlRenderTarget : IRenderTarget
internal class GlRenderTarget : IRenderTargetWithCorruptionInfo
{
private readonly GRContext _grContext;
private IGlPlatformSurfaceRenderTarget _surface;
@ -21,6 +21,8 @@ namespace Avalonia.Skia
public void Dispose() => _surface.Dispose();
public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true;
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
var session = _surface.BeginDraw();

3
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -111,8 +111,7 @@ namespace Avalonia.Skia
Width = size.Width,
Height = size.Height,
Dpi = dpi,
DisableTextLcdRendering = false,
GrContext = GrContext
DisableTextLcdRendering = false
};
return new SurfaceRenderTarget(createInfo);

24
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@ -160,19 +160,19 @@ namespace Avalonia.Win32.Interop.Wpf
return rv;
}
void MouseEvent(RawMouseEventType type, MouseEventArgs e)
=> _ttl.Input?.Invoke(new RawMouseEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type,
void MouseEvent(RawPointerEventType type, MouseEventArgs e)
=> _ttl.Input?.Invoke(new RawPointerEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type,
e.GetPosition(this).ToAvaloniaPoint(), GetModifiers()));
protected override void OnMouseDown(MouseButtonEventArgs e)
{
RawMouseEventType type;
RawPointerEventType type;
if(e.ChangedButton == MouseButton.Left)
type = RawMouseEventType.LeftButtonDown;
type = RawPointerEventType.LeftButtonDown;
else if (e.ChangedButton == MouseButton.Middle)
type = RawMouseEventType.MiddleButtonDown;
type = RawPointerEventType.MiddleButtonDown;
else if (e.ChangedButton == MouseButton.Right)
type = RawMouseEventType.RightButtonDown;
type = RawPointerEventType.RightButtonDown;
else
return;
MouseEvent(type, e);
@ -181,13 +181,13 @@ namespace Avalonia.Win32.Interop.Wpf
protected override void OnMouseUp(MouseButtonEventArgs e)
{
RawMouseEventType type;
RawPointerEventType type;
if (e.ChangedButton == MouseButton.Left)
type = RawMouseEventType.LeftButtonUp;
type = RawPointerEventType.LeftButtonUp;
else if (e.ChangedButton == MouseButton.Middle)
type = RawMouseEventType.MiddleButtonUp;
type = RawPointerEventType.MiddleButtonUp;
else if (e.ChangedButton == MouseButton.Right)
type = RawMouseEventType.RightButtonUp;
type = RawPointerEventType.RightButtonUp;
else
return;
MouseEvent(type, e);
@ -196,14 +196,14 @@ namespace Avalonia.Win32.Interop.Wpf
protected override void OnMouseMove(MouseEventArgs e)
{
MouseEvent(RawMouseEventType.Move, e);
MouseEvent(RawPointerEventType.Move, e);
}
protected override void OnMouseWheel(MouseWheelEventArgs e) =>
_ttl.Input?.Invoke(new RawMouseWheelEventArgs(_mouse, (uint) e.Timestamp, _inputRoot,
e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers()));
protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawMouseEventType.LeaveWindow, e);
protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e);
protected override void OnKeyDown(KeyEventArgs e)
=> _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown,

1
src/Windows/Avalonia.Win32/CursorFactory.cs

@ -41,6 +41,7 @@ namespace Avalonia.Win32
private static readonly Dictionary<StandardCursorType, int> CursorTypeMapping = new Dictionary
<StandardCursorType, int>
{
{StandardCursorType.None, 0},
{StandardCursorType.AppStarting, 32650},
{StandardCursorType.Arrow, 32512},
{StandardCursorType.Cross, 32515},

72
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -574,6 +574,7 @@ namespace Avalonia.Win32.Interop
WM_AFXLAST = 0x037F,
WM_PENWINFIRST = 0x0380,
WM_PENWINLAST = 0x038F,
WM_TOUCH = 0x0240,
WM_APP = 0x8000,
WM_USER = 0x0400,
@ -836,10 +837,16 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
[DllImport("user32")]
public static extern IntPtr GetMessageExtraInfo();
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")]
public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx);
[DllImport("user32.dll")]
public static extern void RegisterTouchWindow(IntPtr hWnd, int flags);
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
@ -1035,6 +1042,17 @@ namespace Avalonia.Win32.Interop
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi);
[DllImport("user32")]
public static extern bool GetTouchInputInfo(
IntPtr hTouchInput,
uint cInputs,
[Out]TOUCHINPUT[] pInputs,
int cbSize
);
[DllImport("user32")]
public static extern bool CloseTouchInputHandle(IntPtr hTouchInput);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
@ -1309,6 +1327,60 @@ namespace Avalonia.Win32.Interop
public IntPtr hIconSm;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOUCHINPUT
{
public int X;
public int Y;
public IntPtr Source;
public uint Id;
public TouchInputFlags Flags;
public int Mask;
public uint Time;
public IntPtr ExtraInfo;
public int CxContact;
public int CyContact;
}
[Flags]
public enum TouchInputFlags
{
/// <summary>
/// Movement has occurred. Cannot be combined with TOUCHEVENTF_DOWN.
/// </summary>
TOUCHEVENTF_MOVE = 0x0001,
/// <summary>
/// The corresponding touch point was established through a new contact. Cannot be combined with TOUCHEVENTF_MOVE or TOUCHEVENTF_UP.
/// </summary>
TOUCHEVENTF_DOWN = 0x0002,
/// <summary>
/// A touch point was removed.
/// </summary>
TOUCHEVENTF_UP = 0x0004,
/// <summary>
/// A touch point is in range. This flag is used to enable touch hover support on compatible hardware. Applications that do not want support for hover can ignore this flag.
/// </summary>
TOUCHEVENTF_INRANGE = 0x0008,
/// <summary>
/// Indicates that this TOUCHINPUT structure corresponds to a primary contact point. See the following text for more information on primary touch points.
/// </summary>
TOUCHEVENTF_PRIMARY = 0x0010,
/// <summary>
/// When received using GetTouchInputInfo, this input was not coalesced.
/// </summary>
TOUCHEVENTF_NOCOALESCE = 0x0020,
/// <summary>
/// The touch event came from the user's palm.
/// </summary>
TOUCHEVENTF_PALM = 0x0080
}
[Flags]
public enum OpenFileNameFlags
{

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

@ -40,6 +40,7 @@ namespace Avalonia
{
public bool UseDeferredRendering { get; set; } = true;
public bool AllowEglInitialization { get; set; }
public bool? EnableMultitouch { get; set; }
}
}
@ -59,7 +60,8 @@ namespace Avalonia.Win32
CreateMessageWindow();
}
public static bool UseDeferredRendering { get; set; }
public static bool UseDeferredRendering => Options.UseDeferredRendering;
public static Win32PlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(
UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK),
@ -74,6 +76,7 @@ namespace Avalonia.Win32
public static void Initialize(Win32PlatformOptions options)
{
Options = options;
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<IStandardCursorFactory>().ToConstant(CursorFactory.Instance)
@ -88,7 +91,7 @@ namespace Avalonia.Win32
.Bind<IPlatformIconLoader>().ToConstant(s_instance);
if (options.AllowEglInitialization)
Win32GlManager.Initialize();
UseDeferredRendering = options.UseDeferredRendering;
_uiThread = Thread.CurrentThread;
if (OleContext.Current != null)

104
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -30,7 +30,10 @@ namespace Avalonia.Win32
private UnmanagedMethods.WndProc _wndProcDelegate;
private string _className;
private IntPtr _hwnd;
private bool _multitouch;
private TouchDevice _touchDevice = new TouchDevice();
private IInputRoot _owner;
private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
private bool _trackingMouse;
private bool _decorated = true;
private bool _resizable = true;
@ -148,7 +151,9 @@ namespace Avalonia.Win32
if (customRendererFactory != null)
return customRendererFactory.Create(root, loop);
return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root);
return Win32Platform.UseDeferredRendering ?
(IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) :
new ImmediateRenderer(root);
}
public void Resize(Size value)
@ -414,6 +419,15 @@ namespace Avalonia.Win32
IntPtr.Zero);
}
bool ShouldIgnoreTouchEmulatedMessage()
{
if (!_multitouch)
return false;
var marker = 0xFF515700L;
var info = GetMessageExtraInfo().ToInt64();
return (info & marker) == marker;
}
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")]
protected virtual IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
@ -519,34 +533,40 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN:
e = new RawMouseEventArgs(
if(ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
? RawMouseEventType.LeftButtonDown
? RawPointerEventType.LeftButtonDown
: msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN
? RawMouseEventType.RightButtonDown
: RawMouseEventType.MiddleButtonDown,
? RawPointerEventType.RightButtonDown
: RawPointerEventType.MiddleButtonDown,
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP:
case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP:
case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP:
e = new RawMouseEventArgs(
if(ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
? RawMouseEventType.LeftButtonUp
? RawPointerEventType.LeftButtonUp
: msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP
? RawMouseEventType.RightButtonUp
: RawMouseEventType.MiddleButtonUp,
? RawPointerEventType.RightButtonUp
: RawPointerEventType.MiddleButtonUp,
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
case UnmanagedMethods.WindowsMessage.WM_MOUSEMOVE:
if(ShouldIgnoreTouchEmulatedMessage())
break;
if (!_trackingMouse)
{
var tm = new UnmanagedMethods.TRACKMOUSEEVENT
@ -560,11 +580,11 @@ namespace Avalonia.Win32
UnmanagedMethods.TrackMouseEvent(ref tm);
}
e = new RawMouseEventArgs(
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
timestamp,
_owner,
RawMouseEventType.Move,
RawPointerEventType.Move,
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
@ -589,29 +609,50 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE:
_trackingMouse = false;
e = new RawMouseEventArgs(
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
timestamp,
_owner,
RawMouseEventType.LeaveWindow,
RawPointerEventType.LeaveWindow,
new Point(), WindowsKeyboardDevice.Instance.Modifiers);
break;
case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
e = new RawMouseEventArgs(
e = new RawPointerEventArgs(
WindowsMouseDevice.Instance,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
? RawMouseEventType.NonClientLeftButtonDown
? RawPointerEventType.NonClientLeftButtonDown
: msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN
? RawMouseEventType.RightButtonDown
: RawMouseEventType.MiddleButtonDown,
? RawPointerEventType.RightButtonDown
: RawPointerEventType.MiddleButtonDown,
new Point(0, 0), GetMouseModifiers(wParam));
break;
case WindowsMessage.WM_TOUCH:
var touchInputs = new TOUCHINPUT[wParam.ToInt32()];
if (GetTouchInputInfo(lParam, (uint)wParam.ToInt32(), touchInputs, Marshal.SizeOf<TOUCHINPUT>()))
{
foreach (var touchInput in touchInputs)
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id));
}
CloseTouchInputHandle(lParam);
return IntPtr.Zero;
}
break;
case WindowsMessage.WM_NCPAINT:
if (!_decorated)
{
@ -627,18 +668,26 @@ namespace Avalonia.Win32
break;
case UnmanagedMethods.WindowsMessage.WM_PAINT:
UnmanagedMethods.PAINTSTRUCT ps;
if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero)
using (_rendererLock.Lock())
{
var f = Scaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, (r.bottom - r.top) / f));
UnmanagedMethods.EndPaint(_hwnd, ref ps);
UnmanagedMethods.PAINTSTRUCT ps;
if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero)
{
var f = Scaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f,
(r.bottom - r.top) / f));
UnmanagedMethods.EndPaint(_hwnd, ref ps);
}
}
return IntPtr.Zero;
case UnmanagedMethods.WindowsMessage.WM_SIZE:
using (_rendererLock.Lock())
{
// Do nothing here, just block until the pending frame render is completed on the render thread
}
var size = (UnmanagedMethods.SizeCommand)wParam;
if (Resized != null &&
@ -704,7 +753,8 @@ namespace Avalonia.Win32
}
}
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
using (_rendererLock.Lock())
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
}
static InputModifiers GetMouseModifiers(IntPtr wParam)
@ -754,6 +804,10 @@ namespace Avalonia.Win32
Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType);
_multitouch = Win32Platform.Options.EnableMultitouch ?? false;
if (_multitouch)
RegisterTouchWindow(_hwnd, 0);
if (UnmanagedMethods.ShCoreAvailable)
{
uint dpix, dpiy;

1
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="System.Reflection.Emit" Version="4.3.0" ExcludeAssets="All" />
</ItemGroup>
<Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
<Import Project="..\..\..\build\Rx.props" />

16
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@ -86,11 +86,11 @@ namespace Avalonia.iOS
{
var location = touch.LocationInView(this).ToAvalonia();
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
iOSPlatform.MouseDevice,
(uint)touch.Timestamp,
_inputRoot,
RawMouseEventType.LeftButtonUp,
RawPointerEventType.LeftButtonUp,
location,
InputModifiers.None));
}
@ -104,11 +104,11 @@ namespace Avalonia.iOS
{
var location = touch.LocationInView(this).ToAvalonia();
_touchLastPoint = location;
Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
RawMouseEventType.Move, location, InputModifiers.None));
Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
RawPointerEventType.Move, location, InputModifiers.None));
Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
RawMouseEventType.LeftButtonDown, location, InputModifiers.None));
Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
RawPointerEventType.LeftButtonDown, location, InputModifiers.None));
}
}
@ -119,8 +119,8 @@ namespace Avalonia.iOS
{
var location = touch.LocationInView(this).ToAvalonia();
if (iOSPlatform.MouseDevice.Captured != null)
Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
RawMouseEventType.Move, location, InputModifiers.LeftMouseButton));
Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
RawPointerEventType.Move, location, InputModifiers.LeftMouseButton));
else
{
//magic number based on test - correction of 0.02 is working perfect

106
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@ -14,6 +14,8 @@ namespace Avalonia.Controls.UnitTests
{
public class ButtonTests
{
private MouseTestHelper _helper = new MouseTestHelper();
[Fact]
public void Button_Is_Disabled_When_Command_Is_Disabled()
{
@ -102,12 +104,8 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_Raises_Click()
{
var mouse = Mock.Of<IMouseDevice>();
var renderer = Mock.Of<IRenderer>();
IInputElement captured = null;
Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(50, 50));
Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
var pt = new Point(50, 50);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
@ -122,15 +120,15 @@ namespace Avalonia.Controls.UnitTests
target.Click += (s, e) => clicked = true;
RaisePointerEnter(target, mouse);
RaisePointerMove(target, mouse);
RaisePointerPressed(target, mouse, 1, MouseButton.Left);
RaisePointerEnter(target);
RaisePointerMove(target, pt);
RaisePointerPressed(target, 1, MouseButton.Left, pt);
Assert.Equal(captured, target);
Assert.Equal(_helper.Captured, target);
RaisePointerReleased(target, mouse, MouseButton.Left);
RaisePointerReleased(target, MouseButton.Left, pt);
Assert.Equal(captured, null);
Assert.Equal(_helper.Captured, null);
Assert.True(clicked);
}
@ -138,12 +136,8 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside()
{
var mouse = Mock.Of<IMouseDevice>();
var renderer = Mock.Of<IRenderer>();
IInputElement captured = null;
Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(200, 50));
Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
@ -158,16 +152,16 @@ namespace Avalonia.Controls.UnitTests
target.Click += (s, e) => clicked = true;
RaisePointerEnter(target, mouse);
RaisePointerMove(target, mouse);
RaisePointerPressed(target, mouse, 1, MouseButton.Left);
RaisePointerLeave(target, mouse);
RaisePointerEnter(target);
RaisePointerMove(target, new Point(50,50));
RaisePointerPressed(target, 1, MouseButton.Left, new Point(50, 50));
RaisePointerLeave(target);
Assert.Equal(captured, target);
Assert.Equal(_helper.Captured, target);
RaisePointerReleased(target, mouse, MouseButton.Left);
RaisePointerReleased(target, MouseButton.Left, new Point(200, 50));
Assert.Equal(captured, null);
Assert.Equal(_helper.Captured, null);
Assert.False(clicked);
}
@ -175,12 +169,8 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_With_RenderTransform_Raises_Click()
{
var mouse = Mock.Of<IMouseDevice>();
var renderer = Mock.Of<IRenderer>();
IInputElement captured = null;
Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(150, 50));
Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
var pt = new Point(150, 50);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
@ -204,15 +194,15 @@ namespace Avalonia.Controls.UnitTests
target.Click += (s, e) => clicked = true;
RaisePointerEnter(target, mouse);
RaisePointerMove(target, mouse);
RaisePointerPressed(target, mouse, 1, MouseButton.Left);
RaisePointerEnter(target);
RaisePointerMove(target, pt);
RaisePointerPressed(target, 1, MouseButton.Left, pt);
Assert.Equal(captured, target);
Assert.Equal(_helper.Captured, target);
RaisePointerReleased(target, mouse, MouseButton.Left);
RaisePointerReleased(target, MouseButton.Left, pt);
Assert.Equal(captured, null);
Assert.Equal(_helper.Captured, null);
Assert.True(clicked);
}
@ -278,57 +268,29 @@ namespace Avalonia.Controls.UnitTests
public PixelPoint PointToScreen(Point p) => throw new NotImplementedException();
}
private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton)
private void RaisePointerPressed(Button button, int clickCount, MouseButton mouseButton, Point position)
{
button.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
Source = button,
MouseButton = mouseButton,
ClickCount = clickCount,
Device = device,
});
_helper.Down(button, mouseButton, position, clickCount: clickCount);
}
private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton)
private void RaisePointerReleased(Button button, MouseButton mouseButton, Point pt)
{
button.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
Source = button,
MouseButton = mouseButton,
Device = device,
});
_helper.Up(button, mouseButton, pt);
}
private void RaisePointerEnter(Button button, IMouseDevice device)
private void RaisePointerEnter(Button button)
{
button.RaiseEvent(new PointerEventArgs
{
RoutedEvent = InputElement.PointerEnterEvent,
Source = button,
Device = device,
});
_helper.Enter(button);
}
private void RaisePointerLeave(Button button, IMouseDevice device)
private void RaisePointerLeave(Button button)
{
button.RaiseEvent(new PointerEventArgs
{
RoutedEvent = InputElement.PointerLeaveEvent,
Source = button,
Device = device,
});
_helper.Leave(button);
}
private void RaisePointerMove(Button button, IMouseDevice device)
private void RaisePointerMove(Button button, Point pos)
{
button.RaiseEvent(new PointerEventArgs
{
RoutedEvent = InputElement.PointerMovedEvent,
Source = button,
Device = device,
});
_helper.Move(button, pos);
}
private class TestCommand : ICommand

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

@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests
{
public class ComboBoxTests
{
MouseTestHelper _helper = new MouseTestHelper();
[Fact]
public void Clicking_On_Control_Toggles_IsDropDownOpen()
{
@ -23,17 +25,11 @@ namespace Avalonia.Controls.UnitTests
Items = new[] { "Foo", "Bar" },
};
target.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
});
_helper.Down(target);
_helper.Up(target);
Assert.True(target.IsDropDownOpen);
target.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
});
_helper.Down(target);
Assert.False(target.IsDropDownOpen);
}

43
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -14,6 +14,7 @@ namespace Avalonia.Controls.UnitTests
public class ContextMenuTests
{
private Mock<IPopupImpl> popupImpl;
private MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Clicking_On_Control_Toggles_ContextMenu()
@ -31,19 +32,11 @@ namespace Avalonia.Controls.UnitTests
new Window { Content = target };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.None
});
_mouse.Click(target);
Assert.False(sut.IsOpen);
popupImpl.Verify(x => x.Show(), Times.Once);
@ -69,19 +62,11 @@ namespace Avalonia.Controls.UnitTests
Avalonia.Application.Current.MainWindow = window;
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
popupImpl.Verify(x => x.Hide(), Times.Once);
@ -106,11 +91,7 @@ namespace Avalonia.Controls.UnitTests
sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
_mouse.Click(target, MouseButton.Right);
Assert.True(eventCalled);
Assert.False(sut.IsOpen);
@ -136,19 +117,11 @@ namespace Avalonia.Controls.UnitTests
sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; };
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.Right
});
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
target.RaiseEvent(new PointerReleasedEventArgs
{
RoutedEvent = InputElement.PointerReleasedEvent,
MouseButton = MouseButton.None
});
_mouse.Click(target, MouseButton.Right);
Assert.True(eventCalled);
Assert.True(sut.IsOpen);

9
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -16,6 +16,8 @@ namespace Avalonia.Controls.UnitTests
{
public class ListBoxTests
{
private MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Should_Use_ItemTemplate_To_Create_Item_Content()
{
@ -225,12 +227,7 @@ namespace Avalonia.Controls.UnitTests
private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
{
listBox.RaiseEvent(new PointerPressedEventArgs
{
Source = item,
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = mouseButton
});
_mouse.Click(listBox, item, mouseButton);
}
[Fact]

41
tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs

@ -18,6 +18,8 @@ namespace Avalonia.Controls.UnitTests
{
public class ListBoxTests_Single
{
MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Focusing_Item_With_Tab_Should_Not_Select_It()
{
@ -68,12 +70,7 @@ namespace Avalonia.Controls.UnitTests
};
ApplyTemplate(target);
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(0, target.SelectedIndex);
}
@ -90,11 +87,7 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(0, target.SelectedIndex);
}
@ -111,11 +104,7 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(0, target.SelectedIndex);
}
@ -133,11 +122,7 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(-1, target.SelectedIndex);
}
@ -155,11 +140,7 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
target.SelectedIndex = 0;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(0, target.SelectedIndex);
}
@ -177,11 +158,7 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target);
target.SelectedIndex = 1;
target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(target.Presenter.Panel.Children[0]);
Assert.Equal(0, target.SelectedIndex);
}
@ -306,4 +283,4 @@ namespace Avalonia.Controls.UnitTests
target.Presenter.ApplyTemplate();
}
}
}
}

111
tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs

@ -0,0 +1,111 @@
using System.Reactive;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Controls.UnitTests
{
public class MouseTestHelper
{
private Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
private ulong _nextStamp = 1;
private ulong Timestamp() => _nextStamp++;
private InputModifiers _pressedButtons;
public IInputElement Captured => _pointer.Captured;
InputModifiers Convert(MouseButton mouseButton)
=> (mouseButton == MouseButton.Left ? InputModifiers.LeftMouseButton
: mouseButton == MouseButton.Middle ? InputModifiers.MiddleMouseButton
: mouseButton == MouseButton.Right ? InputModifiers.RightMouseButton : InputModifiers.None);
int ButtonCount(PointerPointProperties props)
{
var rv = 0;
if (props.IsLeftButtonPressed)
rv++;
if (props.IsMiddleButtonPressed)
rv++;
if (props.IsRightButtonPressed)
rv++;
return rv;
}
private MouseButton _pressedButton;
InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons;
public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
InputModifiers modifiers = default, int clickCount = 1)
{
Down(target, target, mouseButton, position, modifiers, clickCount);
}
public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
Point position = default, InputModifiers modifiers = default, int clickCount = 1)
{
_pressedButtons |= Convert(mouseButton);
var props = new PointerPointProperties(_pressedButtons);
if (ButtonCount(props) > 1)
Move(target, source, position);
else
{
_pressedButton = mouseButton;
_pointer.Capture((IInputElement)target);
target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
GetModifiers(modifiers), clickCount));
}
}
public void Move(IInteractive target, in Point position, InputModifiers modifiers = default) => Move(target, target, position, modifiers);
public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default)
{
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position,
Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers)));
}
public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
InputModifiers modifiers = default)
=> Up(target, target, mouseButton, position, modifiers);
public void Up(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
Point position = default, InputModifiers modifiers = default)
{
var conv = Convert(mouseButton);
_pressedButtons = (_pressedButtons | conv) ^ conv;
var props = new PointerPointProperties(_pressedButtons);
if (ButtonCount(props) == 0)
{
_pointer.Capture(null);
target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
Timestamp(), props,
GetModifiers(modifiers), _pressedButton));
}
else
Move(target, source, position);
}
public void Click(IInteractive target, MouseButton button = MouseButton.Left, Point position = default,
InputModifiers modifiers = default)
=> Click(target, target, button, position, modifiers);
public void Click(IInteractive target, IInteractive source, MouseButton button = MouseButton.Left,
Point position = default, InputModifiers modifiers = default)
{
Down(target, source, button, position, modifiers);
Up(target, source, button, position, modifiers);
}
public void Enter(IInteractive target)
{
target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default,
Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
}
public void Leave(IInteractive target)
{
target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default,
Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
}
}
}

54
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@ -2,6 +2,7 @@
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Moq;
using Xunit;
@ -9,6 +10,16 @@ namespace Avalonia.Controls.UnitTests.Platform
{
public class DefaultMenuInteractionHandlerTests
{
static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source)
=> new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default);
static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source,
new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true},
default);
static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source,
new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left);
public class TopLevel
{
[Fact]
@ -121,7 +132,8 @@ namespace Avalonia.Controls.UnitTests.Platform
x.IsTopLevel == true &&
x.HasSubMenu == true &&
x.Parent == menu);
var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item };
var e = CreatePressed(item);
target.PointerPressed(item, e);
Mock.Get(menu).Verify(x => x.Close());
@ -141,7 +153,7 @@ namespace Avalonia.Controls.UnitTests.Platform
x.IsTopLevel == true &&
x.HasSubMenu == true &&
x.Parent == menu.Object);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem };
var e = CreateArgs(MenuItem.PointerEnterItemEvent, nextItem);
menu.SetupGet(x => x.SelectedItem).Returns(item);
@ -161,7 +173,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var target = new DefaultMenuInteractionHandler(false);
var menu = new Mock<IMenu>();
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
menu.SetupGet(x => x.SelectedItem).Returns(item);
target.PointerLeave(item, e);
@ -176,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var target = new DefaultMenuInteractionHandler(false);
var menu = new Mock<IMenu>();
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
menu.SetupGet(x => x.IsOpen).Returns(true);
menu.SetupGet(x => x.SelectedItem).Returns(item);
@ -330,7 +342,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerEnterItemEvent, item);
target.PointerEnter(item, e);
@ -346,7 +358,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerEnterItemEvent, item);
target.PointerEnter(item, e);
Mock.Get(item).Verify(x => x.Open(), Times.Never);
@ -366,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var sibling = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerEnterItemEvent, item);
Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling });
@ -386,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item);
target.PointerLeave(item, e);
@ -403,7 +415,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var sibling = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling);
target.PointerLeave(item, e);
@ -419,7 +431,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
target.PointerLeave(item, e);
@ -434,7 +446,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item };
var e = CreateReleased(item);
target.PointerReleased(item, e);
@ -452,8 +464,8 @@ namespace Avalonia.Controls.UnitTests.Platform
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
var childItem = Mock.Of<IMenuItem>(x => x.Parent == item);
var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
var enter = CreateArgs(MenuItem.PointerEnterItemEvent, item);
var leave = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
// Pointer enters item; item is selected.
target.PointerEnter(item, enter);
@ -488,7 +500,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item };
var e = CreatePressed(item);
target.PointerPressed(item, e);
@ -537,5 +549,19 @@ namespace Avalonia.Controls.UnitTests.Platform
_action = action;
}
}
class FakePointer : IPointer
{
public int Id { get; } = Pointer.GetNextFreeId();
public void Capture(IInputElement control)
{
Captured = control;
}
public IInputElement Captured { get; set; }
public PointerType Type { get; }
public bool IsPrimary { get; } = true;
}
}
}

50
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public class SelectingItemsControlTests
{
private MouseTestHelper _helper = new MouseTestHelper();
[Fact]
public void SelectedIndex_Should_Initially_Be_Minus_1()
{
@ -675,12 +677,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_helper.Down((Interactive)target.Presenter.Panel.Children[1]);
var panel = target.Presenter.Panel;
@ -703,11 +700,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_helper.Down(target.Presenter.Panel.Children[1]);
items.RemoveAt(1);
@ -756,6 +749,40 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal("b", target.SelectedItem);
}
[Fact]
public void Mode_For_SelectedIndex_Is_TwoWay_By_Default()
{
var items = new[]
{
new Item(),
new Item(),
new Item(),
};
var vm = new MasterViewModel
{
Child = new ChildViewModel
{
Items = items,
SelectedIndex = 1,
}
};
var target = new SelectingItemsControl { DataContext = vm };
var itemsBinding = new Binding("Child.Items");
var selectedIndBinding = new Binding("Child.SelectedIndex");
target.Bind(SelectingItemsControl.ItemsProperty, itemsBinding);
target.Bind(SelectingItemsControl.SelectedIndexProperty, selectedIndBinding);
Assert.Equal(1, target.SelectedIndex);
target.SelectedIndex = 2;
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(2, vm.Child.SelectedIndex);
}
private FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>(control =>
@ -792,6 +819,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public IList<Item> Items { get; set; }
public Item SelectedItem { get; set; }
public int SelectedIndex { get; set; }
}
private class RootWithItems : TestRoot

15
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -385,6 +385,21 @@ namespace Avalonia.Controls.UnitTests
Assert.True(target.SelectionEnd <= "123".Length);
}
}
[Fact]
public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123456789\r"
};
target.CaretIndex = 11;
Assert.True(true);
}
}
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<IStandardCursorFactory>());

89
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -19,6 +19,8 @@ namespace Avalonia.Controls.UnitTests
{
public class TreeViewTests
{
MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Items_Should_Be_Created()
{
@ -130,11 +132,7 @@ namespace Avalonia.Controls.UnitTests
Assert.NotNull(container);
container.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
});
_mouse.Click(container);
Assert.Equal(item, target.SelectedItem);
Assert.True(container.IsSelected);
@ -165,12 +163,7 @@ namespace Avalonia.Controls.UnitTests
Assert.True(container.IsSelected);
container.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
InputModifiers = InputModifiers.Control
});
_mouse.Click(container, modifiers: InputModifiers.Control);
Assert.Null(target.SelectedItem);
Assert.False(container.IsSelected);
@ -205,13 +198,8 @@ namespace Avalonia.Controls.UnitTests
Assert.True(container1.IsSelected);
container2.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
InputModifiers = InputModifiers.Control
});
_mouse.Click(container2, modifiers: InputModifiers.Control);
Assert.Equal(item2, target.SelectedItem);
Assert.False(container1.IsSelected);
Assert.True(container2.IsSelected);
@ -242,15 +230,15 @@ namespace Avalonia.Controls.UnitTests
var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
ClickContainer(item1Container, InputModifiers.Control);
Assert.True(item1Container.IsSelected);
TreeTestHelper.ClickContainer(item2Container, InputModifiers.Control);
ClickContainer(item2Container, InputModifiers.Control);
Assert.True(item2Container.IsSelected);
Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType<Node>());
TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
ClickContainer(item1Container, InputModifiers.Control);
Assert.False(item1Container.IsSelected);
Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
@ -281,12 +269,12 @@ namespace Avalonia.Controls.UnitTests
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(fromContainer, InputModifiers.None);
Assert.True(fromContainer.IsSelected);
TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
TreeTestHelper.AssertChildrenSelected(target, rootNode);
ClickContainer(toContainer, InputModifiers.Shift);
AssertChildrenSelected(target, rootNode);
}
[Fact]
@ -314,12 +302,12 @@ namespace Avalonia.Controls.UnitTests
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(fromContainer, InputModifiers.None);
Assert.True(fromContainer.IsSelected);
TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
TreeTestHelper.AssertChildrenSelected(target, rootNode);
ClickContainer(toContainer, InputModifiers.Shift);
AssertChildrenSelected(target, rootNode);
}
[Fact]
@ -347,12 +335,12 @@ namespace Avalonia.Controls.UnitTests
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(fromContainer, InputModifiers.None);
TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
TreeTestHelper.AssertChildrenSelected(target, rootNode);
ClickContainer(toContainer, InputModifiers.Shift);
AssertChildrenSelected(target, rootNode);
TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(fromContainer, InputModifiers.None);
Assert.True(fromContainer.IsSelected);
@ -656,7 +644,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent);
TreeTestHelper.AssertChildrenSelected(target, rootNode);
AssertChildrenSelected(target, rootNode);
}
}
@ -687,8 +675,8 @@ namespace Avalonia.Controls.UnitTests
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(toContainer, InputModifiers.Shift);
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
var selectAllGesture = keymap.SelectAll.First();
@ -702,7 +690,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent);
TreeTestHelper.AssertChildrenSelected(target, rootNode);
AssertChildrenSelected(target, rootNode);
}
}
@ -733,8 +721,8 @@ namespace Avalonia.Controls.UnitTests
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(toContainer, InputModifiers.Shift);
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
var selectAllGesture = keymap.SelectAll.First();
@ -748,7 +736,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent);
TreeTestHelper.AssertChildrenSelected(target, rootNode);
AssertChildrenSelected(target, rootNode);
}
}
@ -871,29 +859,22 @@ namespace Avalonia.Controls.UnitTests
}
}
private static class TreeTestHelper
void ClickContainer(IControl container, InputModifiers modifiers)
{
public static void ClickContainer(IControl container, InputModifiers modifiers)
{
container.RaiseEvent(new PointerPressedEventArgs
{
RoutedEvent = InputElement.PointerPressedEvent,
MouseButton = MouseButton.Left,
InputModifiers = modifiers
});
}
_mouse.Click(container, modifiers: modifiers);
}
public static void AssertChildrenSelected(TreeView treeView, Node rootNode)
void AssertChildrenSelected(TreeView treeView, Node rootNode)
{
foreach (var child in rootNode.Children)
{
foreach (var child in rootNode.Children)
{
var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child);
var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child);
Assert.True(container.IsSelected);
}
Assert.True(container.IsSelected);
}
}
private class Node : NotifyingBase
{
private IAvaloniaList<Node> _children;

8
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@ -15,7 +15,7 @@ namespace Avalonia.Input.UnitTests
public class MouseDeviceTests
{
[Fact]
public void Capture_Is_Cleared_When_Control_Removed()
public void Capture_Is_Transferred_To_Parent_When_Control_Removed()
{
Canvas control;
var root = new TestRoot
@ -29,7 +29,7 @@ namespace Avalonia.Input.UnitTests
root.Child = null;
Assert.Null(target.Captured);
Assert.Same(root, target.Captured);
}
[Fact]
@ -225,11 +225,11 @@ namespace Avalonia.Input.UnitTests
private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
{
inputManager.ProcessInput(new RawMouseEventArgs(
inputManager.ProcessInput(new RawPointerEventArgs(
root.MouseDevice,
0,
root,
RawMouseEventType.Move,
RawPointerEventType.Move,
p,
InputModifiers.None));
}

2
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@ -4,6 +4,7 @@
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
@ -19,6 +20,7 @@
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<Compile Include="..\Avalonia.Controls.UnitTests\MouseTestHelper.cs" />
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

44
tests/Avalonia.Interactivity.UnitTests/GestureTests.cs

@ -3,6 +3,7 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.UnitTests;
using Avalonia.Input;
using Xunit;
@ -10,6 +11,8 @@ namespace Avalonia.Interactivity.UnitTests
{
public class GestureTests
{
private MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Tapped_Should_Follow_Pointer_Pressed_Released()
{
@ -27,8 +30,7 @@ namespace Avalonia.Interactivity.UnitTests
border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
border.RaiseEvent(new PointerPressedEventArgs());
border.RaiseEvent(new PointerReleasedEventArgs());
_mouse.Click(border);
Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result);
}
@ -47,8 +49,7 @@ namespace Avalonia.Interactivity.UnitTests
decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
border.RaiseEvent(new PointerPressedEventArgs());
border.RaiseEvent(new PointerReleasedEventArgs());
_mouse.Click(border);
Assert.Equal(new[] { "bt", "dt" }, result);
}
@ -72,11 +73,40 @@ namespace Avalonia.Interactivity.UnitTests
border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
border.RaiseEvent(new PointerPressedEventArgs());
border.RaiseEvent(new PointerReleasedEventArgs());
border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 });
_mouse.Click(border);
_mouse.Down(border, clickCount: 2);
Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result);
}
[Fact]
public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled()
{
Border border = new Border();
var decorator = new Decorator
{
Child = border
};
var result = new List<string>();
decorator.AddHandler(Border.PointerPressedEvent, (s, e) =>
{
result.Add("dp");
e.Handled = true;
});
decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr"));
decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt"));
border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp"));
border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
_mouse.Click(border);
_mouse.Down(border, clickCount: 2);
Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result);
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save