diff --git a/dirs.proj b/dirs.proj
index 4939a158bb..e56320e73f 100644
--- a/dirs.proj
+++ b/dirs.proj
@@ -8,10 +8,11 @@
-
-
+
+
+
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
index 4b814a9cfb..a35f4f3eeb 100644
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ b/native/Avalonia.Native/inc/avalonia-native.h
@@ -144,6 +144,7 @@ enum AvnStandardCursorType
CursorDragMove,
CursorDragCopy,
CursorDragLink,
+ CursorNone
};
enum AvnWindowEdge
diff --git a/native/Avalonia.Native/src/OSX/cursor.h b/native/Avalonia.Native/src/OSX/cursor.h
index a8eb49c0b9..cfe91955d8 100644
--- a/native/Avalonia.Native/src/OSX/cursor.h
+++ b/native/Avalonia.Native/src/OSX/cursor.h
@@ -11,18 +11,24 @@ class Cursor : public ComSingleObject
{
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 s_cursorMap;
diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm
index bd2c94a4d8..799fa9e8e6 100644
--- a/native/Avalonia.Native/src/OSX/cursor.mm
+++ b/native/Avalonia.Native/src/OSX/cursor.mm
@@ -21,6 +21,7 @@ class CursorFactory : public ComSingleObject s_cursorMap =
{
@@ -46,11 +47,13 @@ class CursorFactory : public ComSingleObject(cursor);
this->cursor = avnCursor->GetNative();
UpdateCursor();
+
+ if(avnCursor->IsHidden())
+ {
+ [NSCursor hide];
+ }
+ else
+ {
+ [NSCursor unhide];
+ }
+
return S_OK;
}
}
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index 804ca1f9b8..6a40f7187d 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -11,6 +11,7 @@
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 93f5611ec4..4027c5cd63 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -46,6 +46,12 @@ namespace ControlCatalog.NetCore
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
+ .With(new X11PlatformOptions {EnableMultiTouch = true})
+ .With(new Win32PlatformOptions
+ {
+ EnableMultitouch = true,
+ AllowEglInitialization = true
+ })
.UseSkia()
.UseReactiveUI();
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 9f1899acc5..1cddb9d295 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -31,6 +31,7 @@
+
diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs
new file mode 100644
index 0000000000..a1359519e6
--- /dev/null
+++ b/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 _pointers = new Dictionary();
+
+ 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)));
+ }
+
+ }
+ }
+}
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 344250ee2b..7f63e7725f 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -38,6 +38,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
new file mode 100644
index 0000000000..3eb2276c48
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
index 112925ab0f..463d499aad 100644
--- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
+++ b/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);
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index ef7a2a37ea..546133bb03 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -170,4 +170,4 @@ namespace Avalonia.Utilities
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index cc9e6b7444..70f26288af 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/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);
diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs
index 224d9b782b..53852defb3 100644
--- a/src/Avalonia.Controls/Calendar/CalendarButton.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs
@@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
CalendarLeftMouseButtonUp?.Invoke(this, e);
}
-
- ///
- /// 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.
- ///
- /// Event arguments.
- internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
- {
- e.Handled = false;
- base.OnPointerReleased(e);
- }
}
}
diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs
index 1b36f92fd3..cb2a98e5ca 100644
--- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs
@@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
CalendarDayButtonMouseUp?.Invoke(this, e);
}
-
- ///
- /// 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.
- ///
- /// Event arguments.
- internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
- {
- e.Handled = false;
- base.OnPointerReleased(e);
- }
}
}
diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs
index fb6dacaf81..8232697c18 100644
--- a/src/Avalonia.Controls/Calendar/CalendarItem.cs
+++ b/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;
}
}
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index d8473dc613..5f01c233b8 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/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));
}
///
@@ -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));
}
///
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 73854b9f60..5f63a44717 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/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();
}
diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
index 0918da1a90..76f17332bf 100644
--- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs
+++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
@@ -43,7 +43,7 @@ namespace Avalonia.Platform
_lastPosition = default(Point);
_allowedEffects = allowedEffects;
- using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents))
+ using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents))
{
using (_inputManager.PreProcess.OfType().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)
{
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 30330ef9ac..e7d8018a42 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/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 _activeLogicalGestureScrolls;
///
/// Initializes static members of the 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();
+ _activeLogicalGestureScrolls[e.Id] = delta;
+ }
+
+ Offset = new Vector(x, y);
+ e.Handled = true;
+ }
+ }
+
+ private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e)
+ => _activeLogicalGestureScrolls?.Remove(e.Id);
+
///
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index f349bcf059..e02d46c1df 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/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();
}
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 152b551f9a..7b91d6235d 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/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);
///
/// Defines the property.
diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
index 6293cbfbfd..1ef03b49ce 100644
--- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
+++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
@@ -39,21 +39,21 @@ namespace Avalonia.Controls.Remote.Server
KeyboardDevice = AvaloniaLocator.Current.GetService();
}
- 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,
diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs
index d6537ebbca..fc2c118132 100644
--- a/src/Avalonia.Controls/TabControl.cs
+++ b/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);
+ }
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 04b088e35c..d43957313e 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/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;
}
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index 597734d400..4df1b39400 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/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);
-
///
- 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);
}
///
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 GetControlsBetween(int first, int last)
- {
- return Children.Skip(first).Take(last - first);
+ return finalSize;
}
- private void ArrangeLine(double v, double lineV, IEnumerable 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;
+ }
}
}
- ///
- /// 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)
- ///
- [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; }
}
}
}
diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs
index d3618f30f3..8139af1659 100644
--- a/src/Avalonia.Input/Cursors.cs
+++ b/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
diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
new file mode 100644
index 0000000000..91b224e65a
--- /dev/null
+++ b/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, IGestureRecognizerActionsDispatcher
+ {
+ private readonly IInputElement _inputElement;
+ private List _recognizers;
+ private Dictionary _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();
+ _pointerGrabs = new Dictionary();
+ }
+
+ _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 s_Empty = new List();
+
+ public IEnumerator 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;
+ }
+
+ }
+}
diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
new file mode 100644
index 0000000000..b8ba9e529c
--- /dev/null
+++ b/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
+ }
+}
diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
new file mode 100644
index 0000000000..4f3c7c0bba
--- /dev/null
+++ b/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;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CanHorizontallyScrollProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CanHorizontallyScroll),
+ o => o.CanHorizontallyScroll,
+ (o, v) => o.CanHorizontallyScroll = v);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CanVerticallyScrollProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CanVerticallyScroll),
+ o => o.CanVerticallyScroll,
+ (o, v) => o.CanVerticallyScroll = v);
+
+ ///
+ /// Gets or sets a value indicating whether the content can be scrolled horizontally.
+ ///
+ public bool CanHorizontallyScroll
+ {
+ get => _canHorizontallyScroll;
+ set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the content can be scrolled horizontally.
+ ///
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs
index 8a514ca685..65195394ab 100644
--- a/src/Avalonia.Input/Gestures.cs
+++ b/src/Avalonia.Input/Gestures.cs
@@ -18,6 +18,14 @@ namespace Avalonia.Input
RoutingStrategies.Bubble,
typeof(Gestures));
+ public static readonly RoutedEvent ScrollGestureEvent =
+ RoutedEvent.Register(
+ "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
+
+ public static readonly RoutedEvent ScrollGestureEndedEvent =
+ RoutedEvent.Register(
+ "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));
+ }
}
}
}
diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs
index a1d1bb3eb8..7e6bf657ae 100644
--- a/src/Avalonia.Input/IMouseDevice.cs
+++ b/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
{
///
@@ -11,6 +13,9 @@ namespace Avalonia.Input
///
/// Gets the mouse position, in screen coordinates.
///
+ [Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
+
+ void SceneInvalidated(IInputRoot root, Rect rect);
}
}
diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs
new file mode 100644
index 0000000000..a3f051ce7f
--- /dev/null
+++ b/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
+ }
+}
diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs
index 932cbc989f..de775d90f2 100644
--- a/src/Avalonia.Input/IPointerDevice.cs
+++ b/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);
}
}
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index 07e04486ec..7c687f0d7e 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/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(
"PointerReleased",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the routed event.
+ ///
+ public static readonly RoutedEvent PointerCaptureLostEvent =
+ RoutedEvent.Register(
+ "PointerCaptureLost",
+ RoutingStrategies.Direct);
///
/// Defines the event.
@@ -148,6 +157,7 @@ namespace Avalonia.Input
private bool _isFocused;
private bool _isPointerOver;
+ private GestureRecognizerCollection _gestureRecognizers;
///
/// Initializes static members of the class.
@@ -166,6 +176,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved);
PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed);
PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased);
+ PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged);
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
@@ -263,6 +274,16 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); }
}
+ ///
+ /// 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
+ ///
+ public event EventHandler PointerCaptureLost
+ {
+ add => AddHandler(PointerCaptureLostEvent, value);
+ remove => RemoveHandler(PointerCaptureLostEvent, value);
+ }
+
///
/// Occurs when the mouse wheen is scrolled over the control.
///
@@ -370,6 +391,9 @@ namespace Avalonia.Input
public List KeyBindings { get; } = new List();
+ public GestureRecognizerCollection GestureRecognizers
+ => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
+
///
/// Focuses the control.
///
@@ -460,6 +484,8 @@ namespace Avalonia.Input
/// The event args.
protected virtual void OnPointerMoved(PointerEventArgs e)
{
+ if (_gestureRecognizers?.HandlePointerMoved(e) == true)
+ e.Handled = true;
}
///
@@ -468,6 +494,8 @@ namespace Avalonia.Input
/// The event args.
protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{
+ if (_gestureRecognizers?.HandlePointerPressed(e) == true)
+ e.Handled = true;
}
///
@@ -476,6 +504,17 @@ namespace Avalonia.Input
/// The event args.
protected virtual void OnPointerReleased(PointerReleasedEventArgs e)
{
+ if (_gestureRecognizers?.HandlePointerReleased(e) == true)
+ e.Handled = true;
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The event args.
+ protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ _gestureRecognizers?.HandlePointerCaptureLost(e);
}
///
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index c195209305..4c4d679087 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/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);
+
///
/// Gets the control that is currently capturing by the mouse, if any.
///
@@ -30,27 +30,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. To set the mouse capture, call the
/// method.
///
- 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(
- x => value.DetachedFromVisualTree += x,
- x => value.DetachedFromVisualTree -= x)
- .Take(1)
- .Subscribe(_ => Captured = null);
- }
-
- _captured = value;
- }
- }
-
///
/// Gets the mouse position, in screen coordinates.
///
@@ -71,8 +53,7 @@ namespace Avalonia.Input
///
public virtual void Capture(IInputElement control)
{
- // TODO: Check visibility and enabled state before setting capture.
- Captured = control;
+ _pointer.Capture(control);
}
///
@@ -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(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(device != null);
Contract.Requires(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(device != null);
Contract.Requires(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();
@@ -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(device != null);
Contract.Requires(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(device != null);
Contract.Requires(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(device != null);
Contract.Requires(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(hit != null);
- return Captured ??
+ return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
}
@@ -303,21 +296,22 @@ namespace Avalonia.Input
{
Contract.Requires(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(device != null);
Contract.Requires(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(device != null);
Contract.Requires(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(device != null);
Contract.Requires(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);
diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs
new file mode 100644
index 0000000000..890ad57024
--- /dev/null
+++ b/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(control1.GetSelfAndVisualAncestors().OfType());
+ return control2.GetSelfAndVisualAncestors().OfType().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())
+ {
+ 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().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);
+ }
+}
diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs
index 7b2497c460..c827822192 100644
--- a/src/Avalonia.Input/PointerEventArgs.cs
+++ b/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; }
}
}
diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs
new file mode 100644
index 0000000000..7117b5709c
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs
index 401d1d847c..de1badfe96 100644
--- a/src/Avalonia.Input/PointerWheelEventArgs.cs
+++ b/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;
+ }
}
}
diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs
index 7025965f83..3a8d358931 100644
--- a/src/Avalonia.Input/Properties/AssemblyInfo.cs
+++ b/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")]
diff --git a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs
index d2e5faab6c..186ad99efc 100644
--- a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs
+++ b/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;
}
diff --git a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
similarity index 84%
rename from src/Avalonia.Input/Raw/RawMouseEventArgs.cs
rename to src/Avalonia.Input/Raw/RawPointerEventArgs.cs
index c5637d66cc..b728844e97 100644
--- a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs
+++ b/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
}
///
/// A raw mouse event.
///
- public class RawMouseEventArgs : RawInputEventArgs
+ public class RawPointerEventArgs : RawInputEventArgs
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The associated device.
/// The event timestamp.
@@ -33,11 +36,11 @@ namespace Avalonia.Input.Raw
/// The type of the event.
/// The mouse position, in client DIPs.
/// The input modifiers.
- 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
///
/// Gets the type of the event.
///
- public RawMouseEventType Type { get; private set; }
+ public RawPointerEventType Type { get; private set; }
///
/// Gets the input modifiers.
diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs
new file mode 100644
index 0000000000..5299633b26
--- /dev/null
+++ b/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; }
+ }
+}
diff --git a/src/Avalonia.Input/ScrollGestureEventArgs.cs b/src/Avalonia.Input/ScrollGestureEventArgs.cs
new file mode 100644
index 0000000000..a682e8f0a4
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs
new file mode 100644
index 0000000000..7f473bb320
--- /dev/null
+++ b/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
+{
+ ///
+ /// Handles raw touch events
+ ///
+ /// This class is supposed to be used on per-toplevel basis, don't use a shared one
+ ///
+ ///
+ public class TouchDevice : IInputDevice
+ {
+ Dictionary _pointers = new Dictionary();
+
+ 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));
+ }
+ }
+
+ }
+}
diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs
index 73b8834375..638879ba14 100644
--- a/src/Avalonia.Native/WindowImplBase.cs
+++ b/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;
}
}
diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs
new file mode 100644
index 0000000000..4b9c04f4e6
--- /dev/null
+++ b/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 AllowedPlatformApis = new List
+ {
+ PlatformApi.DirectX9
+ };
+ }
+}
diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs
index b14932acfe..b2b5a1a646 100644
--- a/src/Avalonia.OpenGL/EglDisplay.cs
+++ b/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()?.AllowedPlatformApis
+ ?? new List {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;
diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs
index f5dd413b0f..d2e4543af3 100644
--- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs
+++ b/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();
}
diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs
index 00fcd97af0..0a99778ddf 100644
--- a/src/Avalonia.OpenGL/EglInterface.cs
+++ b/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 Load()
{
var os = AvaloniaLocator.Current.GetService().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
}
}
diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs
index 53da93315c..d198d46e5c 100644
--- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs
+++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs
@@ -6,4 +6,9 @@ namespace Avalonia.OpenGL
{
IGlPlatformSurfaceRenderingSession BeginDraw();
}
-}
\ No newline at end of file
+
+ public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget
+ {
+ bool IsCorrupted { get; }
+ }
+}
diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
index f67cb7f40a..ced26a3004 100644
--- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
+++ b/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));
});
}
}
diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
new file mode 100644
index 0000000000..3f41f54363
--- /dev/null
+++ b/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
+{
+ ///
+ /// 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.
+ ///
+ public class AutoDataTemplateBindingHook : IPropertyBindingHook
+ {
+ private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate
public class CrossFade : IPageTransition
{
- private Animation _fadeOutAnimation;
- private Animation _fadeInAnimation;
+ private readonly Animation _fadeOutAnimation;
+ private readonly Animation _fadeInAnimation;
///
/// Initializes a new instance of the 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)
}
}
diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs
index 522de64ec7..516bea782e 100644
--- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs
+++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs
@@ -23,4 +23,9 @@ namespace Avalonia.Platform
///
IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer);
}
+
+ public interface IRenderTargetWithCorruptionInfo : IRenderTarget
+ {
+ bool IsCorrupted { get; }
+ }
}
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index c83a8436b4..0d077d2a3a 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/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);
diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
index 75d8f036d6..2d4a39e026 100644
--- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
+++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
@@ -7,11 +7,25 @@ namespace Avalonia.Rendering
public class ManagedDeferredRendererLock : IDeferredRendererLock
{
private readonly object _lock = new object();
+
+ ///
+ /// Tries to lock the target surface or window
+ ///
+ /// IDisposable if succeeded to obtain the lock
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock));
return null;
}
+
+ ///
+ /// Enters a waiting lock, only use from platform code, not from the renderer
+ ///
+ public IDisposable Lock()
+ {
+ Monitor.Enter(_lock);
+ return Disposable.Create(() => Monitor.Exit(_lock));
+ }
}
}
diff --git a/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs
new file mode 100644
index 0000000000..dd6cf7ad15
--- /dev/null
+++ b/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
+{
+ ///
+ /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms
+ ///
+
+ public class UiThreadRenderTimer : DefaultRenderTimer
+ {
+ public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond)
+ {
+ }
+
+ protected override IDisposable StartCore(Action 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);
+ }
+ }
+}
diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs
index 40b01117e3..0a8b1ee9c4 100644
--- a/src/Avalonia.X11/X11CursorFactory.cs
+++ b/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 _cursors;
@@ -42,16 +44,34 @@ namespace Avalonia.X11
public X11CursorFactory(IntPtr display)
{
_display = display;
+ _nullCursor = GetNullCursor(display);
_cursors = Enum.GetValues(typeof(CursorFontShape)).Cast()
.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);
+ }
}
}
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index ce03113169..cf5902eff7 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/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
{
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index c5e77fe352..01beebfff1 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/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;
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index ee73ccc907..0a78c0dfd9 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -11,6 +11,7 @@ namespace Avalonia.X11
unsafe class XI2Manager
{
private X11Info _x11;
+ private bool _multitouch;
private Dictionary _clients = new Dictionary();
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>
+ var events = new List
{
- [_pointerDevice.Id] = new List()
+ 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> {[_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 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);
}
}
diff --git a/src/Avalonia.X11/XIStructs.cs b/src/Avalonia.X11/XIStructs.cs
index ef49a72c43..4675ef47f2 100644
--- a/src/Avalonia.X11/XIStructs.cs
+++ b/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
{
diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs
index 8a146f922d..3c41f7bdde 100644
--- a/src/Avalonia.X11/XLib.cs
+++ b/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);
diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs
index a28b1cbb1a..95fa3ba9e3 100644
--- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs
+++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs
@@ -12,6 +12,7 @@ namespace Avalonia.Gtk3
private static readonly Dictionary CursorTypeMapping = new Dictionary
{
+ {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;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs
index 4fad8208b3..aa0f8cde0d 100644
--- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs
+++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs
@@ -2,6 +2,7 @@
{
enum GdkCursorType
{
+ Blank = -2,
CursorIsPixmap = -1,
XCursor = 0,
Arrow = 2,
diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
index c4697462d2..bff50a979d 100644
--- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
+++ b/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;
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
index 4fa57dbf00..b982b98d38 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
+++ b/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)));
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
index 1e3ffc3154..a73c523483 160000
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
@@ -1 +1 @@
-Subproject commit 1e3ffc315401f0b2eb96a0e79b25c2fc19a80d78
+Subproject commit a73c5234831267b23160e01a9fbc83be633f69fc
diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs
index 7c0c42ca37..a7c1d0a38b 100644
--- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs
+++ b/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();
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 362fd028cf..15f38b1c4f 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/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);
diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
index 7005459487..c89d0a15cf 100644
--- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
+++ b/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,
diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs
index e582b5fb82..f1fd74f931 100644
--- a/src/Windows/Avalonia.Win32/CursorFactory.cs
+++ b/src/Windows/Avalonia.Win32/CursorFactory.cs
@@ -41,6 +41,7 @@ namespace Avalonia.Win32
private static readonly Dictionary CursorTypeMapping = new Dictionary
{
+ {StandardCursorType.None, 0},
{StandardCursorType.AppStarting, 32650},
{StandardCursorType.Arrow, 32512},
{StandardCursorType.Cross, 32515},
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index 7d6e8fc8ce..bc7fc1c9fa 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/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
+ {
+ ///
+ /// Movement has occurred. Cannot be combined with TOUCHEVENTF_DOWN.
+ ///
+ TOUCHEVENTF_MOVE = 0x0001,
+
+ ///
+ /// The corresponding touch point was established through a new contact. Cannot be combined with TOUCHEVENTF_MOVE or TOUCHEVENTF_UP.
+ ///
+ TOUCHEVENTF_DOWN = 0x0002,
+
+ ///
+ /// A touch point was removed.
+ ///
+ TOUCHEVENTF_UP = 0x0004,
+
+ ///
+ /// 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.
+ ///
+ TOUCHEVENTF_INRANGE = 0x0008,
+
+ ///
+ /// Indicates that this TOUCHINPUT structure corresponds to a primary contact point. See the following text for more information on primary touch points.
+ ///
+ TOUCHEVENTF_PRIMARY = 0x0010,
+
+ ///
+ /// When received using GetTouchInputInfo, this input was not coalesced.
+ ///
+ TOUCHEVENTF_NOCOALESCE = 0x0020,
+
+ ///
+ /// The touch event came from the user's palm.
+ ///
+ TOUCHEVENTF_PALM = 0x0080
+ }
+
[Flags]
public enum OpenFileNameFlags
{
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index f679c2410e..c45bf6389e 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/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().ToSingleton()
.Bind().ToConstant(CursorFactory.Instance)
@@ -88,7 +91,7 @@ namespace Avalonia.Win32
.Bind().ToConstant(s_instance);
if (options.AllowEglInitialization)
Win32GlManager.Initialize();
- UseDeferredRendering = options.UseDeferredRendering;
+
_uiThread = Thread.CurrentThread;
if (OleContext.Current != null)
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 081a713e95..9e8b2d58a6 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/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()))
+ {
+ 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;
diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
index 683c256b7b..e57fcc643f 100644
--- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
+++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs
index 83d10b8b44..15e8b35056 100644
--- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs
+++ b/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
diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
index 9a751d4953..20b2ac5d9d 100644
--- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
+++ b/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();
var renderer = Mock.Of();
- IInputElement captured = null;
- Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50));
- Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(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(), It.IsAny(), It.IsAny>()))
.Returns>((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();
var renderer = Mock.Of();
- IInputElement captured = null;
- Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50));
- Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v);
- Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
.Returns>((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();
var renderer = Mock.Of();
- IInputElement captured = null;
- Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50));
- Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(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(), It.IsAny(), It.IsAny>()))
.Returns>((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
diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index fdbca70350..70ec6c1408 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/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);
}
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index 834c49ba6b..067c66969f 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
@@ -14,6 +14,7 @@ namespace Avalonia.Controls.UnitTests
public class ContextMenuTests
{
private Mock 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);
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index 343d8d41f3..238e214a5d 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/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]
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
index 70d59e82c8..de34558ad1 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
+++ b/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();
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
new file mode 100644
index 0000000000..373bbaed75
--- /dev/null
+++ b/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));
+ }
+
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
index df1846c617..ba4d6ca9c5 100644
--- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
+++ b/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();
var item = Mock.Of(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();
var item = Mock.Of(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();
var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(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();
var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(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(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(x => x.Parent == parentItem);
var sibling = Mock.Of(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();
var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(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(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(x => x.Parent == parentItem);
var sibling = Mock.Of(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();
var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(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();
var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(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(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true);
var childItem = Mock.Of(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();
var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
var item = Mock.Of(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;
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
index 037c16e231..8e421bf0a2 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
+++ b/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(control =>
@@ -792,6 +819,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public IList- Items { get; set; }
public Item SelectedItem { get; set; }
+ public int SelectedIndex { get; set; }
}
private class RootWithItems : TestRoot
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index 0d87f6d0fe..9b62509138 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/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());
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index 15081b184c..b66d6ed11c 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/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());
- TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
+ ClickContainer(item1Container, InputModifiers.Control);
Assert.False(item1Container.IsSelected);
Assert.DoesNotContain(item1, target.SelectedItems.OfType());
@@ -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();
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();
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 _children;
diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
index af034f541b..983f541c2a 100644
--- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
+++ b/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));
}
diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
index 6b19d81034..7316b1de3d 100644
--- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
+++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
@@ -4,6 +4,7 @@
netcoreapp2.0;net47
Library
true
+ latest
@@ -19,6 +20,7 @@
+
diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs
index 931c1b5268..69bdf58f9d 100644
--- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs
+++ b/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();
+
+ 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);
+ }
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
index 795aba153a..3511919e39 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
@@ -137,8 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests
.GetVisualChildren().First()
.GetVisualChildren().First()
.GetVisualChildren().First();
- ((Control)item).RaiseEvent(new PointerPressedEventArgs {ClickCount = 20});
- Assert.Equal(20, w.Args.ClickCount);
+
+ ((Control)item).DataContext = "test";
+ Assert.Equal("test", w.SavedContext);
}
}
@@ -161,10 +162,10 @@ namespace Avalonia.Markup.Xaml.UnitTests
public class XamlIlBugTestsEventHandlerCodeBehind : Window
{
- public PointerPressedEventArgs Args;
- public void HandlePointerPressed(object sender, PointerPressedEventArgs args)
+ public object SavedContext;
+ public void HandleDataContextChanged(object sender, EventArgs args)
{
- Args = args;
+ SavedContext = ((Control)sender).DataContext;
}
public XamlIlBugTestsEventHandlerCodeBehind()
@@ -178,7 +179,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
-
+
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
new file mode 100644
index 0000000000..a79647d355
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
@@ -0,0 +1,9 @@
+// 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 Xunit;
+
+// Required to avoid InvalidOperationException sometimes thrown
+// from Splat.MemoizingMRUCache.cs which is not thread-safe.
+// Thrown when trying to access WhenActivated concurrently.
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs
new file mode 100644
index 0000000000..667462eb91
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs
@@ -0,0 +1,116 @@
+// 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 Xunit;
+using ReactiveUI;
+using Avalonia.ReactiveUI;
+using Avalonia.UnitTests;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Avalonia.VisualTree;
+using Avalonia.Controls.Presenters;
+using Splat;
+using System.Threading.Tasks;
+using System;
+
+namespace Avalonia.ReactiveUI.UnitTests
+{
+ public class AutoDataTemplateBindingHookTest
+ {
+ public class NestedViewModel : ReactiveObject { }
+
+ public class NestedView : ReactiveUserControl { }
+
+ public class ExampleViewModel : ReactiveObject
+ {
+ public ObservableCollection Items { get; } = new ObservableCollection();
+ }
+
+ public class ExampleView : ReactiveUserControl
+ {
+ public ItemsControl List { get; } = new ItemsControl();
+
+ public ExampleView()
+ {
+ Content = List;
+ ViewModel = new ExampleViewModel();
+ this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items);
+ }
+ }
+
+ public AutoDataTemplateBindingHookTest()
+ {
+ Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
+ Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor));
+ }
+
+ [Fact]
+ public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set()
+ {
+ var view = new ExampleView();
+ Assert.NotNull(view.List.ItemTemplate);
+ }
+
+ [Fact]
+ public void Should_Use_View_Model_View_Host_As_Data_Template()
+ {
+ var view = new ExampleView();
+ view.ViewModel.Items.Add(new NestedViewModel());
+
+ view.List.Template = GetTemplate();
+ view.List.ApplyTemplate();
+ view.List.Presenter.ApplyTemplate();
+
+ var child = view.List.Presenter.Panel.Children[0];
+ var container = (ContentPresenter) child;
+ container.UpdateChild();
+
+ Assert.IsType(container.Child);
+ }
+
+ [Fact]
+ public void Should_Resolve_And_Embedd_Appropriate_View_Model()
+ {
+ var view = new ExampleView();
+ var root = new TestRoot { Child = view };
+ view.ViewModel.Items.Add(new NestedViewModel());
+
+ view.List.Template = GetTemplate();
+ view.List.ApplyTemplate();
+ view.List.Presenter.ApplyTemplate();
+
+ var child = view.List.Presenter.Panel.Children[0];
+ var container = (ContentPresenter) child;
+ container.UpdateChild();
+
+ var host = (ViewModelViewHost) container.Child;
+ Assert.IsType(host.ViewModel);
+ Assert.IsType(host.DataContext);
+
+ host.DataContext = "changed context";
+ Assert.IsType(host.ViewModel);
+ Assert.IsType(host.DataContext);
+ }
+
+ private FuncControlTemplate GetTemplate()
+ {
+ return new FuncControlTemplate(parent =>
+ {
+ return new Border
+ {
+ Background = new Media.SolidColorBrush(0xffffffff),
+ Child = new ItemsPresenter
+ {
+ Name = "PART_ItemsPresenter",
+ MemberSelector = parent.MemberSelector,
+ [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
+ }
+ };
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
index d9f1ce47dd..2c81e8fea3 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
+++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
@@ -1,3 +1,6 @@
+// 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.Concurrency;
using System.Reactive.Disposables;
@@ -13,7 +16,7 @@ using Splat;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-namespace Avalonia
+namespace Avalonia.ReactiveUI.UnitTests
{
public class AvaloniaActivationForViewFetcherTest
{
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
new file mode 100644
index 0000000000..328b749ba1
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
@@ -0,0 +1,34 @@
+// 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.Controls;
+using Avalonia.UnitTests;
+using ReactiveUI;
+using Splat;
+using Xunit;
+
+namespace Avalonia.ReactiveUI.UnitTests
+{
+ public class ReactiveUserControlTest
+ {
+ public class ExampleViewModel : ReactiveObject { }
+
+ public class ExampleView : ReactiveUserControl { }
+
+ [Fact]
+ public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model()
+ {
+ var view = new ExampleView();
+ var viewModel = new ExampleViewModel();
+ Assert.Null(view.ViewModel);
+
+ view.DataContext = viewModel;
+ Assert.Equal(view.ViewModel, viewModel);
+ Assert.Equal(view.DataContext, viewModel);
+
+ view.DataContext = null;
+ Assert.Null(view.ViewModel);
+ Assert.Null(view.DataContext);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs
new file mode 100644
index 0000000000..ff77de8d28
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs
@@ -0,0 +1,37 @@
+// 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.Controls;
+using Avalonia.UnitTests;
+using ReactiveUI;
+using Splat;
+using Xunit;
+
+namespace Avalonia.ReactiveUI.UnitTests
+{
+ public class ReactiveWindowTest
+ {
+ public class ExampleViewModel : ReactiveObject { }
+
+ public class ExampleWindow : ReactiveWindow { }
+
+ [Fact]
+ public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var view = new ExampleWindow();
+ var viewModel = new ExampleViewModel();
+ Assert.Null(view.ViewModel);
+
+ view.DataContext = viewModel;
+ Assert.Equal(view.ViewModel, viewModel);
+ Assert.Equal(view.DataContext, viewModel);
+
+ view.DataContext = null;
+ Assert.Null(view.ViewModel);
+ Assert.Null(view.DataContext);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
index 401d169896..8c5b5083e8 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
+++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
@@ -1,3 +1,6 @@
+// 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.Concurrency;
using System.Reactive.Disposables;
@@ -16,7 +19,7 @@ using System.Threading.Tasks;
using System.Reactive;
using Avalonia.ReactiveUI;
-namespace Avalonia
+namespace Avalonia.ReactiveUI.UnitTests
{
public class RoutedViewHostTest
{
@@ -59,8 +62,7 @@ namespace Avalonia
{
Router = screen.Router,
DefaultContent = defaultContent,
- FadeOutAnimation = null,
- FadeInAnimation = null
+ PageTransition = null
};
var root = new TestRoot
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs
new file mode 100644
index 0000000000..f09eea5ec5
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs
@@ -0,0 +1,63 @@
+// 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;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using ReactiveUI;
+using Splat;
+using Xunit;
+
+namespace Avalonia.ReactiveUI.UnitTests
+{
+ public class TransitioningContentControlTest
+ {
+ [Fact]
+ public void Transitioning_Control_Should_Derive_Template_From_Content_Control()
+ {
+ var target = new TransitioningContentControl();
+ var stylable = (IStyledElement)target;
+ Assert.Equal(typeof(ContentControl),stylable.StyleKey);
+ }
+
+ [Fact]
+ public void Transitioning_Control_Template_Should_Be_Instantiated()
+ {
+ var target = new TransitioningContentControl
+ {
+ PageTransition = null,
+ Template = GetTemplate(),
+ Content = "Foo"
+ };
+ target.ApplyTemplate();
+ ((ContentPresenter)target.Presenter).UpdateChild();
+
+ var child = ((IVisual)target).VisualChildren.Single();
+ Assert.IsType(child);
+ child = child.VisualChildren.Single();
+ Assert.IsType(child);
+ child = child.VisualChildren.Single();
+ Assert.IsType(child);
+ }
+
+ private FuncControlTemplate GetTemplate()
+ {
+ return new FuncControlTemplate(parent =>
+ {
+ return new Border
+ {
+ Background = new Media.SolidColorBrush(0xffffffff),
+ Child = new ContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty],
+ [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty],
+ }
+ };
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs
new file mode 100644
index 0000000000..8bed5adcd4
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs
@@ -0,0 +1,74 @@
+// 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.Controls;
+using Avalonia.UnitTests;
+using ReactiveUI;
+using Splat;
+using Xunit;
+
+namespace Avalonia.ReactiveUI.UnitTests
+{
+ public class ViewModelViewHostTest
+ {
+ public class FirstViewModel { }
+
+ public class FirstView : ReactiveUserControl { }
+
+ public class SecondViewModel : ReactiveObject { }
+
+ public class SecondView : ReactiveUserControl { }
+
+ public ViewModelViewHostTest()
+ {
+ Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor));
+ Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor));
+ }
+
+ [Fact]
+ public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel()
+ {
+ var defaultContent = new TextBlock();
+ var host = new ViewModelViewHost
+ {
+ DefaultContent = defaultContent,
+ PageTransition = null
+ };
+
+ var root = new TestRoot
+ {
+ Child = host
+ };
+
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(TextBlock), host.Content.GetType());
+ Assert.Equal(defaultContent, host.Content);
+
+ var first = new FirstViewModel();
+ host.ViewModel = first;
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(FirstView), host.Content.GetType());
+ Assert.Equal(first, ((FirstView)host.Content).DataContext);
+ Assert.Equal(first, ((FirstView)host.Content).ViewModel);
+
+ var second = new SecondViewModel();
+ host.ViewModel = second;
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(SecondView), host.Content.GetType());
+ Assert.Equal(second, ((SecondView)host.Content).DataContext);
+ Assert.Equal(second, ((SecondView)host.Content).ViewModel);
+
+ host.ViewModel = null;
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(TextBlock), host.Content.GetType());
+ Assert.Equal(defaultContent, host.Content);
+
+ host.ViewModel = first;
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(FirstView), host.Content.GetType());
+ Assert.Equal(first, ((FirstView)host.Content).DataContext);
+ Assert.Equal(first, ((FirstView)host.Content).ViewModel);
+ }
+ }
+}
\ No newline at end of file