From 90b405952e13080f842d03c4597b13e374b5b630 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 25 Jun 2020 21:10:02 +0300 Subject: [PATCH 001/149] Extracted ANGLE-based EGL display to a separate class and added some DX-interop features --- .../Angle/AngleEglInterface.cs | 31 +++++++ .../Angle/AngleWin32EglDisplay.cs | 88 +++++++++++++++++++ src/Avalonia.OpenGL/EglConsts.cs | 10 +++ src/Avalonia.OpenGL/EglDisplay.cs | 76 ++++++++-------- src/Avalonia.OpenGL/EglGlPlatformFeature.cs | 7 +- src/Avalonia.OpenGL/EglInterface.cs | 29 ++++-- src/Windows/Avalonia.Win32/Win32GlManager.cs | 3 +- 7 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 src/Avalonia.OpenGL/Angle/AngleEglInterface.cs create mode 100644 src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs new file mode 100644 index 0000000000..375b93c27c --- /dev/null +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Platform.Interop; + +namespace Avalonia.OpenGL.Angle +{ + public class AngleEglInterface : EglInterface + { + [DllImport("libegl.dll", CharSet = CharSet.Ansi)] + static extern IntPtr eglGetProcAddress(string proc); + + public AngleEglInterface() : base(LoadAngle()) + { + + } + + static Func LoadAngle() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException(); + { + var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (disp == IntPtr.Zero) + throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); + return eglGetProcAddress; + } + } + + } +} diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs new file mode 100644 index 0000000000..530411bfea --- /dev/null +++ b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using static Avalonia.OpenGL.EglConsts; + +namespace Avalonia.OpenGL.Angle +{ + public class AngleWin32EglDisplay : EglDisplay + { + struct AngleInfo + { + public IntPtr Display { get; set; } + public AngleOptions.PlatformApi PlatformApi { get; set; } + } + + static AngleInfo CreateAngleDisplay(EglInterface _egl) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException(); + var display = IntPtr.Zero; + AngleOptions.PlatformApi angleApi = default; + { + 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) + { + angleApi = platformApi; + break; + } + } + + if (display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); + return new AngleInfo { Display = display, PlatformApi = angleApi }; + } + } + + private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, info.Display) + { + PlatformApi = info.PlatformApi; + } + + public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl)) + { + + } + + public AngleWin32EglDisplay() : this(new AngleEglInterface()) + { + + } + + public AngleOptions.PlatformApi PlatformApi { get; } + + public IntPtr GetDirect3DDevice() + { + if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) + throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); + if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) + throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); + return d3dDeviceHandle; + } + + public EglSurface WrapDirect3D11Texture(IntPtr handle) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + } + } +} diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/EglConsts.cs index 62fb3faef6..d09537eb86 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/EglConsts.cs @@ -192,5 +192,15 @@ namespace Avalonia.OpenGL public const int EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE = 0x320F; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; + + //EXT_device_query + public const int EGL_DEVICE_EXT = 0x322C; + + //ANGLE_device_d3d + public const int EGL_D3D9_DEVICE_ANGLE = 0x33A0; + public const int EGL_D3D11_DEVICE_ANGLE = 0x33A1; + + public const int EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE = 0x3200; + public const int EGL_D3D_TEXTURE_ANGLE = 0x33A3; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 0436f6ac52..7b194e4346 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -15,7 +15,6 @@ namespace Avalonia.OpenGL private readonly int _surfaceType; public IntPtr Handle => _display; - private AngleOptions.PlatformApi? _angleApi; private int _sampleCount; private int _stencilSize; private GlVersion _version; @@ -24,56 +23,41 @@ namespace Avalonia.OpenGL { } - public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) - { - _egl = egl; + static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + { + var display = IntPtr.Zero; if (platformType == -1 && platformDisplay == IntPtr.Zero) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - 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) - { - _angleApi = platformApi; - break; - } - } - - if (_display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); - } - - if (_display == IntPtr.Zero) - _display = _egl.GetDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) + display = egl.GetDisplay(IntPtr.Zero); } else { - if (_egl.GetPlatformDisplayEXT == null) + if (egl.GetPlatformDisplayEXT == null) throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); + display = egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); } + + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetDisplay", egl); + return display; + } - if (_display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); + public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + : this(egl, CreateDisplay(egl, platformType, platformDisplay, attrs)) + { + + } + public EglDisplay(EglInterface egl, IntPtr display) + { + _egl = egl; + _display = display; + if(_display == IntPtr.Zero) + throw new ArgumentException(); + + if (!_egl.Initialize(_display, out var major, out var minor)) throw OpenGlException.GetFormattedException("eglInitialize", _egl); @@ -172,5 +156,15 @@ namespace Avalonia.OpenGL throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); return new EglSurface(this, _egl, s); } + + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) + { + var s = _egl.CreatePbufferFromClientBuffer(_display, bufferType, handle, + _config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", _egl); + return new EglSurface(this, _egl, s); + } } } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs index f59c6b7751..7e9383432c 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs @@ -20,12 +20,13 @@ namespace Avalonia.OpenGL if (feature != null) AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); } - - public static EglGlPlatformFeature TryCreate() + + public static EglGlPlatformFeature TryCreate() => TryCreate(() => new EglDisplay()); + public static EglGlPlatformFeature TryCreate(Func displayFactory) { try { - var disp = new EglDisplay(); + var disp = displayFactory(); return new EglGlPlatformFeature { _display = disp, diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index c0665a1ea1..666c0d8351 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -17,25 +17,21 @@ namespace Avalonia.OpenGL } + public EglInterface(Func getProcAddress) : base(getProcAddress) + { + + } + public EglInterface(string library) : base(Load(library)) { } - [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) - { - var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); - if (disp == IntPtr.Zero) - throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); - return eglGetProcAddress; - } throw new PlatformNotSupportedException(); } @@ -147,6 +143,21 @@ namespace Avalonia.OpenGL return null; return Marshal.PtrToStringAnsi(rv); } + + public delegate IntPtr EglCreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); + [GlEntryPoint("eglCreatePbufferFromClientBuffer")] + + public EglCreatePbufferFromClientBuffer CreatePbufferFromClientBuffer { get; } + + public delegate bool EglQueryDisplayAttribEXT(IntPtr display, int attr, out IntPtr res); + + [GlEntryPoint("eglQueryDisplayAttribEXT"), GlOptionalEntryPoint] + public EglQueryDisplayAttribEXT QueryDisplayAttribExt { get; } + + public delegate bool EglQueryDeviceAttribEXT(IntPtr display, int attr, out IntPtr res); + + [GlEntryPoint("eglQueryDeviceAttribEXT"), GlOptionalEntryPoint] + public EglQueryDisplayAttribEXT QueryDeviceAttribExt { get; } // ReSharper restore UnassignedGetOnlyAutoProperty } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 585e68056b..bd188ad53a 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,4 +1,5 @@ using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; namespace Avalonia.Win32 { @@ -15,7 +16,7 @@ namespace Avalonia.Win32 { if (!s_attemptedToInitialize) { - EglFeature = EglGlPlatformFeature.TryCreate(); + EglFeature = EglGlPlatformFeature.TryCreate(() => new AngleWin32EglDisplay()); s_attemptedToInitialize = true; } From a34fefc998d9dce6416b8d7de8f89f86869cb3a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 25 Jun 2020 21:32:29 +0300 Subject: [PATCH 002/149] Allow more customization for EglGlPlatformSurface --- src/Avalonia.OpenGL/EglGlPlatformSurface.cs | 84 ++------------ .../EglGlPlatformSurfaceBase.cs | 103 ++++++++++++++++++ 2 files changed, 112 insertions(+), 75 deletions(-) create mode 100644 src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index 3e4befe2c6..21fadff19e 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -3,33 +3,26 @@ using System.Threading; namespace Avalonia.OpenGL { - public class EglGlPlatformSurface : IGlPlatformSurface + public class EglGlPlatformSurface : EglGlPlatformSurfaceBase { - public interface IEglWindowGlPlatformSurfaceInfo - { - IntPtr Handle { get; } - PixelSize Size { get; } - double Scaling { get; } - } - private readonly EglDisplay _display; private readonly EglContext _context; private readonly IEglWindowGlPlatformSurfaceInfo _info; - public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) + public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base() { _display = context.Display; _context = context; _info = info; } - - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { var glSurface = _display.CreateWindowSurface(_info.Handle); return new RenderTarget(_display, _context, glSurface, _info); } - class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + class RenderTarget : EglPlatformSurfaceRenderTargetBase { private readonly EglDisplay _display; private readonly EglContext _context; @@ -38,7 +31,7 @@ namespace Avalonia.OpenGL private PixelSize _initialSize; public RenderTarget(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(display, context) { _display = display; _context = context; @@ -47,70 +40,11 @@ namespace Avalonia.OpenGL _initialSize = info.Size; } - public void Dispose() => _glSurface.Dispose(); + public override void Dispose() => _glSurface.Dispose(); - public bool IsCorrupted => _initialSize != _info.Size; - - public IGlPlatformSurfaceRenderingSession BeginDraw() - { - var l = _context.Lock(); - try - { - if (IsCorrupted) - throw new RenderTargetCorruptedException(); - var restoreContext = _context.MakeCurrent(_glSurface); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - - return new Session(_display, _context, _glSurface, _info, l, restoreContext); - } - catch - { - l.Dispose(); - throw; - } - } + public override bool IsCorrupted => _initialSize != _info.Size; - class Session : IGlPlatformSurfaceRenderingSession - { - private readonly EglContext _context; - private readonly EglSurface _glSurface; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - private readonly EglDisplay _display; - private IDisposable _lock; - private readonly IDisposable _restoreContext; - - - public Session(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable restoreContext) - { - _context = context; - _display = display; - _glSurface = glSurface; - _info = info; - _lock = @lock; - _restoreContext = restoreContext; - } - - public void Dispose() - { - _context.GlInterface.Flush(); - _display.EglInterface.WaitGL(); - _glSurface.SwapBuffers(); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - _restoreContext.Dispose(); - _lock.Dispose(); - } - - public IGlContext Context => _context; - public PixelSize Size => _info.Size; - public double Scaling => _info.Scaling; - public bool IsYFlipped { get; } - } + public override IGlPlatformSurfaceRenderingSession BeginDraw() => base.BeginDraw(_glSurface, _info); } } } diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs new file mode 100644 index 0000000000..00c7c4796c --- /dev/null +++ b/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs @@ -0,0 +1,103 @@ +using System; + +namespace Avalonia.OpenGL +{ + public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface + { + public interface IEglWindowGlPlatformSurfaceInfo + { + IntPtr Handle { get; } + PixelSize Size { get; } + double Scaling { get; } + } + + public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + } + + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + { + private readonly EglDisplay _display; + private readonly EglContext _context; + + protected EglPlatformSurfaceRenderTargetBase(EglDisplay display, EglContext context) + { + _display = display; + _context = context; + } + + public abstract bool IsCorrupted { get; } + + public virtual void Dispose() + { + + } + + public abstract IGlPlatformSurfaceRenderingSession BeginDraw(); + + protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, + EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) + { + var l = _context.Lock(); + try + { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); + var restoreContext = _context.MakeCurrent(surface); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + + return new Session(_display, _context, surface, info, l, restoreContext, onFinish, isYFlipped); + } + catch + { + l.Dispose(); + throw; + } + } + + class Session : IGlPlatformSurfaceRenderingSession + { + private readonly EglContext _context; + private readonly EglSurface _glSurface; + private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; + private readonly IDisposable _lock; + private readonly IDisposable _restoreContext; + private readonly Action _onFinish; + + + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, + IDisposable @lock, IDisposable restoreContext, Action onFinish, bool isYFlipped) + { + IsYFlipped = isYFlipped; + _context = context; + _display = display; + _glSurface = glSurface; + _info = info; + _lock = @lock; + _restoreContext = restoreContext; + _onFinish = onFinish; + } + + public void Dispose() + { + _context.GlInterface.Flush(); + _display.EglInterface.WaitGL(); + _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + _restoreContext.Dispose(); + _lock.Dispose(); + _onFinish?.Invoke(); + } + + public IGlContext Context => _context; + public PixelSize Size => _info.Size; + public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } + } + } +} From a42e8cbdede4d8d2509eaf5ac8a848ef62835351 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Jul 2020 22:19:33 +0200 Subject: [PATCH 003/149] Nullable enable Avalonia.Input. --- src/Avalonia.Input/AccessKeyHandler.cs | 22 +++--- src/Avalonia.Input/Avalonia.Input.csproj | 2 + src/Avalonia.Input/DataObject.cs | 6 +- src/Avalonia.Input/DragDropDevice.cs | 6 +- src/Avalonia.Input/FocusManager.cs | 47 ++++++------ .../GestureRecognizerCollection.cs | 16 ++-- .../ScrollGestureRecognizer.cs | 15 ++-- src/Avalonia.Input/Gestures.cs | 9 ++- src/Avalonia.Input/IAccessKeyHandler.cs | 2 +- src/Avalonia.Input/IDataObject.cs | 6 +- src/Avalonia.Input/IFocusManager.cs | 6 +- src/Avalonia.Input/IInputElement.cs | 2 +- src/Avalonia.Input/IInputRoot.cs | 4 +- src/Avalonia.Input/IKeyboardDevice.cs | 4 +- src/Avalonia.Input/IPointer.cs | 4 +- src/Avalonia.Input/IPointerDevice.cs | 4 +- src/Avalonia.Input/InputElement.cs | 8 +- src/Avalonia.Input/InputExtensions.cs | 2 +- src/Avalonia.Input/KeyEventArgs.cs | 2 +- src/Avalonia.Input/KeyboardDevice.cs | 8 +- .../KeyboardNavigationHandler.cs | 13 ++-- src/Avalonia.Input/MouseDevice.cs | 73 ++++++++++--------- .../Navigation/TabNavigation.cs | 42 ++++++----- src/Avalonia.Input/Pointer.cs | 9 +-- src/Avalonia.Input/PointerEventArgs.cs | 14 ++-- src/Avalonia.Input/Raw/RawDragEventType.cs | 2 +- src/Avalonia.Input/Raw/RawInputEventArgs.cs | 2 +- src/Avalonia.Input/TextInputEventArgs.cs | 4 +- 28 files changed, 172 insertions(+), 162 deletions(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 96f0bb59b3..660584e2ed 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -28,7 +28,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot _owner; + private IInputRoot? _owner; /// /// Whether access keys are currently being shown; @@ -48,17 +48,17 @@ namespace Avalonia.Input /// /// Element to restore following AltKey taking focus. /// - private IInputElement _restoreFocusElement; + private IInputElement? _restoreFocusElement; /// /// The window's main menu. /// - private IMainMenu _mainMenu; + private IMainMenu? _mainMenu; /// /// Gets or sets the window's main menu. /// - public IMainMenu MainMenu + public IMainMenu? MainMenu { get => _mainMenu; set @@ -86,14 +86,12 @@ namespace Avalonia.Input /// public void SetOwner(IInputRoot owner) { - Contract.Requires(owner != null); - if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } - _owner = owner; + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble); @@ -149,7 +147,7 @@ namespace Avalonia.Input // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). - _owner.ShowAccessKeys = _showingAccessKeys = true; + _owner!.ShowAccessKeys = _showingAccessKeys = true; } else { @@ -241,7 +239,7 @@ namespace Avalonia.Input { if (_showingAccessKeys) { - _owner.ShowAccessKeys = false; + _owner!.ShowAccessKeys = false; } } @@ -250,13 +248,13 @@ namespace Avalonia.Input /// private void CloseMenu() { - MainMenu.Close(); - _owner.ShowAccessKeys = _showingAccessKeys = false; + MainMenu!.Close(); + _owner!.ShowAccessKeys = _showingAccessKeys = false; } private void MainMenuClosed(object sender, EventArgs e) { - _owner.ShowAccessKeys = false; + _owner!.ShowAccessKeys = false; } } } diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index ea560ce2ea..58927e2c93 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 diff --git a/src/Avalonia.Input/DataObject.cs b/src/Avalonia.Input/DataObject.cs index 60d7d67606..688f5f9cc8 100644 --- a/src/Avalonia.Input/DataObject.cs +++ b/src/Avalonia.Input/DataObject.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input return _items.ContainsKey(dataFormat); } - public object Get(string dataFormat) + public object? Get(string dataFormat) { if (_items.ContainsKey(dataFormat)) return _items[dataFormat]; @@ -23,12 +23,12 @@ namespace Avalonia.Input return _items.Keys; } - public IEnumerable GetFileNames() + public IEnumerable? GetFileNames() { return Get(DataFormats.FileNames) as IEnumerable; } - public string GetText() + public string? GetText() { return Get(DataFormats.Text) as string; } diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index bcd962bc31..30a08eda17 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -9,9 +9,9 @@ namespace Avalonia.Input { public static readonly DragDropDevice Instance = new DragDropDevice(); - private Interactive _lastTarget = null; + private Interactive? _lastTarget = null; - private Interactive GetTarget(IInputRoot root, Point local) + private Interactive? GetTarget(IInputRoot root, Point local) { var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); if (target != null && DragDrop.GetAllowDrop(target)) @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive? target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 66355da8b9..a1f1478f51 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -15,8 +15,8 @@ namespace Avalonia.Input /// /// The focus scopes in which the focus is currently defined. /// - private readonly ConditionalWeakTable _focusScopes = - new ConditionalWeakTable(); + private readonly ConditionalWeakTable _focusScopes = + new ConditionalWeakTable(); /// /// Initializes a new instance of the class. @@ -37,12 +37,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - public IInputElement Current => KeyboardDevice.Instance?.FocusedElement; + public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; /// /// Gets the current focus scope. /// - public IFocusScope Scope + public IFocusScope? Scope { get; private set; @@ -55,7 +55,7 @@ namespace Avalonia.Input /// The method by which focus was changed. /// Any key modifiers active at the time of focus. public void Focus( - IInputElement control, + IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { @@ -75,17 +75,18 @@ namespace Avalonia.Input // If control is null, set focus to the topmost focus scope. foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) { - IInputElement element; - - if (_focusScopes.TryGetValue(scope, out element) && element != null) + if (_focusScopes.TryGetValue(scope, out var element) && element != null) { Focus(element, method); return; } } - // Couldn't find a focus scope, clear focus. - SetFocusedElement(Scope, null); + if (Scope is object) + { + // Couldn't find a focus scope, clear focus. + SetFocusedElement(Scope, null); + } } } @@ -102,13 +103,13 @@ namespace Avalonia.Input /// public void SetFocusedElement( IFocusScope scope, - IInputElement element, + IInputElement? element, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { - Contract.Requires(scope != null); + scope = scope ?? throw new ArgumentNullException(nameof(scope)); - if (_focusScopes.TryGetValue(scope, out IInputElement existingElement)) + if (_focusScopes.TryGetValue(scope, out var existingElement)) { if (element != existingElement) { @@ -133,11 +134,9 @@ namespace Avalonia.Input /// The new focus scope. public void SetFocusScope(IFocusScope scope) { - Contract.Requires(scope != null); + scope = scope ?? throw new ArgumentNullException(nameof(scope)); - IInputElement e; - - if (!_focusScopes.TryGetValue(scope, out e)) + if (!_focusScopes.TryGetValue(scope, out var e)) { // TODO: Make this do something useful, i.e. select the first focusable // control, select a control that the user has specified to have default @@ -164,17 +163,19 @@ namespace Avalonia.Input /// The focus scopes. private static IEnumerable GetFocusScopeAncestors(IInputElement control) { - while (control != null) + IInputElement? c = control; + + while (c != null) { - var scope = control as IFocusScope; + var scope = c as IFocusScope; - if (scope != null && control.VisualRoot?.IsVisible == true) + if (scope != null && c.VisualRoot?.IsVisible == true) { yield return scope; } - control = control.GetVisualParent() ?? - ((control as IHostedVisualTreeRoot)?.Host as IInputElement); + c = c.GetVisualParent() ?? + ((c as IHostedVisualTreeRoot)?.Host as IInputElement); } } @@ -190,7 +191,7 @@ namespace Avalonia.Input if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { - IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; + IVisual? element = ev.Pointer?.Captured ?? e.Source as IInputElement; while (element != null) { diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs index 112abb1a4e..54ef0b1a68 100644 --- a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -1,8 +1,6 @@ -using System; using System.Collections; using System.Collections.Generic; using Avalonia.Controls; -using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Styling; @@ -11,8 +9,8 @@ namespace Avalonia.Input.GestureRecognizers public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher { private readonly IInputElement _inputElement; - private List _recognizers; - private Dictionary _pointerGrabs; + private List? _recognizers; + private Dictionary? _pointerGrabs; public GestureRecognizerCollection(IInputElement inputElement) @@ -72,7 +70,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) { capture.PointerReleased(e); } @@ -90,7 +88,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) { capture.PointerMoved(e); } @@ -108,7 +106,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return; - _pointerGrabs.Remove(e.Pointer); + _pointerGrabs!.Remove(e.Pointer); foreach (var r in _recognizers) { r.PointerCaptureLost(e.Pointer); @@ -118,8 +116,8 @@ namespace Avalonia.Input.GestureRecognizers void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) { pointer.Capture(_inputElement); - _pointerGrabs[pointer] = recognizer; - foreach (var r in _recognizers) + _pointerGrabs![pointer] = recognizer; + foreach (var r in _recognizers!) { if (r != recognizer) r.PointerCaptureLost(pointer); diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index e022401c8e..3858cc04f2 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using Avalonia.Interactivity; using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers @@ -11,9 +10,9 @@ namespace Avalonia.Input.GestureRecognizers { private bool _scrolling; private Point _trackedRootPoint; - private IPointer _tracking; - private IInputElement _target; - private IGestureRecognizerActionsDispatcher _actions; + private IPointer? _tracking; + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private int _gestureId; @@ -95,7 +94,7 @@ namespace Avalonia.Input.GestureRecognizers _scrolling = true; if (_scrolling) { - _actions.Capture(e.Pointer, this); + _actions!.Capture(e.Pointer, this); } } @@ -110,7 +109,7 @@ namespace Avalonia.Input.GestureRecognizers _trackedRootPoint = rootPoint; if (elapsed.TotalSeconds > 0) _inertia = vector / elapsed.TotalSeconds; - _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); e.Handled = true; } } @@ -128,7 +127,7 @@ namespace Avalonia.Input.GestureRecognizers { _inertia = default; _scrolling = false; - _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + _target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); _gestureId = 0; _lastMoveTimestamp = null; } @@ -165,7 +164,7 @@ namespace Avalonia.Input.GestureRecognizers var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; - _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); + _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 0efc20b196..5751719d61 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -29,7 +29,9 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. private static WeakReference s_lastPress = new WeakReference(null); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. static Gestures() { @@ -69,6 +71,11 @@ namespace Avalonia.Input private static void PointerPressed(RoutedEventArgs ev) { + if (ev.Source is null) + { + return; + } + if (ev.Route == RoutingStrategies.Bubble) { var e = (PointerPressedEventArgs)ev; @@ -76,7 +83,7 @@ namespace Avalonia.Input if (e.ClickCount <= 1) { - s_lastPress = new WeakReference(e.Source); + s_lastPress = new WeakReference(ev.Source); } else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { diff --git a/src/Avalonia.Input/IAccessKeyHandler.cs b/src/Avalonia.Input/IAccessKeyHandler.cs index 3e6510320f..e484d003c7 100644 --- a/src/Avalonia.Input/IAccessKeyHandler.cs +++ b/src/Avalonia.Input/IAccessKeyHandler.cs @@ -8,7 +8,7 @@ namespace Avalonia.Input /// /// Gets or sets the window's main menu. /// - IMainMenu MainMenu { get; set; } + IMainMenu? MainMenu { get; set; } /// /// Sets the owner of the access key handler. diff --git a/src/Avalonia.Input/IDataObject.cs b/src/Avalonia.Input/IDataObject.cs index 1aa8fd63d5..1db008aa3a 100644 --- a/src/Avalonia.Input/IDataObject.cs +++ b/src/Avalonia.Input/IDataObject.cs @@ -23,17 +23,17 @@ namespace Avalonia.Input /// Returns the dragged text if the DataObject contains any text. /// /// - string GetText(); + string? GetText(); /// /// Returns a list of filenames if the DataObject contains filenames. /// /// - IEnumerable GetFileNames(); + IEnumerable? GetFileNames(); /// /// Tries to get the data of the given DataFormat. /// - object Get(string dataFormat); + object? Get(string dataFormat); } } diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index 9122cc428d..e1b5087c3d 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -8,12 +8,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - IInputElement Current { get; } + IInputElement? Current { get; } /// /// Gets the current focus scope. /// - IFocusScope Scope { get; } + IFocusScope? Scope { get; } /// /// Focuses a control. @@ -22,7 +22,7 @@ namespace Avalonia.Input /// The method by which focus was changed. /// Any key modifiers active at the time of focus. void Focus( - IInputElement control, + IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None); diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index c30d74c965..12fec82368 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -78,7 +78,7 @@ namespace Avalonia.Input /// /// Gets or sets the associated mouse cursor. /// - Cursor Cursor { get; } + Cursor? Cursor { get; } /// /// Gets a value indicating whether this control and all its parents are enabled. diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index eeb7e4323e..3e2b8cc477 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input /// /// Gets or sets the input element that the pointer is currently over. /// - IInputElement PointerOverElement { get; set; } + IInputElement? PointerOverElement { get; set; } /// /// Gets or sets a value indicating whether access keys are shown in the window. @@ -31,6 +31,6 @@ namespace Avalonia.Input /// Gets associated mouse device /// [CanBeNull] - IMouseDevice MouseDevice { get; } + IMouseDevice? MouseDevice { get; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index ba7e0484ee..9506dc36fb 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -58,10 +58,10 @@ namespace Avalonia.Input public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { - IInputElement FocusedElement { get; } + IInputElement? FocusedElement { get; } void SetFocusedElement( - IInputElement element, + IInputElement? element, NavigationMethod method, KeyModifiers modifiers); } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index a3f051ce7f..7af48cef82 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -3,8 +3,8 @@ namespace Avalonia.Input public interface IPointer { int Id { get; } - void Capture(IInputElement control); - IInputElement Captured { get; } + void Capture(IInputElement? control); + IInputElement? Captured { get; } PointerType Type { get; } bool IsPrimary { get; } diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index bf001dda15..1f82cb1ed7 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -6,10 +6,10 @@ namespace Avalonia.Input public interface IPointerDevice : IInputDevice { [Obsolete("Use IPointer")] - IInputElement Captured { get; } + IInputElement? Captured { get; } [Obsolete("Use IPointer")] - void Capture(IInputElement control); + void Capture(IInputElement? control); [Obsolete("Use PointerEventArgs.GetPosition")] Point GetPosition(IVisual relativeTo); diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 0616c70d82..25f2d553d7 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -37,8 +37,8 @@ namespace Avalonia.Input /// /// Gets or sets associated mouse cursor. /// - public static readonly StyledProperty CursorProperty = - AvaloniaProperty.Register(nameof(Cursor), null, true); + public static readonly StyledProperty CursorProperty = + AvaloniaProperty.Register(nameof(Cursor), null, true); /// /// Defines the property. @@ -160,7 +160,7 @@ namespace Avalonia.Input private bool _isFocused; private bool _isFocusVisible; private bool _isPointerOver; - private GestureRecognizerCollection _gestureRecognizers; + private GestureRecognizerCollection? _gestureRecognizers; /// /// Initializes static members of the class. @@ -336,7 +336,7 @@ namespace Avalonia.Input /// /// Gets or sets associated mouse cursor. /// - public Cursor Cursor + public Cursor? Cursor { get { return GetValue(CursorProperty); } set { SetValue(CursorProperty, value); } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index 4babe711f2..0c7615a472 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -33,7 +33,7 @@ namespace Avalonia.Input /// The element to test. /// The point on . /// The topmost at the specified position. - public static IInputElement InputHitTest(this IInputElement element, Point p) + public static IInputElement? InputHitTest(this IInputElement element, Point p) { Contract.Requires(element != null); diff --git a/src/Avalonia.Input/KeyEventArgs.cs b/src/Avalonia.Input/KeyEventArgs.cs index 267376262b..67cd5a520a 100644 --- a/src/Avalonia.Input/KeyEventArgs.cs +++ b/src/Avalonia.Input/KeyEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - public IKeyboardDevice Device { get; set; } + public IKeyboardDevice? Device { get; set; } public Key Key { get; set; } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 0321b0bdf3..187670a26b 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -8,9 +8,9 @@ namespace Avalonia.Input { public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged { - private IInputElement _focusedElement; + private IInputElement? _focusedElement; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService(); @@ -18,7 +18,7 @@ namespace Avalonia.Input public IFocusManager FocusManager => AvaloniaLocator.Current.GetService(); - public IInputElement FocusedElement + public IInputElement? FocusedElement { get { @@ -33,7 +33,7 @@ namespace Avalonia.Input } public void SetFocusedElement( - IInputElement element, + IInputElement? element, NavigationMethod method, KeyModifiers keyModifiers) { diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index c425eeeedb..dbefe63789 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -13,7 +13,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot _owner; + private IInputRoot? _owner; /// /// Sets the owner of the keyboard navigation handler. @@ -24,15 +24,12 @@ namespace Avalonia.Input /// public void SetOwner(IInputRoot owner) { - Contract.Requires(owner != null); - if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } - _owner = owner; - + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown); } @@ -45,11 +42,11 @@ namespace Avalonia.Input /// The next element in the specified direction, or null if /// was the last in the requested direction. /// - public static IInputElement GetNext( + public static IInputElement? GetNext( IInputElement element, NavigationDirection direction) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); var customHandler = element.GetSelfAndVisualAncestors() .OfType() @@ -97,7 +94,7 @@ namespace Avalonia.Input NavigationDirection direction, KeyModifiers keyModifiers = KeyModifiers.None) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); var next = GetNext(element, direction); diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 188ddd9835..cec5029c18 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input private readonly Pointer _pointer; private bool _disposed; - public MouseDevice(Pointer pointer = null) + public MouseDevice(Pointer? pointer = null) { _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } @@ -34,7 +34,7 @@ namespace Avalonia.Input /// method. /// [Obsolete("Use IPointer instead")] - public IInputElement Captured => _pointer.Captured; + public IInputElement? Captured => _pointer.Captured; /// /// Gets the mouse position, in screen coordinates. @@ -54,7 +54,7 @@ namespace Avalonia.Input /// within the control's bounds or not. The current mouse capture control is exposed /// by the property. /// - public void Capture(IInputElement control) + public void Capture(IInputElement? control) { _pointer.Capture(control); } @@ -66,7 +66,7 @@ namespace Avalonia.Input /// The mouse position in the control's coordinates. public Point GetPosition(IVisual relativeTo) { - Contract.Requires(relativeTo != null); + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); if (relativeTo.VisualRoot == null) { @@ -75,7 +75,7 @@ namespace Avalonia.Input var rootPoint = relativeTo.VisualRoot.PointToClient(Position); var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform.Value; + return rootPoint * transform!.Value; } public void ProcessRawEvent(RawInputEventArgs e) @@ -126,7 +126,7 @@ namespace Avalonia.Input private void ProcessRawEvent(RawPointerEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); var mouse = (MouseDevice)e.Device; if(mouse._disposed) @@ -173,8 +173,8 @@ namespace Avalonia.Input private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); ClearPointerOver(this, timestamp, root, properties, inputModifiers); } @@ -214,8 +214,8 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -250,10 +250,10 @@ namespace Avalonia.Input private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); - IInputElement source; + IInputElement? source; if (_pointer.Captured == null) { @@ -265,18 +265,23 @@ namespace Avalonia.Input source = _pointer.Captured; } - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); - source?.RaiseEvent(e); - return e.Handled; + source.RaiseEvent(e); + return e.Handled; + } + + return false; } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -298,8 +303,8 @@ namespace Avalonia.Input PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -317,21 +322,21 @@ namespace Avalonia.Input private IInteractive GetSource(IVisual hit) { - Contract.Requires(hit != null); + hit = hit ?? throw new ArgumentNullException(nameof(hit)); return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } - private IInputElement HitTest(IInputElement root, Point p) + private IInputElement? HitTest(IInputElement root, Point p) { - Contract.Requires(root != null); + root = root ?? throw new ArgumentNullException(nameof(root)); return _pointer.Captured ?? root.InputHitTest(p); } - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, PointerPointProperties properties, KeyModifiers inputModifiers) { @@ -343,8 +348,8 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var element = root.PointerOverElement; var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); @@ -384,12 +389,12 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var element = root.InputHitTest(p); @@ -412,11 +417,11 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); - Contract.Requires(element != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); - IInputElement branch = null; + IInputElement? branch = null; var el = element; diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index cd377f1df6..6f6d68940b 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -22,15 +22,17 @@ namespace Avalonia.Input.Navigation /// The next element in the specified direction, or null if /// was the last in the requested direction. /// - public static IInputElement GetNextInTabOrder( + public static IInputElement? GetNextInTabOrder( IInputElement element, NavigationDirection direction, bool outsideElement = false) { - Contract.Requires(element != null); - Contract.Requires( - direction == NavigationDirection.Next || - direction == NavigationDirection.Previous); + element = element ?? throw new ArgumentNullException(nameof(element)); + + if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous) + { + throw new ArgumentException("Invalid direction: must be Next or Previous."); + } var container = element.GetVisualParent(); @@ -110,7 +112,7 @@ namespace Avalonia.Input.Navigation if (customNext.handled) { - yield return customNext.next; + yield return customNext.next!; } else { @@ -143,12 +145,14 @@ namespace Avalonia.Input.Navigation /// If true will not descend into to find next control. /// /// The next element, or null if the element is the last. - private static IInputElement GetNextInContainer( + private static IInputElement? GetNextInContainer( IInputElement element, IInputElement container, NavigationDirection direction, bool outsideElement) { + IInputElement? e = element; + if (direction == NavigationDirection.Next && !outsideElement) { var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); @@ -167,13 +171,13 @@ namespace Avalonia.Input.Navigation // INavigableContainer. if (navigable != null) { - while (element != null) + while (e != null) { - element = navigable.GetControl(direction, element, false); + e = navigable.GetControl(direction, e, false); - if (element != null && - element.CanFocus() && - KeyboardNavigation.GetIsTabStop((InputElement) element)) + if (e != null && + e.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement)e)) { break; } @@ -183,12 +187,12 @@ namespace Avalonia.Input.Navigation { // TODO: Do a spatial search here if the container doesn't implement // INavigableContainer. - element = null; + e = null; } - if (element != null && direction == NavigationDirection.Previous) + if (e != null && direction == NavigationDirection.Previous) { - var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); + var descendant = GetFocusableDescendants(e, direction).LastOrDefault(); if (descendant != null) { @@ -196,7 +200,7 @@ namespace Avalonia.Input.Navigation } } - return element; + return e; } return null; @@ -209,13 +213,13 @@ namespace Avalonia.Input.Navigation /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. - private static IInputElement GetFirstInNextContainer( + private static IInputElement? GetFirstInNextContainer( IInputElement element, IInputElement container, NavigationDirection direction) { var parent = container.GetVisualParent(); - IInputElement next = null; + IInputElement? next = null; if (parent != null) { @@ -268,7 +272,7 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element, NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 00222e92cf..a477711584 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -20,7 +19,7 @@ namespace Avalonia.Input public int Id { get; } - IInputElement FindCommonParent(IInputElement control1, IInputElement control2) + IInputElement? FindCommonParent(IInputElement? control1, IInputElement? control2) { if (control1 == null || control2 == null) return null; @@ -28,12 +27,12 @@ namespace Avalonia.Input return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains); } - protected virtual void PlatformCapture(IInputElement element) + protected virtual void PlatformCapture(IInputElement? element) { } - public void Capture(IInputElement control) + public void Capture(IInputElement? control) { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; @@ -66,7 +65,7 @@ namespace Avalonia.Input } - public IInputElement Captured { get; private set; } + public IInputElement? Captured { get; private set; } public PointerType Type { get; } public bool IsPrimary { get; } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 9cc42ffa69..1cbddf89aa 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -7,14 +7,14 @@ namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs { - private readonly IVisual _rootVisual; + private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; public PointerEventArgs(RoutedEvent routedEvent, - IInteractive source, + IInteractive? source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, + IVisual? rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers) @@ -40,8 +40,8 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); - public IInputElement Captured => _ev.Pointer.Captured; - public void Capture(IInputElement control) + public IInputElement? Captured => _ev.Pointer.Captured; + public void Capture(IInputElement? control) { _ev.Pointer.Capture(control); } @@ -52,7 +52,7 @@ namespace Avalonia.Input public IPointer Pointer { get; } public ulong Timestamp { get; } - private IPointerDevice _device; + private IPointerDevice? _device; [Obsolete("Use Pointer to get pointer-specific information")] public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); @@ -76,7 +76,7 @@ namespace Avalonia.Input public KeyModifiers KeyModifiers { get; } - public Point GetPosition(IVisual relativeTo) + public Point GetPosition(IVisual? relativeTo) { if (_rootVisual == null) return default; diff --git a/src/Avalonia.Input/Raw/RawDragEventType.cs b/src/Avalonia.Input/Raw/RawDragEventType.cs index 9635f77467..77f17a5a41 100644 --- a/src/Avalonia.Input/Raw/RawDragEventType.cs +++ b/src/Avalonia.Input/Raw/RawDragEventType.cs @@ -7,4 +7,4 @@ DragLeave, Drop } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index b85563b24a..dcc5f27a79 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -21,7 +21,7 @@ namespace Avalonia.Input.Raw /// The root from which the event originates. public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { - Contract.Requires(device != null); + device = device ?? throw new ArgumentNullException(nameof(device)); Device = device; Timestamp = timestamp; diff --git a/src/Avalonia.Input/TextInputEventArgs.cs b/src/Avalonia.Input/TextInputEventArgs.cs index 6e763d3b56..cda0103749 100644 --- a/src/Avalonia.Input/TextInputEventArgs.cs +++ b/src/Avalonia.Input/TextInputEventArgs.cs @@ -4,8 +4,8 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - public IKeyboardDevice Device { get; set; } + public IKeyboardDevice? Device { get; set; } - public string Text { get; set; } + public string? Text { get; set; } } } From a3b27632ddcf5f801b2358a031d28fc28ed5d9cb Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Aug 2020 23:00:10 +0300 Subject: [PATCH 004/149] Add .Events projects --- global.json | 2 +- .../Avalonia.ReactiveUI.Events.csproj | 12 ++++++++++++ .../Avalonia.ReactiveUI.Events.UnitTests.csproj | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj create mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj diff --git a/global.json b/global.json index a3fdca9bed..e26c2c7f2c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.101" + "version": "3.1.401" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj new file mode 100644 index 0000000000..b95c8946e2 --- /dev/null +++ b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj @@ -0,0 +1,12 @@ + + + netstandard2.0 + Avalonia.ReactiveUI + + + + + + + + diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj new file mode 100644 index 0000000000..d162a2abe0 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj @@ -0,0 +1,15 @@ + + + netcoreapp3.1 + + + + + + + + + + + + From d683bbbffc019c57ed3de0e431172bfbbcb08fae Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Aug 2020 23:36:42 +0300 Subject: [PATCH 005/149] Add Avalonia.ReactiveUI.Events based on Pharmacist --- Avalonia.sln | 26 ++++++++++++ nukebuild/Build.cs | 40 ++++++++++++++++++- nukebuild/_build.csproj | 1 + ...valonia.ReactiveUI.Events.UnitTests.csproj | 15 ------- 4 files changed, 66 insertions(+), 16 deletions(-) delete mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj diff --git a/Avalonia.sln b/Avalonia.sln index ffd7b4d4f4..71ed289241 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -224,6 +224,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2035,6 +2037,30 @@ Global {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index fbfbf47e1b..8c40a2c45b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using Nuke.Common; using Nuke.Common.Git; @@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild; using Nuke.Common.Tools.Npm; using Nuke.Common.Utilities; using Nuke.Common.Utilities.Collections; +using Pharmacist.Core; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -139,7 +141,7 @@ partial class Build : NukeBuild Target Compile => _ => _ .DependsOn(Clean) .DependsOn(CompileHtmlPreviewer) - .Executes(() => + .Executes(async () => { if (Parameters.IsRunningOnWindows) MsBuildCommon(Parameters.MSBuildSolution, c => c @@ -153,8 +155,44 @@ partial class Build : NukeBuild .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); + + await CompileReactiveEvents(); }); + async Task CompileReactiveEvents() + { + var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration); + var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll") + .Where(file => !file.Contains("Avalonia.Build.Tasks") && + !file.Contains("Avalonia.Remote.Protocol")); + + var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First(); + var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs"); + if (File.Exists(eventsBuildFile)) + File.Delete(eventsBuildFile); + + using (var stream = File.Create(eventsBuildFile)) + using (var writer = new StreamWriter(stream)) + { + await ObservablesForEventGenerator.ExtractEventsFromAssemblies( + writer, avaloniaAssemblies, new string[0], "netstandard2.0" + ); + } + + var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj"); + if (Parameters.IsRunningOnWindows) + MsBuildCommon(eventsProject, c => c + .SetArgumentConfigurator(a => a.Add("/r")) + .AddTargets("Build") + ); + else + DotNetBuild(c => c + .SetProjectFile(eventsProject) + .AddProperty("PackageVersion", Parameters.Version) + .SetConfiguration(Parameters.Configuration) + ); + } + void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 4c64d4ff93..b06e49f2eb 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -17,6 +17,7 @@ + diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj deleted file mode 100644 index d162a2abe0..0000000000 --- a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - netcoreapp3.1 - - - - - - - - - - - - From e3e001e12b174569c25b11353a1aecae5dc961bb Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 21 Aug 2020 13:56:24 +0300 Subject: [PATCH 006/149] Use latest SDK and runtime --- azure-pipelines.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 54645e461e..172fe9cb2b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,16 +35,16 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.101' + displayName: 'Use .NET Core SDK 3.1.401' inputs: packageType: sdk - version: 3.1.101 + version: 3.1.401 - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.1' + displayName: 'Use .NET Core Runtime 3.1.7' inputs: packageType: runtime - version: 3.1.1 + version: 3.1.7 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -112,6 +112,18 @@ jobs: pool: vmImage: 'windows-2019' steps: + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.401' + inputs: + packageType: sdk + version: 3.1.401 + + - task: UseDotNet@2 + displayName: 'Use .NET Core Runtime 3.1.7' + inputs: + packageType: runtime + version: 3.1.7 + - task: CmdLine@2 displayName: 'Install Nuke' inputs: From ded3a566c0e301bdbf871d3cedd7196c87f5d89d Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 21 Aug 2020 14:42:44 +0300 Subject: [PATCH 007/149] Apply DotNetCoreInstaller workaround --- azure-pipelines.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 172fe9cb2b..a267452ff4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -112,18 +112,11 @@ jobs: pool: vmImage: 'windows-2019' steps: - - task: UseDotNet@2 + - task: DotNetCoreInstaller@0 displayName: 'Use .NET Core SDK 3.1.401' inputs: - packageType: sdk version: 3.1.401 - - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.7' - inputs: - packageType: runtime - version: 3.1.7 - - task: CmdLine@2 displayName: 'Install Nuke' inputs: From 9dd34660bf3b236eb7006d80498e790e4b6d95c1 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 21 Aug 2020 15:15:26 +0300 Subject: [PATCH 008/149] Try using .NET Core 3.1.302 --- azure-pipelines.yml | 17 +++++------------ global.json | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a267452ff4..20278ddc71 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,17 +34,10 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' - inputs: - packageType: sdk - version: 3.1.401 - - - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.7' + - task: DotNetCoreInstaller@0 + displayName: 'Use .NET Core SDK 3.1.302' inputs: - packageType: runtime - version: 3.1.7 + version: 3.1.302 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -113,9 +106,9 @@ jobs: vmImage: 'windows-2019' steps: - task: DotNetCoreInstaller@0 - displayName: 'Use .NET Core SDK 3.1.401' + displayName: 'Use .NET Core SDK 3.1.302' inputs: - version: 3.1.401 + version: 3.1.302 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/global.json b/global.json index e26c2c7f2c..2f594d9596 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.401" + "version": "3.1.302" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", From 59b0f81aad47b9bea2f32b23c754e6cb8eea8c7b Mon Sep 17 00:00:00 2001 From: artyom Date: Sat, 22 Aug 2020 21:56:59 +0300 Subject: [PATCH 009/149] Add a smoke test --- Avalonia.sln | 3 ++ dirs.proj | 1 + nukebuild/Build.cs | 1 + ...valonia.ReactiveUI.Events.UnitTests.csproj | 15 +++++++ .../BasicControlEventsTest.cs | 44 +++++++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj create mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs diff --git a/Avalonia.sln b/Avalonia.sln index 71ed289241..551eb5925e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -226,6 +226,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2120,6 +2122,7 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/dirs.proj b/dirs.proj index 26c8f54b23..086a22a77b 100644 --- a/dirs.proj +++ b/dirs.proj @@ -7,6 +7,7 @@ + diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8c40a2c45b..a46b55f6f3 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -240,6 +240,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj new file mode 100644 index 0000000000..19a6fd138e --- /dev/null +++ b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj @@ -0,0 +1,15 @@ + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs b/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs new file mode 100644 index 0000000000..1092c98246 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.ReactiveUI.Events.UnitTests +{ + public class BasicControlEventsTest + { + public class EventsControl : UserControl + { + public bool IsAttached { get; private set; } + + public EventsControl() + { + var attached = this + .Events() + .AttachedToVisualTree + .Select(args => true); + + this.Events() + .DetachedFromVisualTree + .Select(args => false) + .Merge(attached) + .Subscribe(marker => IsAttached = marker); + } + } + + [Fact] + public void Should_Generate_Events_Wrappers() + { + var root = new TestRoot(); + var control = new EventsControl(); + Assert.False(control.IsAttached); + + root.Child = control; + Assert.True(control.IsAttached); + + root.Child = null; + Assert.False(control.IsAttached); + } + } +} From bc0ada009461679ef8517cc30ad1b2e5c0165576 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Mon, 31 Aug 2020 18:35:40 +0200 Subject: [PATCH 010/149] Preferring user-provided string conversion over TypeDescriptor --- src/Avalonia.Controls/ColumnDefinitions.cs | 10 ++++- .../ViewModels/PropertyViewModel.cs | 37 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/ColumnDefinitions.cs index ed4f9dbe99..7e355ab357 100644 --- a/src/Avalonia.Controls/ColumnDefinitions.cs +++ b/src/Avalonia.Controls/ColumnDefinitions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Text; using Avalonia.Collections; namespace Avalonia.Controls @@ -13,7 +14,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ColumnDefinitions() : base () + public ColumnDefinitions() { } @@ -27,6 +28,11 @@ namespace Avalonia.Controls AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x))); } + public override string ToString() + { + return string.Join(",", this.Select(x => x.Width)); + } + /// /// Parses a string representation of column definitions collection. /// @@ -34,4 +40,4 @@ namespace Avalonia.Controls /// The . public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index ddbdae7ed9..2d69764ae8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -8,8 +9,8 @@ namespace Avalonia.Diagnostics.ViewModels internal abstract class PropertyViewModel : ViewModelBase { private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = new[] { typeof(string) }; - private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; + private static readonly Type[] StringParameter = { typeof(string) }; + private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; public abstract object Key { get; } public abstract string Name { get; } @@ -25,19 +26,37 @@ namespace Avalonia.Diagnostics.ViewModels return "(null)"; } - var converter = TypeDescriptor.GetConverter(value); - return converter?.ConvertToString(value) ?? value.ToString(); + //Check if there's an user provided ToString(), prefer that over the TypeDescriptor conversion + if (value.GetType().GetMethod(nameof(ToString), System.Type.EmptyTypes) + .DeclaringType != typeof(object)) + { + return value.ToString(); + } + + try + { + var converter = TypeDescriptor.GetConverter(value); + + return converter.ConvertToString(value); + } + catch + { + return value.ToString(); + } } protected static object ConvertFromString(string s, Type targetType) { - var converter = TypeDescriptor.GetConverter(targetType); - - if (converter != null && converter.CanConvertFrom(typeof(string))) + try { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); + var converter = TypeDescriptor.GetConverter(targetType); + + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); + } } - else + catch { var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); From c25a130903faca7764b7e45f8b13e9fd3fde04fa Mon Sep 17 00:00:00 2001 From: GMIKE Date: Tue, 1 Sep 2020 01:07:49 +0300 Subject: [PATCH 011/149] Useful methods and operations --- src/Avalonia.Visuals/Point.cs | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 4cce2c925b..f9ae1f2fe1 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -76,6 +76,17 @@ namespace Avalonia return left.Equals(right); } + /// + /// Checks for equality between a point and cortege off double numbers s. + /// + /// The point. + /// Cortege off double numbers + /// True if the point and cortege off double numbers are equal; otherwise false. + public static bool operator ==(Point left, (double x, double y) right) + { + return !((left._x.Equals(right.x)) && (left._y.Equals(right.y))); + } + /// /// Checks for inequality between two s. /// @@ -87,6 +98,17 @@ namespace Avalonia return !(left == right); } + /// + /// Checks for inequality between a point and cortege off double numberss. + /// + /// The point. + /// Cortege off double numbers + /// True if the point and cortege off double numbers are unequal; otherwise false. + public static bool operator !=(Point left, (double x, double y) right) + { + return !((left._x == right.x) && (left._y== right.y)); + } + /// /// Adds two points. /// @@ -109,6 +131,28 @@ namespace Avalonia return new Point(a._x + b.X, a._y + b.Y); } + /// + /// Add a cortege off double numbers + /// + /// The point. + /// cortege off doubles + /// A point that is the result of the addition. + public static Point operator +(Point a, (double x, double y) b) + { + return new Point(a._x + b.x, a._y + b.y); + } + + /// + /// Add a double number to both coordinates + /// + /// The point. + /// double number + /// A point that is the result of the addition. + public static Point operator +(Point a, double b) + { + return new Point(a._x + b, a._y + b); + } + /// /// Subtracts two points. /// @@ -131,6 +175,28 @@ namespace Avalonia return new Point(a._x - b.X, a._y - b.Y); } + /// + /// Subtracts a cortege off double numbers + /// + /// The point. + /// cortege off doubles + /// A point that is the result of the subtraction. + public static Point operator -(Point a, (double x, double y) b) + { + return new Point(a._x - b.x, a._y - b.y); + } + + /// + /// Subtracts a double number to both coordinates + /// + /// The point. + /// double number + /// A point that is the result of the subtraction. + public static Point operator -(Point a, double b) + { + return new Point(a._x - b, a._y - b); + } + /// /// Multiplies a point by a factor coordinate-wise /// @@ -147,6 +213,17 @@ namespace Avalonia /// Points having its coordinates multiplied public static Point operator *(double k, Point p) => new Point(p.X * k, p.Y * k); + /// + /// Multiplies a cortege off double numbers + /// + /// The point. + /// cortege off doubles + /// Points having its coordinates multiplied. + public static Point operator *(Point a, (double x, double y) b) + { + return new Point(a._x * b.x, a._y * b.y); + } + /// /// Divides a point by a factor coordinate-wise /// @@ -155,6 +232,17 @@ namespace Avalonia /// Points having its coordinates divided public static Point operator /(Point p, double k) => new Point(p.X / k, p.Y / k); + /// + /// Divides a point by a cortege off double numbers + /// + /// The point. + /// cortege off doubles + /// Points having its coordinates divided + public static Point operator /(Point a, (double x, double y) b) + { + return new Point(a._x / b.x, a._y / b.y); + } + /// /// Applies a matrix to a point. /// @@ -267,5 +355,32 @@ namespace Avalonia { return new Point(_x, y); } + + /// + /// Returns a new point with the opposite coordinates. + /// + /// The new point. + public Point Miror() + { + return new Point(-_x, -_y); + } + + /// + /// Returns a new point with the opposite X coordinate. + /// + /// The new point. + public Point WithMirorX() + { + return new Point(-_x, _y); + } + + /// + /// Returns a new point with the opposite Y coordinate. + /// + /// The new point. + public Point WithMirorY() + { + return new Point(_x, -_y); + } } } From 3cdfa305b59e2e9eb2784b4da470eb5008ca14c1 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 1 Sep 2020 10:27:30 +0200 Subject: [PATCH 012/149] typo, removed using --- .../Diagnostics/ViewModels/PropertyViewModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 2d69764ae8..bd60a46b95 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -26,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels return "(null)"; } - //Check if there's an user provided ToString(), prefer that over the TypeDescriptor conversion + //Check if there's an user-provided ToString(), prefer that over the TypeDescriptor conversion if (value.GetType().GetMethod(nameof(ToString), System.Type.EmptyTypes) .DeclaringType != typeof(object)) { From 8e03d121ac1357d94e9ece9fcd2e06d9b175c264 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Tue, 1 Sep 2020 14:10:20 +0300 Subject: [PATCH 013/149] Deconstruct and IsEmpty propterty --- src/Avalonia.Visuals/Point.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index f9ae1f2fe1..2e77f5528b 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -357,30 +357,22 @@ namespace Avalonia } /// - /// Returns a new point with the opposite coordinates. + /// Deconstructor for decomposition Point /// - /// The new point. - public Point Miror() - { - return new Point(-_x, -_y); - } - - /// - /// Returns a new point with the opposite X coordinate. - /// - /// The new point. - public Point WithMirorX() + /// The X position. + /// The Y position. + public void Deconstruct(out double x, out double y) { - return new Point(-_x, _y); + x = this._x; + y = this._y; } /// - /// Returns a new point with the opposite Y coordinate. + /// Gets a value indicating that Point coordinatrs is zero /// - /// The new point. - public Point WithMirorY() + public bool IsEmpty { - return new Point(_x, -_y); + get { return (_x == 0) && (_y == 0); } } } } From a339c68e8cb5ac4741a6bff936b0b462f1a155ca Mon Sep 17 00:00:00 2001 From: GMIKE Date: Tue, 1 Sep 2020 16:18:15 +0300 Subject: [PATCH 014/149] Equals for cortege off double numbers --- src/Avalonia.Visuals/Point.cs | 44 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 2e77f5528b..52fd4d05c9 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -8,7 +8,7 @@ namespace Avalonia /// /// Defines a point. /// - public readonly struct Point : IEquatable + public readonly struct Point : IEquatable, IEquatable<(double x,double y)> { static Point() { @@ -76,17 +76,6 @@ namespace Avalonia return left.Equals(right); } - /// - /// Checks for equality between a point and cortege off double numbers s. - /// - /// The point. - /// Cortege off double numbers - /// True if the point and cortege off double numbers are equal; otherwise false. - public static bool operator ==(Point left, (double x, double y) right) - { - return !((left._x.Equals(right.x)) && (left._y.Equals(right.y))); - } - /// /// Checks for inequality between two s. /// @@ -98,17 +87,6 @@ namespace Avalonia return !(left == right); } - /// - /// Checks for inequality between a point and cortege off double numberss. - /// - /// The point. - /// Cortege off double numbers - /// True if the point and cortege off double numbers are unequal; otherwise false. - public static bool operator !=(Point left, (double x, double y) right) - { - return !((left._x == right.x) && (left._y== right.y)); - } - /// /// Adds two points. /// @@ -367,6 +345,26 @@ namespace Avalonia y = this._y; } + /// + /// Returns a boolean indicating whether the point is equal to the other given point. + /// + /// The other point to test equality against. + /// True if this point is equal to other; False otherwise. + + + /// + /// Returns a boolean indicating whether the point is equal to cortege off double numbers. + /// + /// ortege off double numbers + /// + /// True if is cortege off double numbersthat equals the current point. + /// + public bool Equals((double x, double y) other) + { + return _x == other.x && + _y == other.y; + } + /// /// Gets a value indicating that Point coordinatrs is zero /// From faad15571ac2beca8b78063c04391ab9b2a809d3 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 1 Sep 2020 15:48:16 +0200 Subject: [PATCH 015/149] Filter out CollectionConverter Making sure to call Parse when conversion with TypeConverter is not possible --- .../ViewModels/PropertyViewModel.cs | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index bd60a46b95..e23d6f1471 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -25,54 +25,47 @@ namespace Avalonia.Diagnostics.ViewModels return "(null)"; } - //Check if there's an user-provided ToString(), prefer that over the TypeDescriptor conversion - if (value.GetType().GetMethod(nameof(ToString), System.Type.EmptyTypes) - .DeclaringType != typeof(object)) + var converter = TypeDescriptor.GetConverter(value); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) { return value.ToString(); } - try - { - var converter = TypeDescriptor.GetConverter(value); + return converter.ConvertToString(value); + } + + private static object InvokeParse(string s, Type targetType) + { + var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - return converter.ConvertToString(value); + if (method != null) + { + return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); } - catch + + method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); + + if (method != null) { - return value.ToString(); + return method.Invoke(null, new object[] { s }); } + + throw new InvalidCastException("Unable to convert value."); } protected static object ConvertFromString(string s, Type targetType) { - try - { - var converter = TypeDescriptor.GetConverter(targetType); + var converter = TypeDescriptor.GetConverter(targetType); - if (converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); - } - } - catch + if (converter.CanConvertFrom(typeof(string))) { - var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); - } - - method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s }); - } + return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); } - throw new InvalidCastException("Unable to convert value."); + return InvokeParse(s, targetType); } } } From f4b1b9dc958018b2b3c21d0c6e733e465cce9141 Mon Sep 17 00:00:00 2001 From: "Artyom V. Gorchakov" Date: Tue, 1 Sep 2020 20:30:56 +0300 Subject: [PATCH 016/149] UseDotNet@2 --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 20278ddc71..6652d1712a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,7 +34,7 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.302' inputs: version: 3.1.302 @@ -105,7 +105,7 @@ jobs: pool: vmImage: 'windows-2019' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.302' inputs: version: 3.1.302 From 21fc8620f204233268d6066bfb4714332ea4f6fd Mon Sep 17 00:00:00 2001 From: artyom Date: Tue, 1 Sep 2020 21:36:41 +0300 Subject: [PATCH 017/149] Try using 3.1.401 --- azure-pipelines.yml | 8 ++++---- global.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6652d1712a..89504a498e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,9 +35,9 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.302' + displayName: 'Use .NET Core SDK 3.1.401' inputs: - version: 3.1.302 + version: 3.1.401 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -106,9 +106,9 @@ jobs: vmImage: 'windows-2019' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.302' + displayName: 'Use .NET Core SDK 3.1.401' inputs: - version: 3.1.302 + version: 3.1.401 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/global.json b/global.json index 3371f05a4d..b2b2da7c4f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.302" + "version": "3.1.401" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", From 031722ebb2f98cedb391aa628e4e7c06fb633a33 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Wed, 2 Sep 2020 19:12:28 +0300 Subject: [PATCH 018/149] Change IsEmpty on IsDefault --- src/Avalonia.Visuals/Point.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 52fd4d05c9..ae713e57a5 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -345,19 +345,12 @@ namespace Avalonia y = this._y; } - /// - /// Returns a boolean indicating whether the point is equal to the other given point. - /// - /// The other point to test equality against. - /// True if this point is equal to other; False otherwise. - - /// /// Returns a boolean indicating whether the point is equal to cortege off double numbers. /// /// ortege off double numbers /// - /// True if is cortege off double numbersthat equals the current point. + /// True if is cortege off double numbersthat equals the current point. /// public bool Equals((double x, double y) other) { @@ -368,7 +361,7 @@ namespace Avalonia /// /// Gets a value indicating that Point coordinatrs is zero /// - public bool IsEmpty + public bool IsDefault { get { return (_x == 0) && (_y == 0); } } From 115c4adadbb5f3d9cadbe53cdb78e617d9602d45 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Wed, 2 Sep 2020 19:51:18 +0300 Subject: [PATCH 019/149] fix for xml --- src/Avalonia.Visuals/Point.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index ae713e57a5..d95512c976 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -110,10 +110,10 @@ namespace Avalonia } /// - /// Add a cortege off double numbers + /// Add a 2-tuple of double /// /// The point. - /// cortege off doubles + /// 2-tuple of double /// A point that is the result of the addition. public static Point operator +(Point a, (double x, double y) b) { @@ -154,10 +154,10 @@ namespace Avalonia } /// - /// Subtracts a cortege off double numbers + /// Subtracts a 2-tuple of double /// /// The point. - /// cortege off doubles + /// 2-tuple of double /// A point that is the result of the subtraction. public static Point operator -(Point a, (double x, double y) b) { @@ -192,10 +192,10 @@ namespace Avalonia public static Point operator *(double k, Point p) => new Point(p.X * k, p.Y * k); /// - /// Multiplies a cortege off double numbers + /// Multiplies a 2-tuple of double /// /// The point. - /// cortege off doubles + /// 2-tuple of double /// Points having its coordinates multiplied. public static Point operator *(Point a, (double x, double y) b) { @@ -211,10 +211,10 @@ namespace Avalonia public static Point operator /(Point p, double k) => new Point(p.X / k, p.Y / k); /// - /// Divides a point by a cortege off double numbers + /// Divides a point by a 2-tuple of doubles /// /// The point. - /// cortege off doubles + /// 2-tuple of double /// Points having its coordinates divided public static Point operator /(Point a, (double x, double y) b) { @@ -346,11 +346,11 @@ namespace Avalonia } /// - /// Returns a boolean indicating whether the point is equal to cortege off double numbers. + /// Returns a boolean indicating whether the point coordinates are equal to 2-tuple of double /// - /// ortege off double numbers + /// 2-tuple of double /// - /// True if is cortege off double numbersthat equals the current point. + /// True if is 2-tuple of double equals coordinates current point. /// public bool Equals((double x, double y) other) { @@ -359,7 +359,7 @@ namespace Avalonia } /// - /// Gets a value indicating that Point coordinatrs is zero + /// Gets a value indicating that point coordinates are zero /// public bool IsDefault { From 9e7c4d1b0943f10a600d03a8722bb19407e92337 Mon Sep 17 00:00:00 2001 From: ShadowDancer Date: Wed, 2 Sep 2020 23:20:01 +0200 Subject: [PATCH 020/149] Update Visual.cs documentation `OnAttachedToVisualTree`/`OnDetachedFromVisualTree` is called only for root visual tree. Keep xmldoc in line with event description. --- src/Avalonia.Visuals/Visual.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index cd6eb6aac7..283d9deb52 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -455,7 +455,7 @@ namespace Avalonia } /// - /// Called when the control is added to a visual tree. + /// Called when the control is added to a rooted visual tree. /// /// The event args. protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -463,7 +463,7 @@ namespace Avalonia } /// - /// Called when the control is removed from a visual tree. + /// Called when the control is removed from a rooted visual tree. /// /// The event args. protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) From c4ebf899e50f079be187c227cfba61c4c9d70ae9 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 3 Sep 2020 15:05:28 +0200 Subject: [PATCH 021/149] Add option to enable/disable dirty rects --- .../Diagnostics/ViewModels/MainViewModel.cs | 27 ++++++++++++++----- .../Diagnostics/Views/MainView.xaml | 7 +++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1c49b24f52..1bd4d711ab 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -9,7 +9,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class MainViewModel : ViewModelBase, IDisposable { - private readonly IControl _root; + private readonly TopLevel _root; private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; @@ -19,8 +19,9 @@ namespace Avalonia.Diagnostics.ViewModels private string _focusedControl; private string _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; + private bool _shouldVisualizeDirtyRects; - public MainViewModel(IControl root) + public MainViewModel(TopLevel root) { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -40,6 +41,22 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } + + public bool ShouldVisualizeDirtyRects + { + get => _shouldVisualizeDirtyRects; + set + { + RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); + + _root.Renderer.DrawDirtyRects = value; + } + } + + public void ToggleVisualizeDirtyRects() + { + ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects; + } public void ToggleVisualizeMarginPadding() { @@ -128,10 +145,7 @@ namespace Avalonia.Diagnostics.ViewModels { var tree = Content as TreePageViewModel; - if (tree != null) - { - tree.SelectControl(control); - } + tree?.SelectControl(control); } public void Dispose() @@ -140,6 +154,7 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); + _root.Renderer.DrawDirtyRects = false; } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 0165398718..e0b73957bf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -24,6 +24,13 @@ IsEnabled="False"/> + + + + + From 2bd8156fae206d321a446836e1ce33b45caca931 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 3 Sep 2020 16:28:29 +0200 Subject: [PATCH 022/149] Also add option to show fps overlay --- .../Diagnostics/ViewModels/MainViewModel.cs | 20 +++++++++++++++++-- .../Diagnostics/Views/MainView.xaml | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1bd4d711ab..bf7d0e232a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -20,6 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels private string _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; + private bool _showFpsOverlay; public MainViewModel(TopLevel root) { @@ -47,9 +48,8 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeDirtyRects; set { - RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); - _root.Renderer.DrawDirtyRects = value; + RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); } } @@ -63,6 +63,21 @@ namespace Avalonia.Diagnostics.ViewModels ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; } + public bool ShowFpsOverlay + { + get => _showFpsOverlay; + set + { + _root.Renderer.DrawFps = value; + RaiseAndSetIfChanged(ref _showFpsOverlay, value); + } + } + + public void ToggleFpsOverlay() + { + ShowFpsOverlay = !ShowFpsOverlay; + } + public ConsoleViewModel Console { get; } public ViewModelBase Content @@ -155,6 +170,7 @@ namespace Avalonia.Diagnostics.ViewModels _logicalTree.Dispose(); _visualTree.Dispose(); _root.Renderer.DrawDirtyRects = false; + _root.Renderer.DrawFps = false; } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index e0b73957bf..8c4db33f91 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -31,6 +31,13 @@ IsEnabled="False"/> + + + + + From ac24e28cc1105d4c79d2a094d99136d0e1d2234d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 3 Sep 2020 20:39:10 +0100 Subject: [PATCH 023/149] Fix IOSurface leak. --- native/Avalonia.Native/src/OSX/rendertarget.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index 1565417c1a..93a33bbbb0 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -110,7 +110,7 @@ if(_renderbuffer != 0) glDeleteRenderbuffers(1, &_renderbuffer); } - IOSurfaceDecrementUseCount(surface); + CFRelease(surface); } @end From d0f0d02b923980f3c113b130035d2796d6535fdf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 3 Sep 2020 22:00:25 +0100 Subject: [PATCH 024/149] install typescript with npm. --- nukebuild/Build.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a46b55f6f3..5b4e5a5bad 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -131,6 +131,7 @@ partial class Build : NukeBuild var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; NpmTasks.NpmInstall(c => c + .AddPackages("typescript") .SetWorkingDirectory(webappDir) .SetArgumentConfigurator(a => a.Add("--silent"))); NpmTasks.NpmRun(c => c From 6eeeb6c8eaa9f4969986ef878b75ce26817c6660 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 3 Sep 2020 22:25:43 +0100 Subject: [PATCH 025/149] skip building web previewer on osx. --- azure-pipelines.yml | 2 +- nukebuild/Build.cs | 2 +- nukebuild/BuildParameters.cs | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 89504a498e..29d2c62caf 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -81,7 +81,7 @@ jobs: export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - nuke --target CiAzureOSX --configuration Release + nuke --target CiAzureOSX --configuration Release --skip-previewer - task: PublishTestResults@2 inputs: diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 5b4e5a5bad..24398accbb 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -126,12 +126,12 @@ partial class Build : NukeBuild Target CompileHtmlPreviewer => _ => _ .DependsOn(Clean) + .OnlyWhenStatic(() => !Parameters.SkipPreviewer) .Executes(() => { var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; NpmTasks.NpmInstall(c => c - .AddPackages("typescript") .SetWorkingDirectory(webappDir) .SetArgumentConfigurator(a => a.Add("--silent"))); NpmTasks.NpmRun(c => c diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 149716b416..a167e9d892 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -19,10 +19,14 @@ public partial class Build [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } + [Parameter("skip-previewer")] + public bool SkipPreviewer { get; set; } + public class BuildParameters { public string Configuration { get; } public bool SkipTests { get; } + public bool SkipPreviewer {get;} public string MainRepo { get; } public string MasterBranch { get; } public string RepositoryName { get; } @@ -63,6 +67,7 @@ public partial class Build // ARGUMENTS Configuration = b.Configuration ?? "Release"; SkipTests = b.SkipTests; + SkipPreviewer = b.SkipPreviewer; // CONFIGURATION MainRepo = "https://github.com/AvaloniaUI/Avalonia"; From 67a87e09cf617f0e8c7c4916da50c610cf3cb677 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 15:48:27 +0100 Subject: [PATCH 026/149] use directx11. --- src/Avalonia.OpenGL/AngleOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 4b9c04f4e6..56435d2705 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -12,7 +12,7 @@ namespace Avalonia.OpenGL public List AllowedPlatformApis = new List { - PlatformApi.DirectX9 + PlatformApi.DirectX11 }; } } From 6ed1669803d62f7a02f75d89b0e0ad66b8d5d983 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 15:49:04 +0100 Subject: [PATCH 027/149] set a sensible cache size for skia.. based on what flutter uses. --- src/Skia/Avalonia.Skia/SkiaOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index cbe0b5ef42..d14120204c 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -16,6 +16,6 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. /// - public long? MaxGpuResourceSizeBytes { get; set; } + public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } } From 1eda1b6c1fbee01f7a09cc47d78c30f33a259111 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:13:18 +0100 Subject: [PATCH 028/149] better way to set default directx apis. --- src/Avalonia.OpenGL/AngleOptions.cs | 5 +---- src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 56435d2705..af1d1177ed 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -10,9 +10,6 @@ namespace Avalonia.OpenGL DirectX11 } - public List AllowedPlatformApis = new List - { - PlatformApi.DirectX11 - }; + public List AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 0436f6ac52..cbc9bbfb85 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,7 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; + ?? new List {AngleOptions.PlatformApi.DirectX11}; foreach (var platformApi in allowedApis) { From 531b64e4665184d9df041f2b6b4e911b22151861 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:35:43 +0100 Subject: [PATCH 029/149] dont allocate lists for angleoptions. --- src/Avalonia.OpenGL/AngleOptions.cs | 2 +- src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index af1d1177ed..84744288ed 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -10,6 +10,6 @@ namespace Avalonia.OpenGL DirectX11 } - public List AllowedPlatformApis { get; set; } = null; + public IList AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index cbc9bbfb85..2c3cadd620 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,7 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX11}; + ?? new [] {AngleOptions.PlatformApi.DirectX11}; foreach (var platformApi in allowedApis) { From a752c398bb809db466f68a5365142f20e40baa93 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:38:47 +0100 Subject: [PATCH 030/149] allow fallback to dx9. --- src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 2c3cadd620..89dee4fc37 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,7 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] {AngleOptions.PlatformApi.DirectX11}; + ?? new [] {AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9}; foreach (var platformApi in allowedApis) { From 8614c060028ded20b6fd5e72103425f929134cb8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:47:25 +0100 Subject: [PATCH 031/149] add WGL option. --- src/Avalonia.OpenGL/AngleOptions.cs | 3 ++- src/Avalonia.OpenGL/EglDisplay.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 84744288ed..40a673b574 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -7,7 +7,8 @@ namespace Avalonia.OpenGL public enum PlatformApi { DirectX9, - DirectX11 + DirectX11, + WGL } public IList AllowedPlatformApis { get; set; } = null; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 89dee4fc37..c84ef4533d 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -36,7 +36,12 @@ namespace Avalonia.OpenGL throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] {AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9}; + ?? new [] + { + AngleOptions.PlatformApi.WGL, + AngleOptions.PlatformApi.DirectX11, + AngleOptions.PlatformApi.DirectX9 + }; foreach (var platformApi in allowedApis) { @@ -45,6 +50,8 @@ namespace Avalonia.OpenGL dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; else if (platformApi == AngleOptions.PlatformApi.DirectX11) dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.WGL) + dapi = EGL_PLATFORM_ANGLE_TYPE_ANGLE; else continue; From fc12b9fee9ba06e48702340878ee79fe3ebb9a87 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 4 Sep 2020 16:50:41 +0100 Subject: [PATCH 032/149] define correct consts for opengl. --- src/Avalonia.OpenGL/EglConsts.cs | 3 +++ src/Avalonia.OpenGL/EglDisplay.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/EglConsts.cs index 62fb3faef6..5cc732ef6a 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/EglConsts.cs @@ -186,6 +186,9 @@ namespace Avalonia.OpenGL public const int EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE = 0x3206; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE = 0x345E; + + public const int EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D; + public const int EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE = 0x320E; //EGL_ANGLE_platform_angle_d3d public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE = 0x3209; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index c84ef4533d..55d0ebb59a 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -51,7 +51,7 @@ namespace Avalonia.OpenGL else if (platformApi == AngleOptions.PlatformApi.DirectX11) dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; else if (platformApi == AngleOptions.PlatformApi.WGL) - dapi = EGL_PLATFORM_ANGLE_TYPE_ANGLE; + dapi = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE; else continue; From 126737fd1bd5d182bdf803adfa7de949b77be0a1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Sep 2020 18:04:06 +0200 Subject: [PATCH 033/149] Update ScrollBar visibility with local value priority. Using `SetValue` with `Style` priority caused a new entry to be added to the `IsVisible` priority store each time the value was updated, causing a leak. Setting it with local value priority subtly changes its semantics but hopefully not so that anyone notices. --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index fc82fcc7a7..477d24dc99 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Primitives _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") }; - SetValue(IsVisibleProperty, isVisible, BindingPriority.Style); + SetValue(IsVisibleProperty, isVisible); } protected override void OnKeyDown(KeyEventArgs e) From 39a82ce66e2c8e2d11226d6a0e7a2557b20380d4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 5 Sep 2020 12:21:20 +0100 Subject: [PATCH 034/149] Remove WGL option. --- src/Avalonia.OpenGL/AngleOptions.cs | 3 +-- src/Avalonia.OpenGL/EglDisplay.cs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 40a673b574..84744288ed 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -7,8 +7,7 @@ namespace Avalonia.OpenGL public enum PlatformApi { DirectX9, - DirectX11, - WGL + DirectX11 } public IList AllowedPlatformApis { get; set; } = null; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 55d0ebb59a..ce5fd50a07 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -38,7 +38,6 @@ namespace Avalonia.OpenGL var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis ?? new [] { - AngleOptions.PlatformApi.WGL, AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 }; @@ -50,8 +49,6 @@ namespace Avalonia.OpenGL dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; else if (platformApi == AngleOptions.PlatformApi.DirectX11) dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.WGL) - dapi = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE; else continue; From 3c62c43cc689a2e727d3e15f7137f82fd26bfef7 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Sun, 6 Sep 2020 04:37:37 +0300 Subject: [PATCH 035/149] Hid maximize box on Windows for `CanResize = false` --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ddc0cc4e42..4929283874 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1007,10 +1007,12 @@ namespace Avalonia.Win32 if (newProperties.IsResizable) { style |= WindowStyles.WS_SIZEFRAME; + style |= WindowStyles.WS_MAXIMIZEBOX; } else { style &= ~WindowStyles.WS_SIZEFRAME; + style &= ~WindowStyles.WS_MAXIMIZEBOX; } SetStyle(style); From 8079ebe1dc3d96e4f2a23897071b16e26a9e505a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 10:49:48 +0200 Subject: [PATCH 036/149] Added failing test for #4599. --- tests/Avalonia.LeakTests/ControlTests.cs | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0b81276240..d61813b50b 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -552,6 +552,42 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ItemsRepeater_Is_Freed() + { + using (Start()) + { + var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; + + Func run = () => + { + var window = new Window + { + Content = new ItemsRepeater(), + }; + + window.Show(); + + window.LayoutManager.ExecuteInitialLayoutPass(); + Assert.IsType(window.Presenter.Child); + + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + + // We are keeping geometry alive to simulate a resource that outlives the control. + GC.KeepAlive(geometry); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow.With( From 60c0a44a8737da790d68c73f9d8036b72396aad3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 10:51:52 +0200 Subject: [PATCH 037/149] Clean up EffectiveViewportChanged earlier. Clean up the `EffectiveViewportChanged` subscriptions before calling `OnDetachedFromVisualTree` so that (un)subscribing to the `EffectiveViewportChanged` event in `OnDetachedFromVisualTree` doesn't cause a leak. --- src/Avalonia.Layout/Layoutable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index e62e22f8ec..aca2965ea6 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -758,8 +758,6 @@ namespace Avalonia.Layout protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTreeCore(e); - if (e.Root is ILayoutRoot r) { if (_layoutUpdated is object) @@ -772,6 +770,8 @@ namespace Avalonia.Layout r.LayoutManager.UnregisterEffectiveViewportListener(this); } } + + base.OnDetachedFromVisualTreeCore(e); } /// From 68504d212d068cf3786eb97199359be1222d4259 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 13:20:22 +0200 Subject: [PATCH 038/149] Use weak event handlers. Seems that C++/WinRT uses weak event handlers by default so we need to do the same here. --- .../Repeater/ItemsRepeater.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 8bc356bdec..772a17e4ea 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -7,10 +7,10 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -681,8 +681,15 @@ namespace Avalonia.Controls if (oldValue != null) { oldValue.UninitializeForContext(LayoutContext); - oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; - oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(newValue.MeasureInvalidated), + InvalidateMeasureForLayout); + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(newValue.ArrangeInvalidated), + InvalidateArrangeForLayout); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -699,8 +706,15 @@ namespace Avalonia.Controls if (newValue != null) { newValue.InitializeForContext(LayoutContext); - newValue.MeasureInvalidated += InvalidateMeasureForLayout; - newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.MeasureInvalidated), + InvalidateMeasureForLayout); + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.ArrangeInvalidated), + InvalidateArrangeForLayout); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; From e579e61f3820cd37e33258b3688cfc974477aa9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 8 Sep 2020 13:46:25 +0200 Subject: [PATCH 039/149] Teaks and remove copy-pasta. --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 8 ++++---- tests/Avalonia.LeakTests/ControlTests.cs | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 772a17e4ea..40f1b8dbb9 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -684,11 +684,11 @@ namespace Avalonia.Controls WeakEventHandlerManager.Unsubscribe( oldValue, - nameof(newValue.MeasureInvalidated), + nameof(AttachedLayout.MeasureInvalidated), InvalidateMeasureForLayout); WeakEventHandlerManager.Unsubscribe( oldValue, - nameof(newValue.ArrangeInvalidated), + nameof(AttachedLayout.ArrangeInvalidated), InvalidateArrangeForLayout); // Walk through all the elements and make sure they are cleared @@ -709,11 +709,11 @@ namespace Avalonia.Controls WeakEventHandlerManager.Subscribe( newValue, - nameof(newValue.MeasureInvalidated), + nameof(AttachedLayout.MeasureInvalidated), InvalidateMeasureForLayout); WeakEventHandlerManager.Subscribe( newValue, - nameof(newValue.ArrangeInvalidated), + nameof(AttachedLayout.ArrangeInvalidated), InvalidateArrangeForLayout); } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index d61813b50b..0c7b966f29 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -557,8 +557,6 @@ namespace Avalonia.LeakTests { using (Start()) { - var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - Func run = () => { var window = new Window @@ -582,9 +580,6 @@ namespace Avalonia.LeakTests dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - - // We are keeping geometry alive to simulate a resource that outlives the control. - GC.KeepAlive(geometry); } } From 04c781ca1431b3bc3d741d9ea864ad00569c33a9 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 8 Sep 2020 14:55:10 +0200 Subject: [PATCH 040/149] Make sure GlyphTypefaces are always cached --- Avalonia.sln | 28 +++++++- .../Presenters/TextPresenter.cs | 4 +- src/Avalonia.Controls/TextBlock.cs | 2 +- .../HeadlessPlatformStubs.cs | 4 +- src/Avalonia.Visuals/ApiCompatBaseline.txt | 12 +++- src/Avalonia.Visuals/Media/FontManager.cs | 67 ++++++------------- src/Avalonia.Visuals/Media/Fonts/FontKey.cs | 40 ----------- .../Media/TextFormatting/TextCharacters.cs | 7 +- src/Avalonia.Visuals/Media/Typeface.cs | 31 ++------- .../Platform/IFontManagerImpl.cs | 5 +- .../Rendering/RendererBase.cs | 2 +- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 7 +- .../Avalonia.Skia/SKTypefaceCollection.cs | 19 +++--- .../SKTypefaceCollectionCache.cs | 2 +- .../Media/FontManagerImpl.cs | 7 +- .../Media/FormattedTextImplTests.cs | 2 +- .../Media/CustomFontManagerImpl.cs | 8 +-- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Avalonia.UnitTests/MockFontManagerImpl.cs | 5 +- .../Media/FontManagerTests.cs | 7 +- 20 files changed, 103 insertions(+), 158 deletions(-) delete mode 100644 src/Avalonia.Visuals/Media/Fonts/FontKey.cs diff --git a/Avalonia.sln b/Avalonia.sln index 66777f33eb..ddcd61408d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -222,9 +222,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -2040,6 +2040,30 @@ Global {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 37490c3ef3..cb7bee1d33 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -282,7 +282,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Constraint = constraint, - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, @@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7e5287f81f..3b9e9c4751 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -452,7 +452,7 @@ namespace Avalonia.Controls return new TextLayout( text ?? string.Empty, - FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + new Typeface(FontFamily, FontStyle, FontWeight), FontSize, Foreground, TextAlignment, diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 763d192693..4c0e2982f4 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -155,9 +155,9 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { - fontKey = new FontKey("Arial", fontStyle, fontWeight); + typeface = new Typeface("Arial", fontStyle, fontWeight); return true; } } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 5058cff26d..5aa497861d 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,5 +1,15 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract. +CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract. +CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract. +TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. -Total Issues: 3 +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. +Total Issues: 13 diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index ad3fee7eb7..db87c7c6c4 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -13,8 +13,8 @@ namespace Avalonia.Media /// public sealed class FontManager { - private readonly ConcurrentDictionary _typefaceCache = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); private readonly FontFamily _defaultFontFamily; public FontManager(IFontManagerImpl platformImpl) @@ -76,79 +76,52 @@ namespace Avalonia.Media PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Returns a new typeface, or an existing one if a matching typeface exists. + /// Returns a new , or an existing one if a matching exists. /// - /// The font family. - /// The font style. - /// The font weight. + /// The typeface. /// - /// The typeface. + /// The . /// - public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, - FontWeight fontWeight = FontWeight.Normal) + public GlyphTypeface GetOrAddGlyphTypeface(Typeface typeface) { while (true) { - if (fontFamily.IsDefault) + if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface)) { - fontFamily = _defaultFontFamily; + return glyphTypeface; } - var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); + glyphTypeface = new GlyphTypeface(typeface); - if (_typefaceCache.TryGetValue(key, out var typeface)) + if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface)) { - return typeface; + return glyphTypeface; } - typeface = new Typeface(fontFamily, fontStyle, fontWeight); - - if (_typefaceCache.TryAdd(key, typeface)) - { - return typeface; - } - - if (fontFamily == _defaultFontFamily) + if (typeface.FontFamily == _defaultFontFamily) { return null; } - fontFamily = _defaultFontFamily; + typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight); } } /// - /// Tries to match a specified character to a typeface that supports specified font properties. - /// Returns null if no fallback was found. + /// Tries to match a specified character to a that supports specified font properties. /// /// The codepoint to match against. /// The font style. /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching . /// - /// The matched typeface. + /// True, if the could match the character to specified parameters, False otherwise. /// - public Typeface MatchCharacter(int codepoint, - FontStyle fontStyle = FontStyle.Normal, - FontWeight fontWeight = FontWeight.Normal, - FontFamily fontFamily = null, CultureInfo culture = null) - { - foreach (var cachedTypeface in _typefaceCache.Values) - { - // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. - if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight - && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) - { - return cachedTypeface; - } - } - - var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : - null; - - return matchedTypeface; - } + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) => + PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface); } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs deleted file mode 100644 index b330db8462..0000000000 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Avalonia.Media.Fonts -{ - public readonly struct FontKey : IEquatable - { - public FontKey(string familyName, FontStyle style, FontWeight weight) - { - FamilyName = familyName; - Style = style; - Weight = weight; - } - - public string FamilyName { get; } - public FontStyle Style { get; } - public FontWeight Weight { get; } - - public override int GetHashCode() - { - var hash = FamilyName.GetHashCode(); - - hash = hash * 31 + (int)Style; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey key && Equals(key); - } - - public bool Equals(FontKey other) - { - return FamilyName == other.FamilyName && - Style == other.Style && - Weight == other.Weight; - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 47e716982c..b91a50a27c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -70,10 +70,11 @@ namespace Avalonia.Media.TextFormatting var codepoint = Codepoint.ReadAt(text, count, out _); //ToDo: Fix FontFamily fallback - currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); + var matchFound = + FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, + defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) + if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { //Fallback found return new ShapeableTextCharacters(text.Take(count), diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 677e930804..17824c3c5e 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -8,17 +8,15 @@ namespace Avalonia.Media /// Represents a typeface. /// [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] - public class Typeface : IEquatable + public readonly struct Typeface : IEquatable { - private GlyphTypeface _glyphTypeface; - /// /// Initializes a new instance of the class. /// /// The font family. /// The font style. /// The font weight. - public Typeface([NotNull]FontFamily fontFamily, + public Typeface([NotNull] FontFamily fontFamily, FontStyle style = FontStyle.Normal, FontWeight weight = FontWeight.Normal) { @@ -45,7 +43,7 @@ namespace Avalonia.Media { } - public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + public static Typeface Default { get; } = new Typeface(FontFamily.Default); /// /// Gets the font family. @@ -68,7 +66,7 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); + public GlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this); public static bool operator !=(Typeface a, Typeface b) { @@ -77,32 +75,17 @@ namespace Avalonia.Media public static bool operator ==(Typeface a, Typeface b) { - if (ReferenceEquals(a, b)) - { - return true; - } - - return !(a is null) && a.Equals(b); + return a.Equals(b); } public override bool Equals(object obj) { - if (obj is Typeface typeface) - { - return Equals(typeface); - } - - return false; + return obj is Typeface typeface && Equals(typeface); } public bool Equals(Typeface other) { - if (other is null) - { - return false; - } - - return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight; + return FontFamily == other.FontFamily && Style == other.Style && Weight == other.Weight; } public override int GetHashCode() diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 59b08aae0a..e562b45ca8 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; namespace Avalonia.Platform { @@ -26,13 +25,13 @@ namespace Avalonia.Platform /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. - /// The matching font key. + /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + FontFamily fontFamily, CultureInfo culture, out Typeface typeface); /// /// Creates a glyph typeface. diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index b37d5d660b..5c9cace4cd 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering _useManualFpsCounting = useManualFpsCounting; _fpsText = new FormattedText { - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), + Typeface = new Typeface(FontFamily.Default), FontSize = s_fontSize }; } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 91bc937475..62ec39346a 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; @@ -31,7 +30,7 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) { SKFontStyle skFontStyle; @@ -81,7 +80,7 @@ namespace Avalonia.Skia continue; } - fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); return true; } @@ -92,7 +91,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 6c2ac17923..71deb1235f 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -2,31 +2,28 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Avalonia.Media; -using Avalonia.Media.Fonts; using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary _typefaces = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddTypeface(FontKey key, SKTypeface typeface) + public void AddTypeface(Typeface key, SKTypeface typeface) { _typefaces.TryAdd(key, typeface); } public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); - - return GetNearestMatch(_typefaces, key); + return GetNearestMatch(_typefaces, typeface); } - private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary typefaces, Typeface key) { - if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) + if (typefaces.TryGetValue(key, out var typeface)) { return typeface; } @@ -42,7 +39,7 @@ namespace Avalonia.Skia { if (weight - j >= 100) { - if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) + if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) { return typeface; } @@ -53,7 +50,7 @@ namespace Avalonia.Skia continue; } - if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) { return typeface; } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 7ca44e7282..de77c9186e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -56,7 +56,7 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily.Name, typeface.FontSlant.ToAvalonia(), + var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 33af15076d..6d95d759ec 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; @@ -34,7 +33,7 @@ namespace Avalonia.Direct2D1.Media public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -51,12 +50,12 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fontFamilyName, fontStyle, fontWeight); return true; } - fontKey = default; + typeface = default; return false; } diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 8683da9a01..7528424521 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight), + new Typeface(fontFamily, fontStyle, fontWeight), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index f36d6d9e4a..a0fe348166 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -39,7 +39,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out FontKey fontKey) + CultureInfo culture, out Typeface typeface) { foreach (var customTypeface in _customTypefaces) { @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight); + typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight); return true; } @@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); return true; } @@ -73,13 +73,13 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } - case "Noto Sans": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); skTypeface = typefaceCollection.Get(typeface); break; } + case FontFamily.DefaultFontFamilyName: case "Noto Mono": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 4a88b259bc..ea806e01fe 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -130,7 +130,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - const string text = "1234الدولي"; + const string text = "ぁぁぁぁالدولي"; var defaultProperties = new GenericTextRunProperties(Typeface.Default); diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index e614c60310..ba3b346f1b 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.UnitTests @@ -26,9 +25,9 @@ namespace Avalonia.UnitTests } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out FontKey fontKey) + CultureInfo culture, out Typeface fontKey) { - fontKey = default; + fontKey = new Typeface(_defaultFamilyName); return false; } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index 81a4ca6495..2b0ffa4ed6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.UnitTests; using Xunit; @@ -15,9 +14,11 @@ namespace Avalonia.Visuals.UnitTests.Media { var fontFamily = new FontFamily("MyFont"); - var typeface = FontManager.Current.GetOrAddTypeface(fontFamily); + var typeface = new Typeface(fontFamily); - Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); + var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + + Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); } } From cf7b2966fe126845cc7019f5700c34eaf8652c85 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 19:34:08 +0100 Subject: [PATCH 041/149] brew extract. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 29d2c62caf..92e4d9b7fb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -61,7 +61,7 @@ jobs: inputs: script: | brew update - brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb + brew extract castxml https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - task: CmdLine@2 displayName: 'Install Nuke' From 71eaddfedbf4d5abd951966f076862a03d64e68c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 19:47:51 +0100 Subject: [PATCH 042/149] dont install castxml on osx. --- azure-pipelines.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4d9b7fb..721a0415f4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -56,13 +56,6 @@ jobs: xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - - task: CmdLine@2 - displayName: 'Install CastXML' - inputs: - script: | - brew update - brew extract castxml https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - - task: CmdLine@2 displayName: 'Install Nuke' inputs: From 588d4377eb85bca48b647b5f826e47f739079c29 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 5 Sep 2020 16:53:07 +0100 Subject: [PATCH 043/149] Radio button and checkbox vertically center the content. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 2 +- src/Avalonia.Themes.Fluent/RadioButton.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 5f82fed08c..0ba6fb81c2 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -12,7 +12,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index acde4ea0be..35cc902d91 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -17,7 +17,7 @@ - + From dd9c4666a97620571c93536e9543cd5a11aee32d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 21:16:43 +0100 Subject: [PATCH 044/149] clarify max skia cache size property. --- src/Skia/Avalonia.Skia/SkiaOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index d14120204c..afd17acf97 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -15,6 +15,8 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. + /// This is set by default to the recommended value for Avalonia. + /// Setting this to null will give you the default Skia value. /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } From d26445fdcdbb2970e4c61ed51c7c41f7fd55fc1b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 8 Sep 2020 21:31:47 +0100 Subject: [PATCH 045/149] use remarks tag. --- src/Skia/Avalonia.Skia/SkiaOptions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index afd17acf97..493263677d 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -15,9 +15,11 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. + /// + /// /// This is set by default to the recommended value for Avalonia. /// Setting this to null will give you the default Skia value. - /// + /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } } From c8e5c1762d7e4ef3806f47f8b47cd45f34156f18 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 Sep 2020 09:51:16 +0100 Subject: [PATCH 046/149] budget surfaces. --- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27b29c6e1e..ed9b06cc97 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -51,7 +51,7 @@ namespace Avalonia.Skia { var imageInfo = MakeImageInfo(width, height, format); if (gpu != null) - return SKSurface.Create(gpu, false, imageInfo); + return SKSurface.Create(gpu, true, imageInfo); return SKSurface.Create(imageInfo); } From ff0a6f0518570169c3e9363afdb4e49421c8feb6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 Sep 2020 10:36:52 +0100 Subject: [PATCH 047/149] dispose old layer before creating new one. --- src/Avalonia.Visuals/Rendering/RenderLayer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Visuals/Rendering/RenderLayer.cs index d6676e25ff..ddf5f4e5cf 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayer.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayer.cs @@ -30,12 +30,13 @@ namespace Avalonia.Rendering { if (Size != size || Scaling != scaling) { + Bitmap.Dispose(); var resized = RefCountable.Create(drawingContext.CreateLayer(size)); using (var context = resized.Item.CreateDrawingContext(null)) { context.Clear(Colors.Transparent); - Bitmap.Dispose(); + Bitmap = resized; Scaling = scaling; Size = size; From a8100d9df53a7159a7aa6aa8ae48b959e726928e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 Sep 2020 10:59:04 +0100 Subject: [PATCH 048/149] Revert "budget surfaces." This reverts commit c8e5c1762d7e4ef3806f47f8b47cd45f34156f18. --- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index ed9b06cc97..27b29c6e1e 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -51,7 +51,7 @@ namespace Avalonia.Skia { var imageInfo = MakeImageInfo(width, height, format); if (gpu != null) - return SKSurface.Create(gpu, true, imageInfo); + return SKSurface.Create(gpu, false, imageInfo); return SKSurface.Create(imageInfo); } From 54fb09a304de8f748f7dbfa0dabb7033e818ca14 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 9 Sep 2020 15:40:36 +0200 Subject: [PATCH 049/149] Fix unit test --- .../Media/TextFormatting/TextFormatterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index ea806e01fe..adcc79e029 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -130,7 +130,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - const string text = "ぁぁぁぁالدولي"; + const string text = "ABCDالدولي"; var defaultProperties = new GenericTextRunProperties(Typeface.Default); From 556b934d77877b9bb546ce4b0fcc6910963ffb84 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Sep 2020 08:58:36 +0200 Subject: [PATCH 050/149] Store args in ClassicDesktopStyleApplicationLifetime. --- .../ClassicDesktopStyleApplicationLifetime.cs | 14 ++++++++++---- .../IClassicDesktopStyleApplicationLifetime.cs | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 6dd5b8cc81..e2c8e7e8e2 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -47,6 +47,11 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public event EventHandler Exit; + /// + /// Gets the arguments passed to the AppBuilder Start method. + /// + public string[] Args { get; set; } + /// public ShutdownMode ShutdownMode { get; set; } @@ -68,9 +73,6 @@ namespace Avalonia.Controls.ApplicationLifetimes else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) Shutdown(); } - - - public void Shutdown(int exitCode = 0) { @@ -123,7 +125,11 @@ namespace Avalonia this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) where T : AppBuilderBase, new() { - var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode}; + var lifetime = new ClassicDesktopStyleApplicationLifetime() + { + Args = args, + ShutdownMode = shutdownMode + }; builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a1006d907b..212f0b8617 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Gets the arguments passed to the + /// + /// method. + /// + string[] Args { get; } + /// /// Gets or sets the . This property indicates whether the application is shutdown explicitly or implicitly. /// If is set to OnExplicitShutdown the application is only closes if Shutdown is called. From 7b8a6226028657d9c2486d59e24aad5064b24611 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Sep 2020 11:16:00 +0200 Subject: [PATCH 051/149] Update API compat baseline. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 11708b360f..af88c569a6 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -12,7 +12,9 @@ MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.Interactivity.RoutedEvent Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args.get()' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. -Total Issues: 16 +Total Issues: 18 From ed7a75acbcb72c890932bace587fe55326804d65 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Sep 2020 12:51:55 +0200 Subject: [PATCH 052/149] Ensure windows set to fullscreen are shown. Previously, `UnmanagedMethods.ShowWindow` wasn't called when trying to show a window with `WindowState="FullScreen"`. This is because win32 conflates showing a window and setting a window state, and setting all windows states except fullscreen require a call to `ShowWindow`, except fullscreen which does *not*. However it does require a call to `ShowWindow` when we are actually showing the window ;) --- src/Windows/Avalonia.Win32/WindowImpl.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4929283874..9c6bce1c90 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -853,7 +853,7 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state) { - ShowWindowCommand command; + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -875,8 +875,8 @@ namespace Avalonia.Win32 case WindowState.FullScreen: newWindowProperties.IsFullScreen = true; - UpdateWindowProperties(newWindowProperties); - return; + command = IsWindowVisible(_hwnd) ? (ShowWindowCommand?)null : ShowWindowCommand.Restore; + break; default: throw new ArgumentException("Invalid WindowState."); @@ -884,7 +884,10 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); - UnmanagedMethods.ShowWindow(_hwnd, command); + if (command.HasValue) + { + UnmanagedMethods.ShowWindow(_hwnd, command.Value); + } if (state == WindowState.Maximized) { From 70d062ba39e618d5084720324d5996e2609465d7 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Thu, 10 Sep 2020 16:26:47 +0100 Subject: [PATCH 053/149] make code more readable. --- src/Avalonia.OpenGL/Angle/AngleEglInterface.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs index 375b93c27c..3ec6ba1772 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -18,11 +18,18 @@ namespace Avalonia.OpenGL.Angle static Func LoadAngle() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { throw new PlatformNotSupportedException(); + } + else { var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (disp == IntPtr.Zero) + { throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); + } + return eglGetProcAddress; } } From ecfd4bf56107b6235a316611d0a11673cd63db32 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 Sep 2020 17:14:49 +0100 Subject: [PATCH 054/149] remove padding from checkbox and radiobutton. --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 1 - src/Avalonia.Themes.Fluent/RadioButton.xaml | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 0ba6fb81c2..04799b3bc2 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -8,7 +8,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 35cc902d91..8fd569816d 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -13,7 +13,6 @@ - From c48c82f171ecb4658e10474636505216547c5ec3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 Sep 2020 17:17:23 +0100 Subject: [PATCH 055/149] formatting. --- src/Avalonia.OpenGL/Angle/AngleEglInterface.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs index 3ec6ba1772..8565d99b45 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -17,21 +17,19 @@ namespace Avalonia.OpenGL.Angle static Func LoadAngle() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - throw new PlatformNotSupportedException(); - } - else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); - + if (disp == IntPtr.Zero) { throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); } - + return eglGetProcAddress; } + + throw new PlatformNotSupportedException(); } } From acab1102081e3d09c359b79174e8706ac127f34b Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 8 Sep 2020 16:57:38 -0500 Subject: [PATCH 056/149] PopupRoot IFocusScope --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index da7352b77f..2721ab879f 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -14,7 +15,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost + public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost, IFocusScope { private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; From 4e2dc7f5dbb9a2c38ce9e24a75318b5b32b38758 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 8 Sep 2020 17:41:25 -0500 Subject: [PATCH 057/149] Add test --- .../Primitives/PopupTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index d9176ca55d..a0b7368a4e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -395,6 +395,34 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Focusable_Controls_In_Popup_Should_Get_Focus() + { + using (CreateServicesWithFocus()) + { + var tb = new TextBox(); + var b = new Button(); + var p = new Popup + { + PlacementTarget = PreparedWindow(), + Child = new StackPanel + { + Children = + { + tb, + b + } + } + }; + ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + + p.Open(); + tb.Focus(); + + Assert.True(FocusManager.Instance?.Current == tb); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -407,6 +435,21 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private IDisposable CreateServicesWithFocus() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + x => + { + if (UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock(x).Object; + }), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); + } + + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p) { var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); From 05c6978617d87c30ae7cfb6a541c397b1547e400 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 10 Sep 2020 16:29:16 -0500 Subject: [PATCH 058/149] Try to get test to pass on CI --- tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index a0b7368a4e..e5dcba9912 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -416,8 +416,12 @@ namespace Avalonia.Controls.UnitTests.Primitives }; ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + p.Opened += (s, e) => + { + tb.Focus(); + }; + p.Open(); - tb.Focus(); Assert.True(FocusManager.Instance?.Current == tb); } From 7782261ec3b3148fac846e1b0e77c36672b4f0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 11 Sep 2020 03:26:58 +0100 Subject: [PATCH 059/149] Added typed AvaloniaProperty.Changed. --- src/Avalonia.Base/ApiCompatBaseline.txt | 3 +++ src/Avalonia.Base/AvaloniaProperty.cs | 17 +++--------- src/Avalonia.Base/AvaloniaProperty`1.cs | 27 +++++++++++++++++++ .../Mixins/SelectableMixin.cs | 4 +-- src/Avalonia.Controls/NativeMenu.Export.cs | 2 +- src/Avalonia.Controls/NativeMenuItem.cs | 2 +- .../Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- .../AvaloniaObjectTests_Direct.cs | 2 +- .../AvaloniaPropertyTests.cs | 4 +-- 10 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/ApiCompatBaseline.txt diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt new file mode 100644 index 0000000000..4668a572c5 --- /dev/null +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Base: +CannotAddAbstractMembers : Member 'protected System.IObservable Avalonia.AvaloniaProperty.GetChanged()' is abstract in the implementation but is missing in the contract. +Total Issues: 1 diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 39391490b0..3ae0445e9b 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -18,7 +17,6 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); @@ -50,7 +48,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _changed = new Subject(); _metadata = new Dictionary(); Name = name; @@ -77,7 +74,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _changed = source._changed; _metadata = new Dictionary(); Name = source.Name; @@ -139,7 +135,7 @@ namespace Avalonia /// An observable that is fired when this property changes on any /// instance. /// - public IObservable Changed => _changed; + public IObservable Changed => GetChanged(); /// /// Gets a method that gets called before and after the property starts being notified on an @@ -474,15 +470,6 @@ namespace Avalonia public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) where TData : struct; - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e) - { - _changed.OnNext(e); - } - /// /// Routes an untyped ClearValue call to a typed call. /// @@ -553,6 +540,8 @@ namespace Avalonia _hasMetadataOverrides = true; } + protected abstract IObservable GetChanged(); + private PropertyMetadata GetMetadataWithOverrides(Type type) { if (type is null) diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 2f26d855f2..7480d9c9c5 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Utilities; @@ -10,6 +11,8 @@ namespace Avalonia /// The value type of the property. public abstract class AvaloniaProperty : AvaloniaProperty { + private readonly Subject> _changed; + /// /// Initializes a new instance of the class. /// @@ -24,6 +27,7 @@ namespace Avalonia Action notifying = null) : base(name, typeof(TValue), ownerType, metadata, notifying) { + _changed = new Subject>(); } /// @@ -38,8 +42,31 @@ namespace Avalonia PropertyMetadata metadata) : base(source, ownerType, metadata) { + _changed = source is AvaloniaProperty p ? p._changed : new Subject>(); } + /// + /// Gets an observable that is fired when this property changes on any + /// instance. + /// + /// + /// An observable that is fired when this property changes on any + /// instance. + /// + + public new IObservable> Changed => _changed; + + /// + /// Notifies the observable. + /// + /// The observable arguments. + internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e) + { + _changed.OnNext(e); + } + + protected override IObservable GetChanged() => Changed; + protected BindingValue TryConvert(object value) { if (value == UnsetValue) diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index d2586ab6e8..e7dbecb06e 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Mixins { Contract.Requires(isSelected != null); - isSelected.Changed.Subscribe(x => + isSelected.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => { var sender = x.Sender as TControl; @@ -58,4 +58,4 @@ namespace Avalonia.Controls.Mixins }); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 776e9d2171..89e4c9e492 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("IsNativeMenuExported property is read-only"); info.ChangingIsExported = false; }); - MenuProperty.Changed.Subscribe(args => + MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => { if (args.Sender is TopLevel tl) { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 4c94d82eb4..d4badbc559 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls static NativeMenuItem() { - MenuProperty.Changed.Subscribe(args => + MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => { var item = (NativeMenuItem)args.Sender; var value = (NativeMenu)args.NewValue; diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index cb7bee1d33..f5115a2f7c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls.Presenters TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); - Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, + Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 3b9e9c4751..d8477840af 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -138,7 +138,7 @@ namespace Avalonia.Controls FontStyleProperty, TextWrappingProperty, FontFamilyProperty, TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty); - Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, + Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, FontWeightProperty.Changed, diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 81a8de1046..83ae663419 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -92,7 +92,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); bool raised = false; - Class1.FooProperty.Changed.Subscribe(e => + Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs e) => raised = e.Property == Class1.FooProperty && (string)e.OldValue == "initial" && (string)e.NewValue == "newvalue" && diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index d7f927372e..19040ff584 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); string value = null; - Class1.FooProperty.Changed.Subscribe(x => value = (string)x.NewValue); + Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => value = (string)x.NewValue); target.SetValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", value); @@ -95,7 +95,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var result = new List(); - Class1.FooProperty.Changed.Subscribe(x => result.Add((string)x.NewValue)); + Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => result.Add((string)x.NewValue)); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "local"); From ca5ec1fba49884af7edba49a982e0b5d849bd8a9 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Fri, 11 Sep 2020 00:22:56 -0400 Subject: [PATCH 060/149] Add left padding for radiobutton and checkbox --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 1 + src/Avalonia.Themes.Fluent/RadioButton.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 04799b3bc2..73c44c02ab 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 8fd569816d..078f51c87a 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -13,6 +13,7 @@ + From 5b252620b8d87343c20471ea19902c013945f9bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 15:36:32 +0200 Subject: [PATCH 061/149] Update ncrunch config. --- Avalonia.v3.ncrunchsolution | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution index bef7e45524..afce1018ec 100644 --- a/Avalonia.v3.ncrunchsolution +++ b/Avalonia.v3.ncrunchsolution @@ -6,6 +6,9 @@ src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll True + + RunApiCompat = false + .ncrunch True From a0dfa32ba5bb4d12d24f262f74d866099adf2f70 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 15:49:31 +0200 Subject: [PATCH 062/149] Added failing test for #4654. --- .../TabControlTests.cs | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index fd52aeb9af..b4378938de 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -122,11 +122,56 @@ namespace Avalonia.Controls.UnitTests Items = collection, }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItem = collection[1]; + + Assert.Same(collection[1], target.SelectedItem); + Assert.Equal(collection[1].Content, target.SelectedContent); + collection.RemoveAt(1); Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0].Content, target.SelectedContent); + } + + [Fact] + public void Removal_Should_Set_New_Item0_When_Item0_Selected() + { + var collection = new ObservableCollection() + { + new TabItem + { + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }; + + var target = new TabControl + { + Template = TabControlTemplate(), + Items = collection, + }; + + Prepare(target); + target.SelectedItem = collection[0]; + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0].Content, target.SelectedContent); + + collection.RemoveAt(0); + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0].Content, target.SelectedContent); } [Fact] @@ -349,6 +394,8 @@ namespace Avalonia.Controls.UnitTests } } + + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => @@ -383,6 +430,13 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private void Prepare(TabControl target) + { + ApplyTemplate(target); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + } + private void ApplyTemplate(TabControl target) { target.ApplyTemplate(); From 10b3c880927e9effeedc2a546b66a987ce3293be Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 17:03:31 +0200 Subject: [PATCH 063/149] Added failing SelectedItem/Index property changed tests. --- .../Primitives/SelectingItemsControlTests.cs | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 00d148093a..c6615f04f9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -554,6 +554,44 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(items.Single().IsSelected); } + [Fact] + public void Removing_Selected_Item_Should_Update_Selection_With_AlwaysSelected() + { + var item0 = new Item(); + var item1 = new Item(); + var items = new AvaloniaList + { + item0, + item1, + }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + SelectionMode = SelectionMode.AlwaysSelected, + }; + + Prepare(target); + target.SelectedIndex = 1; + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + items.RemoveAt(1); + + Assert.Same(item0, target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { item0 }, receivedArgs.AddedItems); + Assert.Equal(new[] { item1 }, receivedArgs.RemovedItems); + Assert.True(items.Single().IsSelected); + } + [Fact] public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit() { @@ -771,6 +809,186 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(called); } + [Fact] + public void Setting_SelectedIndex_Should_Raise_PropertyChanged_Events() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(-1, e.OldValue); + Assert.Equal(1, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Null(e.OldValue); + Assert.Equal("bar", e.NewValue); + ++selectedItemRaised; + } + }; + + target.SelectedIndex = 1; + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); + } + + [Fact] + public void Removing_Selected_Item_Should_Raise_PropertyChanged_Events() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 1; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(1, e.OldValue); + Assert.Equal(-1, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Equal("bar", e.OldValue); + Assert.Null(e.NewValue); + } + }; + + items.RemoveAt(1); + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(0, selectedItemRaised); + } + + [Fact] + public void Removing_Selected_Item0_Should_Raise_PropertyChanged_Events_With_AlwaysSelected() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 0; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Equal("foo", e.OldValue); + Assert.Equal("bar", e.NewValue); + ++selectedItemRaised; + } + }; + + items.RemoveAt(0); + + Assert.Equal(0, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); + } + + [Fact] + public void Removing_Selected_Item1_Should_Raise_PropertyChanged_Events_With_AlwaysSelected() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 1; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(1, e.OldValue); + Assert.Equal(0, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Equal("bar", e.OldValue); + Assert.Equal("foo", e.NewValue); + } + }; + + items.RemoveAt(1); + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(0, selectedItemRaised); + } + + [Fact] + public void Removing_Item_Before_Selection_Should_Raise_PropertyChanged_Events() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 1; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(1, e.OldValue); + Assert.Equal(0, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + ++selectedItemRaised; + } + }; + + items.RemoveAt(0); + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(0, selectedItemRaised); + } + [Fact] public void Order_Of_Setting_Items_And_SelectedIndex_During_Initialization_Should_Not_Matter() { From 5ac25a26f4ee09fd37fd2b3fc386134e884a30a7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 17:08:22 +0200 Subject: [PATCH 064/149] Tweak raising SelectedIndex/Item property changed. --- .../Primitives/SelectingItemsControl.cs | 2 +- .../Selection/SelectionModel.cs | 42 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5f8c5da2f8..fcef3b1d08 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -613,7 +613,7 @@ namespace Avalonia.Controls.Primitives RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex); _oldSelectedIndex = SelectedIndex; } - else if (e.PropertyName == nameof(ISelectionModel.SelectedItem)) + else if (e.PropertyName == nameof(ISelectionModel.SelectedItem) && _oldSelectedItem != SelectedItem) { RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem); _oldSelectedItem = SelectedItem; diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 2556bd4c4c..2181b322b4 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -345,7 +345,9 @@ namespace Avalonia.Controls.Selection LostSelection(this, EventArgs.Empty); } - CommitOperation(update.Operation); + // Don't raise PropertyChanged events here as the OnSourceCollectionChanged event that + // let to this method being called will raise them if necessary. + CommitOperation(update.Operation, raisePropertyChanged: false); } private protected override CollectionChangeState OnItemsAdded(int index, IList items) @@ -430,6 +432,11 @@ namespace Avalonia.Controls.Selection RaisePropertyChanged(nameof(SelectedIndex)); } + if (e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) + { + RaisePropertyChanged(nameof(SelectedItem)); + } + if (oldAnchorIndex != _anchorIndex) { RaisePropertyChanged(nameof(AnchorIndex)); @@ -611,7 +618,7 @@ namespace Avalonia.Controls.Selection } } - private void CommitOperation(Operation operation) + private void CommitOperation(Operation operation, bool raisePropertyChanged = true) { try { @@ -679,23 +686,26 @@ namespace Avalonia.Controls.Selection } } - if (oldSelectedIndex != _selectedIndex) + if (raisePropertyChanged) { - indexesChanged = true; - RaisePropertyChanged(nameof(SelectedIndex)); - RaisePropertyChanged(nameof(SelectedItem)); - } + if (oldSelectedIndex != _selectedIndex) + { + indexesChanged = true; + RaisePropertyChanged(nameof(SelectedIndex)); + RaisePropertyChanged(nameof(SelectedItem)); + } - if (oldAnchorIndex != _anchorIndex) - { - indexesChanged = true; - RaisePropertyChanged(nameof(AnchorIndex)); - } + if (oldAnchorIndex != _anchorIndex) + { + indexesChanged = true; + RaisePropertyChanged(nameof(AnchorIndex)); + } - if (indexesChanged) - { - RaisePropertyChanged(nameof(SelectedIndexes)); - RaisePropertyChanged(nameof(SelectedItems)); + if (indexesChanged) + { + RaisePropertyChanged(nameof(SelectedIndexes)); + RaisePropertyChanged(nameof(SelectedItems)); + } } } finally From 90dc7ea95238ab696316b89fd00066d13f4a0541 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 17:33:15 +0200 Subject: [PATCH 065/149] Fix failing test but actual issue not fixed. Fixed the failing test for #4654 but actual issue still remains. Needs more test. --- src/Avalonia.Controls/TabControl.cs | 55 ++++++----------------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index f81e355a7d..f7e7126324 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -66,7 +67,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent()); } /// @@ -145,55 +146,21 @@ namespace Avalonia.Controls protected override void OnContainersMaterialized(ItemContainerEventArgs e) { base.OnContainersMaterialized(e); - - if (SelectedContent != null || SelectedIndex == -1) - { - return; - } - - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); - - if (container == null) - { - return; - } - - UpdateSelectedContent(container); - } - - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) - { - var index = (int)e.NewValue; - - if (index == -1) - { - SelectedContentTemplate = null; - - SelectedContent = null; - - return; - } - - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); - - if (container == null) - { - return; - } - - UpdateSelectedContent(container); + UpdateSelectedContent(); } - private void UpdateSelectedContent(IContentControl item) + private void UpdateSelectedContent() { - if (SelectedContentTemplate != item.ContentTemplate) + if (SelectedIndex == -1) { - SelectedContentTemplate = item.ContentTemplate; + SelectedContent = SelectedContentTemplate = null; } - - if (SelectedContent != item.Content) + else { - SelectedContent = item.Content; + var container = SelectedItem as IContentControl ?? + ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl; + SelectedContentTemplate = container?.ContentTemplate; + SelectedContent = container?.Content; } } From a38bdcf146a8f9f4647d20634b3e7f8689e781d6 Mon Sep 17 00:00:00 2001 From: "Artyom V. Gorchakov" Date: Fri, 11 Sep 2020 18:49:48 +0300 Subject: [PATCH 066/149] Correct ReactiveUI.Events Package Id --- .../Avalonia.ReactiveUI.Events.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj index b95c8946e2..75eeb92f42 100644 --- a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj +++ b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj @@ -1,7 +1,7 @@  netstandard2.0 - Avalonia.ReactiveUI + Avalonia.ReactiveUI.Events From f4017162cd5ca4c65cd92a2a7f7a90c638505640 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 20:34:42 +0200 Subject: [PATCH 067/149] Add additional failing test for #4654. --- .../TabControlTests.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index b4378938de..e6f7ac601f 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -174,6 +174,36 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(collection[0].Content, target.SelectedContent); } + [Fact] + public void Removal_Should_Set_New_Item0_When_Item0_Selected_With_DataTemplate() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var collection = new ObservableCollection() + { + new Item("first"), + new Item("second"), + new Item("3rd"), + }; + + var target = new TabControl + { + Template = TabControlTemplate(), + Items = collection, + }; + + Prepare(target); + target.SelectedItem = collection[0]; + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0], target.SelectedContent); + + collection.RemoveAt(0); + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0], target.SelectedContent); + } + [Fact] public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate() { @@ -394,8 +424,6 @@ namespace Avalonia.Controls.UnitTests } } - - private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => From 82273680f7bcb29c098c0f240e082bbf1fb43928 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Sep 2020 20:35:11 +0200 Subject: [PATCH 068/149] Update selected content when containers recycled. Hopefully fixes #4654 once and for all. --- src/Avalonia.Controls/TabControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index f7e7126324..306a9d3e6a 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -149,6 +149,12 @@ namespace Avalonia.Controls UpdateSelectedContent(); } + protected override void OnContainersRecycled(ItemContainerEventArgs e) + { + base.OnContainersRecycled(e); + UpdateSelectedContent(); + } + private void UpdateSelectedContent() { if (SelectedIndex == -1) From 6275a1ca27078db13cf5a553b43d4b085a7395f1 Mon Sep 17 00:00:00 2001 From: artyom Date: Sat, 12 Sep 2020 00:29:14 +0300 Subject: [PATCH 069/149] Don't test code that is generated at nukebuild time --- Avalonia.sln | 27 ------------ nukebuild/Build.cs | 1 - ...valonia.ReactiveUI.Events.UnitTests.csproj | 15 ------- .../BasicControlEventsTest.cs | 44 ------------------- 4 files changed, 87 deletions(-) delete mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj delete mode 100644 tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs diff --git a/Avalonia.sln b/Avalonia.sln index ddcd61408d..aeae8f2f1f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -224,8 +224,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" -EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2040,30 +2038,6 @@ Global {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2122,7 +2096,6 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 24398accbb..7e2bbc13bc 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -241,7 +241,6 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); - RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj deleted file mode 100644 index 19a6fd138e..0000000000 --- a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - netcoreapp3.1 - - - - - - - - - - - - diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs b/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs deleted file mode 100644 index 1092c98246..0000000000 --- a/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.UnitTests; -using Xunit; - -namespace Avalonia.ReactiveUI.Events.UnitTests -{ - public class BasicControlEventsTest - { - public class EventsControl : UserControl - { - public bool IsAttached { get; private set; } - - public EventsControl() - { - var attached = this - .Events() - .AttachedToVisualTree - .Select(args => true); - - this.Events() - .DetachedFromVisualTree - .Select(args => false) - .Merge(attached) - .Subscribe(marker => IsAttached = marker); - } - } - - [Fact] - public void Should_Generate_Events_Wrappers() - { - var root = new TestRoot(); - var control = new EventsControl(); - Assert.False(control.IsAttached); - - root.Child = control; - Assert.True(control.IsAttached); - - root.Child = null; - Assert.False(control.IsAttached); - } - } -} From b30120770c6fce6b581b9f47424df011930db005 Mon Sep 17 00:00:00 2001 From: artyom Date: Sat, 12 Sep 2020 00:52:23 +0300 Subject: [PATCH 070/149] Ignore Events_Avalonia.cs which is autogenerated --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b5a46e16f4..7d672c7755 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ ClientBin/ *.[Pp]ublish.xml *.pfx *.publishsettings +Events_Avalonia.cs # RIA/Silverlight projects Generated_Code/ From d84a98f65ac04f03f40bbc4b33c9201fc660e1cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 12 Sep 2020 00:17:15 +0200 Subject: [PATCH 071/149] Added failing test for #4272. --- .../Primitives/SelectingItemsControlTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index c6615f04f9..b6619aaa73 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1594,6 +1594,31 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "foo" }, target.SelectedItems); } + [Fact] + public void Preserves_Initial_SelectedItems_When_Bound() + { + // Issue #4272 (there are two issues there, this addresses the second one). + var vm = new SelectionViewModel + { + Items = { "foo", "bar", "baz" }, + SelectedItems = { "bar" }, + }; + + var target = new ListBox + { + [!ListBox.ItemsProperty] = new Binding("Items"), + [!ListBox.SelectedItemsProperty] = new Binding("SelectedItems"), + DataContext = vm, + }; + + Prepare(target); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot @@ -1663,6 +1688,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public SelectionViewModel() { Items = new ObservableCollection(); + SelectedItems = new ObservableCollection(); } public int SelectedIndex @@ -1676,6 +1702,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } } private class RootWithItems : TestRoot From 12f0fc046a892922c6c2b683c467a5a95f720931 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sat, 12 Sep 2020 11:47:12 +0500 Subject: [PATCH 072/149] Update Microsoft.NETFramework.ReferenceAssemblies --- build/NetFX.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/NetFX.props b/build/NetFX.props index ed5cb6dd69..8ffc9ec561 100644 --- a/build/NetFX.props +++ b/build/NetFX.props @@ -1,7 +1,7 @@  - + From 4f161701bd1713fe5f2b88a420fb04eb1b7591f8 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Sun, 13 Sep 2020 05:01:33 -0400 Subject: [PATCH 073/149] Do not override BindingMode in the DataGridBoundColumn --- src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 8e82bf1a38..440a277a10 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -49,8 +49,13 @@ namespace Avalonia.Controls { if(_binding is Avalonia.Data.Binding binding) { + if (binding.Mode == BindingMode.OneWayToSource) + { + throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource"); + } + // Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. - if (!String.IsNullOrEmpty(binding.Path)) + if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default) { binding.Mode = BindingMode.TwoWay; } From f8e75bf424b7697ac94bb6bebb440ce6c7b2ab26 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Sun, 13 Sep 2020 05:19:01 -0400 Subject: [PATCH 074/149] Remove obsolete comments --- src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 440a277a10..1e72a07760 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -51,10 +51,9 @@ namespace Avalonia.Controls { if (binding.Mode == BindingMode.OneWayToSource) { - throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource"); + throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead."); } - // Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default) { binding.Mode = BindingMode.TwoWay; From e1045a09c9a173450afe5c241d583134336cef8d Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sun, 13 Sep 2020 21:30:19 +0500 Subject: [PATCH 075/149] Improve for the backend web preview --- .../Remote/HtmlTransport/HtmlTransport.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 55e2df8890..03dcf85b87 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -38,11 +38,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri) { if (listenUri.Scheme != "http") - throw new ArgumentException("listenUri"); + throw new ArgumentException("URI scheme is not HTTP.", nameof(listenUri)); var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build."; _resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames() - .Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary( + .Where(r => r.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase) + && r.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) + .ToDictionary( r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3), r => { From 22580652fa6f0893c90f1db544dd69eafb980f13 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Sep 2020 10:11:30 +0200 Subject: [PATCH 076/149] More failing SelectionModel tests. --- .../Selection/SelectionModelTests_Multiple.cs | 56 +++++++++++++++++++ .../Selection/SelectionModelTests_Single.cs | 56 ++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index 3eddd35465..5d0c6d31e1 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -121,6 +121,34 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(0, raised); } + [Fact] + public void Initializing_Source_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(false); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + target.Select(2); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } + [Fact] public void Initializing_Source_Respects_Range_SourceItem_Order() { @@ -152,6 +180,34 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal("bar", target.SelectedItem); Assert.Equal(new[] { "bar" }, target.SelectedItems); } + + [Fact] + public void Changing_Source_To_Null_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + target.Select(2); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = null; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } } public class SelectedIndex diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 1b37730797..66a2cef921 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -174,6 +174,33 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(new[] { "bar" }, target.SelectedItems); } + [Fact] + public void Initializing_Source_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(false); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } + [Fact] public void Changing_Source_To_Null_Doesnt_Clear_Selection() { @@ -194,7 +221,7 @@ namespace Avalonia.Controls.UnitTests.Selection } [Fact] - public void Changing_Source_To_NonNUll_First_Clears_Old_Selection() + public void Changing_Source_To_NonNull_First_Clears_Old_Selection() { var target = CreateTarget(); var raised = 0; @@ -219,6 +246,33 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, raised); } + [Fact] + public void Changing_Source_To_Null_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = null; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } + [Fact] public void Raises_PropertyChanged() { From aeaaccb7e0fe239f9994cc01d753b99211e057c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Sep 2020 10:17:01 +0200 Subject: [PATCH 077/149] Raise item(s) property changed on Source changing. --- src/Avalonia.Controls/Selection/SelectionModel.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 2181b322b4..326bd10655 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -692,6 +692,10 @@ namespace Avalonia.Controls.Selection { indexesChanged = true; RaisePropertyChanged(nameof(SelectedIndex)); + } + + if (oldSelectedIndex != _selectedIndex || operation.IsSourceUpdate) + { RaisePropertyChanged(nameof(SelectedItem)); } @@ -704,6 +708,10 @@ namespace Avalonia.Controls.Selection if (indexesChanged) { RaisePropertyChanged(nameof(SelectedIndexes)); + } + + if (indexesChanged || operation.IsSourceUpdate) + { RaisePropertyChanged(nameof(SelectedItems)); } } From c01c7c937838265fc8031934c1bca7703ec9952b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 15 Sep 2020 02:50:27 -0700 Subject: [PATCH 078/149] opengl es 3.1, 3.0 and 2.0 --- src/Avalonia.OpenGL/EglDisplay.cs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 7b194e4346..ca7e9b66c3 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -67,13 +67,38 @@ namespace Avalonia.OpenGL { Attributes = new[] { - EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_CONTEXT_MAJOR_VERSION, 3, + EGL_CONTEXT_MINOR_VERSION, 1, + EGL_NONE + }, + Api = EGL_OPENGL_ES_API, + RenderableTypeBit = EGL_OPENGL_ES3_BIT, + Version = new GlVersion(GlProfileType.OpenGLES, 3, 1) + }, + new + { + Attributes = new[] + { + EGL_CONTEXT_MAJOR_VERSION, 3, + EGL_CONTEXT_MINOR_VERSION, 0, + EGL_NONE + }, + Api = EGL_OPENGL_ES_API, + RenderableTypeBit = EGL_OPENGL_ES3_BIT, + Version = new GlVersion(GlProfileType.OpenGLES, 3, 0) + }, + new + { + Attributes = new[] + { + EGL_CONTEXT_MAJOR_VERSION, 2, + EGL_CONTEXT_MINOR_VERSION, 0, EGL_NONE }, Api = EGL_OPENGL_ES_API, RenderableTypeBit = EGL_OPENGL_ES2_BIT, Version = new GlVersion(GlProfileType.OpenGLES, 2, 0) - } + }, }) { if (!_egl.BindApi(cfg.Api)) From a6d83cd32a8e373b7b00f58b18b2947f135da3fa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 15 Sep 2020 06:12:47 -0700 Subject: [PATCH 079/149] support any gles version with angle using angle options. --- src/Avalonia.OpenGL/AngleOptions.cs | 8 +++ src/Avalonia.OpenGL/EglDisplay.cs | 63 +++++++++++----------- src/Avalonia.X11/Glx/GlxDisplay.cs | 2 +- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 4 +- src/Avalonia.X11/X11Platform.cs | 4 +- 5 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 84744288ed..462bd56cbe 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -10,6 +10,14 @@ namespace Avalonia.OpenGL DirectX11 } + public IList GlProfiles { get; set; } = new List + { + new GlVersion(GlProfileType.OpenGLES, 3, 2), + new GlVersion(GlProfileType.OpenGLES, 3, 1), + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; + public IList AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index ca7e9b66c3..9edcaf2bc0 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; using static Avalonia.OpenGL.EglConsts; @@ -61,45 +62,45 @@ namespace Avalonia.OpenGL if (!_egl.Initialize(_display, out var major, out var minor)) throw OpenGlException.GetFormattedException("eglInitialize", _egl); - foreach (var cfg in new[] + var glProfiles = AvaloniaLocator.Current.GetService()?.GlProfiles + ?? new[] + { + new GlVersion(GlProfileType.OpenGLES, 3, 2), + new GlVersion(GlProfileType.OpenGLES, 3, 1), + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; + + var cfgs = glProfiles.Select(x => { - new - { - Attributes = new[] - { - EGL_CONTEXT_MAJOR_VERSION, 3, - EGL_CONTEXT_MINOR_VERSION, 1, - EGL_NONE - }, - Api = EGL_OPENGL_ES_API, - RenderableTypeBit = EGL_OPENGL_ES3_BIT, - Version = new GlVersion(GlProfileType.OpenGLES, 3, 1) - }, - new + var typeBit = EGL_OPENGL_ES3_BIT; + + switch (x.Major) { - Attributes = new[] - { - EGL_CONTEXT_MAJOR_VERSION, 3, - EGL_CONTEXT_MINOR_VERSION, 0, - EGL_NONE - }, - Api = EGL_OPENGL_ES_API, - RenderableTypeBit = EGL_OPENGL_ES3_BIT, - Version = new GlVersion(GlProfileType.OpenGLES, 3, 0) - }, - new + case 2: + typeBit = EGL_OPENGL_ES2_BIT; + break; + + case 1: + typeBit = EGL_OPENGL_ES_BIT; + break; + } + + return new { Attributes = new[] { - EGL_CONTEXT_MAJOR_VERSION, 2, - EGL_CONTEXT_MINOR_VERSION, 0, + EGL_CONTEXT_MAJOR_VERSION, x.Major, + EGL_CONTEXT_MINOR_VERSION, x.Minor, EGL_NONE }, Api = EGL_OPENGL_ES_API, - RenderableTypeBit = EGL_OPENGL_ES2_BIT, - Version = new GlVersion(GlProfileType.OpenGLES, 2, 0) - }, - }) + RenderableTypeBit = typeBit, + Version = x + }; + }); + + foreach (var cfg in cfgs) { if (!_egl.BindApi(cfg.Api)) continue; diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 903d6b570b..b82895d12c 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -18,7 +18,7 @@ namespace Avalonia.X11.Glx public XVisualInfo* VisualInfo => _visual; public GlxContext DeferredContext { get; } public GlxInterface Glx { get; } = new GlxInterface(); - public GlxDisplay(X11Info x11, List probeProfiles) + public GlxDisplay(X11Info x11, IList probeProfiles) { _x11 = x11; _probeProfiles = probeProfiles.ToList(); diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index e3250e6733..ad3a54bcc1 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -12,7 +12,7 @@ namespace Avalonia.X11.Glx public GlxContext DeferredContext { get; private set; } public IGlContext MainContext => DeferredContext; - public static bool TryInitialize(X11Info x11, List glProfiles) + public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) @@ -24,7 +24,7 @@ namespace Avalonia.X11.Glx return false; } - public static GlxGlPlatformFeature TryCreate(X11Info x11, List glProfiles) + public static GlxGlPlatformFeature TryCreate(X11Info x11, IList glProfiles) { try { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7f3255d4da..d7bd81db98 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,7 +103,7 @@ namespace Avalonia public bool UseDBusMenu { get; set; } public bool UseDeferredRendering { get; set; } = true; - public List GlProfiles { get; set; } = new List + public IList GlProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), new GlVersion(GlProfileType.OpenGL, 3, 2), @@ -113,7 +113,7 @@ namespace Avalonia new GlVersion(GlProfileType.OpenGLES, 2, 0) }; - public List GlxRendererBlacklist { get; set; } = new List + public IList GlxRendererBlacklist { get; set; } = new List { // llvmpipe is a software GL rasterizer. If it's returned by glGetString, // that usually means that something in the system is horribly misconfigured From b73ba990777c9727cad4efcd64b968c33b86f336 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Sep 2020 15:16:34 +0200 Subject: [PATCH 080/149] Reafactor SelectingItemsControl selection. - Remove `SelectedItemsSync` and store `SelectedItems` in a new `InternalSelectionModel` - Store transient `SelectingItemsControl` state in an `UpdateState` object Fixes #4272 --- .../Primitives/SelectingItemsControl.cs | 225 +++++++++++--- .../Selection/InternalSelectionModel.cs | 251 ++++++++++++++++ .../Selection/SelectionModel.cs | 56 +++- .../Utils/SelectedItemsSync.cs | 283 ------------------ .../Selection/InternalSelectionModelTests.cs | 243 +++++++++++++++ .../Utils/SelectedItemsSyncTests.cs | 278 ----------------- 6 files changed, 712 insertions(+), 624 deletions(-) create mode 100644 src/Avalonia.Controls/Selection/InternalSelectionModel.cs delete mode 100644 src/Avalonia.Controls/Utils/SelectedItemsSync.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index fcef3b1d08..3e106afdc0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Selection; -using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; @@ -70,8 +69,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - protected static readonly DirectProperty SelectedItemsProperty = - AvaloniaProperty.RegisterDirect( + protected static readonly DirectProperty SelectedItemsProperty = + AvaloniaProperty.RegisterDirect( nameof(SelectedItems), o => o.SelectedItems, (o, v) => o.SelectedItems = v); @@ -111,12 +110,11 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); - private SelectedItemsSync? _selectedItemsSync; private ISelectionModel? _selection; private int _oldSelectedIndex; private object? _oldSelectedItem; - private int _initializing; private bool _ignoreContainerSelectionChanged; + private UpdateState? _updateState; /// /// Initializes static members of the class. @@ -149,8 +147,23 @@ namespace Avalonia.Controls.Primitives /// public int SelectedIndex { - get => Selection.SelectedIndex; - set => Selection.SelectedIndex = value; + get + { + return _updateState?.SelectedIndex.HasValue == true ? + _updateState.SelectedIndex.Value : + Selection.SelectedIndex; + } + set + { + if (_updateState is object) + { + _updateState.SelectedIndex = value; + } + else + { + Selection.SelectedIndex = value; + } + } } /// @@ -158,17 +171,56 @@ namespace Avalonia.Controls.Primitives /// public object? SelectedItem { - get => Selection.SelectedItem; - set => Selection.SelectedItem = value; + get + { + return _updateState?.SelectedItem.HasValue == true ? + _updateState.SelectedItem.Value : + Selection.SelectedItem; + } + set + { + if (_updateState is object) + { + _updateState.SelectedItem = value; + } + else + { + Selection.SelectedItem = value; + } + } } /// /// Gets or sets the selected items. /// - protected IList SelectedItems + /// + /// By default returns a collection that can be modified in order to manipulate the control + /// selection, however this property will return null if is + /// re-assigned; you should only use _either_ Selection or SelectedItems. + /// + protected IList? SelectedItems { - get => SelectedItemsSync.SelectedItems; - set => SelectedItemsSync.SelectedItems = value; + get + { + return _updateState?.SelectedItems.HasValue == true ? + _updateState.SelectedItems.Value : + (Selection as InternalSelectionModel)?.SelectedItems; + } + set + { + if (_updateState is object) + { + _updateState.SelectedItems = new Optional(value); + } + else if (Selection is InternalSelectionModel i) + { + i.SelectedItems = value; + } + else + { + throw new InvalidOperationException("Cannot set both Selection and SelectedItems."); + } + } } /// @@ -178,19 +230,30 @@ namespace Avalonia.Controls.Primitives { get { - if (_selection is null) + if (_updateState?.Selection.HasValue == true) { - _selection = CreateDefaultSelectionModel(); - InitializeSelectionModel(_selection); + return _updateState.Selection.Value; } + else + { + if (_selection is null) + { + _selection = CreateDefaultSelectionModel(); + InitializeSelectionModel(_selection); + } - return _selection; + return _selection; + } } set { value ??= CreateDefaultSelectionModel(); - if (_selection != value) + if (_updateState is object) + { + _updateState.Selection = new Optional(value); + } + else if (_selection != value) { if (value.Source != null && value.Source != Items) { @@ -234,20 +297,18 @@ namespace Avalonia.Controls.Primitives /// protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; - private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection); - /// public override void BeginInit() { base.BeginInit(); - ++_initializing; + BeginUpdating(); } /// public override void EndInit() { base.EndInit(); - --_initializing; + EndUpdating(); } /// @@ -351,30 +412,14 @@ namespace Avalonia.Controls.Primitives protected override void OnDataContextBeginUpdate() { base.OnDataContextBeginUpdate(); - ++_initializing; - - if (_selection is object) - { - _selection.Source = null; - } + BeginUpdating(); } /// protected override void OnDataContextEndUpdate() { base.OnDataContextEndUpdate(); - --_initializing; - - if (_selection is object && _initializing == 0) - { - _selection.Source = Items; - - if (Items is null) - { - _selection.Clear(); - _selectedItemsSync?.SelectedItems?.Clear(); - } - } + EndUpdating(); } protected override void OnInitialized() @@ -411,9 +456,7 @@ namespace Avalonia.Controls.Primitives { base.OnPropertyChanged(change); - if (change.Property == ItemsProperty && - _initializing == 0 && - _selection is object) + if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); _selection.Source = newValue; @@ -789,7 +832,7 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new SelectionModel + return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; @@ -797,7 +840,7 @@ namespace Avalonia.Controls.Primitives private void InitializeSelectionModel(ISelectionModel model) { - if (_initializing == 0) + if (_updateState is null) { model.Source = Items; } @@ -825,9 +868,6 @@ namespace Avalonia.Controls.Primitives UpdateContainerSelection(); - _selectedItemsSync ??= new SelectedItemsSync(model); - _selectedItemsSync.SelectionModel = model; - if (SelectedIndex != -1) { RaiseEvent(new SelectionChangedEventArgs( @@ -845,5 +885,96 @@ namespace Avalonia.Controls.Primitives model.SelectionChanged -= OnSelectionModelSelectionChanged; } } + + private void BeginUpdating() + { + _updateState ??= new UpdateState(); + _updateState.UpdateCount++; + } + + private void EndUpdating() + { + if (_updateState is object && --_updateState.UpdateCount == 0) + { + var state = _updateState; + _updateState = null; + + if (state.Selection.HasValue) + { + Selection = state.Selection.Value; + } + + if (state.SelectedItems.HasValue) + { + SelectedItems = state.SelectedItems.Value; + } + + Selection.Source = Items; + + if (Items is null) + { + Selection.Clear(); + } + + if (state.SelectedIndex.HasValue) + { + SelectedIndex = state.SelectedIndex.Value; + } + else if (state.SelectedItem.HasValue) + { + SelectedItem = state.SelectedItem.Value; + } + } + } + + // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to + // defer changes to the selection model because we have no idea in which order properties + // will be set. Consider: + // + // - Both Items and SelectedItem are bound + // - The DataContext changes + // - The binding for SelectedItem updates first, producing an item + // - Items is searched to find the index of the new selected item + // - However Items isn't yet updated; the item is not found + // - SelectedIndex is incorrectly set to -1 + // + // This logic cannot be encapsulated in SelectionModel because the selection model can also + // be bound, consider: + // + // - Both Items and Selection are bound + // - The DataContext changes + // - The binding for Items updates first + // - The new items are assigned to Selection.Source + // - The binding for Selection updates, producing a new SelectionModel + // - Both the old and new SelectionModels have the incorrect Source + private class UpdateState + { + private Optional _selectedIndex; + private Optional _selectedItem; + + public int UpdateCount { get; set; } + public Optional Selection { get; set; } + public Optional SelectedItems { get; set; } + + public Optional SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + _selectedItem = default; + } + } + + public Optional SelectedItem + { + get => _selectedItem; + set + { + _selectedItem = value; + _selectedIndex = default; + } + } + } } } diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs new file mode 100644 index 0000000000..dd7b7d25cd --- /dev/null +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Controls.Selection +{ + internal class InternalSelectionModel : SelectionModel + { + private IList? _selectedItems; + private bool _ignoreModelChanges; + private bool _ignoreSelectedItemsChanges; + + public InternalSelectionModel() + { + SelectionChanged += OnSelectionChanged; + SourceReset += OnSourceReset; + } + + [AllowNull] + public new IList SelectedItems + { + get + { + if (_selectedItems is null) + { + _selectedItems = new AvaloniaList(); + SubscribeToSelectedItems(); + } + + return _selectedItems; + } + set + { + value ??= new AvaloniaList(); + + if (value.IsFixedSize) + { + throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems."); + } + + if (_selectedItems != value) + { + UnsubscribeFromSelectedItems(); + _selectedItems = value; + SyncFromSelectedItems(); + SubscribeToSelectedItems(); + + if (ItemsView is null) + { + SetInitSelectedItems(value); + } + } + } + } + + private protected override void SetSource(IEnumerable? value) + { + try + { + _ignoreSelectedItemsChanges = true; + base.SetSource(value); + } + finally + { + _ignoreSelectedItemsChanges = false; + } + + SyncToSelectedItems(); + } + + private void SyncToSelectedItems() + { + if (_selectedItems is object) + { + try + { + _ignoreSelectedItemsChanges = true; + _selectedItems.Clear(); + + foreach (var i in base.SelectedItems) + { + _selectedItems.Add(i); + } + } + finally + { + _ignoreSelectedItemsChanges = false; + } + } + } + + private void SyncFromSelectedItems() + { + if (Source is null || _selectedItems is null) + { + return; + } + + try + { + _ignoreModelChanges = true; + + using (BatchUpdate()) + { + Clear(); + Add(_selectedItems); + } + } + finally + { + _ignoreModelChanges = false; + } + } + + private void SubscribeToSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnSelectedItemsCollectionChanged; + } + } + + private void UnsubscribeFromSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnSelectedItemsCollectionChanged; + } + } + + private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + if (_ignoreModelChanges) + { + return; + } + + try + { + var items = SelectedItems; + var deselected = e.DeselectedItems.ToList(); + var selected = e.SelectedItems.ToList(); + + _ignoreSelectedItemsChanges = true; + + foreach (var i in deselected) + { + items.Remove(i); + } + + foreach (var i in selected) + { + items.Add(i); + } + } + finally + { + _ignoreSelectedItemsChanges = false; + } + } + + private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems(); + + private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (_ignoreSelectedItemsChanges) + { + return; + } + + if (_selectedItems == null) + { + throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); + } + + void Remove() + { + foreach (var i in e.OldItems) + { + var index = IndexOf(Source, i); + + if (index != -1) + { + Deselect(index); + } + } + } + + try + { + using var operation = BatchUpdate(); + + _ignoreModelChanges = true; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(); + break; + case NotifyCollectionChangedAction.Replace: + Remove(); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + Clear(); + Add(_selectedItems); + break; + } + } + finally + { + _ignoreModelChanges = false; + } + } + + private void Add(IList newItems) + { + foreach (var i in newItems) + { + var index = IndexOf(Source, i); + + if (index != -1) + { + Select(index); + } + } + } + + private static int IndexOf(object? source, object? item) + { + if (source is IList l) + { + return l.IndexOf(item); + } + else if (source is ItemsSourceView v) + { + return v.IndexOf(item); + } + + return -1; + } + } +} diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 326bd10655..fd27cb340a 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -20,8 +20,7 @@ namespace Avalonia.Controls.Selection private SelectedItems? _selectedItems; private SelectedItems.Untyped? _selectedItemsUntyped; private EventHandler? _untypedSelectionChanged; - [AllowNull] private T _initSelectedItem = default; - private bool _hasInitSelectedItem; + private IList? _initSelectedItems; public SelectionModel() { @@ -82,7 +81,19 @@ namespace Avalonia.Controls.Selection [MaybeNull, AllowNull] public T SelectedItem { - get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem; + get + { + if (ItemsView is object) + { + return GetItemAt(_selectedIndex); + } + else if (_initSelectedItems is object && _initSelectedItems.Count > 0) + { + return (T)_initSelectedItems[0]; + } + + return default; + } set { if (ItemsView is object) @@ -92,8 +103,9 @@ namespace Avalonia.Controls.Selection else { Clear(); - _initSelectedItem = value; - _hasInitSelectedItem = true; +#pragma warning disable CS8601 + SetInitSelectedItems(new T[] { value }); +#pragma warning restore CS8601 } } } @@ -102,9 +114,10 @@ namespace Avalonia.Controls.Selection { get { - if (ItemsView is null && _hasInitSelectedItem) + if (ItemsView is null && _initSelectedItems is object) { - return new[] { _initSelectedItem }; + return _initSelectedItems is IReadOnlyList i ? + i : _initSelectedItems.Cast().ToList(); } return _selectedItems ??= new SelectedItems(this); @@ -258,8 +271,7 @@ namespace Avalonia.Controls.Selection o.SelectedIndex = -1; } - _initSelectedItem = default; - _hasInitSelectedItem = false; + _initSelectedItems = null; } public void SelectAll() => SelectRange(0, int.MaxValue); @@ -270,7 +282,7 @@ namespace Avalonia.Controls.Selection PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - private void SetSource(IEnumerable? value) + private protected virtual void SetSource(IEnumerable? value) { if (base.Source != value) { @@ -292,11 +304,14 @@ namespace Avalonia.Controls.Selection { update.Operation.IsSourceUpdate = true; - if (_hasInitSelectedItem) + if (_initSelectedItems is object && ItemsView is object) { - SelectedItem = _initSelectedItem; - _initSelectedItem = default; - _hasInitSelectedItem = false; + foreach (T i in _initSelectedItems) + { + Select(ItemsView.IndexOf(i)); + } + + _initSelectedItems = null; } else { @@ -466,6 +481,16 @@ namespace Avalonia.Controls.Selection return true; } + private protected void SetInitSelectedItems(IList items) + { + if (Source is object) + { + throw new InvalidOperationException("Cannot set init selected items when Source is set."); + } + + _initSelectedItems = items; + } + protected override void OnSourceCollectionChangeFinished() { if (_operation is object) @@ -539,8 +564,7 @@ namespace Avalonia.Controls.Selection o.SelectedIndex = o.AnchorIndex = start; } - _initSelectedItem = default; - _hasInitSelectedItem = false; + _initSelectedItems = null; } [return: MaybeNull] diff --git a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs deleted file mode 100644 index 83b62c7b6e..0000000000 --- a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using Avalonia.Collections; -using Avalonia.Controls.Selection; - -#nullable enable - -namespace Avalonia.Controls.Utils -{ - /// - /// Synchronizes an with a list of SelectedItems. - /// - internal class SelectedItemsSync : IDisposable - { - private ISelectionModel _selectionModel; - private IList _selectedItems; - private bool _updatingItems; - private bool _updatingModel; - - public SelectedItemsSync(ISelectionModel model) - { - _selectionModel = model ?? throw new ArgumentNullException(nameof(model)); - _selectedItems = new AvaloniaList(); - SyncSelectedItemsWithSelectionModel(); - SubscribeToSelectedItems(_selectedItems); - SubscribeToSelectionModel(model); - } - - public ISelectionModel SelectionModel - { - get => _selectionModel; - set - { - if (_selectionModel != value) - { - value = value ?? throw new ArgumentNullException(nameof(value)); - UnsubscribeFromSelectionModel(_selectionModel); - _selectionModel = value; - SubscribeToSelectionModel(_selectionModel); - SyncSelectedItemsWithSelectionModel(); - } - } - } - - public IList SelectedItems - { - get => _selectedItems; - set - { - value ??= new AvaloniaList(); - - if (_selectedItems != value) - { - if (value.IsFixedSize) - { - throw new NotSupportedException( - "Cannot assign fixed size selection to SelectedItems."); - } - - UnsubscribeFromSelectedItems(_selectedItems); - _selectedItems = value; - SubscribeToSelectedItems(_selectedItems); - SyncSelectionModelWithSelectedItems(); - } - } - } - - public void Dispose() - { - UnsubscribeFromSelectedItems(_selectedItems); - UnsubscribeFromSelectionModel(_selectionModel); - } - - private void SyncSelectedItemsWithSelectionModel() - { - _updatingItems = true; - - try - { - _selectedItems.Clear(); - - if (_selectionModel.Source is object) - { - foreach (var i in _selectionModel.SelectedItems) - { - _selectedItems.Add(i); - } - } - } - finally - { - _updatingItems = false; - } - } - - private void SyncSelectionModelWithSelectedItems() - { - _updatingModel = true; - - try - { - if (_selectionModel.Source is object) - { - using (_selectionModel.BatchUpdate()) - { - SelectionModel.Clear(); - Add(_selectedItems); - } - } - } - finally - { - _updatingModel = false; - } - } - - private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_updatingItems) - { - return; - } - - if (_selectedItems == null) - { - throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); - } - - void Remove() - { - foreach (var i in e.OldItems) - { - var index = IndexOf(SelectionModel.Source, i); - - if (index != -1) - { - SelectionModel.Deselect(index); - } - } - } - - try - { - using var operation = SelectionModel.BatchUpdate(); - - _updatingModel = true; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Add(e.NewItems); - break; - case NotifyCollectionChangedAction.Remove: - Remove(); - break; - case NotifyCollectionChangedAction.Replace: - Remove(); - Add(e.NewItems); - break; - case NotifyCollectionChangedAction.Reset: - SelectionModel.Clear(); - Add(_selectedItems); - break; - } - } - finally - { - _updatingModel = false; - } - } - - private void Add(IList newItems) - { - foreach (var i in newItems) - { - var index = IndexOf(SelectionModel.Source, i); - - if (index != -1) - { - SelectionModel.Select(index); - } - } - } - - private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(ISelectionModel.Source)) - { - if (_selectedItems.Count > 0) - { - SyncSelectionModelWithSelectedItems(); - } - else - { - SyncSelectedItemsWithSelectionModel(); - } - } - } - - private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) - { - if (_updatingModel || _selectionModel.Source is null) - { - return; - } - - try - { - var deselected = e.DeselectedItems.ToList(); - var selected = e.SelectedItems.ToList(); - - _updatingItems = true; - - foreach (var i in deselected) - { - _selectedItems.Remove(i); - } - - foreach (var i in selected) - { - _selectedItems.Add(i); - } - } - finally - { - _updatingItems = false; - } - } - - private void SelectionModelSourceReset(object sender, EventArgs e) - { - SyncSelectionModelWithSelectedItems(); - } - - - private void SubscribeToSelectedItems(IList selectedItems) - { - if (selectedItems is INotifyCollectionChanged incc) - { - incc.CollectionChanged += SelectedItemsCollectionChanged; - } - } - - private void SubscribeToSelectionModel(ISelectionModel model) - { - model.PropertyChanged += SelectionModelPropertyChanged; - model.SelectionChanged += SelectionModelSelectionChanged; - model.SourceReset += SelectionModelSourceReset; - } - - private void UnsubscribeFromSelectedItems(IList selectedItems) - { - if (selectedItems is INotifyCollectionChanged incc) - { - incc.CollectionChanged -= SelectedItemsCollectionChanged; - } - } - - private void UnsubscribeFromSelectionModel(ISelectionModel model) - { - model.PropertyChanged -= SelectionModelPropertyChanged; - model.SelectionChanged -= SelectionModelSelectionChanged; - model.SourceReset -= SelectionModelSourceReset; - } - - private static int IndexOf(object? source, object? item) - { - if (source is IList l) - { - return l.IndexOf(item); - } - else if (source is ItemsSourceView v) - { - return v.IndexOf(item); - } - - return -1; - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs new file mode 100644 index 0000000000..0f3ecef969 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Controls.Selection; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Selection +{ + public class InternalSelectionModelTests + { + [Fact] + public void Selecting_Item_Adds_To_SelectedItems() + { + var target = CreateTarget(); + + target.Select(0); + + Assert.Equal(new[] { "foo" }, target.SelectedItems); + } + + [Fact] + public void Selecting_Duplicate_On_Model_Adds_To_SelectedItems() + { + var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); + + target.SelectRange(1, 4); + + Assert.Equal(new[] { "bar", "baz", "foo", "bar" }, target.SelectedItems); + } + + [Fact] + public void Deselecting_On_Model_Removes_SelectedItem() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.Deselect(1); + + Assert.Equal(new[] { "baz" }, target.SelectedItems); + } + + [Fact] + public void Deselecting_Duplicate_On_Model_Removes_SelectedItem() + { + var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); + + target.SelectRange(1, 2); + target.Select(4); + target.Deselect(4); + + Assert.Equal(new[] { "baz", "bar" }, target.SelectedItems); + } + + [Fact] + public void Adding_To_SelectedItems_Selects_On_Model() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.SelectedItems.Add("foo"); + + Assert.Equal(new[] { 0, 1, 2 }, target.SelectedIndexes); + Assert.Equal(new[] { "bar", "baz", "foo" }, target.SelectedItems); + } + + [Fact] + public void Removing_From_SelectedItems_Deselects_On_Model() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.SelectedItems.Remove("baz"); + + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } + + [Fact] + public void Replacing_SelectedItem_Updates_Model() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.SelectedItems[0] = "foo"; + + Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); + Assert.Equal(new[] { "foo", "baz" }, target.SelectedItems); + } + + [Fact] + public void Clearing_SelectedItems_Updates_Model() + { + var target = CreateTarget(); + + target.SelectedItems.Clear(); + + Assert.Empty(target.SelectedIndexes); + } + + [Fact] + public void Setting_SelectedItems_Updates_Model() + { + var target = CreateTarget(); + var oldItems = target.SelectedItems; + + var newItems = new AvaloniaList { "foo", "baz" }; + target.SelectedItems = newItems; + + Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); + Assert.Same(newItems, target.SelectedItems); + Assert.NotSame(oldItems, target.SelectedItems); + Assert.Equal(new[] { "foo", "baz" }, newItems); + } + + [Fact] + public void Setting_Items_To_Null_Clears_Selection() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.SelectedItems = null; + + Assert.Empty(target.SelectedIndexes); + } + + [Fact] + public void Setting_Items_To_Null_Creates_Empty_Items() + { + var target = CreateTarget(); + var oldItems = target.SelectedItems; + + target.SelectedItems = null; + + Assert.NotNull(target.SelectedItems); + Assert.NotSame(oldItems, target.SelectedItems); + Assert.IsType>(target.SelectedItems); + } + + [Fact] + public void Adds_Null_SelectedItems_When_Source_Is_Null() + { + var target = CreateTarget(nullSource: true); + + target.SelectRange(1, 2); + Assert.Equal(new object[] { null, null }, target.SelectedItems); + } + + [Fact] + public void Updates_SelectedItems_When_Source_Changes_From_Null() + { + var target = CreateTarget(nullSource: true); + + target.SelectRange(1, 2); + Assert.Equal(new object[] { null, null }, target.SelectedItems); + + target.Source = new[] { "foo", "bar", "baz" }; + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); + } + + [Fact] + public void Updates_SelectedItems_When_Source_Changes_To_Null() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); + + target.Source = null; + Assert.Equal(new object[] { null, null }, target.SelectedItems); + } + + [Fact] + public void SelectedItems_Can_Be_Set_Before_Source() + { + var target = CreateTarget(nullSource: true); + var items = new AvaloniaList { "foo", "bar", "baz" }; + var selectedItems = new AvaloniaList { "bar" }; + + target.SelectedItems = selectedItems; + target.Source = items; + + Assert.Equal(1, target.SelectedIndex); + } + + [Fact] + public void Does_Not_Accept_Fixed_Size_Items() + { + var target = CreateTarget(); + + Assert.Throws(() => + target.SelectedItems = new[] { "foo", "bar", "baz" }); + } + + [Fact] + public void Restores_Selection_On_Items_Reset() + { + var items = new ResettingCollection(new[] { "foo", "bar", "baz" }); + var target = CreateTarget(source: items); + + target.SelectedIndex = 1; + items.Reset(new[] { "baz", "foo", "bar" }); + + Assert.Equal(2, target.SelectedIndex); + } + + private static InternalSelectionModel CreateTarget( + bool singleSelect = false, + IList source = null, + bool nullSource = false) + { + source ??= !nullSource ? new[] { "foo", "bar", "baz" } : null; + + var result = new InternalSelectionModel + { + SingleSelect = singleSelect, + }; + + ((ISelectionModel)result).Source = source; + return result; + } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(IEnumerable items) + { + AddRange(items); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs deleted file mode 100644 index 3899d9dfbf..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using Avalonia.Collections; -using Avalonia.Controls.Selection; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Utils -{ - public class SelectedItemsSyncTests - { - [Fact] - public void Initial_Items_Are_From_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - Assert.Equal(new[] { "bar", "baz" }, items); - } - - [Fact] - public void Selecting_On_Model_Adds_Item() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - target.SelectionModel.Select(0); - - Assert.Equal(new[] { "bar", "baz", "foo" }, items); - } - - [Fact] - public void Selecting_Duplicate_On_Model_Adds_Item() - { - var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var items = target.SelectedItems; - - target.SelectionModel.Select(4); - - Assert.Equal(new[] { "bar", "baz", "bar" }, items); - } - - [Fact] - public void Deselecting_On_Model_Removes_Item() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - target.SelectionModel.Deselect(1); - - Assert.Equal(new[] { "baz" }, items); - } - - [Fact] - public void Deselecting_Duplicate_On_Model_Removes_Item() - { - var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var items = target.SelectedItems; - - target.SelectionModel.Select(4); - target.SelectionModel.Deselect(4); - - Assert.Equal(new[] { "baz", "bar" }, items); - } - - [Fact] - public void Reassigning_Model_Resets_Items() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - var newModel = new SelectionModel - { - Source = (string[])target.SelectionModel.Source, - SingleSelect = false - }; - - newModel.Select(0); - newModel.Select(1); - - target.SelectionModel = newModel; - - Assert.Equal(new[] { "foo", "bar" }, items); - } - - [Fact] - public void Reassigning_Model_Tracks_New_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - var newModel = new SelectionModel - { - Source = (string[])target.SelectionModel.Source, - SingleSelect = false - }; - - target.SelectionModel = newModel; - - newModel.Select(0); - newModel.Select(1); - - Assert.Equal(new[] { "foo", "bar" }, items); - } - - [Fact] - public void Adding_To_Items_Selects_On_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items.Add("foo"); - - Assert.Equal(new[] { 0, 1, 2 }, target.SelectionModel.SelectedIndexes); - Assert.Equal(new[] { "bar", "baz", "foo" }, items); - } - - [Fact] - public void Removing_From_Items_Deselects_On_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items.Remove("baz"); - - Assert.Equal(new[] { 1 }, target.SelectionModel.SelectedIndexes); - Assert.Equal(new[] { "bar" }, items); - } - - [Fact] - public void Replacing_Item_Updates_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items[0] = "foo"; - - Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes); - Assert.Equal(new[] { "foo", "baz" }, items); - } - - [Fact] - public void Clearing_Items_Updates_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items.Clear(); - - Assert.Empty(target.SelectionModel.SelectedIndexes); - } - - [Fact] - public void Setting_Items_Updates_Model() - { - var target = CreateTarget(); - var oldItems = target.SelectedItems; - - var newItems = new AvaloniaList { "foo", "baz" }; - target.SelectedItems = newItems; - - Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes); - Assert.Same(newItems, target.SelectedItems); - Assert.NotSame(oldItems, target.SelectedItems); - Assert.Equal(new[] { "foo", "baz" }, newItems); - } - - [Fact] - public void Setting_Items_Subscribes_To_Model() - { - var target = CreateTarget(); - var items = new AvaloniaList { "foo", "baz" }; - - target.SelectedItems = items; - target.SelectionModel.Select(1); - - Assert.Equal(new[] { "foo", "baz", "bar" }, items); - } - - [Fact] - public void Setting_Items_To_Null_Creates_Empty_Items() - { - var target = CreateTarget(); - var oldItems = target.SelectedItems; - - target.SelectedItems = null; - - var newItems = Assert.IsType>(target.SelectedItems); - - Assert.NotSame(oldItems, newItems); - } - - [Fact] - public void Handles_Null_Model_Source() - { - var model = new SelectionModel { SingleSelect = false }; - model.Select(1); - - var target = new SelectedItemsSync(model); - var items = target.SelectedItems; - - Assert.Empty(items); - - model.Select(2); - model.Source = new[] { "foo", "bar", "baz" }; - - Assert.Equal(new[] { "bar", "baz" }, items); - } - - [Fact] - public void Does_Not_Accept_Fixed_Size_Items() - { - var target = CreateTarget(); - - Assert.Throws(() => - target.SelectedItems = new[] { "foo", "bar", "baz" }); - } - - [Fact] - public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source() - { - var model = new SelectionModel(); - var target = new SelectedItemsSync(model); - var items = new AvaloniaList { "foo", "bar", "baz" }; - var selectedItems = new AvaloniaList { "bar" }; - - target.SelectedItems = selectedItems; - model.Source = items; - - Assert.Equal(1, model.SelectedIndex); - } - - [Fact] - public void Restores_Selection_On_Items_Reset() - { - var items = new ResettingCollection(new[] { "foo", "bar", "baz" }); - var model = new SelectionModel { Source = items }; - var target = new SelectedItemsSync(model); - - model.SelectedIndex = 1; - items.Reset(new[] { "baz", "foo", "bar" }); - - Assert.Equal(2, model.SelectedIndex); - } - - private static SelectedItemsSync CreateTarget( - IEnumerable items = null) - { - items ??= new[] { "foo", "bar", "baz" }; - - var model = new SelectionModel { Source = items, SingleSelect = false }; - model.SelectRange(1, 2); - - var target = new SelectedItemsSync(model); - return target; - } - - private class ResettingCollection : List, INotifyCollectionChanged - { - public ResettingCollection(IEnumerable items) - { - AddRange(items); - } - - public void Reset(IEnumerable items) - { - Clear(); - AddRange(items); - CollectionChanged?.Invoke( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - public event NotifyCollectionChangedEventHandler CollectionChanged; - } - } -} From faaa8f28e97ed7a4f01b0e8f39a1e6539c4d2865 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Sep 2020 16:31:48 +0200 Subject: [PATCH 081/149] Added failing tests for #4048. --- .../Primitives/SelectingItemsControlTests.cs | 21 +++++++++++++++++++ .../Selection/InternalSelectionModelTests.cs | 11 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index b6619aaa73..f3d0a8f94b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1619,6 +1619,27 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "bar" }, target.SelectedItems); } + [Fact] + public void Preserves_SelectedItem_When_Items_Changed() + { + // Issue #4048 + var target = new SelectingItemsControl + { + Items = new[] { "foo", "bar", "baz"}, + SelectedItem = "bar", + }; + + Prepare(target); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + + target.Items = new[] { "qux", "foo", "bar" }; + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs index 0f3ecef969..9626bd18ef 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -205,6 +205,17 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(2, target.SelectedIndex); } + [Fact] + public void Preserves_Selection_On_Source_Changed() + { + var target = CreateTarget(); + + target.SelectedIndex = 1; + target.Source = new[] { "baz", "foo", "bar" }; + + Assert.Equal(2, target.SelectedIndex); + } + private static InternalSelectionModel CreateTarget( bool singleSelect = false, IList source = null, From 290f28675c54653cadfb8422e398149b19254bcf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Sep 2020 16:32:03 +0200 Subject: [PATCH 082/149] Preserve selection when source changes. Fixes #4048. --- .../Selection/InternalSelectionModel.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index dd7b7d25cd..7c2e0b912b 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -60,6 +61,14 @@ namespace Avalonia.Controls.Selection private protected override void SetSource(IEnumerable? value) { + object?[]? oldSelection = null; + + if (Source is object && value is object) + { + oldSelection = new object?[SelectedItems.Count]; + SelectedItems.CopyTo(oldSelection, 0); + } + try { _ignoreSelectedItemsChanges = true; @@ -70,7 +79,18 @@ namespace Avalonia.Controls.Selection _ignoreSelectedItemsChanges = false; } - SyncToSelectedItems(); + if (oldSelection is null) + { + SyncToSelectedItems(); + } + else + { + foreach (var i in oldSelection) + { + var index = ItemsView!.IndexOf(i); + Select(index); + } + } } private void SyncToSelectedItems() From 5015c431a8bd07a83e36c156f62d6089fd023c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Tue, 15 Sep 2020 17:02:28 +0200 Subject: [PATCH 083/149] Fix nullable annotations. --- src/Avalonia.Input/KeyGesture.cs | 12 +++++------- src/Avalonia.Input/PointerEventArgs.cs | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index ad447794bc..aa6fcc8bff 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; using System.Text; namespace Avalonia.Input @@ -29,7 +27,7 @@ namespace Avalonia.Input KeyModifiers = modifiers; } - public bool Equals(KeyGesture other) + public bool Equals(KeyGesture? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; @@ -37,12 +35,12 @@ namespace Avalonia.Input return Key == other.Key && KeyModifiers == other.KeyModifiers; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - return obj is KeyGesture && Equals((KeyGesture)obj); + return obj is KeyGesture gesture && Equals(gesture); } public override int GetHashCode() @@ -53,12 +51,12 @@ namespace Avalonia.Input } } - public static bool operator ==(KeyGesture left, KeyGesture right) + public static bool operator ==(KeyGesture? left, KeyGesture? right) { return Equals(left, right); } - public static bool operator !=(KeyGesture left, KeyGesture right) + public static bool operator !=(KeyGesture? left, KeyGesture? right) { return !Equals(left, right); } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 1cbddf89aa..451f80b1df 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -86,14 +86,14 @@ namespace Avalonia.Input } [Obsolete("Use GetCurrentPoint")] - public PointerPoint GetPointerPoint(IVisual relativeTo) => GetCurrentPoint(relativeTo); + public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); /// /// Returns the PointerPoint associated with the current event /// /// The visual which coordinate system to use. Pass null for toplevel coordinate system /// - public PointerPoint GetCurrentPoint(IVisual relativeTo) + public PointerPoint GetCurrentPoint(IVisual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); /// From 517a52f988fd6f0262e19f30b20b42b4671e2c13 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Sep 2020 19:50:24 +0200 Subject: [PATCH 084/149] Failing tests for SelectedItems property changed events. --- .../Primitives/SelectingItemsControlTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index f3d0a8f94b..618f18b3ee 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1640,6 +1640,62 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("bar", target.SelectedItem); } + [Fact] + public void Setting_SelectedItems_Raises_PropertyChanged() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + }; + + var raised = 0; + var newValue = new AvaloniaList(); + + Prepare(target); + + target.PropertyChanged += (s, e) => + { + if (e.Property == ListBox.SelectedItemsProperty) + { + Assert.Null(e.OldValue); + Assert.Same(newValue, e.NewValue); + ++raised; + } + }; + + target.SelectedItems = newValue; + + Assert.Equal(1, raised); + } + + [Fact] + public void Setting_Selection_Raises_SelectedItems_PropertyChanged() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + }; + + var raised = 0; + var oldValue = target.SelectedItems; + + Prepare(target); + + target.PropertyChanged += (s, e) => + { + if (e.Property == ListBox.SelectedItemsProperty) + { + Assert.Same(oldValue, e.OldValue); + Assert.Null(e.NewValue); + ++raised; + } + }; + + target.Selection = new SelectionModel(); + + Assert.Equal(1, raised); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot @@ -1750,6 +1806,12 @@ namespace Avalonia.Controls.UnitTests.Primitives set => base.Selection = value; } + public new IList SelectedItems + { + get => base.SelectedItems; + set => base.SelectedItems = value; + } + public new SelectionMode SelectionMode { get => base.SelectionMode; From 9e0c92c6307961e7c6dca23bbcb343527fea0e82 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 10:22:32 +0200 Subject: [PATCH 085/149] Raise SelectedItems property changed events. --- .../Primitives/SelectingItemsControl.cs | 44 +++++++++-- .../Selection/InternalSelectionModel.cs | 40 +++++----- .../Selection/InternalSelectionModelTests.cs | 78 +++++++++---------- 3 files changed, 99 insertions(+), 63 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 3e106afdc0..1df3224fd4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -113,6 +113,7 @@ namespace Avalonia.Controls.Primitives private ISelectionModel? _selection; private int _oldSelectedIndex; private object? _oldSelectedItem; + private IList? _oldSelectedItems; private bool _ignoreContainerSelectionChanged; private UpdateState? _updateState; @@ -149,6 +150,10 @@ namespace Avalonia.Controls.Primitives { get { + // When a Begin/EndInit/DataContext update is in place we return the value to be + // updated here, even though it's not yet active and the property changed notification + // has not yet been raised. If we don't do this then the old value will be written back + // to the source when two-way bound, and the update value will be lost. return _updateState?.SelectedIndex.HasValue == true ? _updateState.SelectedIndex.Value : Selection.SelectedIndex; @@ -173,6 +178,7 @@ namespace Avalonia.Controls.Primitives { get { + // See SelectedIndex setter for more information. return _updateState?.SelectedItem.HasValue == true ? _updateState.SelectedItem.Value : Selection.SelectedItem; @@ -202,9 +208,19 @@ namespace Avalonia.Controls.Primitives { get { - return _updateState?.SelectedItems.HasValue == true ? - _updateState.SelectedItems.Value : - (Selection as InternalSelectionModel)?.SelectedItems; + // See SelectedIndex setter for more information. + if (_updateState?.SelectedItems.HasValue == true) + { + return _updateState.SelectedItems.Value; + } + else if (Selection is InternalSelectionModel ism) + { + var result = ism.WritableSelectedItems; + _oldSelectedItems = result; + return result; + } + + return null; } set { @@ -214,7 +230,7 @@ namespace Avalonia.Controls.Primitives } else if (Selection is InternalSelectionModel i) { - i.SelectedItems = value; + i.WritableSelectedItems = value; } else { @@ -275,6 +291,15 @@ namespace Avalonia.Controls.Primitives } InitializeSelectionModel(_selection); + + if (_oldSelectedItems != SelectedItems) + { + RaisePropertyChanged( + SelectedItemsProperty, + new Optional(_oldSelectedItems), + new BindingValue(SelectedItems)); + _oldSelectedItems = SelectedItems; + } } } } @@ -651,7 +676,7 @@ namespace Avalonia.Controls.Primitives ScrollIntoView(Selection.AnchorIndex); } } - else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex)) + else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) { RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex); _oldSelectedIndex = SelectedIndex; @@ -661,6 +686,15 @@ namespace Avalonia.Controls.Primitives RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem); _oldSelectedItem = SelectedItem; } + else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && + _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) + { + RaisePropertyChanged( + SelectedItemsProperty, + new Optional(_oldSelectedItems), + new BindingValue(SelectedItems)); + _oldSelectedItems = SelectedItems; + } } /// diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index 7c2e0b912b..a28e4b2785 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Selection { internal class InternalSelectionModel : SelectionModel { - private IList? _selectedItems; + private IList? _writableSelectedItems; private bool _ignoreModelChanges; private bool _ignoreSelectedItemsChanges; @@ -23,17 +23,17 @@ namespace Avalonia.Controls.Selection } [AllowNull] - public new IList SelectedItems + public IList WritableSelectedItems { get { - if (_selectedItems is null) + if (_writableSelectedItems is null) { - _selectedItems = new AvaloniaList(); + _writableSelectedItems = new AvaloniaList(); SubscribeToSelectedItems(); } - return _selectedItems; + return _writableSelectedItems; } set { @@ -44,10 +44,10 @@ namespace Avalonia.Controls.Selection throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems."); } - if (_selectedItems != value) + if (_writableSelectedItems != value) { UnsubscribeFromSelectedItems(); - _selectedItems = value; + _writableSelectedItems = value; SyncFromSelectedItems(); SubscribeToSelectedItems(); @@ -55,6 +55,8 @@ namespace Avalonia.Controls.Selection { SetInitSelectedItems(value); } + + RaisePropertyChanged(nameof(WritableSelectedItems)); } } } @@ -65,8 +67,8 @@ namespace Avalonia.Controls.Selection if (Source is object && value is object) { - oldSelection = new object?[SelectedItems.Count]; - SelectedItems.CopyTo(oldSelection, 0); + oldSelection = new object?[WritableSelectedItems.Count]; + WritableSelectedItems.CopyTo(oldSelection, 0); } try @@ -95,16 +97,16 @@ namespace Avalonia.Controls.Selection private void SyncToSelectedItems() { - if (_selectedItems is object) + if (_writableSelectedItems is object) { try { _ignoreSelectedItemsChanges = true; - _selectedItems.Clear(); + _writableSelectedItems.Clear(); foreach (var i in base.SelectedItems) { - _selectedItems.Add(i); + _writableSelectedItems.Add(i); } } finally @@ -116,7 +118,7 @@ namespace Avalonia.Controls.Selection private void SyncFromSelectedItems() { - if (Source is null || _selectedItems is null) + if (Source is null || _writableSelectedItems is null) { return; } @@ -128,7 +130,7 @@ namespace Avalonia.Controls.Selection using (BatchUpdate()) { Clear(); - Add(_selectedItems); + Add(_writableSelectedItems); } } finally @@ -139,7 +141,7 @@ namespace Avalonia.Controls.Selection private void SubscribeToSelectedItems() { - if (_selectedItems is INotifyCollectionChanged incc) + if (_writableSelectedItems is INotifyCollectionChanged incc) { incc.CollectionChanged += OnSelectedItemsCollectionChanged; } @@ -147,7 +149,7 @@ namespace Avalonia.Controls.Selection private void UnsubscribeFromSelectedItems() { - if (_selectedItems is INotifyCollectionChanged incc) + if (_writableSelectedItems is INotifyCollectionChanged incc) { incc.CollectionChanged += OnSelectedItemsCollectionChanged; } @@ -162,7 +164,7 @@ namespace Avalonia.Controls.Selection try { - var items = SelectedItems; + var items = WritableSelectedItems; var deselected = e.DeselectedItems.ToList(); var selected = e.SelectedItems.ToList(); @@ -193,7 +195,7 @@ namespace Avalonia.Controls.Selection return; } - if (_selectedItems == null) + if (_writableSelectedItems == null) { throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); } @@ -231,7 +233,7 @@ namespace Avalonia.Controls.Selection break; case NotifyCollectionChangedAction.Reset: Clear(); - Add(_selectedItems); + Add(_writableSelectedItems); break; } } diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs index 9626bd18ef..b64812e290 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -11,23 +11,23 @@ namespace Avalonia.Controls.UnitTests.Selection public class InternalSelectionModelTests { [Fact] - public void Selecting_Item_Adds_To_SelectedItems() + public void Selecting_Item_Adds_To_WritableSelectedItems() { var target = CreateTarget(); target.Select(0); - Assert.Equal(new[] { "foo" }, target.SelectedItems); + Assert.Equal(new[] { "foo" }, target.WritableSelectedItems); } [Fact] - public void Selecting_Duplicate_On_Model_Adds_To_SelectedItems() + public void Selecting_Duplicate_On_Model_Adds_To_WritableSelectedItems() { var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); target.SelectRange(1, 4); - Assert.Equal(new[] { "bar", "baz", "foo", "bar" }, target.SelectedItems); + Assert.Equal(new[] { "bar", "baz", "foo", "bar" }, target.WritableSelectedItems); } [Fact] @@ -38,7 +38,7 @@ namespace Avalonia.Controls.UnitTests.Selection target.SelectRange(1, 2); target.Deselect(1); - Assert.Equal(new[] { "baz" }, target.SelectedItems); + Assert.Equal(new[] { "baz" }, target.WritableSelectedItems); } [Fact] @@ -50,31 +50,31 @@ namespace Avalonia.Controls.UnitTests.Selection target.Select(4); target.Deselect(4); - Assert.Equal(new[] { "baz", "bar" }, target.SelectedItems); + Assert.Equal(new[] { "baz", "bar" }, target.WritableSelectedItems); } [Fact] - public void Adding_To_SelectedItems_Selects_On_Model() + public void Adding_To_WritableSelectedItems_Selects_On_Model() { var target = CreateTarget(); target.SelectRange(1, 2); - target.SelectedItems.Add("foo"); + target.WritableSelectedItems.Add("foo"); Assert.Equal(new[] { 0, 1, 2 }, target.SelectedIndexes); - Assert.Equal(new[] { "bar", "baz", "foo" }, target.SelectedItems); + Assert.Equal(new[] { "bar", "baz", "foo" }, target.WritableSelectedItems); } [Fact] - public void Removing_From_SelectedItems_Deselects_On_Model() + public void Removing_From_WritableSelectedItems_Deselects_On_Model() { var target = CreateTarget(); target.SelectRange(1, 2); - target.SelectedItems.Remove("baz"); + target.WritableSelectedItems.Remove("baz"); Assert.Equal(new[] { 1 }, target.SelectedIndexes); - Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { "bar" }, target.WritableSelectedItems); } [Fact] @@ -83,34 +83,34 @@ namespace Avalonia.Controls.UnitTests.Selection var target = CreateTarget(); target.SelectRange(1, 2); - target.SelectedItems[0] = "foo"; + target.WritableSelectedItems[0] = "foo"; Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); - Assert.Equal(new[] { "foo", "baz" }, target.SelectedItems); + Assert.Equal(new[] { "foo", "baz" }, target.WritableSelectedItems); } [Fact] - public void Clearing_SelectedItems_Updates_Model() + public void Clearing_WritableSelectedItems_Updates_Model() { var target = CreateTarget(); - target.SelectedItems.Clear(); + target.WritableSelectedItems.Clear(); Assert.Empty(target.SelectedIndexes); } [Fact] - public void Setting_SelectedItems_Updates_Model() + public void Setting_WritableSelectedItems_Updates_Model() { var target = CreateTarget(); - var oldItems = target.SelectedItems; + var oldItems = target.WritableSelectedItems; var newItems = new AvaloniaList { "foo", "baz" }; - target.SelectedItems = newItems; + target.WritableSelectedItems = newItems; Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); - Assert.Same(newItems, target.SelectedItems); - Assert.NotSame(oldItems, target.SelectedItems); + Assert.Same(newItems, target.WritableSelectedItems); + Assert.NotSame(oldItems, target.WritableSelectedItems); Assert.Equal(new[] { "foo", "baz" }, newItems); } @@ -120,7 +120,7 @@ namespace Avalonia.Controls.UnitTests.Selection var target = CreateTarget(); target.SelectRange(1, 2); - target.SelectedItems = null; + target.WritableSelectedItems = null; Assert.Empty(target.SelectedIndexes); } @@ -129,56 +129,56 @@ namespace Avalonia.Controls.UnitTests.Selection public void Setting_Items_To_Null_Creates_Empty_Items() { var target = CreateTarget(); - var oldItems = target.SelectedItems; + var oldItems = target.WritableSelectedItems; - target.SelectedItems = null; + target.WritableSelectedItems = null; - Assert.NotNull(target.SelectedItems); - Assert.NotSame(oldItems, target.SelectedItems); - Assert.IsType>(target.SelectedItems); + Assert.NotNull(target.WritableSelectedItems); + Assert.NotSame(oldItems, target.WritableSelectedItems); + Assert.IsType>(target.WritableSelectedItems); } [Fact] - public void Adds_Null_SelectedItems_When_Source_Is_Null() + public void Adds_Null_WritableSelectedItems_When_Source_Is_Null() { var target = CreateTarget(nullSource: true); target.SelectRange(1, 2); - Assert.Equal(new object[] { null, null }, target.SelectedItems); + Assert.Equal(new object[] { null, null }, target.WritableSelectedItems); } [Fact] - public void Updates_SelectedItems_When_Source_Changes_From_Null() + public void Updates_WritableSelectedItems_When_Source_Changes_From_Null() { var target = CreateTarget(nullSource: true); target.SelectRange(1, 2); - Assert.Equal(new object[] { null, null }, target.SelectedItems); + Assert.Equal(new object[] { null, null }, target.WritableSelectedItems); target.Source = new[] { "foo", "bar", "baz" }; - Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { "bar", "baz" }, target.WritableSelectedItems); } [Fact] - public void Updates_SelectedItems_When_Source_Changes_To_Null() + public void Updates_WritableSelectedItems_When_Source_Changes_To_Null() { var target = CreateTarget(); target.SelectRange(1, 2); - Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { "bar", "baz" }, target.WritableSelectedItems); target.Source = null; - Assert.Equal(new object[] { null, null }, target.SelectedItems); + Assert.Equal(new object[] { null, null }, target.WritableSelectedItems); } [Fact] - public void SelectedItems_Can_Be_Set_Before_Source() + public void WritableSelectedItems_Can_Be_Set_Before_Source() { var target = CreateTarget(nullSource: true); var items = new AvaloniaList { "foo", "bar", "baz" }; - var selectedItems = new AvaloniaList { "bar" }; + var WritableSelectedItems = new AvaloniaList { "bar" }; - target.SelectedItems = selectedItems; + target.WritableSelectedItems = WritableSelectedItems; target.Source = items; Assert.Equal(1, target.SelectedIndex); @@ -190,7 +190,7 @@ namespace Avalonia.Controls.UnitTests.Selection var target = CreateTarget(); Assert.Throws(() => - target.SelectedItems = new[] { "foo", "bar", "baz" }); + target.WritableSelectedItems = new[] { "foo", "bar", "baz" }); } [Fact] From e240b0d3228050870aa1f65ebb8e553bb3d7227e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 11:26:40 +0200 Subject: [PATCH 086/149] Added sandbox app. This is a blank app for use in repros, experiments and perf testing. Please don't commit changes here. --- Avalonia.sln | 27 +++++++++++++++++++++++++++ samples/Sandbox/App.axaml | 8 ++++++++ samples/Sandbox/App.axaml.cs | 22 ++++++++++++++++++++++ samples/Sandbox/MainWindow.axaml | 4 ++++ samples/Sandbox/MainWindow.axaml.cs | 20 ++++++++++++++++++++ samples/Sandbox/Program.cs | 17 +++++++++++++++++ samples/Sandbox/Sandbox.csproj | 18 ++++++++++++++++++ 7 files changed, 116 insertions(+) create mode 100644 samples/Sandbox/App.axaml create mode 100644 samples/Sandbox/App.axaml.cs create mode 100644 samples/Sandbox/MainWindow.axaml create mode 100644 samples/Sandbox/MainWindow.axaml.cs create mode 100644 samples/Sandbox/Program.cs create mode 100644 samples/Sandbox/Sandbox.csproj diff --git a/Avalonia.sln b/Avalonia.sln index ddcd61408d..ffcf266b0c 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -226,6 +226,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2064,6 +2066,30 @@ Global {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2123,6 +2149,7 @@ Global {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml new file mode 100644 index 0000000000..699781eb94 --- /dev/null +++ b/samples/Sandbox/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/Sandbox/App.axaml.cs b/samples/Sandbox/App.axaml.cs new file mode 100644 index 0000000000..7eb8345784 --- /dev/null +++ b/samples/Sandbox/App.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace Sandbox +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } + } +} diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml new file mode 100644 index 0000000000..6929f192c7 --- /dev/null +++ b/samples/Sandbox/MainWindow.axaml @@ -0,0 +1,4 @@ + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs new file mode 100644 index 0000000000..b7222e043d --- /dev/null +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sandbox +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs new file mode 100644 index 0000000000..4d7eda8d9f --- /dev/null +++ b/samples/Sandbox/Program.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.ReactiveUI; + +namespace Sandbox +{ + public class Program + { + static void Main(string[] args) + { + AppBuilder.Configure() + .UsePlatformDetect() + .UseReactiveUI() + .LogToDebug() + .StartWithClassicDesktopLifetime(args); + } + } +} diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj new file mode 100644 index 0000000000..1a0a8a7ce5 --- /dev/null +++ b/samples/Sandbox/Sandbox.csproj @@ -0,0 +1,18 @@ + + + + WinExe + netcoreapp3.1 + true + + + + + + + + + + + + From 5bf12ac0f0be4d1624ccf22776f5ad7a7e8cf82d Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 12:58:05 +0300 Subject: [PATCH 087/149] Added TextSelector and AutoCompleteMode properties to the AutoCompleteBox Now it's possible to change the logic for modifying the text of the control after choosing the autocomplete option --- src/Avalonia.Controls/AutoCompleteBox.cs | 222 ++++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..16b7d09e8a 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -225,6 +225,52 @@ namespace Avalonia.Controls Custom = 13, } + /// + /// Represents the selector used by the + /// control to + /// determine how the specified text should be modified with an item. + /// + /// + /// Modified text that will be used by the + /// . + /// + /// The string used as the basis for filtering. + /// + /// The selected item that should be combined with the + /// parameter. + /// + /// + /// The type used for filtering the + /// . + /// At the moment this type known only as a string. + /// + public delegate string AutoCompleteSelector(string search, T item); + + /// + /// Specifies how the selected autocomplete result should be treated. + /// + public enum AutoCompleteMode + { + /// + /// Specifies that the text will be replaced + /// with the selected autocomplete result. + /// + Replace = 0, + + /// + /// Specifies that the selected autocomplete result + /// will be appended to the text. + /// + Append = 1, + + /// + /// Specifies that a custom selector is used. This mode is used when + /// the + /// property is set. + /// + Custom = 2 + } + /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -362,6 +408,8 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _textSelector = AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace); + public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -499,6 +547,17 @@ namespace Avalonia.Controls defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); + /// + /// Gets the identifier for the + /// + /// dependency property. + /// + public static readonly StyledProperty AutoCompleteModeProperty = + AvaloniaProperty.Register( + nameof(AutoCompleteMode), + defaultValue: AutoCompleteMode.Replace, + validate: IsValidAutoCompleteMode); + /// /// Identifies the /// @@ -528,6 +587,21 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextSelector), + o => o.TextSelector, + (o, v) => o.TextSelector = v, + unsetValue: AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace)); + /// /// Identifies the /// @@ -578,6 +652,19 @@ namespace Avalonia.Controls } } + private static bool IsValidAutoCompleteMode(AutoCompleteMode mode) + { + switch (mode) + { + case AutoCompleteMode.Replace: + case AutoCompleteMode.Append: + case AutoCompleteMode.Custom: + return true; + default: + return false; + } + } + /// /// Handle the change of the IsEnabled property. /// @@ -728,6 +815,19 @@ namespace Avalonia.Controls TextFilter = AutoCompleteSearch.GetFilter(mode); } + /// + /// AutoCompleteModeProperty property changed handler. + /// + /// Event arguments. + private void OnAutoCompleteModePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteMode mode = (AutoCompleteMode)e.NewValue; + + // Sets the text selector for the new value + if (mode != AutoCompleteMode.Custom) + TextSelector = AutoCompleteSelection.GetSelector(mode); + } + /// /// ItemFilterProperty property changed handler. /// @@ -748,6 +848,25 @@ namespace Avalonia.Controls } } + /// + /// TextSelectorProperty property changed handler. + /// + /// Event arguments. + private void OnTextSelectorPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteSelector value = e.NewValue as AutoCompleteSelector; + + // If null, revert to the "Replace" predicate + if (value == null) + { + AutoCompleteMode = AutoCompleteMode.Replace; + } + else if (value.Method.DeclaringType != typeof(AutoCompleteSelection)) + { + AutoCompleteMode = AutoCompleteMode.Custom; + } + } + /// /// ItemsSourceProperty property changed handler. /// @@ -793,6 +912,8 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); + AutoCompleteModeProperty.Changed.AddClassHandler((x,e) => x.OnAutoCompleteModePropertyChanged(e)); + TextSelectorProperty.Changed.AddClassHandler((x,e) => x.OnTextSelectorPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -1015,6 +1136,31 @@ namespace Avalonia.Controls set { SetValue(FilterModeProperty, value); } } + /// + /// Gets or sets how the text in the text box will be modified + /// with the selected autocomplete item. + /// + /// + /// One of the + /// values. The default is + /// . + /// + /// The specified value is not a valid + /// . + /// + /// + /// Use the AutoCompleteMode property to specify the way the text will + /// be modified with the selected autocomplete item. For example, text + /// can be modified in a predefined or custom way. The autocomplete + /// mode is automatically set to Custom if you set the TextSelector + /// property. + /// + public AutoCompleteMode AutoCompleteMode + { + get { return GetValue(AutoCompleteModeProperty); } + set { SetValue(AutoCompleteModeProperty, value); } + } + public string Watermark { get { return GetValue(WatermarkProperty); } @@ -1061,6 +1207,26 @@ namespace Avalonia.Controls set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } + /// + /// Gets or sets the custom method that combines the user-entered + /// text to and one of the items specified by the + /// . + /// + /// + /// The custom method that combines the user-entered + /// text to and one of the items specified by the + /// . + /// + /// + /// The AutoCompleteMode is automatically set to Custom if you set + /// the TextSelector property. + /// + public AutoCompleteSelector TextSelector + { + get { return _textSelector; } + set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); } + } + public Func>> AsyncPopulator { get { return _asyncPopulator; } @@ -2331,7 +2497,7 @@ namespace Avalonia.Controls } else { - text = FormatValue(newItem, true); + text = TextSelector(SearchText, FormatValue(newItem, true)); } // Update the Text property and the TextBox values @@ -2590,6 +2756,60 @@ namespace Avalonia.Controls } } + /// + /// A predefined set of selector functions for the known, built-in + /// AutoCompleteMode enumeration values. + /// + private static class AutoCompleteSelection + { + /// + /// Index function that retrieves the selector for the provided + /// AutoCompleteMode. + /// + /// The built-in autocomplete mode. + /// Returns the string-based selector function. + public static AutoCompleteSelector GetSelector(AutoCompleteMode completeMode) + { + switch (completeMode) + { + case AutoCompleteMode.Replace: + return Replace; + case AutoCompleteMode.Append: + return Append; + case AutoCompleteMode.Custom: + default: + return null; + } + } + + /// + /// Implements AutoCompleteMode.Replace. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// + /// Return the and ignores the + /// . + /// + private static string Replace(string text, string value) + { + return value ?? String.Empty; + } + + /// + /// Implements AutoCompleteMode.Append. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// + /// Returns the concatenated string. + /// + private static string Append(string text, string value) + { + return text + value; + } + } + /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. From 9a1dd58273ff33d3c53cfb9d9f288d21e8e6bf38 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 12:59:58 +0300 Subject: [PATCH 088/149] Well, default AutoCompleteMode.Append isn't doing well, tbh --- src/Avalonia.Controls/AutoCompleteBox.cs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 16b7d09e8a..22b09ef110 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -257,18 +257,12 @@ namespace Avalonia.Controls /// Replace = 0, - /// - /// Specifies that the selected autocomplete result - /// will be appended to the text. - /// - Append = 1, - /// /// Specifies that a custom selector is used. This mode is used when /// the /// property is set. /// - Custom = 2 + Custom = 1 } /// @@ -657,7 +651,6 @@ namespace Avalonia.Controls switch (mode) { case AutoCompleteMode.Replace: - case AutoCompleteMode.Append: case AutoCompleteMode.Custom: return true; default: @@ -2774,8 +2767,6 @@ namespace Avalonia.Controls { case AutoCompleteMode.Replace: return Replace; - case AutoCompleteMode.Append: - return Append; case AutoCompleteMode.Custom: default: return null; @@ -2795,19 +2786,6 @@ namespace Avalonia.Controls { return value ?? String.Empty; } - - /// - /// Implements AutoCompleteMode.Append. - /// - /// The AutoCompleteBox prefix text. - /// The item's string value. - /// - /// Returns the concatenated string. - /// - private static string Append(string text, string value) - { - return text + value; - } } /// From 0fefe3b5af4529624378f47e8cbbd9e980045164 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Sep 2020 03:02:07 -0700 Subject: [PATCH 089/149] dont use egl 3.2 and 3.1 as they are still wip. --- src/Avalonia.OpenGL/AngleOptions.cs | 2 -- src/Avalonia.OpenGL/EglDisplay.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 462bd56cbe..0807eb7ab4 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -12,8 +12,6 @@ namespace Avalonia.OpenGL public IList GlProfiles { get; set; } = new List { - new GlVersion(GlProfileType.OpenGLES, 3, 2), - new GlVersion(GlProfileType.OpenGLES, 3, 1), new GlVersion(GlProfileType.OpenGLES, 3, 0), new GlVersion(GlProfileType.OpenGLES, 2, 0) }; diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index 9edcaf2bc0..7f41e75d6a 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -65,8 +65,6 @@ namespace Avalonia.OpenGL var glProfiles = AvaloniaLocator.Current.GetService()?.GlProfiles ?? new[] { - new GlVersion(GlProfileType.OpenGLES, 3, 2), - new GlVersion(GlProfileType.OpenGLES, 3, 1), new GlVersion(GlProfileType.OpenGLES, 3, 0), new GlVersion(GlProfileType.OpenGLES, 2, 0) }; From 14f314341ed61f2bf66fb79f9be57b3b51c8766b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 12:21:13 +0200 Subject: [PATCH 090/149] Fix scrolling to selection. A few `AutoScrollToSelectedItem` improvements: - Scroll to current selected item when it's set to true - Scroll to current selected item when list first displayed - Scroll to current selected item when attached to visual tree if the selection was changed while it wasn't attached Fixes #4100 --- src/Avalonia.Controls/ListBox.cs | 1 + .../Primitives/SelectingItemsControl.cs | 48 ++++++- .../Primitives/SelectingItemsControlTests.cs | 119 +++++++++++++++++- 3 files changed, 162 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f7e86d697a..d1b8038581 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -163,6 +163,7 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); Scroll = e.NameScope.Find("PART_ScrollViewer"); } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 1df3224fd4..b07bd13850 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -116,6 +116,7 @@ namespace Avalonia.Controls.Primitives private IList? _oldSelectedItems; private bool _ignoreContainerSelectionChanged; private UpdateState? _updateState; + private bool _hasScrolledToSelectedItem; /// /// Initializes static members of the class. @@ -381,6 +382,28 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + AutoScrollToSelectedItemIfNecessary(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e) + { + LayoutUpdated -= ExecuteScrollWhenLayoutUpdated; + AutoScrollToSelectedItemIfNecessary(); + } + + if (AutoScrollToSelectedItem) + { + LayoutUpdated += ExecuteScrollWhenLayoutUpdated; + } + } + /// protected override void OnContainersMaterialized(ItemContainerEventArgs e) { @@ -481,6 +504,10 @@ namespace Avalonia.Controls.Primitives { base.OnPropertyChanged(change); + if (change.Property == AutoScrollToSelectedItemProperty) + { + AutoScrollToSelectedItemIfNecessary(); + } if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); @@ -669,12 +696,10 @@ namespace Avalonia.Controls.Primitives /// The event args. private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem) + if (e.PropertyName == nameof(ISelectionModel.AnchorIndex)) { - if (Selection.AnchorIndex > 0) - { - ScrollIntoView(Selection.AnchorIndex); - } + _hasScrolledToSelectedItem = false; + AutoScrollToSelectedItemIfNecessary(); } else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) { @@ -751,6 +776,19 @@ namespace Avalonia.Controls.Primitives } } + private void AutoScrollToSelectedItemIfNecessary() + { + if (AutoScrollToSelectedItem && + !_hasScrolledToSelectedItem && + Presenter is object && + Selection.AnchorIndex > 0 && + ((IVisual)this).IsAttachedToVisualTree) + { + ScrollIntoView(Selection.AnchorIndex); + _hasScrolledToSelectedItem = true; + } + } + /// /// Called when a container raises the . /// diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 618f18b3ee..192d6e0286 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1402,12 +1402,36 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = items, }; + var raised = false; + Prepare(target); + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + target.SelectedIndex = 2; + + Assert.True(raised); + } + + [Fact] + public void AutoScrollToSelectedItem_Causes_Scroll_To_Initial_SelectedItem() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + }; var raised = false; - target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); target.SelectedIndex = 2; + Prepare(target); Assert.True(raised); } @@ -1451,6 +1475,99 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void AutoScrollToSelectedItem_Scrolls_When_Reattached_To_Visual_Tree_If_Selection_Changed_While_Detached_From_Visual_Tree() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 2, + }; + + var raised = false; + + Prepare(target); + + var root = (TestRoot)target.Parent; + + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + + root.Child = null; + target.SelectedIndex = 1; + root.Child = target; + + Assert.True(raised); + } + + [Fact] + public void AutoScrollToSelectedItem_Doesnt_Scroll_If_Reattached_To_Visual_Tree_With_No_Selection_Change() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 2, + }; + + var raised = false; + + Prepare(target); + + var root = (TestRoot)target.Parent; + + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + + root.Child = null; + root.Child = target; + + Assert.False(raised); + } + + [Fact] + public void AutoScrollToSelectedItem_Causes_Scroll_When_Turned_On() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + AutoScrollToSelectedItem = false, + }; + + Prepare(target); + + var raised = false; + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + target.SelectedIndex = 2; + + Assert.False(raised); + + target.AutoScrollToSelectedItem = true; + + Assert.True(raised); + } + [Fact] public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() { From aaeda72aec0bb8a0e84eb0e03310f93571299233 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 12:21:46 +0200 Subject: [PATCH 091/149] Removed unused methods. --- .../Primitives/SelectingItemsControl.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index b07bd13850..3b089921ef 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -849,14 +849,6 @@ namespace Avalonia.Controls.Primitives } } - private void MarkContainersUnselected() - { - foreach (var container in ItemContainerGenerator.Containers) - { - MarkContainerSelected(container.ContainerControl, false); - } - } - /// /// Sets an item container's 'selected' class or . /// @@ -872,23 +864,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Sets an item container's 'selected' class or . - /// - /// The item. - /// Whether the item should be selected or deselected. - private int MarkItemSelected(object item, bool selected) - { - var index = IndexOf(Items, item); - - if (index != -1) - { - MarkItemSelected(index, selected); - } - - return index; - } - private void UpdateContainerSelection() { if (Presenter?.Panel is IPanel panel) From 0fa3350a9d704d8d94b6635b0db80d59c9518d87 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Wed, 16 Sep 2020 13:25:38 +0300 Subject: [PATCH 092/149] Made tests for the AutoCompleteMode and TextSelector properties --- .../AutoCompleteBoxTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 57cea91834..2205384542 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,6 +363,47 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Test_Selectors() + { + Assert.Equal(GetSelector(AutoCompleteMode.Replace)("Never", "gonna"), "gonna"); + Assert.Equal(GetSelector(AutoCompleteMode.Replace)("give", "you"), "you"); + Assert.NotEqual(GetSelector(AutoCompleteMode.Replace)("up", "!"), "42"); + } + + [Fact] + public void AutoCompleteMode_Changes_To_Custom_And_Back() + { + RunTest((control, textbox) => + { + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Custom); + + control.AutoCompleteMode = AutoCompleteMode.Replace; + Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); + Assert.Equal(control.TextSelector, GetSelector(AutoCompleteMode.Replace)); + }); + } + + [Fact] + public void Custom_TextSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.TextSelector("4", "2"), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); + }); + } + /// /// Retrieves a defined predicate filter through a new AutoCompleteBox /// control instance. @@ -375,6 +416,17 @@ namespace Avalonia.Controls.UnitTests .TextFilter; } + /// + /// Retrieves a defined selector through a new AutoCompleteBox + /// control instance. + /// + /// The AutoCompleteMode of interest. + /// Returns the selector instance. + private static AutoCompleteSelector GetSelector(AutoCompleteMode mode) + { + return new AutoCompleteBox { AutoCompleteMode = mode }.TextSelector; + } + /// /// Creates a large list of strings for AutoCompleteBox testing. /// From 8e1c27756582614885fff6d280a7634f0d86e032 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Sep 2020 03:40:50 -0700 Subject: [PATCH 093/149] update to release of skiasharp. --- build/SkiaSharp.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index d7d04c7971..f2e7df36cd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + From 1b4fe4f5632b3ddb47bec68d43dcaa4e97675cbc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 13:55:59 +0200 Subject: [PATCH 094/149] Refactor ListBoxPage. - Make the list box fill available space - `SelectionMode` is a `[Flags]` enum so a combo box didn't make sense - Add check for `AutoScrollToSelectedItem` --- samples/ControlCatalog/MainView.xaml | 5 +- samples/ControlCatalog/Pages/ListBoxPage.xaml | 50 ++++++++---------- .../ViewModels/ListBoxPageViewModel.cs | 51 ++++++++++++++----- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index efc90357ed..790813fda0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -45,7 +45,10 @@ - + + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index edf3d41bf5..3521ad71a9 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -1,35 +1,25 @@ - - ListBox - Hosts a collection of ListBoxItem. - - - - - - - - - - - - - Single - Multiple - Toggle - AlwaysSelected - - + + + ListBox + Hosts a collection of ListBoxItem. - + + Multiple + Toggle + AlwaysSelected + AutoScrollToSelectedItem + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index d088576998..a963e8b6eb 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -10,15 +10,30 @@ namespace ControlCatalog.ViewModels { public class ListBoxPageViewModel : ReactiveObject { + private bool _multiple; + private bool _toggle; + private bool _alwaysSelected; + private bool _autoScrollToSelectedItem = true; private int _counter; - private SelectionMode _selectionMode; + private ObservableAsPropertyHelper _selectionMode; public ListBoxPageViewModel() { Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + Selection = new SelectionModel(); Selection.Select(1); + _selectionMode = this.WhenAnyValue( + x => x.Multiple, + x => x.Toggle, + x => x.AlwaysSelected, + (m, t, a) => + (m ? SelectionMode.Multiple : 0) | + (t ? SelectionMode.Toggle : 0) | + (a ? SelectionMode.AlwaysSelected : 0)) + .ToProperty(this, x => x.SelectionMode); + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); RemoveItemCommand = ReactiveCommand.Create(() => @@ -42,25 +57,37 @@ namespace ControlCatalog.ViewModels } public ObservableCollection Items { get; } - public SelectionModel Selection { get; } + public SelectionMode SelectionMode => _selectionMode.Value; - public ReactiveCommand AddItemCommand { get; } + public bool Multiple + { + get => _multiple; + set => this.RaiseAndSetIfChanged(ref _multiple, value); + } - public ReactiveCommand RemoveItemCommand { get; } + public bool Toggle + { + get => _toggle; + set => this.RaiseAndSetIfChanged(ref _toggle, value); + } - public ReactiveCommand SelectRandomItemCommand { get; } + public bool AlwaysSelected + { + get => _alwaysSelected; + set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value); + } - public SelectionMode SelectionMode + public bool AutoScrollToSelectedItem { - get => _selectionMode; - set - { - Selection.Clear(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } + get => _autoScrollToSelectedItem; + set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); } + public ReactiveCommand AddItemCommand { get; } + public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } + private string GenerateItem() => $"Item {_counter++.ToString()}"; } } From 172feab259256061170894ab709a112a100bbbcb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 14:01:18 +0200 Subject: [PATCH 095/149] Fix removing selected items in ListBoxPage. Previously, would remove all items if `AlwaysSelected` enabled. --- samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index a963e8b6eb..f75bc32105 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -38,9 +38,11 @@ namespace ControlCatalog.ViewModels RemoveItemCommand = ReactiveCommand.Create(() => { - while (Selection.Count > 0) + var items = Selection.SelectedItems.ToList(); + + foreach (var item in items) { - Items.Remove(Selection.SelectedItems.First()); + Items.Remove(item); } }); From 293564e7049bbd07a9ba5194415921d52f739a01 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Sep 2020 14:44:59 +0200 Subject: [PATCH 096/149] Off-by-one error. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 3b089921ef..c954f9fd4a 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -781,7 +781,7 @@ namespace Avalonia.Controls.Primitives if (AutoScrollToSelectedItem && !_hasScrolledToSelectedItem && Presenter is object && - Selection.AnchorIndex > 0 && + Selection.AnchorIndex >= 0 && ((IVisual)this).IsAttachedToVisualTree) { ScrollIntoView(Selection.AnchorIndex); From ffcc0c1156a5e368b9262c9430438e89faf06bdf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 14 Sep 2020 12:56:52 +0200 Subject: [PATCH 097/149] Allow ScrollViewer to scroll the content up, down, left and right by one page. --- src/Avalonia.Controls/ScrollViewer.cs | 36 +++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 4600301410..6b75149d62 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -448,6 +448,38 @@ namespace Avalonia.Controls Offset += new Vector(_smallChange.Width, 0); } + /// + /// Scrolls the content upward by one page. + /// + public void PageUp() + { + VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); + } + + /// + /// Scrolls the content downward by one page. + /// + public void PageDown() + { + VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); + } + + /// + /// Scrolls the content left by one page. + /// + public void PageLeft() + { + HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0); + } + + /// + /// Scrolls the content tight by one page. + /// + public void PageRight() + { + HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum); + } + /// /// Scrolls to the top-left corner of the content. /// @@ -623,12 +655,12 @@ namespace Avalonia.Controls { if (e.Key == Key.PageUp) { - VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); + PageUp(); e.Handled = true; } else if (e.Key == Key.PageDown) { - VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); + PageDown(); e.Handled = true; } } From 82240033dc12d476deb333cff99f9239abb06662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 16 Sep 2020 20:09:52 +0100 Subject: [PATCH 098/149] Changed property copy constructors to require same property type. --- src/Avalonia.Base/AvaloniaProperty`1.cs | 23 +++++++++++++++++++---- src/Avalonia.Base/DirectPropertyBase.cs | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 7480d9c9c5..d5549e979b 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -31,18 +31,33 @@ namespace Avalonia } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + [Obsolete("Use constructor with AvaloniaProperty instead.", true)] + protected AvaloniaProperty( + AvaloniaProperty source, + Type ownerType, + PropertyMetadata metadata) + : this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata) + { + } + + /// + /// Initializes a new instance of the class. /// /// The property to copy. /// The new owner type. /// Optional overridden metadata. protected AvaloniaProperty( - AvaloniaProperty source, - Type ownerType, + AvaloniaProperty source, + Type ownerType, PropertyMetadata metadata) : base(source, ownerType, metadata) { - _changed = source is AvaloniaProperty p ? p._changed : new Subject>(); + _changed = source._changed; } /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index dbc2625b86..a2f113adb7 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -32,15 +32,30 @@ namespace Avalonia } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The property to copy. /// The new owner type. /// Optional overridden metadata. + [Obsolete("Use constructor with DirectPropertyBase instead.", true)] protected DirectPropertyBase( AvaloniaProperty source, Type ownerType, PropertyMetadata metadata) + : this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + protected DirectPropertyBase( + DirectPropertyBase source, + Type ownerType, + PropertyMetadata metadata) : base(source, ownerType, metadata) { } From 6992bd72ceabe81dc58f0af82186e53c1575dd2d Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 17 Sep 2020 00:37:56 -0400 Subject: [PATCH 099/149] Add Sorting event and use ListSortDirection instead of bool property --- .../Collections/DataGridSortDescription.cs | 42 +++++--- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 + .../DataGridColumn.cs | 2 +- .../DataGridColumnHeader.cs | 97 ++++++++++--------- .../DataGridColumns.cs | 5 + src/Avalonia.Controls.DataGrid/EventArgs.cs | 4 +- 6 files changed, 91 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index f741d40571..ffb36d187d 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -1,19 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Text; -using Avalonia.Controls; using Avalonia.Controls.Utils; -using Avalonia.Utilities; namespace Avalonia.Collections { public abstract class DataGridSortDescription { public virtual string PropertyPath => null; - public virtual bool Descending => false; + + [Obsolete("Use Direction property to read or override sorting direction.")] + public virtual bool Descending => Direction == ListSortDirection.Descending; + + public virtual ListSortDirection Direction => ListSortDirection.Ascending; public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); public abstract IComparer Comparer { get; } @@ -105,7 +107,7 @@ namespace Avalonia.Collections private class DataGridPathSortDescription : DataGridSortDescription { - private readonly bool _descending; + private readonly ListSortDirection _direction; private readonly string _propertyPath; private readonly Lazy _cultureSensitiveComparer; private readonly Lazy> _comparer; @@ -130,19 +132,19 @@ namespace Avalonia.Collections public override string PropertyPath => _propertyPath; public override IComparer Comparer => _comparer.Value; - public override bool Descending => _descending; + public override ListSortDirection Direction => _direction; - public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture) + public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, CultureInfo culture) { _propertyPath = propertyPath; - _descending = descending; + _direction = direction; _cultureSensitiveComparer = new Lazy(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); } - private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending) + private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction) { _propertyPath = inner._propertyPath; - _descending = descending; + _direction = direction; _propertyType = inner._propertyType; _cultureSensitiveComparer = inner._cultureSensitiveComparer; _internalComparer = inner._internalComparer; @@ -201,7 +203,7 @@ namespace Avalonia.Collections result = _internalComparer?.Compare(v1, v2) ?? 0; - if (_descending) + if (Direction == ListSortDirection.Descending) return -result; else return result; @@ -218,7 +220,7 @@ namespace Avalonia.Collections } public override IOrderedEnumerable OrderBy(IEnumerable seq) { - if(_descending) + if (Direction == ListSortDirection.Descending) { return seq.OrderByDescending(o => GetValue(o), InternalComparer); } @@ -229,7 +231,7 @@ namespace Avalonia.Collections } public override IOrderedEnumerable ThenBy(IOrderedEnumerable seq) { - if (_descending) + if (Direction == ListSortDirection.Descending) { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } @@ -241,13 +243,21 @@ namespace Avalonia.Collections internal override DataGridSortDescription SwitchSortDirection() { - return new DataGridPathSortDescription(this, !_descending); + var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; + return new DataGridPathSortDescription(this, newDirection); } } - public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null) + public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null) + { + return new DataGridPathSortDescription(propertyPath, direction, culture); + } + + + [Obsolete("Use overload taking a ListSortDirection.")] + public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null) { - return new DataGridPathSortDescription(propertyPath, descending, culture); + return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, culture); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index f7903086ab..9ca0b91523 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1229,6 +1229,11 @@ namespace Avalonia.Controls remove { AddHandler(SelectionChangedEvent, value); } } + /// + /// Occurs when the sorting request is triggered. + /// + public event EventHandler Sorting; + /// /// Occurs when a /// object becomes available for reuse. diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 128fbde0c1..23c4acdf6c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -1047,4 +1047,4 @@ namespace Avalonia.Controls } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 017718bc92..25aae99942 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -161,13 +161,14 @@ namespace Avalonia.Controls var sort = OwningColumn.GetSortDescription(); if (sort != null) { - CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; + CurrentSortingState = sort.Direction; } } + PseudoClasses.Set(":sortascending", - CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); + CurrentSortingState == ListSortDirection.Ascending); PseudoClasses.Set(":sortdescending", - CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); + CurrentSortingState == ListSortDirection.Descending); } internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn) @@ -215,70 +216,76 @@ namespace Avalonia.Controls internal void ProcessSort(KeyModifiers keyModifiers) { // if we can sort: - // - DataConnection.AllowSort is true, and // - AllowUserToSortColumns and CanSort are true, and - // - OwningColumn is bound, and - // - SortDescriptionsCollection exists, and - // - the column's data type is comparable + // - OwningColumn is bound // then try to sort if (OwningColumn != null && OwningGrid != null && OwningGrid.EditingRow == null && OwningColumn != OwningGrid.ColumnsInternal.FillerColumn - && OwningGrid.DataConnection.AllowSort && OwningGrid.CanUserSortColumns - && OwningColumn.CanUserSort - && OwningGrid.DataConnection.SortDescriptions != null) + && OwningColumn.CanUserSort) { - DataGrid owningGrid = OwningGrid; + var ea = new DataGridColumnEventArgs(OwningColumn); + OwningGrid.OnColumnSorting(ea); - DataGridSortDescription newSort; + if (!ea.Handled && OwningGrid.DataConnection.AllowSort && OwningGrid.DataConnection.SortDescriptions != null) + { + // - DataConnection.AllowSort is true, and + // - SortDescriptionsCollection exists, and + // - the column's data type is comparable - KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); + DataGrid owningGrid = OwningGrid; + DataGridSortDescription newSort; - DataGridSortDescription sort = OwningColumn.GetSortDescription(); - IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; - Debug.Assert(collectionView != null); - using (collectionView.DeferRefresh()) - { - // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand - if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0) - { - owningGrid.DataConnection.SortDescriptions.Clear(); - } + KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); + + DataGridSortDescription sort = OwningColumn.GetSortDescription(); + IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; + Debug.Assert(collectionView != null); - // if ctrl is held down, we only clear the sort directions - if (!ctrl) + using (collectionView.DeferRefresh()) { - if (sort != null) + // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand + if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0) { - newSort = sort.SwitchSortDirection(); + owningGrid.DataConnection.SortDescriptions.Clear(); + } - // changing direction should not affect sort order, so we replace this column's - // sort description instead of just adding it to the end of the collection - int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); - if (oldIndex >= 0) + // if ctrl is held down, we only clear the sort directions + if (!ctrl) + { + if (sort != null) { - owningGrid.DataConnection.SortDescriptions.Remove(sort); - owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + newSort = sort.SwitchSortDirection(); + + // changing direction should not affect sort order, so we replace this column's + // sort description instead of just adding it to the end of the collection + int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); + if (oldIndex >= 0) + { + owningGrid.DataConnection.SortDescriptions.Remove(sort); + owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + } + else + { + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } } else { + string propertyName = OwningColumn.GetSortPropertyName(); + // no-opt if we couldn't find a property to sort on + if (string.IsNullOrEmpty(propertyName)) + { + return; + } + + newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); + owningGrid.DataConnection.SortDescriptions.Add(newSort); } } - else - { - string propertyName = OwningColumn.GetSortPropertyName(); - // no-opt if we couldn't find a property to sort on - if (string.IsNullOrEmpty(propertyName)) - { - return; - } - - newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); - owningGrid.DataConnection.SortDescriptions.Add(newSort); - } } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 5b75bc73f9..46bcd0d347 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -33,6 +33,11 @@ namespace Avalonia.Controls ColumnReordering?.Invoke(this, e); } + protected internal virtual void OnColumnSorting(DataGridColumnEventArgs e) + { + Sorting?.Invoke(this, e); + } + /// /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total /// width is adjusted by the given amount, if possible. If the total desired adjustment amount diff --git a/src/Avalonia.Controls.DataGrid/EventArgs.cs b/src/Avalonia.Controls.DataGrid/EventArgs.cs index 10e2be795e..7590a8ed61 100644 --- a/src/Avalonia.Controls.DataGrid/EventArgs.cs +++ b/src/Avalonia.Controls.DataGrid/EventArgs.cs @@ -289,7 +289,7 @@ namespace Avalonia.Controls /// /// Provides data for column-related events. /// - public class DataGridColumnEventArgs : EventArgs + public class DataGridColumnEventArgs : HandledEventArgs { /// /// Initializes a new instance of the class. @@ -566,4 +566,4 @@ namespace Avalonia.Controls private set; } } -} \ No newline at end of file +} From 8dd60eefe1ae90713ce994eb2c638b7704933d13 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 17 Sep 2020 01:25:49 -0400 Subject: [PATCH 100/149] Allow to set custom IComparer in DataGridSortDescription.FromPath --- .../Collections/DataGridSortDescription.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index ffb36d187d..46d9cd039f 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -120,7 +120,7 @@ namespace Avalonia.Collections { if (_internalComparerTyped == null && _internalComparer != null) { - if (_internalComparerTyped is IComparer c) + if (_internalComparer is IComparer c) _internalComparerTyped = c; else _internalComparerTyped = Comparer.Create((x, y) => _internalComparer.Compare(x, y)); @@ -134,11 +134,12 @@ namespace Avalonia.Collections public override IComparer Comparer => _comparer.Value; public override ListSortDirection Direction => _direction; - public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, CultureInfo culture) + public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, IComparer internalComparer, CultureInfo culture) { _propertyPath = propertyPath; _direction = direction; _cultureSensitiveComparer = new Lazy(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); + _internalComparer = internalComparer; _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); } private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction) @@ -250,14 +251,19 @@ namespace Avalonia.Collections public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null) { - return new DataGridPathSortDescription(propertyPath, direction, culture); + return new DataGridPathSortDescription(propertyPath, direction, null, culture); } [Obsolete("Use overload taking a ListSortDirection.")] public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null) { - return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, culture); + return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture); + } + + public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, IComparer internalComparer = null) + { + return new DataGridPathSortDescription(propertyPath, direction, internalComparer, null); } } From b94748795adfafb5ebbea7d116f4d388378a9979 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 17 Sep 2020 01:28:21 -0400 Subject: [PATCH 101/149] Add custom IComparer sorting to the DataGrid page --- .../ControlCatalog/Pages/DataGridPage.xaml.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 0b7fb12aff..1893c4e5e7 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -1,8 +1,12 @@ +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using ControlCatalog.Models; using Avalonia.Collections; +using Avalonia.Data; namespace ControlCatalog.Pages { @@ -11,13 +15,23 @@ namespace ControlCatalog.Pages public DataGridPage() { this.InitializeComponent(); + + var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer()); + var colelctionView1 = new DataGridCollectionView(Countries.All); + colelctionView1.SortDescriptions.Add(dataGridSortDescription); var dg1 = this.FindControl("dataGrid1"); dg1.IsReadOnly = true; dg1.LoadingRow += Dg1_LoadingRow; - var collectionView1 = new DataGridCollectionView(Countries.All); - //collectionView.GroupDescriptions.Add(new PathGroupDescription("Region")); - - dg1.Items = collectionView1; + dg1.Sorting += (s, a) => + { + var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path; + if (property == dataGridSortDescription.PropertyPath + && !colelctionView1.SortDescriptions.Contains(dataGridSortDescription)) + { + colelctionView1.SortDescriptions.Add(dataGridSortDescription); + } + }; + dg1.Items = colelctionView1; var dg2 = this.FindControl("dataGridGrouping"); dg2.IsReadOnly = true; @@ -53,5 +67,20 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); } + + private class ReversedStringComparer : IComparer, IComparer + { + public int Compare(object x, object y) + { + if (x is string left && y is string right) + { + var reversedLeft = new string(left.Reverse().ToArray()); + var reversedRight = new string(right.Reverse().ToArray()); + return reversedLeft.CompareTo(reversedRight); + } + + return Comparer.Default.Compare(x, y); + } + } } } From 92f1c06a9691d2541ee8e785a7c65a0d2ff89a0b Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 17 Sep 2020 01:36:41 -0400 Subject: [PATCH 102/149] Update DataGrid tests --- .../Collections/DataGridSortDescription.cs | 4 ++-- .../Collections/DataGridSortDescriptionTests.cs | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index 46d9cd039f..fe115f5012 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -261,9 +261,9 @@ namespace Avalonia.Collections return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture); } - public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, IComparer internalComparer = null) + public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer) { - return new DataGridPathSortDescription(propertyPath, direction, internalComparer, null); + return new DataGridPathSortDescription(propertyPath, direction, comparer, null); } } diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs index a1a734f650..04d7ce3fc7 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Xunit; @@ -18,7 +19,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("c", "c"), }; var expectedResult = items.OrderBy(i => i.Prop1).ToList(); - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: false); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), ListSortDirection.Ascending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.OrderBy(items).ToList(); @@ -36,7 +37,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("c", "c"), }; var expectedResult = items.OrderByDescending(i => i.Prop1).ToList(); - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: true); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), ListSortDirection.Descending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.OrderBy(items).ToList(); @@ -61,7 +62,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("a", "b"), new Item("a", "c"), }; - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: false); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), ListSortDirection.Ascending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.ThenBy(items).ToList(); @@ -86,7 +87,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("a", "b"), new Item("a", "a"), }; - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: true); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), ListSortDirection.Descending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.ThenBy(items).ToList(); From 8c3458e9306cbfb17305b3bc3ae6362434a8a0d8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 12:00:36 +0200 Subject: [PATCH 103/149] Remove sandbox from ncrunch. --- .ncrunch/Sandbox.v3.ncrunchproject | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .ncrunch/Sandbox.v3.ncrunchproject diff --git a/.ncrunch/Sandbox.v3.ncrunchproject b/.ncrunch/Sandbox.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Sandbox.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 73ffcf4648ee3a6afbe123c752e2913ef76d304d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 12:21:22 +0200 Subject: [PATCH 104/149] Added failing tests simulating tabstrip/carousel pair. --- .../Primitives/SelectingItemsControlTests.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 192d6e0286..514d3b5475 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1813,6 +1813,88 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, raised); } + [Fact] + public void Handles_Removing_Last_Item_In_Two_Controls_With_Bound_SelectedIndex() + { + var items = new ObservableCollection { "foo" }; + + // Simulates problem with TabStrip and Carousel with bound SelectedIndex. + var tabStrip = new TestSelector + { + Items = items, + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var carousel = new TestSelector + { + Items = items, + [!Carousel.SelectedIndexProperty] = tabStrip[!TabStrip.SelectedIndexProperty], + }; + + var tabStripRaised = 0; + var carouselRaised = 0; + + tabStrip.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++tabStripRaised; + }; + + carousel.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++carouselRaised; + }; + + items.RemoveAt(0); + + Assert.Equal(1, tabStripRaised); + Assert.Equal(1, carouselRaised); + } + + [Fact] + public void Handles_Removing_Last_Item_In_Controls_With_Bound_SelectedItem() + { + var items = new ObservableCollection { "foo" }; + + // Simulates problem with TabStrip and Carousel with bound SelectedItem. + var tabStrip = new TestSelector + { + Items = items, + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var carousel = new TestSelector + { + Items = items, + [!Carousel.SelectedItemProperty] = tabStrip[!TabStrip.SelectedItemProperty], + }; + + var tabStripRaised = 0; + var carouselRaised = 0; + + tabStrip.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++tabStripRaised; + }; + + carousel.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++carouselRaised; + }; + + items.RemoveAt(0); + + Assert.Equal(1, tabStripRaised); + Assert.Equal(1, carouselRaised); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot From b5f81e52909e2edd1ce12e346af031c460cd8490 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 12:25:34 +0200 Subject: [PATCH 105/149] Don't coerce when deselecting a range. If the deselection happens because of a `Clear` during a source collection changed event handler, then the selected indexes may not be in the valid range of the post-change items. In this case, we still want the items to be cleared, and not coercing the range has no downside. --- src/Avalonia.Controls/Selection/SelectionModel.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index fd27cb340a..2492129780 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -242,12 +242,7 @@ namespace Avalonia.Controls.Selection { using var update = BatchUpdate(); var o = update.Operation; - var range = CoerceRange(start, end); - - if (range.Begin == -1) - { - return; - } + var range = new IndexRange(start, end); if (RangesEnabled) { From f79f910c4df91ba54c483eaf02fdcaef81841a00 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 12:35:36 +0200 Subject: [PATCH 106/149] Coerce lower bound for deselecting items. We can't deselect -1. --- src/Avalonia.Controls/Selection/SelectionModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 2492129780..3b5d57a7b8 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -242,7 +242,7 @@ namespace Avalonia.Controls.Selection { using var update = BatchUpdate(); var o = update.Operation; - var range = new IndexRange(start, end); + var range = new IndexRange(Math.Max(0, start), end); if (RangesEnabled) { From 91d18be95465844b287cab8f575c87ba752a1f68 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 17 Sep 2020 16:34:32 +0300 Subject: [PATCH 107/149] Remove Avalonia.ReactiveUI.Events.UnitTests from solution file --- Avalonia.sln | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index ffcf266b0c..34ad19b41d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -224,8 +224,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject Global @@ -2042,30 +2040,6 @@ Global {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -2148,7 +2122,6 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From 3608deeeb1c48dd0e91da8f52482069dfdf2fd54 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 16 Sep 2020 18:13:07 +0300 Subject: [PATCH 108/149] Fixed SleepLoopRenderTimer --- src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs index 79e029ccd2..9cc94ffac3 100644 --- a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs @@ -45,14 +45,13 @@ namespace Avalonia.Rendering void LoopProc() { - var now = _st.Elapsed; - var lastTick = now; - + var lastTick = _st.Elapsed; while (true) { + var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); - + lastTick = now; lock (_lock) { if (_count == 0) @@ -63,7 +62,7 @@ namespace Avalonia.Rendering } _tick?.Invoke(now); - now = _st.Elapsed; + } } From 6ca33a86c57fcf9aaa412b1df454c276662ca3a9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 17 Sep 2020 11:16:16 -0400 Subject: [PATCH 109/149] Update samples/ControlCatalog/Pages/DataGridPage.xaml.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dariusz Komosiński --- samples/ControlCatalog/Pages/DataGridPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 1893c4e5e7..9b7bf3ab16 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -17,7 +17,7 @@ namespace ControlCatalog.Pages this.InitializeComponent(); var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer()); - var colelctionView1 = new DataGridCollectionView(Countries.All); + var collectionView1 = new DataGridCollectionView(Countries.All); colelctionView1.SortDescriptions.Add(dataGridSortDescription); var dg1 = this.FindControl("dataGrid1"); dg1.IsReadOnly = true; From 6650407c2538e64e9de1f70739b1f3b90ce3bd4f Mon Sep 17 00:00:00 2001 From: GMIKE Date: Thu, 17 Sep 2020 22:04:20 +0300 Subject: [PATCH 110/149] Deconstructors --- src/Avalonia.Visuals/Point.cs | 87 ------------------------------- src/Avalonia.Visuals/Size.cs | 11 ++++ src/Avalonia.Visuals/Thickness.cs | 16 ++++++ src/Avalonia.Visuals/Vector.cs | 11 ++++ 4 files changed, 38 insertions(+), 87 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index d95512c976..a23ad98aeb 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -109,28 +109,6 @@ namespace Avalonia return new Point(a._x + b.X, a._y + b.Y); } - /// - /// Add a 2-tuple of double - /// - /// The point. - /// 2-tuple of double - /// A point that is the result of the addition. - public static Point operator +(Point a, (double x, double y) b) - { - return new Point(a._x + b.x, a._y + b.y); - } - - /// - /// Add a double number to both coordinates - /// - /// The point. - /// double number - /// A point that is the result of the addition. - public static Point operator +(Point a, double b) - { - return new Point(a._x + b, a._y + b); - } - /// /// Subtracts two points. /// @@ -153,28 +131,6 @@ namespace Avalonia return new Point(a._x - b.X, a._y - b.Y); } - /// - /// Subtracts a 2-tuple of double - /// - /// The point. - /// 2-tuple of double - /// A point that is the result of the subtraction. - public static Point operator -(Point a, (double x, double y) b) - { - return new Point(a._x - b.x, a._y - b.y); - } - - /// - /// Subtracts a double number to both coordinates - /// - /// The point. - /// double number - /// A point that is the result of the subtraction. - public static Point operator -(Point a, double b) - { - return new Point(a._x - b, a._y - b); - } - /// /// Multiplies a point by a factor coordinate-wise /// @@ -191,17 +147,6 @@ namespace Avalonia /// Points having its coordinates multiplied public static Point operator *(double k, Point p) => new Point(p.X * k, p.Y * k); - /// - /// Multiplies a 2-tuple of double - /// - /// The point. - /// 2-tuple of double - /// Points having its coordinates multiplied. - public static Point operator *(Point a, (double x, double y) b) - { - return new Point(a._x * b.x, a._y * b.y); - } - /// /// Divides a point by a factor coordinate-wise /// @@ -210,17 +155,6 @@ namespace Avalonia /// Points having its coordinates divided public static Point operator /(Point p, double k) => new Point(p.X / k, p.Y / k); - /// - /// Divides a point by a 2-tuple of doubles - /// - /// The point. - /// 2-tuple of double - /// Points having its coordinates divided - public static Point operator /(Point a, (double x, double y) b) - { - return new Point(a._x / b.x, a._y / b.y); - } - /// /// Applies a matrix to a point. /// @@ -344,26 +278,5 @@ namespace Avalonia x = this._x; y = this._y; } - - /// - /// Returns a boolean indicating whether the point coordinates are equal to 2-tuple of double - /// - /// 2-tuple of double - /// - /// True if is 2-tuple of double equals coordinates current point. - /// - public bool Equals((double x, double y) other) - { - return _x == other.x && - _y == other.y; - } - - /// - /// Gets a value indicating that point coordinates are zero - /// - public bool IsDefault - { - get { return (_x == 0) && (_y == 0); } - } } } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index f87b336b50..0faa4cc496 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -276,5 +276,16 @@ namespace Avalonia { return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _width, _height); } + + /// + /// Deconstructor for decomposition Size + /// + /// The width. + /// The height. + public void Deconstruct(out double width, out double height) + { + width = this._width; + height = this._height; + } } } diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index b03e91bf34..aba34e6895 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -272,5 +272,21 @@ namespace Avalonia { return $"{_left},{_top},{_right},{_bottom}"; } + + /// + /// Deconstructor for decomposition Thickness + /// + /// The thickness on the left. + /// The thickness on the top. + /// The thickness on the right. + /// The thickness on the bottom. + public void Deconstruct(out double left, out double top, out double right, out double bottom) + { + left = this._left; + top = this._top; + right = this._right; + bottom = this._bottom; + + } } } diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 6059dc3971..ab8c6991dc 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -333,5 +333,16 @@ namespace Avalonia /// public static Vector UnitY => new Vector(0, 1); + + /// + /// Deconstructor for decomposition Vector + /// + /// The X component. + /// The Y poscomponentition. + public void Deconstruct(out double x, out double y) + { + x = this._x; + y = this._y; + } } } From 6f84af5b7abe6bdce581aea0fc44011193477741 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Thu, 17 Sep 2020 22:05:28 +0300 Subject: [PATCH 111/149] remove IEquatable<(double x,double y)> --- src/Avalonia.Visuals/Point.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index a23ad98aeb..7cc61c5120 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -8,7 +8,7 @@ namespace Avalonia /// /// Defines a point. /// - public readonly struct Point : IEquatable, IEquatable<(double x,double y)> + public readonly struct Point : IEquatable { static Point() { From 019043469b2f6cbc73e13364b8756f9e6d36578b Mon Sep 17 00:00:00 2001 From: GMIKE Date: Thu, 17 Sep 2020 22:16:53 +0300 Subject: [PATCH 112/149] IsDefault methods --- src/Avalonia.Visuals/Point.cs | 8 ++++++++ src/Avalonia.Visuals/Size.cs | 8 ++++++++ src/Avalonia.Visuals/Thickness.cs | 11 +++++++++-- src/Avalonia.Visuals/Vector.cs | 8 ++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 7cc61c5120..30dd01ce38 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -278,5 +278,13 @@ namespace Avalonia x = this._x; y = this._y; } + + /// + /// Gets a value indicating that point coordinates are zero + /// + public bool IsDefault + { + get { return (_x == 0) && (_y == 0); } + } } } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 0faa4cc496..bff6d787ee 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -287,5 +287,13 @@ namespace Avalonia width = this._width; height = this._height; } + + /// + /// Gets a value indicating that width and height are zero + /// + public bool IsDefault + { + get { return (_width == 0) && (_height == 0); } + } } } diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index aba34e6895..33ac001cef 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -285,8 +285,15 @@ namespace Avalonia left = this._left; top = this._top; right = this._right; - bottom = this._bottom; - + bottom = this._bottom; + } + + /// + /// Gets a value indicating that thickness are zero + /// + public bool IsDefault + { + get { return (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } } } } diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index ab8c6991dc..67c04e4bb3 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -344,5 +344,13 @@ namespace Avalonia x = this._x; y = this._y; } + + /// + /// Gets a value indicating that Vector components are zero + /// + public bool IsDefault + { + get { return (_x == 0) && (_y == 0); } + } } } From d1fef034869c651ca37c86f70dac345c3a700202 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 17 Sep 2020 15:28:50 -0400 Subject: [PATCH 113/149] Make SwitchSortDirection public --- .../Collections/DataGridSortDescription.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index fe115f5012..662ff91329 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -28,7 +28,7 @@ namespace Avalonia.Collections return seq.ThenBy(o => o, Comparer); } - internal virtual DataGridSortDescription SwitchSortDirection() + public virtual DataGridSortDescription SwitchSortDirection() { return this; } @@ -242,7 +242,7 @@ namespace Avalonia.Collections } } - internal override DataGridSortDescription SwitchSortDirection() + public override DataGridSortDescription SwitchSortDirection() { var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; return new DataGridPathSortDescription(this, newDirection); From 59db76f21416910360c91e1690952696696a0e2d Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Thu, 17 Sep 2020 16:24:55 -0400 Subject: [PATCH 114/149] Fix build errors --- samples/ControlCatalog/Pages/DataGridPage.xaml.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 9b7bf3ab16..2a30f4d91b 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer()); var collectionView1 = new DataGridCollectionView(Countries.All); - colelctionView1.SortDescriptions.Add(dataGridSortDescription); + collectionView1.SortDescriptions.Add(dataGridSortDescription); var dg1 = this.FindControl("dataGrid1"); dg1.IsReadOnly = true; dg1.LoadingRow += Dg1_LoadingRow; @@ -26,12 +26,12 @@ namespace ControlCatalog.Pages { var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path; if (property == dataGridSortDescription.PropertyPath - && !colelctionView1.SortDescriptions.Contains(dataGridSortDescription)) + && !collectionView1.SortDescriptions.Contains(dataGridSortDescription)) { - colelctionView1.SortDescriptions.Add(dataGridSortDescription); + collectionView1.SortDescriptions.Add(dataGridSortDescription); } }; - dg1.Items = colelctionView1; + dg1.Items = collectionView1; var dg2 = this.FindControl("dataGridGrouping"); dg2.IsReadOnly = true; From 32fe0e60a7f9ff20d1c3173e4471638c64f6f0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Thu, 17 Sep 2020 23:30:34 +0100 Subject: [PATCH 115/149] Use typed property changed args instead of changing handler args type. --- src/Avalonia.Controls/Mixins/SelectableMixin.cs | 4 ++-- src/Avalonia.Controls/NativeMenu.Export.cs | 4 ++-- src/Avalonia.Controls/NativeMenuItem.cs | 4 ++-- tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs | 6 +++--- tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index e7dbecb06e..c9e2b684cb 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -42,13 +42,13 @@ namespace Avalonia.Controls.Mixins { Contract.Requires(isSelected != null); - isSelected.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => + isSelected.Changed.Subscribe(x => { var sender = x.Sender as TControl; if (sender != null) { - ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); + ((IPseudoClasses)sender.Classes).Set(":selected", x.NewValue.GetValueOrDefault()); sender.RaiseEvent(new RoutedEventArgs { diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 89e4c9e492..0349df842b 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -73,11 +73,11 @@ namespace Avalonia.Controls throw new InvalidOperationException("IsNativeMenuExported property is read-only"); info.ChangingIsExported = false; }); - MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => + MenuProperty.Changed.Subscribe(args => { if (args.Sender is TopLevel tl) { - GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); + GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); } }); } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index d4badbc559..a0fec9e677 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -20,10 +20,10 @@ namespace Avalonia.Controls static NativeMenuItem() { - MenuProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs args) => + MenuProperty.Changed.Subscribe(args => { var item = (NativeMenuItem)args.Sender; - var value = (NativeMenu)args.NewValue; + var value = args.NewValue.GetValueOrDefault(); if (value.Parent != null && value.Parent != item) throw new InvalidOperationException("NativeMenu already has a parent"); value.Parent = item; diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 83ae663419..20172eea88 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -92,10 +92,10 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); bool raised = false; - Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs e) => + Class1.FooProperty.Changed.Subscribe(e => raised = e.Property == Class1.FooProperty && - (string)e.OldValue == "initial" && - (string)e.NewValue == "newvalue" && + e.OldValue.GetValueOrDefault() == "initial" && + e.NewValue.GetValueOrDefault() == "newvalue" && e.Priority == BindingPriority.LocalValue); target.SetValue(Class1.FooProperty, "newvalue"); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 19040ff584..8e5d8b7be2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); string value = null; - Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => value = (string)x.NewValue); + Class1.FooProperty.Changed.Subscribe(x => value = x.NewValue.GetValueOrDefault()); target.SetValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", value); @@ -95,7 +95,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var result = new List(); - Class1.FooProperty.Changed.Subscribe((AvaloniaPropertyChangedEventArgs x) => result.Add((string)x.NewValue)); + Class1.FooProperty.Changed.Subscribe(x => result.Add(x.NewValue.GetValueOrDefault())); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "local"); From e574acd85f831360e97350d7a6a1d57620de684d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 09:03:10 +0200 Subject: [PATCH 116/149] Mark TabOnceActiveElement value as nullable. --- src/Avalonia.Input/KeyboardNavigation.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 722215f8b7..6ef3c4fd60 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -25,12 +25,11 @@ namespace Avalonia.Input /// attached property set to , this property /// defines to which child the focus should move. /// - public static readonly AttachedProperty TabOnceActiveElementProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty TabOnceActiveElementProperty = + AvaloniaProperty.RegisterAttached( "TabOnceActiveElement", typeof(KeyboardNavigation)); - /// /// Defines the IsTabStop attached property. /// @@ -68,7 +67,7 @@ namespace Avalonia.Input /// /// The container. /// The active element for the container. - public static IInputElement GetTabOnceActiveElement(InputElement element) + public static IInputElement? GetTabOnceActiveElement(InputElement element) { return element.GetValue(TabOnceActiveElementProperty); } @@ -78,7 +77,7 @@ namespace Avalonia.Input /// /// The container. /// The active element for the container. - public static void SetTabOnceActiveElement(InputElement element, IInputElement value) + public static void SetTabOnceActiveElement(InputElement element, IInputElement? value) { element.SetValue(TabOnceActiveElementProperty, value); } From 3f47f6dba35ae9954b8970f9b5586736f44c3f4c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 09:08:08 +0200 Subject: [PATCH 117/149] Added failing test for AvaloniaList.CopyTo. `Can_CopyTo_Array_Of_Same_Type` passes but `Can_CopyTo_Array_Of_Base_Type` fails. --- ...Controls.UnitTests.net47.v3.ncrunchproject | 1 + .../Collections/AvaloniaListTests.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject b/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject index e9d39b0c74..f30a20df78 100644 --- a/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject @@ -3,5 +3,6 @@ MissingOrIgnoredProjectReference + Avalonia.Controls.UnitTests.TimePickerTests \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 19700cadab..d5ac01a092 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; @@ -334,5 +335,27 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + + [Fact] + public void Can_CopyTo_Array_Of_Same_Type() + { + var target = new AvaloniaList { "foo", "bar", "baz" }; + var result = new string[3]; + + target.CopyTo(result, 0); + + Assert.Equal(target, result); + } + + [Fact] + public void Can_CopyTo_Array_Of_Base_Type() + { + var target = new AvaloniaList { "foo", "bar", "baz" }; + var result = new object[3]; + + ((IList)target).CopyTo(result, 0); + + Assert.Equal(target, result); + } } } From 8350ab1399a310782982c586cc1a829280a7c6ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 17:42:48 +0200 Subject: [PATCH 118/149] Copy ICollection.CopyTo implementation from the BCL. Did not handle differing types. --- src/Avalonia.Base/Collections/AvaloniaList.cs | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index f201cfab1f..d43b4e04bb 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -543,7 +543,73 @@ namespace Avalonia.Collections /// void ICollection.CopyTo(Array array, int index) { - _inner.CopyTo((T[])array, index); + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (array.Rank != 1) + { + throw new ArgumentException("Multi-dimensional arrays are not supported."); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException("Non-zero lower bounds are not supported."); + } + + if (index < 0) + { + throw new ArgumentException("Invalid index."); + } + + if (array.Length - index < Count) + { + throw new ArgumentException("The target array is too small."); + } + + if (array is T[] tArray) + { + _inner.CopyTo(tArray, index); + } + else + { + // + // Catch the obvious case assignment will fail. + // We can't find all possible problems by doing the check though. + // For example, if the element type of the Array is derived from T, + // we can't figure out if we can successfully copy the element beforehand. + // + Type targetType = array.GetType().GetElementType()!; + Type sourceType = typeof(T); + if (!(targetType.IsAssignableFrom(sourceType) || sourceType.IsAssignableFrom(targetType))) + { + throw new ArgumentException("Invalid array type"); + } + + // + // We can't cast array of value type to object[], so we don't support + // widening of primitive types here. + // + object[] objects = array as object[]; + if (objects == null) + { + throw new ArgumentException("Invalid array type"); + } + + int count = _inner.Count; + try + { + for (int i = 0; i < count; i++) + { + objects[index++] = _inner[i]; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Invalid array type"); + } + } } /// From 89684e5b94d51b432a4476d089c1f1a2df8782a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Sep 2020 17:46:07 +0200 Subject: [PATCH 119/149] Short-circuit setting source to current value. --- src/Avalonia.Controls/Selection/InternalSelectionModel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index a28e4b2785..fcdaf44166 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -63,6 +63,11 @@ namespace Avalonia.Controls.Selection private protected override void SetSource(IEnumerable? value) { + if (Source == value) + { + return; + } + object?[]? oldSelection = null; if (Source is object && value is object) From 544686b78d99674bf50984ce5e97008a8a3d319a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 11:41:04 +0200 Subject: [PATCH 120/149] Don't try to SelectAll with single selection. `Toggle` doesn't mean multiple selection. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c954f9fd4a..e34b3b145f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -491,8 +491,7 @@ namespace Avalonia.Controls.Primitives if (ItemCount > 0 && Match(keymap.SelectAll) && - (((SelectionMode & SelectionMode.Multiple) != 0) || - (SelectionMode & SelectionMode.Toggle) != 0)) + SelectionMode.HasFlag(SelectionMode.Multiple)) { Selection.SelectAll(); e.Handled = true; From 08a8badd7afbf87c45d985de815b82b9d64fc6f0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 00:28:56 +0200 Subject: [PATCH 121/149] Don't try to realize index -1. `Algorithm_GetAnchorForTargetElement` can return -1 and when it does so we try to realize that item, which obviously doesn't exist, so boom. --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index cd7f725f18..eace54d2e0 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -211,7 +211,7 @@ namespace Avalonia.Layout anchorPosition = new Point(anchorBounds.X, anchorBounds.Y); } } - else + else if (anchorIndex >= 0) { // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index // which is not in the realized range. Eg. insert one item at index 0 for a grid layout. From 58c333b6f16eb13581bed38f9f337204727ada9b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 18 Sep 2020 16:43:01 +0200 Subject: [PATCH 122/149] Added PseudoClassesAttribute. And apply it to our controls. Can be used by designers to add auto-completion support for pseudoclasses. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 ++ src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 ++ .../DataGridColumnHeader.cs | 2 ++ src/Avalonia.Controls.DataGrid/DataGridRow.cs | 2 ++ .../DataGridRowGroupHeader.cs | 2 ++ .../DataGridRowHeader.cs | 2 ++ src/Avalonia.Controls/AutoCompleteBox.cs | 2 ++ src/Avalonia.Controls/Button.cs | 2 ++ src/Avalonia.Controls/ButtonSpinner.cs | 2 ++ .../Calendar/CalendarButton.cs | 2 ++ .../Calendar/CalendarDayButton.cs | 2 ++ src/Avalonia.Controls/Calendar/CalendarItem.cs | 2 ++ src/Avalonia.Controls/Chrome/CaptionButtons.cs | 2 ++ src/Avalonia.Controls/Chrome/TitleBar.cs | 2 ++ src/Avalonia.Controls/DataValidationErrors.cs | 2 ++ .../DateTimePickers/DatePicker.cs | 4 +++- .../DateTimePickers/TimePicker.cs | 4 +++- src/Avalonia.Controls/Expander.cs | 3 ++- src/Avalonia.Controls/ItemsControl.cs | 2 ++ src/Avalonia.Controls/ListBoxItem.cs | 2 ++ src/Avalonia.Controls/MenuItem.cs | 2 ++ .../Notifications/NotificationCard.cs | 2 ++ .../Notifications/WindowNotificationManager.cs | 2 ++ src/Avalonia.Controls/Primitives/ScrollBar.cs | 2 ++ src/Avalonia.Controls/Primitives/Thumb.cs | 2 ++ .../Primitives/ToggleButton.cs | 2 ++ src/Avalonia.Controls/Primitives/Track.cs | 2 ++ src/Avalonia.Controls/ProgressBar.cs | 2 ++ src/Avalonia.Controls/Slider.cs | 2 ++ src/Avalonia.Controls/SplitView.cs | 7 ++++++- src/Avalonia.Controls/TabItem.cs | 2 ++ src/Avalonia.Controls/TextBox.cs | 2 ++ src/Avalonia.Controls/ToggleSwitch.cs | 4 +++- src/Avalonia.Controls/ToolTip.cs | 2 ++ src/Avalonia.Controls/TreeViewItem.cs | 2 ++ src/Avalonia.Input/InputElement.cs | 2 ++ .../Metadata/PseudoClassesAttribute.cs | 18 ++++++++++++++++++ 37 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 9ca0b91523..7c57ea3db9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,12 +24,14 @@ using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Displays data in a customizable grid. /// + [PseudoClasses(":invalid")] public partial class DataGrid : TemplatedControl { private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e5fbfa1a81..445dc541a7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 25aae99942..856d1f6566 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -14,12 +14,14 @@ using Avalonia.Utilities; using System; using Avalonia.Controls.Utils; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Represents an individual column header. /// + [PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")] public class DataGridColumnHeader : ContentControl { private enum DragMode diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index d5ce8dba75..c3562c53a4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 0833247439..1e03b134b1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -13,6 +14,7 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 8f8b1742ba..0cd3589a57 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Media; using System.Diagnostics; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..6bbb4f0b75 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -14,6 +14,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; @@ -30,6 +31,7 @@ namespace Avalonia.Controls /// /// event. /// + [PseudoClasses(":dropdownopen")] public class PopulatedEventArgs : EventArgs { /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index b54eb2ac57..e94d00b2ff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -28,6 +29,7 @@ namespace Avalonia.Controls /// /// A button control. /// + [PseudoClasses(":pressed")] public class Button : ContentControl { /// diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 44f66d397a..5fe2cf3704 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -15,6 +16,7 @@ namespace Avalonia.Controls /// /// Represents a spinner control that includes two Buttons. /// + [PseudoClasses(":left", ":right")] public class ButtonSpinner : Spinner { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 80370df145..76af933b55 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using System; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// Represents a button on a /// . /// + [PseudoClasses(":selected", ":inactive", ":btnfocused")] public sealed class CalendarButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 3a39bd10fa..d5748bb9e4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -5,10 +5,12 @@ using System; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Input; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")] public sealed class CalendarDayButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 0be7c4f67e..e9ea942142 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives /// Represents the currently displayed month or year on a /// . /// + [PseudoClasses(":calendardisabled")] public sealed class CalendarItem : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index a86cbc271b..cd60130c5b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index c0c8076dd8..fbddb06952 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws a titlebar when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class TitleBar : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index dfe9a16532..3c64691816 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// /// You will probably only want to create instances inside of control templates. /// + [PseudoClasses(":error")] public class DataValidationErrors : ContentControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index a41c159980..8d893154eb 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Interactivity; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// + [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index e54da1fb3a..e4ff5e9e5b 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using System; @@ -9,6 +10,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a time /// + [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 43882b70c8..9ff2e41fa9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,6 @@ using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; namespace Avalonia.Controls { @@ -12,6 +12,7 @@ namespace Avalonia.Controls Right } + [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { public static readonly StyledProperty ContentTransitionProperty = diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a3dfe33641..3aec06e4eb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -18,6 +19,7 @@ namespace Avalonia.Controls /// /// Displays a collection of items. /// + [PseudoClasses(":empty", ":singleitem")] public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener { /// diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index e04c79987f..4fe5f4de40 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Input; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// A selectable item in a . /// + [PseudoClasses(":pressed", ":selected")] public class ListBoxItem : ContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index b4d3272471..3d8ab3ae48 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index f90746bf06..cdbace3ced 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications /// /// Control that represents and displays a notification. /// + [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { private bool _isClosed; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 6d9f6b8b77..8f5c6faf40 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives; using Avalonia.Rendering; using Avalonia.Data; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Notifications { /// /// An that displays notifications in a . /// + [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 477d24dc99..a7fb7ae08c 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -4,6 +4,7 @@ using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Primitives { @@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives /// /// A scrollbar control. /// + [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { /// diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 96810ed01b..348922b71d 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,9 +1,11 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed")] public class Thumb : TemplatedControl { public static readonly RoutedEvent DragStartedEvent = diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 13031ddad8..f96ca9310d 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. /// + [PseudoClasses(":checked", ":unchecked", ":indeterminate")] public class ToggleButton : Button { /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 29e7f28b44..9399f5fb31 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -12,6 +13,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { public static readonly DirectProperty MinimumProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a92f24a050..161f09d9b6 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// + [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { public class ProgressBarTemplateProperties : AvaloniaObject diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 293cbac82f..6e08e78813 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -39,6 +40,7 @@ namespace Avalonia.Controls /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// + [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { /// diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index b71858f796..8267efc466 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -73,6 +74,10 @@ namespace Avalonia.Controls /// /// A control with two views: A collapsible pane and an area for content /// + [PseudoClasses(":open", ":closed")] + [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] + [PseudoClasses(":left", ":right")] + [PseudoClasses(":lightdismiss")] public class SplitView : TemplatedControl { /* diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 6320443a13..593643a1eb 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// An item in a or . /// + [PseudoClasses(":pressed", ":selected")] public class TabItem : HeaderedContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 73a1ae3335..0fe3ac62e4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -13,9 +13,11 @@ using Avalonia.Metadata; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { + [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index c32f2d8102..662a355dac 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A Toggle Switch control. /// + [PseudoClasses(":dragging")] public class ToggleSwitch : ToggleButton { private Panel _knobsPanel; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index d56ff5752f..71bd0726d4 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// To add a tooltip to a control, use the attached property, /// assigning the content that you want displayed. /// + [PseudoClasses(":open")] public class ToolTip : ContentControl { /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4942d4d313..8ce258b546 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,5 +1,6 @@ using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// An item in a . /// + [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { /// diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 25f2d553d7..9ace7fd92d 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; @@ -12,6 +13,7 @@ namespace Avalonia.Input /// /// Implements input-related functionality for a control. /// + [PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")] public class InputElement : Interactive, IInputElement { /// diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs new file mode 100644 index 0000000000..66d0282b92 --- /dev/null +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Controls.Metadata +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class PseudoClassesAttribute : Attribute + { + public PseudoClassesAttribute(params string[] pseudoClasses) + { + PseudoClasses = pseudoClasses; + } + + public IReadOnlyList PseudoClasses { get; } + } +} From c2fad766e52eb47531e848c745e4325d9f53069c Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sat, 19 Sep 2020 15:46:44 +0200 Subject: [PATCH 123/149] Rework GlyphRun BaselineOrigin --- samples/RenderDemo/Pages/GlyphRunPage.xaml.cs | 3 +- .../Primitives/AccessText.cs | 4 +- src/Avalonia.Controls/TextBlock.cs | 5 +- .../HeadlessPlatformRenderInterface.cs | 2 +- src/Avalonia.Visuals/ApiCompatBaseline.txt | 20 +++++++- src/Avalonia.Visuals/Media/DrawingContext.cs | 5 +- src/Avalonia.Visuals/Media/GlyphRun.cs | 49 ++++++++++++++----- src/Avalonia.Visuals/Media/GlyphRunDrawing.cs | 13 +---- src/Avalonia.Visuals/Media/TextDecoration.cs | 17 ++++--- .../Media/TextFormatting/DrawableTextRun.cs | 7 ++- .../TextFormatting/ShapedTextCharacters.cs | 11 ++--- .../Media/TextFormatting/TextFormatterImpl.cs | 6 +-- .../Media/TextFormatting/TextLayout.cs | 17 ++++--- .../Media/TextFormatting/TextLine.cs | 3 +- .../Media/TextFormatting/TextLineImpl.cs | 29 +++++------ .../Media/TextFormatting/TextLineMetrics.cs | 2 +- .../Platform/IDrawingContextImpl.cs | 3 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 4 +- .../Rendering/SceneGraph/GlyphRunNode.cs | 13 ++--- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 8 +-- .../Media/DrawingContextImpl.cs | 7 +-- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 22 files changed, 132 insertions(+), 98 deletions(-) diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 7f15845596..ddee880288 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -61,7 +61,6 @@ namespace RenderDemo.Pages { Foreground = Brushes.Black, GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), - BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale) }; drawingGroup.Children.Add(glyphRunDrawing); @@ -69,7 +68,7 @@ namespace RenderDemo.Pages var geometryDrawing = new GeometryDrawing { Pen = new Pen(Brushes.Black), - Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } + Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) } }; drawingGroup.Children.Add(geometryDrawing); diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 89f672deaa..7a5e6ce426 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -126,7 +126,7 @@ namespace Avalonia.Controls.Primitives if (shapedTextCharacters.GlyphRun.Characters.End < textPosition) { - currentX += shapedTextCharacters.GlyphRun.Bounds.Width; + currentX += shapedTextCharacters.Size.Width; continue; } @@ -143,7 +143,7 @@ namespace Avalonia.Controls.Primitives width = 0.0; } - return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height); + return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height); } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 3b9e9c4751..6f978ce86a 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -434,7 +434,10 @@ namespace Avalonia.Controls var padding = Padding; - TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top)); + using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, padding.Top))) + { + TextLayout.Draw(context); + } } /// diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 3ae6c8c30e..4f6af0a41b 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -385,7 +385,7 @@ namespace Avalonia.Headless } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 5aa497861d..148916932f 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,15 +1,33 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.GlyphRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Media.GlyphRunDrawing.BaselineOriginProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Point Avalonia.Media.GlyphRunDrawing.BaselineOrigin.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.GlyphRunDrawing.BaselineOrigin.set(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract. CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract. CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract. TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.DrawableTextRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size.get()' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.ShapedTextCharacters.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. -Total Issues: 13 +Total Issues: 31 diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index ba7191d7a6..ae4c927ae2 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -206,14 +206,13 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - /// The baseline origin of the glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { Contract.Requires(glyphRun != null); if (foreground != null) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + PlatformImpl.DrawGlyphRun(foreground, glyphRun); } } diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index da3a1f721c..14ab083b4f 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -16,8 +16,9 @@ namespace Avalonia.Media private IGlyphRunImpl _glyphRunImpl; private GlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; - private Rect? _bounds; + private Size? _size; private int _biDiLevel; + private Point? _baselineOrigin; private ReadOnlySlice _glyphIndices; private ReadOnlySlice _glyphAdvances; @@ -89,6 +90,20 @@ namespace Avalonia.Media set => Set(ref _fontRenderingEmSize, value); } + /// + /// Gets or sets the baseline origin of the. + /// + public Point BaselineOrigin + { + get + { + _baselineOrigin ??= CalculateBaselineOrigin(); + + return _baselineOrigin.Value; + } + set => Set(ref _baselineOrigin, value); + } + /// /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. /// @@ -156,16 +171,13 @@ namespace Avalonia.Media /// /// Gets or sets the conservative bounding box of the . /// - public Rect Bounds + public Size Size { get { - if (_bounds == null) - { - _bounds = CalculateBounds(); - } + _size ??= CalculateSize(); - return _bounds.Value; + return _size.Value; } } @@ -200,7 +212,7 @@ namespace Avalonia.Media if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End) { - return Bounds.Width; + return Size.Width; } var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); @@ -257,7 +269,7 @@ namespace Avalonia.Media } //After - if (distance > Bounds.Size.Width) + if (distance > Size.Width) { isInside = false; @@ -529,12 +541,21 @@ namespace Avalonia.Media } /// - /// Calculates the bounds of the . + /// Calculates the default baseline origin of the . + /// + /// The baseline origin. + private Point CalculateBaselineOrigin() + { + return new Point(0, -GlyphTypeface.Ascent * Scale); + } + + /// + /// Calculates the size of the . /// /// /// The calculated bounds. /// - private Rect CalculateBounds() + private Size CalculateSize() { var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; @@ -555,7 +576,7 @@ namespace Avalonia.Media } } - return new Rect(0, GlyphTypeface.Ascent * Scale, width, height); + return new Size(width, height); } private void Set(ref T field, T value) @@ -590,11 +611,15 @@ namespace Avalonia.Media throw new InvalidOperationException(); } + _baselineOrigin = new Point(0, -GlyphTypeface.Ascent * Scale); + var platformRenderInterface = AvaloniaLocator.Current.GetService(); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + _size = new Size(width, height); } void IDisposable.Dispose() diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs index d0ea113a6f..7e0d5c3c81 100644 --- a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs @@ -8,9 +8,6 @@ public static readonly StyledProperty GlyphRunProperty = AvaloniaProperty.Register(nameof(GlyphRun)); - public static readonly StyledProperty BaselineOriginProperty = - AvaloniaProperty.Register(nameof(BaselineOrigin)); - public IBrush Foreground { get => GetValue(ForegroundProperty); @@ -23,12 +20,6 @@ set => SetValue(GlyphRunProperty, value); } - public Point BaselineOrigin - { - get => GetValue(BaselineOriginProperty); - set => SetValue(BaselineOriginProperty, value); - } - public override void Draw(DrawingContext context) { if (GlyphRun == null) @@ -36,12 +27,12 @@ return; } - context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + context.DrawGlyphRun(Foreground, GlyphRun); } public override Rect GetBounds() { - return GlyphRun?.Bounds ?? default; + return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; } } } diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs index 681fc5d499..d9b3f664ce 100644 --- a/src/Avalonia.Visuals/Media/TextDecoration.cs +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -155,8 +155,7 @@ namespace Avalonia.Media /// /// The drawing context. /// The shaped characters that are decorated. - /// The origin. - internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters, Point origin) + internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters) { var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize; var fontMetrics = shapedTextCharacters.FontMetrics; @@ -181,16 +180,20 @@ namespace Avalonia.Media break; } + var origin = new Point(); + switch (Location) { - case TextDecorationLocation.Overline: - origin += new Point(0, fontMetrics.Ascent); + case TextDecorationLocation.Baseline: + origin += shapedTextCharacters.GlyphRun.BaselineOrigin; break; case TextDecorationLocation.Strikethrough: - origin += new Point(0, -fontMetrics.StrikethroughPosition); + origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, + shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.StrikethroughPosition); break; case TextDecorationLocation.Underline: - origin += new Point(0, -fontMetrics.UnderlinePosition); + origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, + shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.UnderlinePosition); break; } @@ -207,7 +210,7 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); - drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Bounds.Width, 0)); + drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Size.Width, 0)); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs index 56790cc0db..338c92f6b1 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs @@ -6,15 +6,14 @@ public abstract class DrawableTextRun : TextRun { /// - /// Gets the bounds. + /// Gets the size. /// - public abstract Rect Bounds { get; } + public abstract Size Size { get; } /// /// Draws the at the given origin. /// /// The drawing context. - /// The origin. - public abstract void Draw(DrawingContext drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 9e67a03f45..09ecc0a026 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting public override int TextSourceLength { get; } /// - public override Rect Bounds => GlyphRun.Bounds; + public override Size Size => GlyphRun.Size; /// /// Gets the font metrics. @@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting public GlyphRun GlyphRun { get; } /// - public override void Draw(DrawingContext drawingContext, Point origin) + public override void Draw(DrawingContext drawingContext) { if (GlyphRun.GlyphIndices.Length == 0) { @@ -64,11 +64,10 @@ namespace Avalonia.Media.TextFormatting if (Properties.BackgroundBrush != null) { - drawingContext.DrawRectangle(Properties.BackgroundBrush, null, - new Rect(origin.X, origin.Y + FontMetrics.Ascent, Bounds.Width, Bounds.Height)); + drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size)); } - drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun, origin); + drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun); if (Properties.TextDecorations == null) { @@ -77,7 +76,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textDecoration in Properties.TextDecorations) { - textDecoration.Draw(drawingContext, this, origin); + textDecoration.Draw(drawingContext, this); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index b116249fd4..3e85f0f6f0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = textCharacters.GlyphRun; - if (glyphRun.Bounds.Width < availableWidth) + if (glyphRun.Size.Width < availableWidth) { return glyphRun.Characters.Length; } @@ -348,7 +348,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[runIndex]; - if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) + if (currentWidth + currentRun.Size.Width > availableWidth) { var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); @@ -421,7 +421,7 @@ namespace Avalonia.Media.TextFormatting return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); } - currentWidth += currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.Size.Width; currentLength += currentRun.GlyphRun.Characters.Length; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index df1ecb4067..0c5179f88b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -115,22 +115,27 @@ namespace Avalonia.Media.TextFormatting /// Draws the text layout. /// /// The drawing context. - /// The origin. - public void Draw(DrawingContext context, Point origin) + public void Draw(DrawingContext context) { if (!TextLines.Any()) { return; } - var currentY = origin.Y; + var currentY = 0.0; foreach (var textLine in TextLines) { - var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, - _paragraphProperties.TextAlignment); + using (context.PushPostTransform(Matrix.CreateTranslation(0, currentY))) + { + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); - textLine.Draw(context, new Point(origin.X + offsetX, currentY)); + using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, 0))) + { + textLine.Draw(context); + } + } currentY += textLine.LineMetrics.Size.Height; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index c052fb8948..8a1efa0611 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -51,8 +51,7 @@ namespace Avalonia.Media.TextFormatting /// Draws the at the given origin. /// /// The drawing context. - /// The origin. - public abstract void Draw(DrawingContext drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext); /// /// Create a collapsed line based on collapsed text properties. diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 51092cddda..f5e87d097b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -33,17 +33,18 @@ namespace Avalonia.Media.TextFormatting public override bool HasCollapsed { get; } /// - public override void Draw(DrawingContext drawingContext, Point origin) + public override void Draw(DrawingContext drawingContext) { - var currentX = origin.X; + var currentX = 0.0; foreach (var textRun in _textRuns) { - var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline); - - textRun.Draw(drawingContext, baselineOrigin); + using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, 0))) + { + textRun.Draw(drawingContext); + } - currentX += textRun.Bounds.Width; + currentX += textRun.Size.Width; } } @@ -64,13 +65,13 @@ namespace Avalonia.Media.TextFormatting var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); - var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width; + var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width; while (runIndex < _textRuns.Count) { var currentRun = _textRuns[runIndex]; - currentWidth += currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.Size.Width; if (currentWidth > availableWidth) { @@ -125,7 +126,7 @@ namespace Avalonia.Media.TextFormatting return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true); } - availableWidth -= currentRun.GlyphRun.Bounds.Width; + availableWidth -= currentRun.Size.Width; collapsedLength += currentRun.GlyphRun.Characters.Length; @@ -133,7 +134,7 @@ namespace Avalonia.Media.TextFormatting } textLineMetrics = - new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width), + new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width), LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed); return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null, @@ -156,12 +157,12 @@ namespace Avalonia.Media.TextFormatting { characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); - if (distance <= run.Bounds.Width) + if (distance <= run.Size.Width) { break; } - distance -= run.Bounds.Width; + distance -= run.Size.Width; } return characterHit; @@ -229,7 +230,7 @@ namespace Avalonia.Media.TextFormatting { if (codepointIndex > textRun.Text.End) { - currentDistance += textRun.Bounds.Width; + currentDistance += textRun.Size.Width; continue; } @@ -405,7 +406,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < shapedTextCharacters.Count; i++) { - shapedWidth += shapedTextCharacters[i].Bounds.Width; + shapedWidth += shapedTextCharacters[i].Size.Width; } return shapedWidth; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 6875cc1c04..c4d7527659 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media.TextFormatting var fontMetrics = new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize); - lineWidth += shapedRun.Bounds.Width; + lineWidth += shapedRun.Size.Width; if (ascent > fontMetrics.Ascent) { diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index c87946b3ea..019614ae80 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -84,8 +84,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - /// The baseline origin of the glyph run. - void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 4a364998fd..cb6b1f59d4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -204,13 +204,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) { - Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground))); + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); } else diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs index bdf05c4f86..a6dba1bd32 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,20 +18,17 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// The baseline origin of the glyph run. /// Child scenes for drawing visual brushes. public GlyphRunNode( Matrix transform, IBrush foreground, GlyphRun glyphRun, - Point baselineOrigin, IDictionary childScenes = null) - : base(glyphRun.Bounds.Translate(baselineOrigin), transform) + : base(new Rect(glyphRun.Size), transform) { Transform = transform; Foreground = foreground?.ToImmutable(); GlyphRun = glyphRun; - BaselineOrigin = baselineOrigin; ChildScenes = childScenes; } @@ -49,11 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - /// Gets the baseline origin. - /// - public Point BaselineOrigin { get; set; } - /// public override IDictionary ChildScenes { get; } @@ -61,7 +54,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + context.DrawGlyphRun(Foreground, GlyphRun); } /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a155fd863b..98528a128a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -401,16 +401,16 @@ namespace Avalonia.Skia } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; ConfigureTextRendering(paintWrapper); - Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, - (float)baselineOrigin.Y, paintWrapper.Paint); + Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.BaselineOrigin.X, + (float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e0de40525f..258a51db5a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -324,13 +324,14 @@ namespace Avalonia.Direct2D1.Media /// The foreground. /// The glyph run. /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; - _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); + _renderTarget.DrawGlyphRun(glyphRun.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, + brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index bf41381b52..f3e1c37705 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = shapedRun.GlyphRun; - var width = glyphRun.Bounds.Width; + var width = glyphRun.Size.Width; var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _); From 0042e82db2766931acde34de6e994abed235698f Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Sat, 19 Sep 2020 13:42:25 -0400 Subject: [PATCH 124/149] Add resizing ui test for RenderDemo --- samples/RenderDemo/MainWindow.xaml | 8 +++- .../ViewModels/MainWindowViewModel.cs | 48 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 770960d7c4..93fbe5e412 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -3,8 +3,8 @@ x:Class="RenderDemo.MainWindow" Title="AvaloniaUI Rendering Test" xmlns:pages="clr-namespace:RenderDemo.Pages" - Width="800" - Height="600"> + Width="{Binding Width, Mode=TwoWay}" + Height="{Binding Height, Mode=TwoWay}"> @@ -24,6 +24,10 @@ + + + diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index d2d789a687..eda5e80530 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ -using System; -using System.Reactive; +using System.Reactive; +using System.Threading.Tasks; + using ReactiveUI; namespace RenderDemo.ViewModels @@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels { private bool drawDirtyRects = false; private bool drawFps = true; + private double width = 800; + private double height = 600; public MainWindowViewModel() { ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); + ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync); } public bool DrawDirtyRects { - get { return drawDirtyRects; } - set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } + get => drawDirtyRects; + set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } public bool DrawFps { - get { return drawFps; } - set { this.RaiseAndSetIfChanged(ref drawFps, value); } + get => drawFps; + set => this.RaiseAndSetIfChanged(ref drawFps, value); + } + + public double Width + { + get => width; + set => this.RaiseAndSetIfChanged(ref width, value); + } + + public double Height + { + get => height; + set => this.RaiseAndSetIfChanged(ref height, value); } public ReactiveCommand ToggleDrawDirtyRects { get; } public ReactiveCommand ToggleDrawFps { get; } + public ReactiveCommand ResizeWindow { get; } + + private async Task ResizeWindowAsync() + { + for (int i = 0; i < 30; i++) + { + Width += 10; + Height += 5; + await Task.Delay(10); + } + + await Task.Delay(10); + + for (int i = 0; i < 30; i++) + { + Width -= 10; + Height -= 5; + await Task.Delay(10); + } + } } } From 7481aea60620b05a915fdb03b20ee56dc731908c Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 00:57:08 +0300 Subject: [PATCH 125/149] Removed AutoCompleteMode enum --- src/Avalonia.Controls/AutoCompleteBox.cs | 148 +----------------- .../AutoCompleteBoxTests.cs | 35 ----- 2 files changed, 4 insertions(+), 179 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 22b09ef110..c9b50a46fd 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -246,25 +246,6 @@ namespace Avalonia.Controls /// public delegate string AutoCompleteSelector(string search, T item); - /// - /// Specifies how the selected autocomplete result should be treated. - /// - public enum AutoCompleteMode - { - /// - /// Specifies that the text will be replaced - /// with the selected autocomplete result. - /// - Replace = 0, - - /// - /// Specifies that a custom selector is used. This mode is used when - /// the - /// property is set. - /// - Custom = 1 - } - /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -402,7 +383,7 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); - private AutoCompleteSelector _textSelector = AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace); + private AutoCompleteSelector _textSelector; public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -541,17 +522,6 @@ namespace Avalonia.Controls defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); - /// - /// Gets the identifier for the - /// - /// dependency property. - /// - public static readonly StyledProperty AutoCompleteModeProperty = - AvaloniaProperty.Register( - nameof(AutoCompleteMode), - defaultValue: AutoCompleteMode.Replace, - validate: IsValidAutoCompleteMode); - /// /// Identifies the /// @@ -593,8 +563,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect>( nameof(TextSelector), o => o.TextSelector, - (o, v) => o.TextSelector = v, - unsetValue: AutoCompleteSelection.GetSelector(AutoCompleteMode.Replace)); + (o, v) => o.TextSelector = v); /// /// Identifies the @@ -646,18 +615,6 @@ namespace Avalonia.Controls } } - private static bool IsValidAutoCompleteMode(AutoCompleteMode mode) - { - switch (mode) - { - case AutoCompleteMode.Replace: - case AutoCompleteMode.Custom: - return true; - default: - return false; - } - } - /// /// Handle the change of the IsEnabled property. /// @@ -808,19 +765,6 @@ namespace Avalonia.Controls TextFilter = AutoCompleteSearch.GetFilter(mode); } - /// - /// AutoCompleteModeProperty property changed handler. - /// - /// Event arguments. - private void OnAutoCompleteModePropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - AutoCompleteMode mode = (AutoCompleteMode)e.NewValue; - - // Sets the text selector for the new value - if (mode != AutoCompleteMode.Custom) - TextSelector = AutoCompleteSelection.GetSelector(mode); - } - /// /// ItemFilterProperty property changed handler. /// @@ -841,25 +785,6 @@ namespace Avalonia.Controls } } - /// - /// TextSelectorProperty property changed handler. - /// - /// Event arguments. - private void OnTextSelectorPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - AutoCompleteSelector value = e.NewValue as AutoCompleteSelector; - - // If null, revert to the "Replace" predicate - if (value == null) - { - AutoCompleteMode = AutoCompleteMode.Replace; - } - else if (value.Method.DeclaringType != typeof(AutoCompleteSelection)) - { - AutoCompleteMode = AutoCompleteMode.Custom; - } - } - /// /// ItemsSourceProperty property changed handler. /// @@ -905,8 +830,6 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); - AutoCompleteModeProperty.Changed.AddClassHandler((x,e) => x.OnAutoCompleteModePropertyChanged(e)); - TextSelectorProperty.Changed.AddClassHandler((x,e) => x.OnTextSelectorPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -1129,31 +1052,6 @@ namespace Avalonia.Controls set { SetValue(FilterModeProperty, value); } } - /// - /// Gets or sets how the text in the text box will be modified - /// with the selected autocomplete item. - /// - /// - /// One of the - /// values. The default is - /// . - /// - /// The specified value is not a valid - /// . - /// - /// - /// Use the AutoCompleteMode property to specify the way the text will - /// be modified with the selected autocomplete item. For example, text - /// can be modified in a predefined or custom way. The autocomplete - /// mode is automatically set to Custom if you set the TextSelector - /// property. - /// - public AutoCompleteMode AutoCompleteMode - { - get { return GetValue(AutoCompleteModeProperty); } - set { SetValue(AutoCompleteModeProperty, value); } - } - public string Watermark { get { return GetValue(WatermarkProperty); } @@ -2490,7 +2388,8 @@ namespace Avalonia.Controls } else { - text = TextSelector(SearchText, FormatValue(newItem, true)); + string formattedValue = FormatValue(newItem, true); + text = TextSelector == null ? formattedValue : TextSelector(SearchText, formattedValue); } // Update the Text property and the TextBox values @@ -2749,45 +2648,6 @@ namespace Avalonia.Controls } } - /// - /// A predefined set of selector functions for the known, built-in - /// AutoCompleteMode enumeration values. - /// - private static class AutoCompleteSelection - { - /// - /// Index function that retrieves the selector for the provided - /// AutoCompleteMode. - /// - /// The built-in autocomplete mode. - /// Returns the string-based selector function. - public static AutoCompleteSelector GetSelector(AutoCompleteMode completeMode) - { - switch (completeMode) - { - case AutoCompleteMode.Replace: - return Replace; - case AutoCompleteMode.Custom: - default: - return null; - } - } - - /// - /// Implements AutoCompleteMode.Replace. - /// - /// The AutoCompleteBox prefix text. - /// The item's string value. - /// - /// Return the and ignores the - /// . - /// - private static string Replace(string text, string value) - { - return value ?? String.Empty; - } - } - /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 2205384542..2e609132d6 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,30 +363,6 @@ namespace Avalonia.Controls.UnitTests }); } - [Fact] - public void Test_Selectors() - { - Assert.Equal(GetSelector(AutoCompleteMode.Replace)("Never", "gonna"), "gonna"); - Assert.Equal(GetSelector(AutoCompleteMode.Replace)("give", "you"), "you"); - Assert.NotEqual(GetSelector(AutoCompleteMode.Replace)("up", "!"), "42"); - } - - [Fact] - public void AutoCompleteMode_Changes_To_Custom_And_Back() - { - RunTest((control, textbox) => - { - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); - - control.TextSelector = (text, item) => text + item; - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Custom); - - control.AutoCompleteMode = AutoCompleteMode.Replace; - Assert.Equal(control.AutoCompleteMode, AutoCompleteMode.Replace); - Assert.Equal(control.TextSelector, GetSelector(AutoCompleteMode.Replace)); - }); - } - [Fact] public void Custom_TextSelector() { @@ -416,17 +392,6 @@ namespace Avalonia.Controls.UnitTests .TextFilter; } - /// - /// Retrieves a defined selector through a new AutoCompleteBox - /// control instance. - /// - /// The AutoCompleteMode of interest. - /// Returns the selector instance. - private static AutoCompleteSelector GetSelector(AutoCompleteMode mode) - { - return new AutoCompleteBox { AutoCompleteMode = mode }.TextSelector; - } - /// /// Creates a large list of strings for AutoCompleteBox testing. /// From bc87efc65fbae7ab338716f1e89a43b48fea6428 Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 01:56:05 +0300 Subject: [PATCH 126/149] Added ItemSelector property to the AutoCompleteBox --- src/Avalonia.Controls/AutoCompleteBox.cs | 54 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c9b50a46fd..c119fd1964 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -242,7 +242,7 @@ namespace Avalonia.Controls /// /// The type used for filtering the /// . - /// At the moment this type known only as a string. + /// This type can be either a string or an object. /// public delegate string AutoCompleteSelector(string search, T item); @@ -383,6 +383,7 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _itemSelector; private AutoCompleteSelector _textSelector; public static readonly RoutedEvent SelectionChangedEvent = @@ -551,6 +552,20 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemSelector), + o => o.ItemSelector, + (o, v) => o.ItemSelector = v); + /// /// Identifies the /// @@ -1100,18 +1115,32 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that combines the user-entered - /// text to and one of the items specified by the + /// text and one of the items specified by the /// . /// /// /// The custom method that combines the user-entered - /// text to and one of the items specified by the + /// text and one of the items specified by the /// . /// - /// - /// The AutoCompleteMode is automatically set to Custom if you set - /// the TextSelector property. - /// + public AutoCompleteSelector ItemSelector + { + get { return _itemSelector; } + set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); } + } + + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// public AutoCompleteSelector TextSelector { get { return _textSelector; } @@ -2386,10 +2415,17 @@ namespace Avalonia.Controls { text = SearchText; } + else if (TextSelector != null) + { + text = TextSelector(SearchText, FormatValue(newItem, true)); + } + else if (ItemSelector != null) + { + text = ItemSelector(SearchText, newItem); + } else { - string formattedValue = FormatValue(newItem, true); - text = TextSelector == null ? formattedValue : TextSelector(SearchText, formattedValue); + text = FormatValue(newItem, true); } // Update the Text property and the TextBox values From 997385eab7307201943dc94b88d417b5e3207fbd Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Mon, 21 Sep 2020 01:56:34 +0300 Subject: [PATCH 127/149] Made test for AutoCompleteBox.ItemSelector --- .../AutoCompleteBoxTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 2e609132d6..3e78e951e2 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -379,6 +379,23 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); }); } + + [Fact] + public void Custom_ItemSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.ItemSelector = (text, item) => text + item; + Assert.Equal(control.ItemSelector("4", 2), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); + }); + } /// /// Retrieves a defined predicate filter through a new AutoCompleteBox From 14aff78be5e40a92f2c58431e2472d882f0488c5 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Mon, 21 Sep 2020 03:16:14 -0700 Subject: [PATCH 128/149] use sealed for attribute class. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dariusz Komosiński --- .../Controls/Metadata/PseudoClassesAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs index 66d0282b92..0060767565 100644 --- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Avalonia.Controls.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class PseudoClassesAttribute : Attribute + public sealed class PseudoClassesAttribute : Attribute { public PseudoClassesAttribute(params string[] pseudoClasses) { From 3f3ec4b8351a1f034f71584751ca71ba03fe089b Mon Sep 17 00:00:00 2001 From: Kir-Antipov Date: Tue, 22 Sep 2020 03:40:10 +0300 Subject: [PATCH 129/149] Added custom TextSelector example --- .../Pages/AutoCompleteBoxPage.xaml | 5 ++ .../Pages/AutoCompleteBoxPage.xaml.cs | 58 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index f90a0c4658..a49616e543 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -51,6 +51,11 @@ Width="200" Margin="0,0,0,8" FilterMode="None"/> + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index f9d6a72a3a..574cc79a7d 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -92,13 +92,28 @@ namespace ControlCatalog.Pages } public StateData[] States { get; private set; } + private LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + public AutoCompleteBoxPage() { this.InitializeComponent(); States = BuildAllStates(); + Sentences = BuildAllSentences(); - foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) { box.Items = States; } @@ -116,6 +131,11 @@ namespace ControlCatalog.Pages var asyncBox = this.FindControl("AsyncBox"); asyncBox.AsyncPopulator = PopulateAsync; + + var customAutocompleteBox = this.FindControl("CustomAutocompleteBox"); + customAutocompleteBox.Items = Sentences.SelectMany(x => x); + customAutocompleteBox.TextFilter = LastWordContains; + customAutocompleteBox.TextSelector = AppendWord; } private IEnumerable GetAllAutoCompleteBox() { @@ -137,6 +157,42 @@ namespace ControlCatalog.Pages .ToList(); } + private bool LastWordContains(string searchText, string item) + { + var words = searchText.Split(' '); + var options = Sentences.Select(x => x.First).ToArray(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; j < options.Length; ++j) + { + var option = options[j]; + if (option == null) + continue; + + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string text, string item) + { + string[] parts = text.Split(' '); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); From ab354827244116f18b331d8cf022c151ccf44493 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 22 Sep 2020 00:27:51 -0400 Subject: [PATCH 130/149] Make Fluent buttons animation consistent --- src/Avalonia.Themes.Fluent/Button.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index e58e8758d2..021c6eae1d 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - From 95f07a9f9687adc93c0783defd1aebba7fbf47d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 23 Sep 2020 01:04:03 -0400 Subject: [PATCH 131/149] Only specific buttons should have pressing animation --- src/Avalonia.Themes.Fluent/Button.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 021c6eae1d..8522c933ae 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - From 1430bf86e32b820aac26a159c3ee98a33b58f5dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Sep 2020 12:13:38 +0200 Subject: [PATCH 132/149] Use screen bounds Bottom instead of Height. Secondard screens on Windows can have a Y offset (i.e. `bounds.Y != 0`) if they're offset vertically from the primary screen, and when this was the case the popup wasn't getting properly constrained as the bottom is not equal to the height. Fixes #4726 --- .../Primitives/PopupPositioning/ManagedPopupPositioner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 8c464c7aad..7f1dbdf592 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom)) { - unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y); + unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y); } if (IsValid(unconstrainedRect)) From 6c52a7cb64ca660d1b03abfca22918e0a4704199 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 23 Sep 2020 16:16:09 -0500 Subject: [PATCH 133/149] Move IFocusScope to IPopupHost --- src/Avalonia.Controls/Primitives/IPopupHost.cs | 3 ++- src/Avalonia.Controls/Primitives/PopupRoot.cs | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index e424bf683d..82a49c4189 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -13,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// () or an which is created /// on an . /// - public interface IPopupHost : IDisposable + public interface IPopupHost : IDisposable, IFocusScope { /// /// Sets the control to display in the popup. diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 2721ab879f..da7352b77f 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -15,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost, IFocusScope + public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; From 52075d6cd98527d7eb75c890c3d1f0a91fe6e203 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 23 Sep 2020 16:16:54 -0500 Subject: [PATCH 134/149] Fix Test --- .../Primitives/PopupTests.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index e5dcba9912..53a8db2176 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -400,11 +400,13 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServicesWithFocus()) { + var window = PreparedWindow(); + var tb = new TextBox(); var b = new Button(); var p = new Popup { - PlacementTarget = PreparedWindow(), + PlacementTarget = window, Child = new StackPanel { Children = @@ -415,15 +417,28 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + window.Show(); - p.Opened += (s, e) => + p.Open(); + + if(p.Host is OverlayPopupHost host) { - tb.Focus(); - }; + //Need to measure/arrange for visual children to show up + //in OverlayPopupHost + host.Measure(Size.Infinity); + host.Arrange(new Rect(host.DesiredSize)); + } - p.Open(); + tb.Focus(); Assert.True(FocusManager.Instance?.Current == tb); + + //Ensure focus remains in the popup + var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next); + + Assert.True(nextFocus == b); + + p.Close(); } } From 888dd2b61b2bc1b1deadae4ed2c325874e5558a7 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 25 Sep 2020 16:30:51 +0200 Subject: [PATCH 135/149] Simplify PushPostTransform usage --- .../Media/TextFormatting/TextLayout.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 0c5179f88b..daa8807bf6 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -126,15 +126,12 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in TextLines) { - using (context.PushPostTransform(Matrix.CreateTranslation(0, currentY))) - { - var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, - _paragraphProperties.TextAlignment); + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); - using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, 0))) - { - textLine.Draw(context); - } + using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, currentY))) + { + textLine.Draw(context); } currentY += textLine.LineMetrics.Size.Height; From b9985a8fa0fde48ae98af48bd3f60c4bbdde90d4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Sep 2020 14:47:24 +0300 Subject: [PATCH 136/149] Refactorings for opengl context and surface management --- .../ControlCatalog/Pages/OpenGlPage.xaml.cs | 1 + src/Avalonia.Native/AvaloniaNativePlatform.cs | 8 +- ... AvaloniaNativePlatformOpenGlInterface.cs} | 41 ++-- src/Avalonia.Native/PopupImpl.cs | 6 +- src/Avalonia.Native/WindowImpl.cs | 6 +- src/Avalonia.Native/WindowImplBase.cs | 2 +- .../Angle/AngleEglInterface.cs | 1 + .../Angle/AngleWin32EglDisplay.cs | 10 +- .../{ => Controls}/OpenGlControlBase.cs | 192 +++++++++------- src/Avalonia.OpenGL/{ => Egl}/EglConsts.cs | 2 +- src/Avalonia.OpenGL/{ => Egl}/EglContext.cs | 69 ++++-- src/Avalonia.OpenGL/{ => Egl}/EglDisplay.cs | 53 ++--- src/Avalonia.OpenGL/{ => Egl}/EglErrors.cs | 2 +- .../Egl/EglGlPlatformSurface.cs | 54 +++++ .../{ => Egl}/EglGlPlatformSurfaceBase.cs | 47 ++-- src/Avalonia.OpenGL/{ => Egl}/EglInterface.cs | 2 +- .../Egl/EglPlatformOpenGlInterface.cs | 72 ++++++ src/Avalonia.OpenGL/{ => Egl}/EglSurface.cs | 11 +- src/Avalonia.OpenGL/EglGlPlatformFeature.cs | 43 ---- src/Avalonia.OpenGL/EglGlPlatformSurface.cs | 51 ----- src/Avalonia.OpenGL/GlInterface.cs | 13 ++ src/Avalonia.OpenGL/IGlContext.cs | 2 + .../IOpenGlAwarePlatformRenderInterface.cs | 2 +- .../IPlatformOpenGlInterface.cs | 13 ++ .../IWindowingPlatformGlFeature.cs | 8 - .../Imaging/IOpenGlBitmapImpl.cs | 17 ++ .../Imaging/IOpenGlTextureBitmapImpl.cs | 13 -- ...OpenGlTextureBitmap.cs => OpenGlBitmap.cs} | 34 ++- src/Avalonia.OpenGL/OpenGlException.cs | 1 + .../{ => Surfaces}/IGlPlatformSurface.cs | 2 +- .../IGlPlatformSurfaceRenderTarget.cs | 2 +- .../IGlPlatformSurfaceRenderingSession.cs | 2 +- src/Avalonia.X11/Glx/GlxContext.cs | 53 ++++- src/Avalonia.X11/Glx/GlxDisplay.cs | 8 +- src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs | 25 +-- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 13 +- src/Avalonia.X11/X11Platform.cs | 5 +- src/Avalonia.X11/X11Window.cs | 9 +- .../LinuxFramebufferPlatform.cs | 4 +- .../Output/DrmOutput.cs | 18 +- .../Output/IGlOutputBackend.cs | 9 + src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 2 +- .../Gpu/OpenGl/GlRenderTarget.cs | 1 + .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 9 +- .../Gpu/OpenGl/OpenGlBitmapImpl.cs | 207 ++++++++++++++++++ .../Gpu/OpenGlTextureBitmapImpl.cs | 81 ------- .../Avalonia.Skia/PlatformRenderInterface.cs | 6 +- src/Windows/Avalonia.Win32/Win32GlManager.cs | 9 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 +- src/iOS/Avalonia.iOS/EaglDisplay.cs | 20 +- src/iOS/Avalonia.iOS/EaglLayerSurface.cs | 3 +- src/iOS/Avalonia.iOS/Platform.cs | 2 +- 52 files changed, 786 insertions(+), 486 deletions(-) rename src/Avalonia.Native/{GlPlatformFeature.cs => AvaloniaNativePlatformOpenGlInterface.cs} (76%) rename src/Avalonia.OpenGL/{ => Controls}/OpenGlControlBase.cs (50%) rename src/Avalonia.OpenGL/{ => Egl}/EglConsts.cs (99%) rename src/Avalonia.OpenGL/{ => Egl}/EglContext.cs (55%) rename src/Avalonia.OpenGL/{ => Egl}/EglDisplay.cs (79%) rename src/Avalonia.OpenGL/{ => Egl}/EglErrors.cs (96%) create mode 100644 src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs rename src/Avalonia.OpenGL/{ => Egl}/EglGlPlatformSurfaceBase.cs (67%) rename src/Avalonia.OpenGL/{ => Egl}/EglInterface.cs (99%) create mode 100644 src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs rename src/Avalonia.OpenGL/{ => Egl}/EglSurface.cs (57%) delete mode 100644 src/Avalonia.OpenGL/EglGlPlatformFeature.cs delete mode 100644 src/Avalonia.OpenGL/EglGlPlatformSurface.cs create mode 100644 src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs delete mode 100644 src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs create mode 100644 src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs delete mode 100644 src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs rename src/Avalonia.OpenGL/Imaging/{OpenGlTextureBitmap.cs => OpenGlBitmap.cs} (54%) rename src/Avalonia.OpenGL/{ => Surfaces}/IGlPlatformSurface.cs (77%) rename src/Avalonia.OpenGL/{ => Surfaces}/IGlPlatformSurfaceRenderTarget.cs (89%) rename src/Avalonia.OpenGL/{ => Surfaces}/IGlPlatformSurfaceRenderingSession.cs (86%) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs delete mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index 6c13a5ac22..cb79bf219a 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; using Avalonia.Platform.Interop; using Avalonia.Threading; using static Avalonia.OpenGL.GlConsts; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 804cf7f8ac..e8b2f065c7 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -16,7 +16,7 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; - private GlPlatformFeature _glFeature; + private AvaloniaNativePlatformOpenGlInterface _platformGl; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); @@ -116,8 +116,8 @@ namespace Avalonia.Native { try { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(_platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay())); } catch (Exception) { @@ -128,7 +128,7 @@ namespace Avalonia.Native public IWindowImpl CreateWindow() { - return new WindowImpl(_factory, _options, _glFeature); + return new WindowImpl(_factory, _options, _platformGl); } public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Native/GlPlatformFeature.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs similarity index 76% rename from src/Avalonia.Native/GlPlatformFeature.cs rename to src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index e321db6eda..dbe968b82f 100644 --- a/src/Avalonia.Native/GlPlatformFeature.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -2,21 +2,20 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; +using Avalonia.OpenGL.Surfaces; using Avalonia.Threading; namespace Avalonia.Native { - class GlPlatformFeature : IWindowingPlatformGlFeature + class AvaloniaNativePlatformOpenGlInterface : IPlatformOpenGlInterface { private readonly IAvnGlDisplay _display; - public GlPlatformFeature(IAvnGlDisplay display) + public AvaloniaNativePlatformOpenGlInterface(IAvnGlDisplay display) { _display = display; var immediate = display.CreateContext(null); - var deferred = display.CreateContext(immediate); - int major, minor; GlInterface glInterface; using (immediate.MakeCurrent()) @@ -33,19 +32,22 @@ namespace Avalonia.Native } GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); - - ImmediateContext = new GlContext(GlDisplay, immediate, _version); - DeferredContext = new GlContext(GlDisplay, deferred, _version); + MainContext = new GlContext(GlDisplay, null, immediate, _version); } - internal IGlContext ImmediateContext { get; } - public IGlContext MainContext => DeferredContext; - internal GlContext DeferredContext { get; } + internal GlContext MainContext { get; } + public IGlContext PrimaryContext => MainContext; + + public bool CanShareContexts => true; + public bool CanCreateContexts => true; internal GlDisplay GlDisplay; private readonly GlVersion _version; + public IGlContext CreateSharedContext() => new GlContext(GlDisplay, + MainContext, _display.CreateContext(MainContext.Context), _version); + public IGlContext CreateContext() => new GlContext(GlDisplay, - _display.CreateContext(((GlContext)ImmediateContext).Context), _version); + null, _display.CreateContext(null), _version); } class GlDisplay @@ -72,11 +74,13 @@ namespace Avalonia.Native class GlContext : IGlContext { private readonly GlDisplay _display; + private readonly GlContext _sharedWith; public IAvnGlContext Context { get; private set; } - public GlContext(GlDisplay display, IAvnGlContext context, GlVersion version) + public GlContext(GlDisplay display, GlContext sharedWith, IAvnGlContext context, GlVersion version) { _display = display; + _sharedWith = sharedWith; Context = context; Version = version; } @@ -86,6 +90,17 @@ namespace Avalonia.Native public int SampleCount => _display.SampleCount; public int StencilSize => _display.StencilSize; public IDisposable MakeCurrent() => Context.MakeCurrent(); + public IDisposable EnsureCurrent() => MakeCurrent(); + + public bool IsSharedWith(IGlContext context) + { + var c = (GlContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + public void Dispose() { @@ -108,7 +123,7 @@ namespace Avalonia.Native public IGlPlatformSurfaceRenderingSession BeginDraw() { - var feature = (GlPlatformFeature)AvaloniaLocator.Current.GetService(); + var feature = (AvaloniaNativePlatformOpenGlInterface)AvaloniaLocator.Current.GetService(); return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 2d246e08d2..2f98385038 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - private readonly GlPlatformFeature _glFeature; + private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - GlPlatformFeature glFeature, + AvaloniaNativePlatformOpenGlInterface glFeature, IWindowBaseImpl parent) : base(opts, glFeature) { _factory = factory; @@ -23,7 +23,7 @@ namespace Avalonia.Native _parent = parent; using (var e = new PopupEvents(this)) { - var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + var context = _opts.UseGpu ? glFeature?.MainContext : null; Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); } PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 885591495b..11a0ebce61 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,19 +14,19 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - private readonly GlPlatformFeature _glFeature; + private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; IAvnWindow _native; private double _extendTitleBarHeight = -1; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - GlPlatformFeature glFeature) : base(opts, glFeature) + AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature) { _factory = factory; _opts = opts; _glFeature = glFeature; using (var e = new WindowEvents(this)) { - var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + var context = _opts.UseGpu ? glFeature?.MainContext : null; Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 56cf544d9d..5d35c773d7 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -61,7 +61,7 @@ namespace Avalonia.Native private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) + internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) { _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs index 8565d99b45..8c9b028164 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Platform.Interop; diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs index 1a42ed90c2..191fb53204 100644 --- a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs +++ b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; - -using static Avalonia.OpenGL.EglConsts; +using Avalonia.OpenGL.Egl; +using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Angle { @@ -52,7 +52,7 @@ namespace Avalonia.OpenGL.Angle } } - private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, info.Display) + private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, false, info.Display) { PlatformApi = info.PlatformApi; } @@ -78,11 +78,11 @@ namespace Avalonia.OpenGL.Angle return d3dDeviceHandle; } - public EglSurface WrapDirect3D11Texture(IntPtr handle) + public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle) { if (PlatformApi != AngleOptions.PlatformApi.DirectX11) throw new InvalidOperationException("Current platform API is " + PlatformApi); - return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); } } } diff --git a/src/Avalonia.OpenGL/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs similarity index 50% rename from src/Avalonia.OpenGL/OpenGlControlBase.cs rename to src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 8567dcae20..33773ed8e2 100644 --- a/src/Avalonia.OpenGL/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -3,44 +3,83 @@ using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.OpenGL.Imaging; -using Avalonia.Rendering; -using Avalonia.VisualTree; using static Avalonia.OpenGL.GlConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { private IGlContext _context; - private int _fb, _texture, _renderBuffer; - private OpenGlTextureBitmap _bitmap; - private PixelSize _oldSize; + private int _fb, _depthBuffer; + private OpenGlBitmap _bitmap; + private IOpenGlBitmapAttachment _attachment; + private PixelSize _depthBufferSize; private bool _glFailed; + private bool _initialized; protected GlVersion GlVersion { get; private set; } public sealed override void Render(DrawingContext context) { if(!EnsureInitialized()) return; - + using (_context.MakeCurrent()) { - using (_bitmap.Lock()) - { - var gl = _context.GlInterface; - gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - if (_oldSize != GetPixelSize()) - ResizeTexture(gl); - - OnOpenGlRender(gl, _fb); - gl.Flush(); - } + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); + EnsureTextureAttachment(); + EnsureDepthBufferAttachment(_context.GlInterface); + if(!CheckFramebufferStatus(_context.GlInterface)) + return; + + OnOpenGlRender(_context.GlInterface, _fb); + _attachment.Present(); } context.DrawImage(_bitmap, new Rect(_bitmap.Size), Bounds); base.Render(context); } + + private void CheckError(GlInterface gl) + { + int err; + while ((err = gl.GetError()) != GL_NO_ERROR) + Console.WriteLine(err); + } + + void EnsureTextureAttachment() + { + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); + if (_bitmap == null || _attachment == null || _bitmap.PixelSize != GetPixelSize()) + { + _attachment?.Dispose(); + _attachment = null; + _bitmap?.Dispose(); + _bitmap = null; + _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _attachment = _bitmap.CreateFramebufferAttachment(_context); + } + } + + void EnsureDepthBufferAttachment(GlInterface gl) + { + var size = GetPixelSize(); + if (size == _depthBufferSize && _depthBuffer != 0) + return; + + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + if (_depthBuffer != 0) gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + + var oneArr = new int[1]; + gl.GenRenderbuffers(1, oneArr); + _depthBuffer = oneArr[0]; + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + } - void DoCleanup(bool callUserDeinit) + void DoCleanup() { if (_context != null) { @@ -50,16 +89,19 @@ namespace Avalonia.OpenGL gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.DeleteFramebuffers(1, new[] { _fb }); - using (_bitmap.Lock()) - _bitmap.SetTexture(0, 0, new PixelSize(1, 1), 1); - gl.DeleteTextures(1, new[] { _texture }); - gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); - _bitmap.Dispose(); + gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + _attachment?.Dispose(); + _attachment = null; + _bitmap?.Dispose(); + _bitmap = null; try { - if (callUserDeinit) + if (_initialized) + { + _initialized = false; OnOpenGlDeinit(_context.GlInterface, _fb); + } } finally { @@ -72,11 +114,11 @@ namespace Avalonia.OpenGL protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - DoCleanup(true); + DoCleanup(); base.OnDetachedFromVisualTree(e); } - bool EnsureInitialized() + private bool EnsureInitializedCore() { if (_context != null) return true; @@ -84,34 +126,43 @@ namespace Avalonia.OpenGL if (_glFailed) return false; - var feature = AvaloniaLocator.Current.GetService(); + var feature = AvaloniaLocator.Current.GetService(); if (feature == null) return false; + if (!feature.CanShareContexts) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); + return false; + } try { - _context = feature.CreateContext(); - + _context = feature.CreateSharedContext(); } catch (Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - _glFailed = true; return false; } GlVersion = _context.Version; try { - _bitmap = new OpenGlTextureBitmap(); + _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + if (!_bitmap.SupportsContext(_context)) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create OpenGlBitmap: OpenGL context is not compatible"); + return false; + } } catch (Exception e) { _context.Dispose(); _context = null; Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e); - _glFailed = true; + "Unable to initialize OpenGL: unable to create OpenGlBitmap: {exception}", e); return false; } @@ -119,80 +170,55 @@ namespace Avalonia.OpenGL { try { - _oldSize = GetPixelSize(); + _depthBufferSize = GetPixelSize(); var gl = _context.GlInterface; var oneArr = new int[1]; gl.GenFramebuffers(1, oneArr); _fb = oneArr[0]; gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - - gl.GenTextures(1, oneArr); - _texture = oneArr[0]; - ResizeTexture(gl); - - gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + EnsureDepthBufferAttachment(gl); + EnsureTextureAttachment(); - var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) - { - int code; - while ((code = gl.GetError()) != 0) - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {code}", code); - - _glFailed = true; - return false; - } + return CheckFramebufferStatus(gl); } catch(Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to initialize OpenGL FBO: {exception}", e); - _glFailed = true; + return false; } - - if (!_glFailed) - OnOpenGlInit(_context.GlInterface, _fb); } + } - if (_glFailed) + private bool CheckFramebufferStatus(GlInterface gl) + { + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { - DoCleanup(false); + int code; + while ((code = gl.GetError()) != 0) + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL FBO: {code}", code); + return false; } return true; } - void ResizeTexture(GlInterface gl) + private bool EnsureInitialized() { - var size = GetPixelSize(); - - gl.GetIntegerv( GL_TEXTURE_BINDING_2D, out var oldTexture); - gl.BindTexture(GL_TEXTURE_2D, _texture); - gl.TexImage2D(GL_TEXTURE_2D, 0, - GlVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8, - size.Width, size.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - - gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); - var oneArr = new int[1]; - gl.GenRenderbuffers(1, oneArr); - _renderBuffer = oneArr[0]; - gl.BindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); - gl.RenderbufferStorage(GL_RENDERBUFFER, - GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, - size.Width, size.Height); - gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderBuffer); - gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); - using (_bitmap.Lock()) - _bitmap.SetTexture(_texture, GL_RGBA8, size, 1); + if (_initialized) + return true; + _glFailed = !(_initialized = EnsureInitializedCore()); + if (_glFailed) + return false; + using (_context.MakeCurrent()) + OnOpenGlInit(_context.GlInterface, _fb); + return true; } - PixelSize GetPixelSize() + private PixelSize GetPixelSize() { var scaling = VisualRoot.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs similarity index 99% rename from src/Avalonia.OpenGL/EglConsts.cs rename to src/Avalonia.OpenGL/Egl/EglConsts.cs index 8e44004f2d..58f5f1cef5 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -1,6 +1,6 @@ // ReSharper disable UnusedMember.Global // ReSharper disable IdentifierTypo -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public static class EglConsts { diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs similarity index 55% rename from src/Avalonia.OpenGL/EglContext.cs rename to src/Avalonia.OpenGL/Egl/EglContext.cs index 871665e857..5365354418 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,23 +1,25 @@ using System; using System.Reactive.Disposables; using System.Threading; -using static Avalonia.OpenGL.EglConsts; +using static Avalonia.OpenGL.Egl.EglConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglContext : IGlContext { private readonly EglDisplay _disp; private readonly EglInterface _egl; + private readonly EglContext _sharedWith; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface, + public EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, Func offscreenSurface, GlVersion version, int sampleCount, int stencilSize) { _disp = display; _egl = egl; + _sharedWith = sharedWith; Context = ctx; - OffscreenSurface = offscreenSurface; + OffscreenSurface = offscreenSurface(this); Version = version; SampleCount = sampleCount; StencilSize = stencilSize; @@ -33,21 +35,17 @@ namespace Avalonia.OpenGL public int StencilSize { get; } public EglDisplay Display => _disp; - public IDisposable Lock() - { - Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); - } - class RestoreContext : IDisposable { private readonly EglInterface _egl; + private readonly object _l; private readonly IntPtr _display; private IntPtr _context, _read, _draw; - public RestoreContext(EglInterface egl, IntPtr defDisplay) + public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { _egl = egl; + _l = l; _display = _egl.GetCurrentDisplay(); if (_display == IntPtr.Zero) _display = defDisplay; @@ -59,29 +57,52 @@ namespace Avalonia.OpenGL public void Dispose() { _egl.MakeCurrent(_display, _draw, _read, _context); + Monitor.Exit(_l); } } - public IDisposable MakeCurrent() + public IDisposable MakeCurrent() => MakeCurrent(OffscreenSurface); + + public IDisposable MakeCurrent(EglSurface surface) { - var old = new RestoreContext(_egl, _disp.Handle); - _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - return old; + Monitor.Enter(_lock); + var success = false; + try + { + var old = new RestoreContext(_egl, _disp.Handle, _lock); + var surf = surface ?? OffscreenSurface; + _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) + throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + success = true; + return old; + } + finally + { + if(!success) + Monitor.Enter(_lock); + } } - public IDisposable MakeCurrent(EglSurface surface) + public IDisposable EnsureCurrent() { - var old = new RestoreContext(_egl, _disp.Handle); - var surf = surface ?? OffscreenSurface; - _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - return old; + if(IsCurrent) + return Disposable.Empty; + return MakeCurrent(); } + public bool IsSharedWith(IGlContext context) + { + var c = (EglContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + + public bool IsCurrent => _egl.GetCurrentDisplay() == _disp.Handle && _egl.GetCurrentContext() == Context; + public void Dispose() { _egl.DestroyContext(_disp.Handle, Context); diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs similarity index 79% rename from src/Avalonia.OpenGL/EglDisplay.cs rename to src/Avalonia.OpenGL/Egl/EglDisplay.cs index 7f41e75d6a..fd3de854f5 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,26 +1,25 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -using Avalonia.Platform.Interop; -using static Avalonia.OpenGL.EglConsts; +using static Avalonia.OpenGL.Egl.EglConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglDisplay { private readonly EglInterface _egl; + public bool SupportsSharing { get; } private readonly IntPtr _display; private readonly IntPtr _config; private readonly int[] _contextAttributes; private readonly int _surfaceType; public IntPtr Handle => _display; + public IntPtr Config => _config; private int _sampleCount; private int _stencilSize; private GlVersion _version; - public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) + public EglDisplay(EglInterface egl, bool supportsSharing) : this(egl, supportsSharing, -1, IntPtr.Zero, null) { } @@ -45,15 +44,16 @@ namespace Avalonia.OpenGL return display; } - public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) - : this(egl, CreateDisplay(egl, platformType, platformDisplay, attrs)) + public EglDisplay(EglInterface egl, bool supportsSharing, int platformType, IntPtr platformDisplay, int[] attrs) + : this(egl, supportsSharing, CreateDisplay(egl, platformType, platformDisplay, attrs)) { } - public EglDisplay(EglInterface egl, IntPtr display) + public EglDisplay(EglInterface egl, bool supportsSharing, IntPtr display) { _egl = egl; + SupportsSharing = supportsSharing; _display = display; if(_display == IntPtr.Zero) throw new ArgumentException(); @@ -136,7 +136,12 @@ namespace Avalonia.OpenGL throw new OpenGlException("No suitable EGL config was found"); } - public EglDisplay() : this(new EglInterface()) + public EglDisplay() : this(false) + { + + } + + public EglDisplay(bool supportsSharing) : this(new EglInterface(), supportsSharing) { } @@ -144,6 +149,9 @@ namespace Avalonia.OpenGL public EglInterface EglInterface => _egl; public EglContext CreateContext(IGlContext share) { + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); + if((_surfaceType|EGL_PBUFFER_BIT) == 0) throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); var shareCtx = (EglContext)share; @@ -158,37 +166,22 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf), + var rv = new EglContext(this, _egl, shareCtx, ctx, context => new EglSurface(this, context, surf), _version, _sampleCount, _stencilSize); return rv; } public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) { + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); + var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, ctx, offscreenSurface, _version, _sampleCount, _stencilSize); + var rv = new EglContext(this, _egl, share, ctx, _ => offscreenSurface, _version, _sampleCount, _stencilSize); rv.MakeCurrent(null); return rv; } - - public EglSurface CreateWindowSurface(IntPtr window) - { - var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE}); - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); - return new EglSurface(this, _egl, s); - } - - public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) - { - var s = _egl.CreatePbufferFromClientBuffer(_display, bufferType, handle, - _config, attribs); - - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", _egl); - return new EglSurface(this, _egl, s); - } } } diff --git a/src/Avalonia.OpenGL/EglErrors.cs b/src/Avalonia.OpenGL/Egl/EglErrors.cs similarity index 96% rename from src/Avalonia.OpenGL/EglErrors.cs rename to src/Avalonia.OpenGL/Egl/EglErrors.cs index bfe46f2b69..d89bbb499f 100644 --- a/src/Avalonia.OpenGL/EglErrors.cs +++ b/src/Avalonia.OpenGL/Egl/EglErrors.cs @@ -1,4 +1,4 @@ -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public enum EglErrors { diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs new file mode 100644 index 0000000000..3d58660d47 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -0,0 +1,54 @@ +using Avalonia.OpenGL.Surfaces; + +namespace Avalonia.OpenGL.Egl +{ + public class EglGlPlatformSurface : EglGlPlatformSurfaceBase + { + private readonly EglPlatformOpenGlInterface _egl; + private readonly IEglWindowGlPlatformSurfaceInfo _info; + + public EglGlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) : base() + { + _egl = egl; + _info = info; + } + + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + var glSurface = _egl.CreateWindowSurface(_info.Handle); + return new RenderTarget(_egl, glSurface, _info); + } + + class RenderTarget : EglPlatformSurfaceRenderTargetBase + { + private readonly EglPlatformOpenGlInterface _egl; + private EglSurface _glSurface; + private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _currentSize; + + public RenderTarget(EglPlatformOpenGlInterface egl, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(egl) + { + _egl = egl; + _glSurface = glSurface; + _info = info; + _currentSize = info.Size; + } + + public override void Dispose() => _glSurface.Dispose(); + + public override IGlPlatformSurfaceRenderingSession BeginDraw() + { + if (_info.Size != _currentSize || _glSurface == null) + { + _glSurface?.Dispose(); + _glSurface = null; + _glSurface = _egl.CreateWindowSurface(_info.Handle); + _currentSize = _info.Size; + } + return base.BeginDraw(_glSurface, _info); + } + } + } +} + diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs similarity index 67% rename from src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs rename to src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index 00c7c4796c..4ea6766de2 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -1,6 +1,7 @@ using System; +using Avalonia.OpenGL.Surfaces; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface { @@ -14,19 +15,15 @@ namespace Avalonia.OpenGL public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); } - public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTarget { - private readonly EglDisplay _display; - private readonly EglContext _context; + private readonly EglPlatformOpenGlInterface _egl; - protected EglPlatformSurfaceRenderTargetBase(EglDisplay display, EglContext context) + protected EglPlatformSurfaceRenderTargetBase(EglPlatformOpenGlInterface egl) { - _display = display; - _context = context; + _egl = egl; } - public abstract bool IsCorrupted { get; } - public virtual void Dispose() { @@ -37,22 +34,25 @@ namespace Avalonia.OpenGL protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) { - var l = _context.Lock(); + + var restoreContext = _egl.PrimaryEglContext.MakeCurrent(surface); + var success = false; try { - if (IsCorrupted) - throw new RenderTargetCorruptedException(); - var restoreContext = _context.MakeCurrent(surface); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - - return new Session(_display, _context, surface, info, l, restoreContext, onFinish, isYFlipped); + var egli = _egl.Display.EglInterface; + egli.WaitClient(); + egli.WaitGL(); + egli.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + + _egl.PrimaryContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + + success = true; + return new Session(_egl.Display, _egl.PrimaryEglContext, surface, info, restoreContext, onFinish, isYFlipped); } - catch + finally { - l.Dispose(); - throw; + if(!success) + restoreContext.Dispose(); } } @@ -62,21 +62,19 @@ namespace Avalonia.OpenGL private readonly EglSurface _glSurface; private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglDisplay _display; - private readonly IDisposable _lock; private readonly IDisposable _restoreContext; private readonly Action _onFinish; public Session(EglDisplay display, EglContext context, EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable restoreContext, Action onFinish, bool isYFlipped) + IDisposable restoreContext, Action onFinish, bool isYFlipped) { IsYFlipped = isYFlipped; _context = context; _display = display; _glSurface = glSurface; _info = info; - _lock = @lock; _restoreContext = restoreContext; _onFinish = onFinish; } @@ -90,7 +88,6 @@ namespace Avalonia.OpenGL _display.EglInterface.WaitGL(); _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); _restoreContext.Dispose(); - _lock.Dispose(); _onFinish?.Invoke(); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs similarity index 99% rename from src/Avalonia.OpenGL/EglInterface.cs rename to src/Avalonia.OpenGL/Egl/EglInterface.cs index 666c0d8351..8055226042 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglInterface : GlInterfaceBase { diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..476f65a774 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -0,0 +1,72 @@ +using System; +using Avalonia.Logging; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Egl +{ + public class EglPlatformOpenGlInterface : IPlatformOpenGlInterface + { + public EglDisplay Display { get; private set; } + public bool CanCreateContexts => true; + public bool CanShareContexts => Display.SupportsSharing; + + public EglContext PrimaryEglContext { get; } + public IGlContext PrimaryContext => PrimaryEglContext; + + public EglPlatformOpenGlInterface(EglDisplay display) + { + Display = display; + PrimaryEglContext = display.CreateContext(null); + } + + public static void TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + } + + public static EglPlatformOpenGlInterface TryCreate() => TryCreate(() => new EglDisplay()); + public static EglPlatformOpenGlInterface TryCreate(Func displayFactory) + { + try + { + return new EglPlatformOpenGlInterface(displayFactory()); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); + return null; + } + } + + public IGlContext CreateContext() => Display.CreateContext(null); + public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryEglContext); + + + public EglSurface CreateWindowSurface(IntPtr window) + { + using (PrimaryContext.MakeCurrent()) + { + var s = Display.EglInterface.CreateWindowSurface(Display.Handle, Display.Config, window, + new[] { EGL_NONE, EGL_NONE }); + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateWindowSurface", Display.EglInterface); + return new EglSurface(Display, PrimaryEglContext, s); + } + } + + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) + { + using (PrimaryContext.MakeCurrent()) + { + var s = Display.EglInterface.CreatePbufferFromClientBuffer(Display.Handle, bufferType, handle, + Display.Config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", Display.EglInterface); + return new EglSurface(Display, PrimaryEglContext, s); + } + } + } +} diff --git a/src/Avalonia.OpenGL/EglSurface.cs b/src/Avalonia.OpenGL/Egl/EglSurface.cs similarity index 57% rename from src/Avalonia.OpenGL/EglSurface.cs rename to src/Avalonia.OpenGL/Egl/EglSurface.cs index 5ac56a00e3..a93751ca9e 100644 --- a/src/Avalonia.OpenGL/EglSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglSurface.cs @@ -1,22 +1,25 @@ using System; using System.Runtime.InteropServices; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglSurface : SafeHandle { private readonly EglDisplay _display; + private readonly EglContext _context; private readonly EglInterface _egl; - public EglSurface(EglDisplay display, EglInterface egl, IntPtr surface) : base(surface, true) + public EglSurface(EglDisplay display, EglContext context, IntPtr surface) : base(surface, true) { _display = display; - _egl = egl; + _context = context; + _egl = display.EglInterface; } protected override bool ReleaseHandle() { - _egl.DestroySurface(_display.Handle, handle); + using (_context.MakeCurrent()) + _egl.DestroySurface(_display.Handle, handle); return true; } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs deleted file mode 100644 index 7e9383432c..0000000000 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Avalonia.Logging; - -namespace Avalonia.OpenGL -{ - public class EglGlPlatformFeature : IWindowingPlatformGlFeature - { - private EglDisplay _display; - public EglDisplay Display => _display; - public IGlContext CreateContext() - { - return _display.CreateContext(DeferredContext); - } - public EglContext DeferredContext { get; private set; } - public IGlContext MainContext => DeferredContext; - - public static void TryInitialize() - { - var feature = TryCreate(); - if (feature != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); - } - - public static EglGlPlatformFeature TryCreate() => TryCreate(() => new EglDisplay()); - public static EglGlPlatformFeature TryCreate(Func displayFactory) - { - try - { - var disp = displayFactory(); - return new EglGlPlatformFeature - { - _display = disp, - DeferredContext = disp.CreateContext(null) - }; - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); - return null; - } - } - } -} diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs deleted file mode 100644 index 21fadff19e..0000000000 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading; - -namespace Avalonia.OpenGL -{ - public class EglGlPlatformSurface : EglGlPlatformSurfaceBase - { - private readonly EglDisplay _display; - private readonly EglContext _context; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base() - { - _display = context.Display; - _context = context; - _info = info; - } - - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() - { - var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_display, _context, glSurface, _info); - } - - class RenderTarget : EglPlatformSurfaceRenderTargetBase - { - private readonly EglDisplay _display; - private readonly EglContext _context; - private readonly EglSurface _glSurface; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - private PixelSize _initialSize; - - public RenderTarget(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(display, context) - { - _display = display; - _context = context; - _glSurface = glSurface; - _info = info; - _initialSize = info.Size; - } - - public override void Dispose() => _glSurface.Dispose(); - - public override bool IsCorrupted => _initialSize != _info.Size; - - public override IGlPlatformSurfaceRenderingSession BeginDraw() => base.BeginDraw(_glSurface, _info); - } - } -} - diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 23188e7dbf..ea2fe0a99c 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -82,6 +82,9 @@ namespace Avalonia.OpenGL [GlEntryPoint("glFlush")] public Action Flush { get; } + + [GlEntryPoint("glFinish")] + public Action Finish { get; } public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] @@ -144,6 +147,10 @@ namespace Avalonia.OpenGL [GlEntryPoint("glBindTexture")] public GlBindTexture BindTexture { get; } + public delegate void GlActiveTexture(int texture); + [GlEntryPoint("glActiveTexture")] + public GlActiveTexture ActiveTexture { get; } + public delegate void GlDeleteTextures(int count, int[] textures); [GlEntryPoint("glDeleteTextures")] public GlDeleteTextures DeleteTextures { get; } @@ -154,6 +161,12 @@ namespace Avalonia.OpenGL [GlEntryPoint("glTexImage2D")] public GlTexImage2D TexImage2D { get; } + public delegate void GlCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, + int width, int height); + + [GlEntryPoint("glCopyTexSubImage2D")] + public GlCopyTexSubImage2D CopyTexSubImage2D { get; } + public delegate void GlTexParameteri(int target, int name, int value); [GlEntryPoint("glTexParameteri")] public GlTexParameteri TexParameteri { get; } diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index eb4313fba9..50868db873 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -9,5 +9,7 @@ namespace Avalonia.OpenGL int SampleCount { get; } int StencilSize { get; } IDisposable MakeCurrent(); + IDisposable EnsureCurrent(); + bool IsSharedWith(IGlContext context); } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs index 30f83745ad..fdb9162164 100644 --- a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs +++ b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs @@ -4,6 +4,6 @@ namespace Avalonia.OpenGL { public interface IOpenGlAwarePlatformRenderInterface { - IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); } } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..5ee5df1e85 --- /dev/null +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -0,0 +1,13 @@ +namespace Avalonia.OpenGL +{ + public interface IPlatformOpenGlInterface + { + IGlContext PrimaryContext { get; } + IGlContext CreateSharedContext(); + bool CanShareContexts { get; } + bool CanCreateContexts { get; } + IGlContext CreateContext(); + /*IGlContext TryCreateContext(GlVersion version); + */ + } +} diff --git a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs b/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs deleted file mode 100644 index b91496f42b..0000000000 --- a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.OpenGL -{ - public interface IWindowingPlatformGlFeature - { - IGlContext CreateContext(); - IGlContext MainContext { get; } - } -} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs new file mode 100644 index 0000000000..aef4f601be --- /dev/null +++ b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL.Imaging +{ + public interface IOpenGlBitmapImpl : IBitmapImpl + { + IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); + bool SupportsContext(IGlContext context); + } + + public interface IOpenGlBitmapAttachment : IDisposable + { + void Present(); + } +} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs deleted file mode 100644 index e5f3691569..0000000000 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Avalonia.Media.Imaging; -using Avalonia.Platform; - -namespace Avalonia.OpenGL.Imaging -{ - public interface IOpenGlTextureBitmapImpl : IBitmapImpl - { - IDisposable Lock(); - void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling); - void SetDirty(); - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs similarity index 54% rename from src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs rename to src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs index 558eae8fdf..7af44cd624 100644 --- a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs +++ b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs @@ -6,32 +6,30 @@ using Avalonia.Threading; namespace Avalonia.OpenGL.Imaging { - public class OpenGlTextureBitmap : Bitmap, IAffectsRender + public class OpenGlBitmap : Bitmap, IAffectsRender { - private IOpenGlTextureBitmapImpl _impl; - static IOpenGlTextureBitmapImpl CreateOrThrow() + private IOpenGlBitmapImpl _impl; + + public OpenGlBitmap(PixelSize size, Vector dpi) + : base(CreateOrThrow(size, dpi)) { - if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface - glAware)) - throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); - return glAware.CreateOpenGlTextureBitmap(); + _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; } - public OpenGlTextureBitmap() - : base(CreateOrThrow()) + static IOpenGlBitmapImpl CreateOrThrow(PixelSize size, Vector dpi) { - _impl = (IOpenGlTextureBitmapImpl)PlatformImpl.Item; + if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface + glAware)) + throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); + return glAware.CreateOpenGlBitmap(size, dpi); } - public IDisposable Lock() => _impl.Lock(); + public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => + _impl.CreateFramebufferAttachment(context, SetIsDirty); - public void SetTexture(int textureId, int internalFormat, PixelSize size, double dpiScaling) - { - _impl.SetBackBuffer(textureId, internalFormat, size, dpiScaling); - SetIsDirty(); - } - - public void SetIsDirty() + public bool SupportsContext(IGlContext context) => _impl.SupportsContext(context); + + void SetIsDirty() { if (Dispatcher.UIThread.CheckAccess()) CallInvalidated(); diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index d3cd7d059e..196f507ad8 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.OpenGL.Egl; namespace Avalonia.OpenGL { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurface.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs similarity index 77% rename from src/Avalonia.OpenGL/IGlPlatformSurface.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs index 22d36b4472..875c215336 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs @@ -1,4 +1,4 @@ -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurface { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs similarity index 89% rename from src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs index d198d46e5c..f89b6f04f5 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurfaceRenderTarget : IDisposable { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs similarity index 86% rename from src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs index 89911a20a8..da06eab1e7 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurfaceRenderingSession : IDisposable { diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index 0349a6e26e..e9cb88cb8f 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -8,18 +8,21 @@ namespace Avalonia.X11.Glx { public IntPtr Handle { get; } public GlxInterface Glx { get; } + private readonly GlxContext _sharedWith; private readonly X11Info _x11; private readonly IntPtr _defaultXid; private readonly bool _ownsPBuffer; private readonly object _lock = new object(); - public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + GlxContext sharedWith, GlVersion version, int sampleCount, int stencilSize, X11Info x11, IntPtr defaultXid, bool ownsPBuffer) { Handle = handle; Glx = glx; + _sharedWith = sharedWith; _x11 = x11; _defaultXid = defaultXid; _ownsPBuffer = ownsPBuffer; @@ -37,25 +40,21 @@ namespace Avalonia.X11.Glx public int SampleCount { get; } public int StencilSize { get; } - public IDisposable Lock() - { - Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); - } - class RestoreContext : IDisposable { private GlxInterface _glx; private IntPtr _defaultDisplay; + private readonly object _l; private IntPtr _display; private IntPtr _context; private IntPtr _read; private IntPtr _draw; - public RestoreContext(GlxInterface glx, IntPtr defaultDisplay) + public RestoreContext(GlxInterface glx, IntPtr defaultDisplay, object l) { _glx = glx; _defaultDisplay = defaultDisplay; + _l = l; _display = _glx.GetCurrentDisplay(); _context = _glx.GetCurrentContext(); _read = _glx.GetCurrentReadDrawable(); @@ -66,19 +65,49 @@ namespace Avalonia.X11.Glx { var disp = _display == IntPtr.Zero ? _defaultDisplay : _display; _glx.MakeContextCurrent(disp, _draw, _read, _context); + Monitor.Exit(_l); } } public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); + public IDisposable EnsureCurrent() + { + if(IsCurrent) + return Disposable.Empty; + return MakeCurrent(); + } + + public bool IsSharedWith(IGlContext context) + { + var c = (GlxContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } public IDisposable MakeCurrent(IntPtr xid) { - var old = new RestoreContext(Glx, _x11.Display); - if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) - throw new OpenGlException("glXMakeContextCurrent failed "); - return old; + Monitor.Enter(_lock); + var success = false; + try + { + var old = new RestoreContext(Glx, _x11.Display, _lock); + if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) + throw new OpenGlException("glXMakeContextCurrent failed "); + + success = true; + return old; + } + finally + { + if (!success) + Monitor.Exit(_lock); + } } + public bool IsCurrent => Glx.GetCurrentContext() == Handle; + public void Dispose() { Glx.DestroyContext(_x11.Display, Handle); diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index b82895d12c..fa8c866c09 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -113,9 +113,9 @@ namespace Avalonia.X11.Glx } - public GlxContext CreateContext() => CreateContext(DeferredContext); - - GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, + public GlxContext CreateContext() => CreateContext(); + + public GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, share.SampleCount, share.StencilSize, true); GlxContext CreateContext(IntPtr defaultXid, IGlContext share, @@ -144,7 +144,7 @@ namespace Avalonia.X11.Glx if (handle != IntPtr.Zero) { _version = profile; - return new GlxContext(new GlxInterface(), handle, this, profile, + return new GlxContext(new GlxInterface(), handle, this, (GlxContext)share, profile, sampleCount, stencilSize, _x11, defaultXid, ownsPBuffer); } diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index ae6b0eb353..cb4ab4aca0 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -1,5 +1,8 @@ using System; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.X11.Glx { @@ -40,33 +43,26 @@ namespace Avalonia.X11.Glx public IGlPlatformSurfaceRenderingSession BeginDraw() { - var l = _context.Lock(); - try - { - - return new Session(_context, _info, l, _context.MakeCurrent(_info.Handle)); - } - catch - { - l.Dispose(); - throw; - } + var oldContext = _context.MakeCurrent(_info.Handle); + + // Reset to default FBO first + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, 0); + + return new Session(_context, _info, oldContext); } class Session : IGlPlatformSurfaceRenderingSession { private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - private IDisposable _lock; private readonly IDisposable _clearContext; public IGlContext Context => _context; public Session(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable clearContext) + IDisposable clearContext) { _context = context; _info = info; - _lock = @lock; _clearContext = clearContext; } @@ -77,7 +73,6 @@ namespace Avalonia.X11.Glx _context.Display.SwapBuffers(_info.Handle); _context.Glx.WaitX(); _clearContext.Dispose(); - _lock.Dispose(); } public PixelSize Size => _info.Size; diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index ad3a54bcc1..6735a32ffe 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -5,31 +5,34 @@ using Avalonia.OpenGL; namespace Avalonia.X11.Glx { - class GlxGlPlatformFeature : IWindowingPlatformGlFeature + class GlxPlatformOpenGlInterface : IPlatformOpenGlInterface { public GlxDisplay Display { get; private set; } + public bool CanCreateContexts => true; + public bool CanShareContexts => true; public IGlContext CreateContext() => Display.CreateContext(); + public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } - public IGlContext MainContext => DeferredContext; + public IGlContext PrimaryContext => DeferredContext; public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); return true; } return false; } - public static GlxGlPlatformFeature TryCreate(X11Info x11, IList glProfiles) + public static GlxPlatformOpenGlInterface TryCreate(X11Info x11, IList glProfiles) { try { var disp = new GlxDisplay(x11, glProfiles); - return new GlxGlPlatformFeature + return new GlxPlatformOpenGlInterface { Display = disp, DeferredContext = disp.DeferredContext diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d7bd81db98..c6db146f7b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -7,6 +7,7 @@ using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.X11; @@ -70,9 +71,9 @@ namespace Avalonia.X11 if (options.UseGpu) { if (options.UseEGL) - EglGlPlatformFeature.TryInitialize(); + EglPlatformOpenGlInterface.TryInitialize(); else - GlxGlPlatformFeature.TryInitialize(Info, Options.GlProfiles); + GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 0c0b942bcd..2cd3b973d8 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -12,6 +12,7 @@ using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -65,7 +66,7 @@ namespace Avalonia.X11 _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; - var glfeature = AvaloniaLocator.Current.GetService(); + var glfeature = AvaloniaLocator.Current.GetService(); XSetWindowAttributes attr = new XSetWindowAttributes(); var valueMask = default(SetWindowValuemask); @@ -87,13 +88,13 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - var glx = glfeature as GlxGlPlatformFeature; + var glx = glfeature as GlxPlatformOpenGlInterface; if (glx != null) visualInfo = *glx.Display.VisualInfo; else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglGlPlatformFeature; + var egl = glfeature as EglPlatformOpenGlInterface; var visual = IntPtr.Zero; var depth = 24; @@ -168,7 +169,7 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface(egl.DeferredContext, + new EglGlPlatformSurface(egl, new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index db37e4af0b..8801f71f9a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -32,8 +32,8 @@ namespace Avalonia.LinuxFramebuffer void Initialize() { Threading = new InternalPlatformThreadingInterface(); - if (_fb is IWindowingPlatformGlFeature glFeature) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature); + if (_fb is IGlOutputBackend gl) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 7a5d20fc83..72eed9e543 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -4,6 +4,8 @@ using System.ComponentModel; using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform.Interop; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; @@ -11,13 +13,16 @@ using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats; namespace Avalonia.LinuxFramebuffer.Output { - public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature + public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmCard _card; private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } - public IGlContext MainContext => _deferredContext; + public IGlContext PrimaryContext => _deferredContext; + + private EglPlatformOpenGlInterface _platformGl; + public IPlatformOpenGlInterface PlatformOpenGlInterface => _platformGl; public DrmOutput(string path = null) { @@ -132,10 +137,9 @@ namespace Avalonia.LinuxFramebuffer.Output if(_gbmTargetSurface == null) throw new InvalidOperationException("Unable to create GBM surface"); - - - _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null); - _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface); + _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); + _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); + _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); EglContext CreateContext(EglContext share) @@ -144,7 +148,7 @@ namespace Avalonia.LinuxFramebuffer.Output GbmBoFlags.GBM_BO_USE_RENDERING); if (offSurf == null) throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); - return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); + return _eglDisplay.CreateContext(share, _platformGl.CreateWindowSurface(offSurf)); } _deferredContext = CreateContext(null); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs new file mode 100644 index 0000000000..7bc73d590c --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs @@ -0,0 +1,9 @@ +using Avalonia.OpenGL; + +namespace Avalonia.LinuxFramebuffer.Output +{ + public interface IGlOutputBackend : IOutputBackend + { + public IPlatformOpenGlInterface PlatformOpenGlInterface { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 987e2c089c..1a7a9b75cf 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -19,6 +19,6 @@ namespace Avalonia.Skia public interface IOpenGlAwareSkiaGpu : ISkiaGpu { - IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 081db5d26a..6df8df9a4c 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 9278de2137..46d42dfdab 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; +using Avalonia.OpenGL.Surfaces; using SkiaSharp; namespace Avalonia.Skia @@ -8,10 +9,12 @@ namespace Avalonia.Skia class GlSkiaGpu : IOpenGlAwareSkiaGpu { private GRContext _grContext; + private IGlContext _glContext; - public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes) + public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) { - var context = gl.MainContext; + var context = openGl.PrimaryContext; + _glContext = context; using (context.MakeCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? @@ -40,6 +43,6 @@ namespace Avalonia.Skia return null; } - public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() => new OpenGlTextureBitmapImpl(); + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs new file mode 100644 index 0000000000..2ebf7c680b --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Imaging; +using Avalonia.Utilities; +using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.Skia +{ + class GlOpenGlBitmapImpl : IOpenGlBitmapImpl, IDrawableBitmapImpl + { + private readonly IGlContext _context; + private readonly object _lock = new object(); + private IGlPresentableOpenGlSurface _surface; + + public GlOpenGlBitmapImpl(IGlContext context, PixelSize pixelSize, Vector dpi) + { + _context = context; + PixelSize = pixelSize; + Dpi = dpi; + } + + public Vector Dpi { get; } + public PixelSize PixelSize { get; } + public int Version { get; private set; } + public void Save(string fileName) => throw new NotSupportedException(); + + public void Save(Stream stream) => throw new NotSupportedException(); + + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + lock (_lock) + { + if (_surface == null) + return; + using (_surface.Lock()) + { + using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, + new GRGlTextureInfo( + GlConsts.GL_TEXTURE_2D, (uint)_surface.GetTextureId(), + (uint)_surface.InternalFormat))) + using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888)) + { + // Again, silently ignore, if something went wrong it's not our fault + if (surface == null) + return; + + using (var snapshot = surface.Snapshot()) + context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); + } + + } + } + } + + public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback) + { + if (!SupportsContext(context)) + throw new OpenGlException("Context is not supported for texture sharing"); + return new SharedOpenGlBitmapAttachment(this, context, presentCallback); + } + + public bool SupportsContext(IGlContext context) + { + // TODO: negotiated platform surface sharing + return _context.IsSharedWith(context); + } + + public void Dispose() + { + + } + + internal void Present(IGlPresentableOpenGlSurface surface) + { + lock (_lock) + { + _surface = surface; + } + } + } + + interface IGlPresentableOpenGlSurface : IDisposable + { + int GetTextureId(); + int InternalFormat { get; } + IDisposable Lock(); + } + + class SharedOpenGlBitmapAttachment : IOpenGlBitmapAttachment, IGlPresentableOpenGlSurface + { + private readonly GlOpenGlBitmapImpl _bitmap; + private readonly IGlContext _context; + private readonly Action _presentCallback; + private readonly int _fbo; + private readonly int _texture; + private readonly int _frontBuffer; + private bool _disposed; + private readonly DisposableLock _lock = new DisposableLock(); + + public SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) + { + _bitmap = bitmap; + _context = context; + _presentCallback = presentCallback; + using (_context.EnsureCurrent()) + { + var glVersion = _context.Version; + InternalFormat = glVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + + _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); + if (_fbo == 0) + throw new OpenGlException("Current FBO is 0"); + + { + var gl = _context.GlInterface; + + var textures = new int[2]; + gl.GenTextures(2, textures); + _texture = textures[0]; + _frontBuffer = textures[1]; + + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + foreach (var t in textures) + { + gl.BindTexture(GL_TEXTURE_2D, t); + gl.TexImage2D(GL_TEXTURE_2D, 0, + InternalFormat, + _bitmap.PixelSize.Width, _bitmap.PixelSize.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + } + + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + } + } + } + + public void Present() + { + using (_context.MakeCurrent()) + { + if (_disposed) + throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); + + var gl = _context.GlInterface; + + gl.Finish(); + using (Lock()) + { + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + gl.GetIntegerv(GL_ACTIVE_TEXTURE, out var oldActive); + + gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); + gl.BindTexture(GL_TEXTURE_2D, _frontBuffer); + gl.ActiveTexture(GL_TEXTURE0); + + gl.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, _bitmap.PixelSize.Width, + _bitmap.PixelSize.Height); + + gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + gl.ActiveTexture(oldActive); + + gl.Finish(); + } + } + + _bitmap.Present(this); + _presentCallback(); + } + + public void Dispose() + { + var gl = _context.GlInterface; + _bitmap.Present(null); + + if(_disposed) + return; + using (_context.MakeCurrent()) + using (Lock()) + { + if(_disposed) + return; + _disposed = true; + gl.DeleteTextures(2, new[] { _texture, _frontBuffer }); + } + } + + int IGlPresentableOpenGlSurface.GetTextureId() + { + return _frontBuffer; + } + + public int InternalFormat { get; } + + public IDisposable Lock() => _lock.Lock(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs deleted file mode 100644 index 8d007e35f3..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.IO; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; -using Avalonia.Skia.Helpers; -using Avalonia.Utilities; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class OpenGlTextureBitmapImpl : IOpenGlTextureBitmapImpl, IDrawableBitmapImpl - { - private DisposableLock _lock = new DisposableLock(); - private int _textureId; - private int _internalFormat; - - public void Dispose() - { - using (Lock()) - { - _textureId = 0; - PixelSize = new PixelSize(1, 1); - Version++; - } - } - - public Vector Dpi { get; private set; } = new Vector(96, 96); - public PixelSize PixelSize { get; private set; } = new PixelSize(1, 1); - public int Version { get; private set; } = 0; - - public void Save(string fileName) => throw new System.NotSupportedException(); - public void Save(Stream stream) => throw new System.NotSupportedException(); - - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) - { - // For now silently ignore - if (context.GrContext == null) - return; - - using (Lock()) - { - if (_textureId == 0) - return; - using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, - new GRGlTextureInfo( - GlConsts.GL_TEXTURE_2D, (uint)_textureId, - (uint)_internalFormat))) - using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, - SKColorType.Rgba8888)) - { - // Again, silently ignore, if something went wrong it's not our fault - if (surface == null) - return; - - using (var snapshot = surface.Snapshot()) - context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); - } - } - } - - public IDisposable Lock() => _lock.Lock(); - - public void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling) - { - using (_lock.Lock()) - { - _textureId = textureId; - _internalFormat = internalFormat; - PixelSize = pixelSize; - Dpi = new Vector(96 * dpiScaling, 96 * dpiScaling); - Version++; - } - } - - public void SetDirty() - { - using (_lock.Lock()) - Version++; - } - } -} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index c4f70df7c0..b9c1cbc673 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia return; } - var gl = AvaloniaLocator.Current.GetService(); + var gl = AvaloniaLocator.Current.GetService(); if (gl != null) _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); } @@ -256,10 +256,10 @@ namespace Avalonia.Skia } - public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) { if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) - return glAware.CreateOpenGlTextureBitmap(); + return glAware.CreateOpenGlBitmap(size, dpi); if (_skiaGpu == null) throw new PlatformNotSupportedException("GPU acceleration is not available"); throw new PlatformNotSupportedException( diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index bd188ad53a..fbc56e7703 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,26 +1,27 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; namespace Avalonia.Win32 { static class Win32GlManager { /// This property is initialized if drawing platform requests OpenGL support - public static EglGlPlatformFeature EglFeature { get; private set; } + public static EglPlatformOpenGlInterface EglPlatformInterface { get; private set; } private static bool s_attemptedToInitialize; public static void Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => + AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => { if (!s_attemptedToInitialize) { - EglFeature = EglGlPlatformFeature.TryCreate(() => new AngleWin32EglDisplay()); + EglPlatformInterface = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); s_attemptedToInitialize = true; } - return EglFeature; + return EglPlatformInterface; }); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9c6bce1c90..cb85e14e5a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Win32.Input; @@ -103,8 +105,8 @@ namespace Avalonia.Win32 CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - if (Win32GlManager.EglFeature != null) - _gl = new EglGlPlatformSurface(Win32GlManager.EglFeature.DeferredContext, this); + if (Win32GlManager.EglPlatformInterface != null) + _gl = new EglGlPlatformSurface(Win32GlManager.EglPlatformInterface, this); Screen = new ScreenImpl(); diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 635df43407..f9c787b6a8 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,15 +1,18 @@ using System; +using System.Reactive.Disposables; using Avalonia.OpenGL; using OpenGLES; using OpenTK.Graphics.ES30; namespace Avalonia.iOS { - class EaglFeature : IWindowingPlatformGlFeature + class EaglFeature : IPlatformOpenGlInterface { + public IGlContext PrimaryContext => Context; + public IGlContext CreateSharedContext() => throw new NotSupportedException(); + public bool CanShareContexts => false; + public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); - - public IGlContext MainContext => Context; public GlContext Context { get; } = new GlContext(); } @@ -61,9 +64,18 @@ namespace Avalonia.iOS return new ResetContext(old); } + public IDisposable EnsureCurrent() + { + if(EAGLContext.CurrentContext == Context) + return Disposable.Empty; + return MakeCurrent(); + } + + public bool IsSharedWith(IGlContext context) => false; + public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); public GlInterface GlInterface { get; } public int SampleCount { get; } = 0; public int StencilSize { get; } = 9; } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 64912b8ae3..5e5e1da949 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using Avalonia.OpenGL; +using Avalonia.OpenGL.Surfaces; using CoreAnimation; using OpenTK.Graphics.ES30; @@ -91,4 +92,4 @@ namespace Avalonia.iOS } } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index b484559ff3..28bccb6637 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -26,7 +26,7 @@ namespace Avalonia.iOS var keyboard = new KeyboardDevice(); var softKeyboard = new SoftKeyboardHelper(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(GlFeature) + .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) From 48c6a170518d18a1395c2aff683314c0291d85c7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Sep 2020 10:25:53 +0300 Subject: [PATCH 137/149] Skip surface snapshot when doing a blit --- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27b29c6e1e..428087ac56 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -109,10 +109,16 @@ namespace Avalonia.Skia /// public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) { - using (var image = SnapshotImage()) + if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) { - context.Canvas.DrawImage(image, sourceRect, destRect, paint); + _surface.Canvas.Flush(); + _surface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); } + else + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } } /// From eb9ddd90013d3017340107fb083f0130a388b35b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Sep 2020 15:52:28 +0100 Subject: [PATCH 138/149] implement a simple PathIcon control to allow simple mono-tone icons to easily be used. --- src/Avalonia.Controls/PathIcon.cs | 22 ++++++++++++++++++++++ src/Avalonia.Themes.Default/PathIcon.xaml | 17 +++++++++++++++++ src/Avalonia.Themes.Fluent/PathIcon.xaml | 17 +++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/Avalonia.Controls/PathIcon.cs create mode 100644 src/Avalonia.Themes.Default/PathIcon.xaml create mode 100644 src/Avalonia.Themes.Fluent/PathIcon.xaml diff --git a/src/Avalonia.Controls/PathIcon.cs b/src/Avalonia.Controls/PathIcon.cs new file mode 100644 index 0000000000..e3a97b9544 --- /dev/null +++ b/src/Avalonia.Controls/PathIcon.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public class PathIcon : TemplatedControl + { + static PathIcon() + { + AffectsRender(SourceProperty); + } + + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + public Geometry Source + { + get { return GetValue(SourceProperty); } + set { SetValue(SourceProperty, value); } + } + } +} diff --git a/src/Avalonia.Themes.Default/PathIcon.xaml b/src/Avalonia.Themes.Default/PathIcon.xaml new file mode 100644 index 0000000000..bebebba90b --- /dev/null +++ b/src/Avalonia.Themes.Default/PathIcon.xaml @@ -0,0 +1,17 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/PathIcon.xaml b/src/Avalonia.Themes.Fluent/PathIcon.xaml new file mode 100644 index 0000000000..3fc25c50f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/PathIcon.xaml @@ -0,0 +1,17 @@ + + + From 71d02073275b5110ad473e2ad34fabe997ba2a32 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Sep 2020 15:54:17 +0100 Subject: [PATCH 139/149] add a preview. --- src/Avalonia.Themes.Fluent/PathIcon.xaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/PathIcon.xaml b/src/Avalonia.Themes.Fluent/PathIcon.xaml index 3fc25c50f8..6f7c3bcb52 100644 --- a/src/Avalonia.Themes.Fluent/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/PathIcon.xaml @@ -1,5 +1,13 @@ + + + + M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z + + + + diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 9ed3207235..30c6d39856 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -75,5 +75,8 @@ 18 8 + + 20 + 20 diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 46488c1c57..134e804c53 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -20,5 +20,7 @@ 1 2 10,6,6,5 + 20 + 20 From 376bb1ad49dc3aa629cb98d84b35c8b2078f4207 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 29 Sep 2020 13:27:00 +0300 Subject: [PATCH 144/149] fix issue #3113 compile avalonia.native.dylib with nuke --- nukebuild/Build.cs | 14 ++++++++++++-- src/Avalonia.Native/Avalonia.Native.csproj | 8 +++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 7e2bbc13bc..097815cc69 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -138,9 +138,19 @@ partial class Build : NukeBuild .SetWorkingDirectory(webappDir) .SetCommand("dist")); }); - - Target Compile => _ => _ + + Target CompileNative => _ => _ .DependsOn(Clean) + .OnlyWhenStatic(() => EnvironmentInfo.IsOsx) + .Executes(() => + { + var project = $"{RootDirectory}/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/"; + var args = $"-project {project} -configuration {Parameters.Configuration} CONFIGURATION_BUILD_DIR={RootDirectory}/Build/Products/Release"; + ProcessTasks.StartProcess("xcodebuild", args).AssertZeroExitCode(); + }); + + Target Compile => _ => _ + .DependsOn(Clean, CompileNative) .DependsOn(CompileHtmlPreviewer) .Executes(async () => { diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index f084411c2f..49bd578290 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -1,7 +1,8 @@  - false + $([MSBuild]::IsOSPlatform(OSX)) + $(PackAvaloniaNative) true netstandard2.0 /usr/bin/castxml @@ -10,8 +11,9 @@ false - + + libAvaloniaNative.dylib runtimes/osx/native/libAvaloniaNative.dylib true PreserveNewest @@ -26,4 +28,4 @@ - + \ No newline at end of file From 1a730b7c9e54504969e23a53c6975a95266eb6fd Mon Sep 17 00:00:00 2001 From: GMIKE Date: Tue, 29 Sep 2020 15:19:53 +0300 Subject: [PATCH 145/149] Update and fix comments for the documentation --- src/Avalonia.Visuals/Point.cs | 8 ++++---- src/Avalonia.Visuals/Size.cs | 4 ++-- src/Avalonia.Visuals/Thickness.cs | 4 ++-- src/Avalonia.Visuals/Vector.cs | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 30dd01ce38..f4612ac1d0 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -269,10 +269,10 @@ namespace Avalonia } /// - /// Deconstructor for decomposition Point + /// Deconstructs the point into it's X and Y coordinates. /// - /// The X position. - /// The Y position. + /// The X coordinate. + /// The Y coordinate. public void Deconstruct(out double x, out double y) { x = this._x; @@ -280,7 +280,7 @@ namespace Avalonia } /// - /// Gets a value indicating that point coordinates are zero + /// Gets a value indicating whether the X and Y coordinates are zero. /// public bool IsDefault { diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index bff6d787ee..e80565b5c0 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -278,7 +278,7 @@ namespace Avalonia } /// - /// Deconstructor for decomposition Size + /// Deconstructs the size into it's Width and Height values. /// /// The width. /// The height. @@ -289,7 +289,7 @@ namespace Avalonia } /// - /// Gets a value indicating that width and height are zero + /// Gets a value indicating whether the Width and Height values are zero. /// public bool IsDefault { diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index 33ac001cef..898f828de0 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -274,7 +274,7 @@ namespace Avalonia } /// - /// Deconstructor for decomposition Thickness + /// Deconstructor the thickness into left, top, right and bottom thickness values. /// /// The thickness on the left. /// The thickness on the top. @@ -289,7 +289,7 @@ namespace Avalonia } /// - /// Gets a value indicating that thickness are zero + /// Gets a value indicating whether the left, top, right and bottom thickness values are zero. /// public bool IsDefault { diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 67c04e4bb3..1e09588b89 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -335,10 +335,10 @@ namespace Avalonia => new Vector(0, 1); /// - /// Deconstructor for decomposition Vector + /// Deconstructs the vector into it's X and Y components. /// /// The X component. - /// The Y poscomponentition. + /// The Y component. public void Deconstruct(out double x, out double y) { x = this._x; @@ -346,7 +346,7 @@ namespace Avalonia } /// - /// Gets a value indicating that Vector components are zero + /// Gets a value indicating whether the X and Y components are zero. /// public bool IsDefault { From 6a6136d37c180766c3e9f7da2b109c7e6daebc1f Mon Sep 17 00:00:00 2001 From: GMIKE Date: Tue, 29 Sep 2020 15:24:46 +0300 Subject: [PATCH 146/149] it's to its --- src/Avalonia.Visuals/Point.cs | 2 +- src/Avalonia.Visuals/Size.cs | 2 +- src/Avalonia.Visuals/Thickness.cs | 2 +- src/Avalonia.Visuals/Vector.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index f4612ac1d0..7324f5fbd0 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -269,7 +269,7 @@ namespace Avalonia } /// - /// Deconstructs the point into it's X and Y coordinates. + /// Deconstructs the point into its X and Y coordinates. /// /// The X coordinate. /// The Y coordinate. diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index e80565b5c0..d87d2c5fc2 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -278,7 +278,7 @@ namespace Avalonia } /// - /// Deconstructs the size into it's Width and Height values. + /// Deconstructs the size into its Width and Height values. /// /// The width. /// The height. diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index 898f828de0..6d69c4d9a9 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -274,7 +274,7 @@ namespace Avalonia } /// - /// Deconstructor the thickness into left, top, right and bottom thickness values. + /// Deconstructor the thickness into its left, top, right and bottom thickness values. /// /// The thickness on the left. /// The thickness on the top. diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 1e09588b89..2fcf804f14 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -335,7 +335,7 @@ namespace Avalonia => new Vector(0, 1); /// - /// Deconstructs the vector into it's X and Y components. + /// Deconstructs the vector into its X and Y components. /// /// The X component. /// The Y component. From afd565abd1b2ccf78a90bf528db537d4025bd4cd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Sep 2020 21:31:55 +0100 Subject: [PATCH 147/149] ensure path icon is centered. --- src/Avalonia.Themes.Default/PathIcon.xaml | 3 ++- src/Avalonia.Themes.Fluent/PathIcon.xaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/PathIcon.xaml b/src/Avalonia.Themes.Default/PathIcon.xaml index 2c9d1baeb4..20b01b46f4 100644 --- a/src/Avalonia.Themes.Default/PathIcon.xaml +++ b/src/Avalonia.Themes.Default/PathIcon.xaml @@ -9,7 +9,8 @@ + Data="{TemplateBinding Data}" + Stretch="Uniform" /> diff --git a/src/Avalonia.Themes.Fluent/PathIcon.xaml b/src/Avalonia.Themes.Fluent/PathIcon.xaml index 4ec5b9aaa4..dfaa476439 100644 --- a/src/Avalonia.Themes.Fluent/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/PathIcon.xaml @@ -17,7 +17,8 @@ + Data="{TemplateBinding Data}" + Stretch="Uniform" /> From bed7e90f540d4c5e01b27cb4f9aabb0cc647fbf9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Sep 2020 21:40:09 +0100 Subject: [PATCH 148/149] fix resource usage. --- src/Avalonia.Themes.Default/PathIcon.xaml | 4 ++-- src/Avalonia.Themes.Fluent/PathIcon.xaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Default/PathIcon.xaml b/src/Avalonia.Themes.Default/PathIcon.xaml index 20b01b46f4..a2d01f7b5b 100644 --- a/src/Avalonia.Themes.Default/PathIcon.xaml +++ b/src/Avalonia.Themes.Default/PathIcon.xaml @@ -2,8 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">