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(x => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + var itemsControl = lastViewProperty?.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate; + return true; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..05edeea683 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, value); } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs new file mode 100644 index 0000000000..1bec5fc365 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningContentControl : ContentControl, IStyleable + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(PageTransition), + new CrossFade(TimeSpan.FromSeconds(0.5))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears and disappears. + /// + public IPageTransition PageTransition + { + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the content with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// TransitioningContentControl uses the default ContentControl + /// template from Avalonia default theme. + /// + Type IStyleable.StyleKey => typeof(ContentControl); + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (PageTransition != null) + await PageTransition.Start(this, null, true); + base.Content = content; + if (PageTransition != null) + await PageTransition.Start(null, this, true); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..5cfa464c37 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index f84e09510b..8f7d56dbc6 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1,1,1,1 0.5 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 18c32b02bc..666596d710 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1 0.5 diff --git a/src/Avalonia.Themes.Default/NotificationCard.xaml b/src/Avalonia.Themes.Default/NotificationCard.xaml index e94cb33d1e..47d5988e8c 100644 --- a/src/Avalonia.Themes.Default/NotificationCard.xaml +++ b/src/Avalonia.Themes.Default/NotificationCard.xaml @@ -13,7 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="8,8,0,0"> - + @@ -40,6 +40,10 @@ + + \ No newline at end of file + diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 6b8cb8b755..614d828259 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -14,8 +14,8 @@ namespace Avalonia.Animation /// 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 -