From 3c3b5b63b3b84606309195d3fe8ece1e72a051da Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Wed, 6 Jun 2018 14:59:29 +0200 Subject: [PATCH 01/88] Use newer WinApi for OpenFileDialog and SaveFileDialog --- .../Interop/UnmanagedMethods.cs | 313 ++++++++++++------ .../Avalonia.Win32/SystemDialogImpl.cs | 228 +++++-------- 2 files changed, 295 insertions(+), 246 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 86dcec410b..e1048bc2e7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1184,16 +1184,6 @@ namespace Avalonia.Win32.Interop public IntPtr hIconSm; } - [Flags] - public enum OpenFileNameFlags - { - OFN_ALLOWMULTISELECT = 0x00000200, - OFN_EXPLORER = 0x00080000, - OFN_HIDEREADONLY = 0x00000004, - OFN_NOREADONLYRETURN = 0x00008000, - OFN_OVERWRITEPROMPT = 0x00000002 - } - public enum HRESULT : uint { S_FALSE = 0x0001, @@ -1213,7 +1203,7 @@ namespace Avalonia.Win32.Interop public const uint SIGDN_FILESYSPATH = 0x80058000; [Flags] - internal enum FOS : uint + public enum FOS : uint { FOS_OVERWRITEPROMPT = 0x00000002, FOS_STRICTFILETYPES = 0x00000004, @@ -1237,135 +1227,246 @@ namespace Avalonia.Win32.Interop FOS_DEFAULTNOMINIMODE = 0x20000000 } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct OpenFileName + public static class ShellIds { - public int lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; - public IntPtr lpstrFilter; - public IntPtr lpstrCustomFilter; - public int nMaxCustFilter; - public int nFilterIndex; - public IntPtr lpstrFile; - public int nMaxFile; - public IntPtr lpstrFileTitle; - public int nMaxFileTitle; - public IntPtr lpstrInitialDir; - public IntPtr lpstrTitle; - public OpenFileNameFlags Flags; - private readonly ushort Unused; - private readonly ushort Unused2; - public IntPtr lpstrDefExt; - public IntPtr lCustData; - public IntPtr lpfnHook; - public IntPtr lpTemplateName; - public IntPtr reservedPtr; - public int reservedInt; - public int flagsEx; - } - } + public static readonly Guid OpenFileDialog = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); + public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); + public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + } - [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IFileDialog - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - [PreserveSig()] - uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes(uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] COMDLG_FILTERSPEC[] rgFilterSpec); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypeIndex([In] uint iFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileTypeIndex(out uint piFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Unadvise([In] uint dwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Unadvise([In] uint dwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOptions([In] uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetOptions(out uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Close([MarshalAs(UnmanagedType.Error)] uint hr); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Close([MarshalAs(UnmanagedType.Error)] uint hr); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetClientGuid([In] ref Guid guid); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetClientGuid([In] ref Guid guid); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint ClearClientData(); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint ClearClientData(); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); - } + } + + [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileOpenDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); - [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IShellItem - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Unadvise([In] uint dwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); - + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Close([MarshalAs(UnmanagedType.Error)] int hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void ClearClientData(); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); + } + + [ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItemArray + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, + [In] ref Guid riid, out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyDescriptionList([In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCount(out uint pdwNumItems); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct PROPERTYKEY + { + public Guid fmtid; + public uint pid; + } + + public enum SIATTRIBFLAGS + { + SIATTRIBFLAGS_AND = 1, + SIATTRIBFLAGS_APPCOMPAT = 3, + SIATTRIBFLAGS_OR = 2 + } + + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItem + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); + + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct COMDLG_FILTERSPEC + { + [MarshalAs(UnmanagedType.LPWStr)] + public string pszName; + [MarshalAs(UnmanagedType.LPWStr)] + public string pszSpec; + } } - + [Flags] internal enum DropEffect : int { diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 9b6bf57e92..d555e93a88 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -1,152 +1,94 @@ +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Win32.Interop; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Platform; -using Avalonia.Win32.Interop; namespace Avalonia.Win32 { class SystemDialogImpl : ISystemDialogImpl { - static char[] ToChars(string s) - { - if (s == null) - return null; - var chars = new char[s.Length]; - for (int c = 0; c < s.Length; c++) - chars[c] = s[c]; - return chars; - } - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) { var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { - var filters = new StringBuilder(); - foreach (var filter in dialog.Filters) - { - var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); - filters.Append(filter.Name); - filters.Append(" ("); - filters.Append(extMask); - filters.Append(")"); - filters.Append('\0'); - filters.Append(extMask); - filters.Append('\0'); - } - if (filters.Length == 0) - filters.Append("All files\0*.*\0"); - filters.Append('\0'); + var result = new string[0]; - var filterBuffer = new char[filters.Length]; - filters.CopyTo(0, filterBuffer, 0, filterBuffer.Length); - - var defExt = ToChars((dialog as SaveFileDialog)?.DefaultExtension); - var fileBuffer = new char[256]; - dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); + Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + var frm = (UnmanagedMethods.IFileDialog)unk; - string userSelectedExt = string.Empty; + var openDialog = dialog as OpenFileDialog; + uint options; + frm.GetOptions(out options); + options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); + if (openDialog?.AllowMultiple == true) + options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT; + frm.SetOptions(options); - var title = ToChars(dialog.Title); - var initialDir = ToChars(dialog.InitialDirectory); + var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; + frm.SetDefaultExtension(defaultExtension); + frm.SetFileName(dialog.InitialFileName ?? ""); + frm.SetTitle(dialog.Title); - fixed (char* pFileBuffer = fileBuffer) - fixed (char* pFilterBuffer = filterBuffer) - fixed (char* pDefExt = defExt) - fixed (char* pInitDir = initialDir) - fixed (char* pTitle = title) + var filters = new List(); + foreach (var filter in dialog.Filters) { - var ofn = new UnmanagedMethods.OpenFileName() - { - hwndOwner = hWnd, - hInstance = IntPtr.Zero, - lCustData = IntPtr.Zero, - nFilterIndex = 0, - Flags = - UnmanagedMethods.OpenFileNameFlags.OFN_EXPLORER | - UnmanagedMethods.OpenFileNameFlags.OFN_HIDEREADONLY, - nMaxCustFilter = 0, - nMaxFile = fileBuffer.Length - 1, - nMaxFileTitle = 0, - lpTemplateName = IntPtr.Zero, - lpfnHook = IntPtr.Zero, - lpstrCustomFilter = IntPtr.Zero, - lpstrDefExt = new IntPtr(pDefExt), - lpstrFile = new IntPtr(pFileBuffer), - lpstrFileTitle = IntPtr.Zero, - lpstrFilter = new IntPtr(pFilterBuffer), - lpstrInitialDir = new IntPtr(pInitDir), - lpstrTitle = new IntPtr(pTitle), - - }; - ofn.lStructSize = Marshal.SizeOf(ofn); - if ((dialog as OpenFileDialog)?.AllowMultiple == true) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_ALLOWMULTISELECT; - - if (dialog is SaveFileDialog) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_NOREADONLYRETURN | - UnmanagedMethods.OpenFileNameFlags.OFN_OVERWRITEPROMPT; - - var pofn = &ofn; - - // We should save the current directory to restore it later. - var currentDirectory = Environment.CurrentDirectory; - - var res = dialog is OpenFileDialog - ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn)) - : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn)); - - // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called - Environment.CurrentDirectory = currentDirectory; - - if (!res) - return null; - if (dialog?.Filters.Count > 0) - userSelectedExt = dialog.Filters[ofn.nFilterIndex - 1].Extensions.FirstOrDefault(); + var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask }); } - var cStart = 0; - string dir = null; - var files = new List(); - for (var c = 0; c < fileBuffer.Length; c++) + if (filters.Count == 0) + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" }); + + frm.SetFileTypes((uint)filters.Count, filters.ToArray()); + frm.SetFileTypeIndex(0); + + if (dialog.InitialDirectory != null) { - if (fileBuffer[c] == 0) + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - //Encountered double zero char - if (cStart == c) - break; - - var s = new string(fileBuffer, cStart, c - cStart); - if (dir == null) - dir = s; - else - files.Add(s); - cStart = c + 1; + frm.SetFolder(directoryShellItem); + frm.SetDefaultFolder(directoryShellItem); } } - if (files.Count == 0) + + if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - if (dialog is SaveFileDialog) + if (openDialog?.AllowMultiple == true) { - if (string.IsNullOrWhiteSpace(Path.GetExtension(dir)) && - !string.IsNullOrWhiteSpace(userSelectedExt) && - !userSelectedExt.Contains("*")) - dir = Path.ChangeExtension(dir, userSelectedExt); + UnmanagedMethods.IShellItemArray shellItemArray; + ((UnmanagedMethods.IFileOpenDialog)frm).GetResults(out shellItemArray); + uint count; + shellItemArray.GetCount(out count); + result = new string[count]; + for (uint i = 0; i < count; i++) + { + UnmanagedMethods.IShellItem shellItem; + shellItemArray.GetItemAt(i, out shellItem); + result[i] = GetAbsoluteFilePath(shellItem); + } + } + else + { + UnmanagedMethods.IShellItem shellItem; + if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + result = new string[] { GetAbsoluteFilePath(shellItem) }; + } } - - return new[] { dir }; } - return files.Select(f => Path.Combine(dir, f)).ToArray(); + return result; }); } @@ -157,11 +99,11 @@ namespace Avalonia.Win32 string result = string.Empty; var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; - var clsid = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); - var iid = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); - var frm = (IFileDialog)unk; + var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); @@ -169,8 +111,8 @@ namespace Avalonia.Win32 if (dialog.InitialDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetFolder(directoryShellItem); @@ -179,8 +121,8 @@ namespace Avalonia.Win32 if (dialog.DefaultDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetDefaultFolder(directoryShellItem); @@ -189,29 +131,35 @@ namespace Avalonia.Win32 if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IShellItem shellItem; + UnmanagedMethods.IShellItem shellItem; if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IntPtr pszString; - if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) - { - if (pszString != IntPtr.Zero) - { - try - { - result = Marshal.PtrToStringAuto(pszString); - } - finally - { - Marshal.FreeCoTaskMem(pszString); - } - } - } + result = GetAbsoluteFilePath(shellItem); } } return result; }); } + + private string GetAbsoluteFilePath(UnmanagedMethods.IShellItem shellItem) + { + IntPtr pszString; + if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + if (pszString != IntPtr.Zero) + { + try + { + return Marshal.PtrToStringAuto(pszString); + } + finally + { + Marshal.FreeCoTaskMem(pszString); + } + } + } + return ""; + } } } From cbca7beefde6c69bc21be5bf5d0eb8919278a057 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 20:53:27 +0200 Subject: [PATCH 02/88] Initial --- .../Media/PathMarkupParser.cs | 805 +++++++++++------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 10 +- .../Media/PathMarkupParserTests.cs | 127 ++- 3 files changed, 610 insertions(+), 332 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9e4a3cbeae..0307701e82 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,50 +5,46 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace Avalonia.Media { /// /// Parses a path markup string. /// - public class PathMarkupParser + public class PathMarkupParser : IDisposable { - private static readonly Dictionary Commands = new Dictionary - { - { 'F', Command.FillRule }, - { 'M', Command.Move }, - { 'L', Command.Line }, - { 'H', Command.HorizontalLine }, - { 'V', Command.VerticalLine }, - { 'Q', Command.QuadraticBezierCurve }, - { 'T', Command.SmoothQuadraticBezierCurve }, - { 'C', Command.CubicBezierCurve }, - { 'S', Command.SmoothCubicBezierCurve }, - { 'A', Command.Arc }, - { 'Z', Command.Close }, - }; - - private static readonly Dictionary FillRules = new Dictionary - { - {'0', FillRule.EvenOdd }, - {'1', FillRule.NonZero } - }; + private static readonly string s_separatorPattern; - private readonly StreamGeometryContext _context; + private Point _currentPoint; + private Point? _previousControlPoint; + private PathGeometry _currentGeometry; + private PathFigure _currentFigure; + private bool _isDisposed; - /// - /// Initializes a new instance of the class. - /// - /// The context for the geometry. - public PathMarkupParser(StreamGeometryContext context) + private static readonly Dictionary s_commands = + new Dictionary + { + { 'F', Command.FillRule }, + { 'M', Command.Move }, + { 'L', Command.Line }, + { 'H', Command.HorizontalLine }, + { 'V', Command.VerticalLine }, + { 'Q', Command.QuadraticBezierCurve }, + { 'T', Command.SmoothQuadraticBezierCurve }, + { 'C', Command.CubicBezierCurve }, + { 'S', Command.SmoothCubicBezierCurve }, + { 'A', Command.Arc }, + { 'Z', Command.Close }, + }; + + static PathMarkupParser() { - _context = context; + s_separatorPattern = CreatesSeparatorPattern(); } - /// - /// Defines the command currently being processed. - /// private enum Command { None, @@ -62,358 +58,581 @@ namespace Avalonia.Media SmoothCubicBezierCurve, SmoothQuadraticBezierCurve, Arc, - Close, + Close } - /// - /// Parses the specified markup string. - /// - /// The markup string. - public void Parse(string s) + public PathGeometry Parse(string s) { - bool openFigure = false; + _currentGeometry = new PathGeometry(); + + var tokens = ParseTokens(s); - using (StringReader reader = new StringReader(s)) + return CreateGeometry(tokens); + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) { - Command command = Command.None; - Point point = new Point(); - bool relative = false; - Point? previousControlPoint = null; + return; + } + + if (disposing) + { + _currentFigure = null; + + _currentGeometry = null; + } + + _isDisposed = true; + } + + private static string CreatesSeparatorPattern() + { + var stringBuilder = new StringBuilder(); + + foreach (var command in s_commands.Keys) + { + stringBuilder.Append(command); + + stringBuilder.Append(char.ToLower(command)); + } + + return @"(?=[" + stringBuilder + "])"; + } + + private static IEnumerable ParseTokens(string s) + { + return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); + } + + private static Point MirrorControlPoint(Point controlPoint, Point center) + { + var dir = controlPoint - center; + + return center + -dir; + } - while (ReadCommand(reader, ref command, ref relative)) + private PathGeometry CreateGeometry(IEnumerable commandTokens) + { + _currentGeometry = new PathGeometry(); + + _currentPoint = new Point(); + + foreach (var commandToken in commandTokens) + { + try { - switch (command) + while (true) { - case Command.FillRule: - _context.SetFillRule(ReadFillRule(reader)); - previousControlPoint = null; - break; - - case Command.Move: - if (openFigure) - { - _context.EndFigure(false); - } - - point = ReadPoint(reader, point, relative); - _context.BeginFigure(point, true); - openFigure = true; - previousControlPoint = null; - break; - - case Command.Line: - point = ReadPoint(reader, point, relative); - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.HorizontalLine: - if (!relative) - { - point = point.WithX(ReadDouble(reader)); - } - else - { - point = new Point(point.X + ReadDouble(reader), point.Y); - } - - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.VerticalLine: - if (!relative) - { - point = point.WithY(ReadDouble(reader)); - } - else - { - point = new Point(point.X, point.Y + ReadDouble(reader)); - } - - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.QuadraticBezierCurve: - { - Point handle = ReadPoint(reader, point, relative); - previousControlPoint = handle; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.QuadraticBezierTo(handle, point); + switch (commandToken.Command) + { + case Command.None: + break; + case Command.FillRule: + SetFillRule(commandToken); + break; + case Command.Move: + AddMove(commandToken); + break; + case Command.Line: + AddLine(commandToken); + break; + case Command.HorizontalLine: + AddHorizontalLine(commandToken); + break; + case Command.VerticalLine: + AddVerticalLine(commandToken); + break; + case Command.CubicBezierCurve: + AddCubicBezierCurve(commandToken); + break; + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(commandToken); break; - } - - case Command.SmoothQuadraticBezierCurve: - { - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.QuadraticBezierTo(previousControlPoint ?? point, end); - point = end; + case Command.SmoothCubicBezierCurve: + AddSmoothCubicBezierCurve(commandToken); break; - } - - case Command.CubicBezierCurve: - { - Point point1 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point point2 = ReadPoint(reader, point, relative); - previousControlPoint = point2; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.CubicBezierTo(point1, point2, point); + case Command.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(commandToken); break; - } - - case Command.SmoothCubicBezierCurve: - { - Point point2 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.CubicBezierTo(previousControlPoint ?? point, point2, end); - previousControlPoint = point2; - point = end; + case Command.Arc: + AddArc(commandToken); break; - } - - case Command.Arc: - { - Size size = ReadSize(reader); - ReadSeparator(reader); - double rotationAngle = ReadDouble(reader); - ReadSeparator(reader); - bool isLargeArc = ReadBool(reader); - ReadSeparator(reader); - SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - - _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); - previousControlPoint = null; + case Command.Close: + CloseFigure(); break; - } + default: + throw new NotSupportedException("Unsupported command"); + } - case Command.Close: - _context.EndFigure(true); - openFigure = false; - previousControlPoint = null; - break; + if (commandToken.HasImplicitCommands) + { + continue; + } - default: - throw new NotSupportedException("Unsupported command"); + break; } } - - if (openFigure) + catch (InvalidDataException) { - _context.EndFigure(false); + break; + } + catch (NotSupportedException) + { + break; } } + + return _currentGeometry; } - private Point MirrorControlPoint(Point controlPoint, Point center) + private void SetFillRule(CommandToken commandToken) { - Point dir = (controlPoint - center); - return center + -dir; + _currentGeometry.FillRule = commandToken.ReadFillRule(); } - private static bool ReadCommand( - StringReader reader, - ref Command command, - ref bool relative) + private void CloseFigure() { - ReadWhitespace(reader); + if (_currentFigure != null && !_currentFigure.IsClosed) + { + _currentFigure.IsClosed = true; + } - int i = reader.Peek(); + _previousControlPoint = null; - if (i == -1) + _currentFigure = null; + } + + private void CreateFigure() + { + _currentFigure = new PathFigure { - return false; + StartPoint = _currentPoint, + IsClosed = false + }; + + _currentGeometry.Figures.Add(_currentFigure); + } + + private void AddSegment(PathSegment segment) + { + if (_currentFigure == null) + { + CreateFigure(); } - else + + _currentFigure.Segments.Add(segment); + } + + private void AddMove(CommandToken commandToken) + { + var currentPoint = commandToken.ReadPoint(); + + _currentPoint = currentPoint; + + CreateFigure(); + + if (!commandToken.HasImplicitCommands) { - char c = (char)i; - Command next = Command.None; + return; + } - if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) + while (commandToken.HasImplicitCommands) + { + AddLine(commandToken); + + if (commandToken.IsRelative) { - if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') && - (command != Command.None)) - { - return true; - } - else - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } + continue; } - command = next; - relative = char.IsLower(c); - reader.Read(); - return true; + _currentPoint = currentPoint; + + CreateFigure(); } } - private static FillRule ReadFillRule(StringReader reader) + private void AddLine(CommandToken commandToken) { - int i = reader.Read(); - if (i == -1) - { - throw new InvalidDataException("Invalid fill rule"); - } - char c = (char)i; - FillRule rule; + _currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (!FillRules.TryGetValue(c, out rule)) + var lineSegment = new LineSegment { - throw new InvalidDataException("Invalid fill rule"); - } + Point = _currentPoint + }; - return rule; + AddSegment(lineSegment); } - private static double ReadDouble(StringReader reader) + private void AddHorizontalLine(CommandToken commandToken) { - ReadWhitespace(reader); + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - // TODO: Handle Infinity, NaN and scientific notation. - StringBuilder b = new StringBuilder(); - bool readSign = false; - bool readPoint = false; - bool readExponent = false; - int i; + var lineSegment = new LineSegment + { + Point = _currentPoint + }; - while ((i = reader.Peek()) != -1) + AddSegment(lineSegment); + } + + private void AddVerticalLine(CommandToken commandToken) + { + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); + + var lineSegment = new LineSegment { - char c = char.ToUpperInvariant((char)i); + Point = _currentPoint + }; - if (((c == '+' || c == '-') && !readSign) || - (c == '.' && !readPoint) || - (c == 'E' && !readExponent) || - char.IsDigit(c)) - { - if (b.Length != 0 && !readExponent && c == '-') - break; - - b.Append(c); - reader.Read(); + AddSegment(lineSegment); + } - if (!readSign) - { - readSign = c == '+' || c == '-'; - } + private void AddCubicBezierCurve(CommandToken commandToken) + { + var point1 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (!readPoint) - { - readPoint = c == '.'; - } + var point2 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (c == 'E') - { - readSign = false; - readExponent = true; - } - } - else - { - break; - } - } + _previousControlPoint = point2; + + var point3 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var bezierSegment = new BezierSegment + { + Point1 = point1, + Point2 = point2, + Point3 = point3 + }; + + AddSegment(bezierSegment); - return double.Parse(b.ToString(), CultureInfo.InvariantCulture); + _currentPoint = point3; } - private static Point ReadPoint(StringReader reader, Point current, bool relative) + private void AddQuadraticBezierCurve(CommandToken commandToken) { - if (!relative) + var start = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + _previousControlPoint = start; + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var quadraticBezierSegment = new QuadraticBezierSegment { - current = new Point(); - } + Point1 = start, + Point2 = end + }; + + AddSegment(quadraticBezierSegment); - ReadWhitespace(reader); - double x = current.X + ReadDouble(reader); - ReadSeparator(reader); - double y = current.Y + ReadDouble(reader); - return new Point(x, y); + _currentPoint = end; } - private static Size ReadSize(StringReader reader) + private void AddSmoothCubicBezierCurve(CommandToken commandToken) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Size(x, y); + var point2 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + var bezierSegment = + new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + + AddSegment(bezierSegment); + + _previousControlPoint = point2; + + _currentPoint = end; } - private static bool ReadBool(StringReader reader) + private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) { - return ReadDouble(reader) != 0; + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + var quadraticBezierSegment = new QuadraticBezierSegment + { + Point1 = _previousControlPoint ?? _currentPoint, + Point2 = end + }; + + AddSegment(quadraticBezierSegment); + + _currentPoint = end; } - private static Point ReadRelativePoint(StringReader reader, Point lastPoint) + private void AddArc(CommandToken commandToken) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Point(lastPoint.X + x, lastPoint.Y + y); + var size = commandToken.ReadSize(); + + var rotationAngle = commandToken.ReadDouble(); + + var isLargeArc = commandToken.ReadBool(); + + var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = end + }; + + AddSegment(arcSegment); + + _currentPoint = end; + + _previousControlPoint = null; } - private static void ReadSeparator(StringReader reader) + private class CommandToken { - int i; - bool readComma = false; + private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; - while ((i = reader.Peek()) != -1) + private CommandToken(Command command, bool isRelative, IEnumerable arguments) { - char c = (char)i; + Command = command; + + IsRelative = isRelative; + + Arguments = new List(arguments); + } + + public Command Command { get; } + + public bool IsRelative { get; } - if (char.IsWhiteSpace(c)) + public bool HasImplicitCommands + { + get { - reader.Read(); + if (CurrentPosition == 0 && Arguments.Count > 0) + { + return true; + } + + return CurrentPosition < Arguments.Count - 1; } - else if (c == ',') + } + + private int CurrentPosition { get; set; } + + private List Arguments { get; } + + public static CommandToken Parse(string s) + { + using (var reader = new StringReader(s)) { - if (readComma) + var command = Command.None; + + var isRelative = false; + + if (!ReadCommand(reader, ref command, ref isRelative)) { - throw new InvalidDataException("Unexpected ','."); + throw new InvalidDataException("No path command declared."); } - readComma = true; - reader.Read(); + var commandArguments = reader.ReadToEnd(); + + var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression); + + var arguments = new List(); + + foreach (Match match in argumentMatches) + { + arguments.Add(match.Value); + } + + return new CommandToken(command, isRelative, arguments); } - else + } + + public FillRule ReadFillRule() + { + if (CurrentPosition == Arguments.Count) { - break; + throw new InvalidDataException("Invalid fill rule"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + switch (value) + { + case "0": + { + return FillRule.EvenOdd; + } + + case "1": + { + return FillRule.NonZero; + } + + default: + throw new InvalidDataException("Invalid fill rule"); } } - } - private static void ReadWhitespace(StringReader reader) - { - int i; + public bool ReadBool() + { + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid boolean value"); + } + + var value = Arguments[CurrentPosition]; - while ((i = reader.Peek()) != -1) + CurrentPosition++; + + switch (value) + { + case "1": + { + return true; + } + + case "0": + { + return false; + } + + default: + throw new InvalidDataException("Invalid boolean value"); + } + } + + public double ReadDouble() { - char c = (char)i; + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid double value"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + return double.Parse(value, CultureInfo.InvariantCulture); + } - if (char.IsWhiteSpace(c)) + public Size ReadSize() + { + var width = ReadDouble(); + + var height = ReadDouble(); + + return new Size(width, height); + } + + public Point ReadPoint() + { + var x = ReadDouble(); + + var y = ReadDouble(); + + return new Point(x, y); + } + + public Point ReadRelativePoint(Point origin) + { + var x = ReadDouble(); + + var y = ReadDouble(); + + return new Point(origin.X + x, origin.Y + y); + } + + private static bool ReadCommand( + TextReader reader, + ref Command command, + ref bool relative) + { + ReadWhitespace(reader); + + var i = reader.Peek(); + + if (i == -1) { - reader.Read(); + return false; } - else + + var c = (char)i; + + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) { - break; + throw new InvalidDataException("Unexpected path command '" + c + "'."); + } + + command = next; + + relative = char.IsLower(c); + + reader.Read(); + + return true; + } + + private static void ReadWhitespace(TextReader reader) + { + int i; + + while ((i = reader.Peek()) != -1) + { + var c = (char)i; + + if (char.IsWhiteSpace(c)) + { + reader.Read(); + } + else + { + break; + } } } } diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 9848a649aa..1983740375 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -35,14 +35,10 @@ namespace Avalonia.Media /// A . public static new StreamGeometry Parse(string s) { - StreamGeometry result = new StreamGeometry(); - - using (StreamGeometryContext ctx = result.Open()) + using (var parser = new PathMarkupParser()) { - PathMarkupParser parser = new PathMarkupParser(ctx); - parser.Parse(s); - return result; - } + return parser.Parse(s); + } } /// diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index e63d23283c..d7eb6129ac 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -8,53 +8,128 @@ using Xunit; namespace Avalonia.Visuals.UnitTests.Media { + using System.Linq; + public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (AvaloniaLocator.EnterScope()) - { - var result = new Mock(); - - var parser = PrepareParser(result); + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse("M10 10"); - parser.Parse("M10 10"); + var figure = geometry.Figures.First(); - result.Verify(x => x.BeginFigure(new Point(10, 10), true)); + Assert.Equal(new Point(10, 10), figure.StartPoint); } } [Fact] public void Parses_Line() { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var result = new Mock(); + var geometry = parser.Parse("M0 0L10 10"); - var parser = PrepareParser(result); + var figure = geometry.Figures.First(); - parser.Parse("M0 0L10 10"); + var segment = figure.Segments.First(); - result.Verify(x => x.LineTo(new Point(10, 10))); + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); } } [Fact] public void Parses_Close() { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var result = new Mock(); + var geometry = parser.Parse("M0 0L10 10z"); + + var figure = geometry.Figures.First(); + + Assert.True(figure.IsClosed); + } + } + + [Fact] + public void Parses_FillMode_Before_Move() + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse("F 1M0,0"); + + Assert.Equal(FillRule.NonZero, geometry.FillRule); + } + } + + [Theory] + [InlineData("M0 0 10 10 20 20")] + [InlineData("M0,0 10,10 20,20")] + [InlineData("M0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Move(string pathData) + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse(pathData); + + var figure = geometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); - var parser = PrepareParser(result); + var lineSegment = (LineSegment)segment; - parser.Parse("M0 0L10 10z"); + Assert.Equal(new Point(10, 10), lineSegment.Point); - result.Verify(x => x.EndFigure(true)); + figure = geometry.Figures[1]; + + segment = figure.Segments[0]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(20, 20), lineSegment.Point); } } + [Theory] + [InlineData("m0 0 10 10 20 20")] + [InlineData("m0,0 10,10 20,20")] + [InlineData("m0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse(pathData); + + var figure = geometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + segment = figure.Segments[1]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(30, 30), lineSegment.Point); + } + } + [Theory] [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("M0 0L10 10z")] @@ -75,29 +150,17 @@ namespace Avalonia.Visuals.UnitTests.Media ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + ".9063 10.1309 30.9062 16.6309 19.9063 Z ")] [InlineData( - "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + - "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + + "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + + "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var parser = PrepareParser(); - parser.Parse(pathData); Assert.True(true); } } - - private static PathMarkupParser PrepareParser(Mock implMock = null) - { - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(Mock.Of()); - - return new PathMarkupParser( - new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of())); - } } } \ No newline at end of file From e576ec178c95ccbd8a5772754ad1c30d87465d26 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 21:06:05 +0200 Subject: [PATCH 03/88] Initial --- src/Avalonia.Controls/AppBuilderBase.cs | 45 +++--- src/Avalonia.Controls/Application.cs | 118 ++++++++++++++- src/Avalonia.Controls/ExitMode.cs | 12 ++ src/Avalonia.Controls/Window.cs | 51 ++++--- src/Avalonia.Controls/WindowCollection.cs | 134 ++++++++++++++++++ .../ApplicationTests.cs | 107 ++++++++++++++ .../WindowTests.cs | 10 +- 7 files changed, 428 insertions(+), 49 deletions(-) create mode 100644 src/Avalonia.Controls/ExitMode.cs create mode 100644 src/Avalonia.Controls/WindowCollection.cs create mode 100644 tests/Avalonia.Controls.UnitTests/ApplicationTests.cs diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 7af3deef34..875f5263c2 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { private static bool s_setupWasAlreadyCalled; - + /// /// Gets or sets the instance. /// @@ -92,7 +92,7 @@ namespace Avalonia.Controls }; } - protected TAppBuilder Self => (TAppBuilder) this; + protected TAppBuilder Self => (TAppBuilder)this; /// /// Registers a callback to call before Start is called on the . @@ -125,7 +125,6 @@ namespace Avalonia.Controls var window = new TMainWindow(); if (dataContextProvider != null) window.DataContext = dataContextProvider(); - window.Show(); Instance.Run(window); } @@ -143,7 +142,6 @@ namespace Avalonia.Controls if (dataContextProvider != null) mainWindow.DataContext = dataContextProvider(); - mainWindow.Show(); Instance.Run(mainWindow); } @@ -209,6 +207,17 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); + /// + /// Sets the shutdown mode of the application. + /// + /// The shutdown mode. + /// + public TAppBuilder SetExitMode(ExitMode exitMode) + { + Instance.ExitMode = exitMode; + return Self; + } + private bool CheckSetup { get; set; } = true; /// @@ -223,20 +232,20 @@ namespace Avalonia.Controls private void SetupAvaloniaModules() { var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.ForWindowingSubsystem == "" - || attribute.ForWindowingSubsystem == WindowingSubsystemName - where attribute.ForRenderingSubsystem == "" - || attribute.ForRenderingSubsystem == RenderingSubsystemName - group attribute by attribute.Name into exports - select (from export in exports - orderby export.ForWindowingSubsystem.Length descending - orderby export.ForRenderingSubsystem.Length descending - select export).First().ModuleType into moduleType - select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors - where constructor.GetParameters().Length == 0 && !constructor.IsStatic - select constructor).Single() into constructor - select (Action)(() => constructor.Invoke(new object[0])); + from attribute in assembly.GetCustomAttributes() + where attribute.ForWindowingSubsystem == "" + || attribute.ForWindowingSubsystem == WindowingSubsystemName + where attribute.ForRenderingSubsystem == "" + || attribute.ForRenderingSubsystem == RenderingSubsystemName + group attribute by attribute.Name into exports + select (from export in exports + orderby export.ForWindowingSubsystem.Length descending + orderby export.ForRenderingSubsystem.Length descending + select export).First().ModuleType into moduleType + select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors + where constructor.GetParameters().Length == 0 && !constructor.IsStatic + select constructor).Single() into constructor + select (Action)(() => constructor.Invoke(new object[0])); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6fdca557eb..ffe4a9c513 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -43,11 +43,15 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + private CancellationTokenSource _mainLoopCancellationTokenSource; + /// /// Initializes a new instance of the class. /// public Application() { + Windows = new WindowCollection(this); + OnExit += OnExiting; } @@ -158,6 +162,40 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; + /// + /// Gets or sets the . This property indicates whether the application exits explicitly or implicitly. + /// If is set to OnExplicitExit the application is only closes if Exit is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + public ExitMode ExitMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + public Window MainWindow { get; set; } + + /// + /// Gets the open windows of the application. + /// + /// + /// The windows. + /// + public WindowCollection Windows { get; } + + /// + /// Gets or sets a value indicating whether this instance is existing. + /// + /// + /// true if this instance is existing; otherwise, false. + /// + internal bool IsExiting { get; set; } + /// /// Initializes the application by loading XAML etc. /// @@ -171,19 +209,81 @@ namespace Avalonia /// The closable to track public void Run(ICloseable closable) { - var source = new CancellationTokenSource(); - closable.Closed += OnExiting; - closable.Closed += (s, e) => source.Cancel(); - Dispatcher.UIThread.MainLoop(source.Token); + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + closable.Closed += (s, e) => Exit(); + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Runs the application's main loop until some condition occurs that is specified by ExitMode. + /// + /// The main window + public void Run(Window mainWindow) + { + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.InvokeAsync( + () => + { + if (mainWindow == null) + { + return; + } + + if (MainWindow != null) + { + return; + } + + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + + MainWindow = mainWindow; + }, + DispatcherPriority.Send); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } - + /// - /// Runs the application's main loop until the is cancelled. + /// Runs the application's main loop until the is canceled. /// /// The token to track public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -191,7 +291,13 @@ namespace Avalonia /// public void Exit() { + IsExiting = true; + + Windows.Clear(); + OnExit?.Invoke(this, EventArgs.Empty); + + _mainLoopCancellationTokenSource?.Cancel(); } /// diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs new file mode 100644 index 0000000000..0c5ecd7171 --- /dev/null +++ b/src/Avalonia.Controls/ExitMode.cs @@ -0,0 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia +{ + public enum ExitMode + { + OnLastWindowClose, + OnMainWindowClose, + OnExplicitExit + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..c19c69ce73 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -49,14 +49,6 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static List s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IReadOnlyList OpenWindows => s_windows; - - /// /// Defines the property. /// public static readonly StyledProperty SizeToContentProperty = @@ -75,7 +67,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(ShowInTaskbar), true); /// - /// Enables or disables the taskbar icon + /// Represents the current window state (normal, minimized, maximized) /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); @@ -117,7 +109,7 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -149,7 +141,7 @@ namespace Avalonia.Controls _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); Screens = new Screens(PlatformImpl?.Screen); } - + /// event EventHandler INameScope.Registered { @@ -199,7 +191,7 @@ namespace Avalonia.Controls get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } - + /// /// Enables or disables the taskbar icon /// @@ -259,6 +251,26 @@ namespace Avalonia.Controls /// public event EventHandler Closing; + private static void AddWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Add(window); + } + + private static void RemoveWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Remove(window); + } + /// /// Closes the window. /// @@ -298,10 +310,9 @@ namespace Avalonia.Controls finally { if (ignoreCancel || !cancelClosing) - { - s_windows.Remove(this); + { PlatformImpl?.Dispose(); - IsVisible = false; + HandleClosed(); } } } @@ -359,7 +370,7 @@ namespace Avalonia.Controls return; } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -400,7 +411,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -409,7 +420,7 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList(); + var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList(); var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault(); SetIsEnabled(affectedWindows, false); @@ -513,8 +524,8 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IsVisible = false; - s_windows.Remove(this); + RemoveWindow(this); + base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs new file mode 100644 index 0000000000..c21a12f05b --- /dev/null +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -0,0 +1,134 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections; +using System.Collections.Generic; + +using Avalonia.Controls; + +namespace Avalonia +{ + public class WindowCollection : IReadOnlyList + { + private readonly Application _application; + private readonly List _windows = new List(); + + public WindowCollection(Application application) + { + _application = application; + } + + /// + /// + /// Gets the number of elements in the collection. + /// + public int Count => _windows.Count; + + /// + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + public Window this[int index] => _windows[index]; + + /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _windows.GetEnumerator(); + } + + /// + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Adds the specified window. + /// + /// The window. + internal void Add(Window window) + { + if (window == null) + { + return; + } + + _windows.Add(window); + } + + /// + /// Removes the specified window. + /// + /// The window. + internal void Remove(Window window) + { + if (window == null) + { + return; + } + + _windows.Remove(window); + + OnRemoveWindow(window); + } + + /// + /// Closes all windows and removes them from the underlying collection. + /// + internal void Clear() + { + while (_windows.Count > 0) + { + _windows[0].Close(); + } + } + + private void OnRemoveWindow(Window window) + { + if (window == null) + { + return; + } + + if (_application.IsExiting) + { + return; + } + + switch (_application.ExitMode) + { + case ExitMode.OnLastWindowClose: + if (Count == 0) + { + _application.Exit(); + } + + break; + case ExitMode.OnMainWindowClose: + if (window == _application.MainWindow) + { + _application.Exit(); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs new file mode 100644 index 0000000000..85f95b2b5c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ApplicationTests + { + [Fact] + public void Should_Exit_After_MainWindow_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnMainWindowClose; + + var mainWindow = new Window(); + + mainWindow.Show(); + + Application.Current.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + mainWindow.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Exit_After_Last_Window_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnLastWindowClose; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Only_Exit_On_Explicit_Exit() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnExplicitExit; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.False(Application.Current.IsExiting); + + Application.Current.Exit(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windows = new List { new Window(), new Window(), new Window(), new Window() }; + + foreach (var window in windows) + { + window.Show(); + } + + Application.Current.Exit(); + + Assert.Empty(Application.Current.Windows); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a85c4df8af..e80ffd97cd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); } } @@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.IsVisible = true; - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); window.Close(); } @@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.Close(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); windowImpl.Object.Closed(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -339,7 +339,7 @@ namespace Avalonia.Controls.UnitTests { // HACK: We really need a decent way to have "statics" that can be scoped to // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + Application.Current.Windows.Clear(); } } } From 61816fdf92c90b2929d3e48dd4e8bd5808d18654 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 11 Jun 2018 14:06:48 +0200 Subject: [PATCH 04/88] Relative move command fix --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 0307701e82..c3fcf19148 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -232,7 +232,9 @@ namespace Avalonia.Media private void AddMove(CommandToken commandToken) { - var currentPoint = commandToken.ReadPoint(); + var currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _currentPoint = currentPoint; From 219a0da799e954516b9c50eb2ad41c8feae533ba Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 17 Jun 2018 18:56:47 +0200 Subject: [PATCH 05/88] Added benchmarks for PathMarkupParser. --- .../Avalonia.Benchmarks.csproj | 5 ++- .../Visuals/Media/PathMarkupParserTests.cs | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 9e92baf0ff..a460ab5bf3 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -1,6 +1,7 @@  - netcoreapp2.0 + netcoreapp2.0 + Exe @@ -15,7 +16,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs new file mode 100644 index 0000000000..6d0027fe23 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Visuals.Media +{ + [MemoryDiagnoser] + public class PathMarkupParserTests : IDisposable + { + private IDisposable _app; + + public PathMarkupParserTests() + { + _app = UnitTestApplication.Start(TestServices.StyledWindow); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Parse_Large_Path() + { + var path = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + + " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + + "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + + " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + + ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + + ".9063 10.1309 30.9062 16.6309 19.9063 Z "; + var parser = new PathMarkupParser(); + var result = parser.Parse(path); + } + } +} From fca102655ea12961e18e03e616adc3bdff6c0817 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 19:34:46 +0200 Subject: [PATCH 06/88] Add ExitMode comments --- src/Avalonia.Controls/ExitMode.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs index 0c5ecd7171..b73fe4a963 100644 --- a/src/Avalonia.Controls/ExitMode.cs +++ b/src/Avalonia.Controls/ExitMode.cs @@ -3,10 +3,24 @@ namespace Avalonia { + /// + /// Enum for ExitMode + /// public enum ExitMode { + /// + /// Indicates an implicit call to Application.Exit when the last window closes. + /// OnLastWindowClose, + + /// + /// Indicates an implicit call to Application.Exit when the main window closes. + /// OnMainWindowClose, + + /// + /// Indicates that the application only exits on an explicit call to Application.Exit. + /// OnExplicitExit } } \ No newline at end of file From b8f8c1ef0f90f8ce7a12446484a6c7dd34e51d4e Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 22:26:03 +0200 Subject: [PATCH 07/88] IGeometryContext implementation --- src/Avalonia.Visuals/Media/PathGeometry.cs | 22 +- .../Media/PathMarkupParser.cs | 198 +++++++++--------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 15 +- .../Media/StreamGeometryContext.cs | 2 +- .../Platform/IGeometryContext.cs | 66 ++++++ .../Platform/IStreamGeometryContextImpl.cs | 54 +---- .../Platform/PathGeometryContext.cs | 85 ++++++++ .../Visuals/Media/PathMarkupParserTests.cs | 22 +- .../Media/PathMarkupParserTests.cs | 61 +++--- 9 files changed, 330 insertions(+), 195 deletions(-) create mode 100644 src/Avalonia.Visuals/Platform/IGeometryContext.cs create mode 100644 src/Avalonia.Visuals/Platform/PathGeometryContext.cs diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index ecda07ada1..5cf98aa5e7 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -8,6 +8,8 @@ using Avalonia.Platform; namespace Avalonia.Media { + using Avalonia.Visuals.Platform; + public class PathGeometry : StreamGeometry { /// @@ -28,7 +30,7 @@ namespace Avalonia.Media static PathGeometry() { - FiguresProperty.Changed.AddClassHandler((s, e) => + FiguresProperty.Changed.AddClassHandler((s, e) => s.OnFiguresChanged(e.NewValue as PathFigures)); } @@ -40,6 +42,24 @@ namespace Avalonia.Media Figures = new PathFigures(); } + /// + /// Parses the specified path data to a . + /// + /// The s. + /// + public static new PathGeometry Parse(string pathData) + { + var pathGeometry = new PathGeometry(); + + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + } + + return pathGeometry; + } + /// /// Gets or sets the figures. /// diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index c3fcf19148..2ffff75de3 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Avalonia.Platform; namespace Avalonia.Media { @@ -17,13 +18,6 @@ namespace Avalonia.Media public class PathMarkupParser : IDisposable { private static readonly string s_separatorPattern; - - private Point _currentPoint; - private Point? _previousControlPoint; - private PathGeometry _currentGeometry; - private PathFigure _currentFigure; - private bool _isDisposed; - private static readonly Dictionary s_commands = new Dictionary { @@ -40,11 +34,32 @@ namespace Avalonia.Media { 'Z', Command.Close }, }; + private IGeometryContext _geometryContext; + private Point _currentPoint; + private Point? _previousControlPoint; + private bool? _isOpen; + private bool _isDisposed; + static PathMarkupParser() { s_separatorPattern = CreatesSeparatorPattern(); } + /// + /// Initializes a new instance of the class. + /// + /// The geometry context. + /// geometryContext + public PathMarkupParser(IGeometryContext geometryContext) + { + if (geometryContext == null) + { + throw new ArgumentNullException(nameof(geometryContext)); + } + + _geometryContext = geometryContext; + } + private enum Command { None, @@ -61,13 +76,15 @@ namespace Avalonia.Media Close } - public PathGeometry Parse(string s) + /// + /// Parses the specified path data and writes the result to the geometryContext of this instance. + /// + /// The path data. + public void Parse(string pathData) { - _currentGeometry = new PathGeometry(); - - var tokens = ParseTokens(s); + var tokens = ParseTokens(pathData); - return CreateGeometry(tokens); + CreateGeometry(tokens); } void IDisposable.Dispose() @@ -84,9 +101,7 @@ namespace Avalonia.Media if (disposing) { - _currentFigure = null; - - _currentGeometry = null; + _geometryContext = null; } _isDisposed = true; @@ -118,11 +133,9 @@ namespace Avalonia.Media return center + -dir; } - private PathGeometry CreateGeometry(IEnumerable commandTokens) + private void CreateGeometry(IEnumerable commandTokens) { - _currentGeometry = new PathGeometry(); - - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -188,46 +201,32 @@ namespace Avalonia.Media break; } } - - return _currentGeometry; - } - - private void SetFillRule(CommandToken commandToken) - { - _currentGeometry.FillRule = commandToken.ReadFillRule(); } - private void CloseFigure() + private void CreateFigure() { - if (_currentFigure != null && !_currentFigure.IsClosed) - { - _currentFigure.IsClosed = true; - } - - _previousControlPoint = null; + _geometryContext.BeginFigure(_currentPoint); - _currentFigure = null; + this._isOpen = true; } - private void CreateFigure() + private void SetFillRule(CommandToken commandToken) { - _currentFigure = new PathFigure - { - StartPoint = _currentPoint, - IsClosed = false - }; + var fillRule = commandToken.ReadFillRule(); - _currentGeometry.Figures.Add(_currentFigure); + _geometryContext.SetFillRule(fillRule); } - private void AddSegment(PathSegment segment) + private void CloseFigure() { - if (_currentFigure == null) + if (_isOpen == true) { - CreateFigure(); + _geometryContext.EndFigure(true); } - _currentFigure.Segments.Add(segment); + _previousControlPoint = null; + + _isOpen = null; } private void AddMove(CommandToken commandToken) @@ -266,66 +265,64 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddHorizontalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) - : _currentPoint.WithX(commandToken.ReadDouble()); + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddVerticalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) - : _currentPoint.WithY(commandToken.ReadDouble()); + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddCubicBezierCurve(CommandToken commandToken) { var point1 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = point2; var point3 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var bezierSegment = new BezierSegment + if (_isOpen == null) { - Point1 = point1, - Point2 = point2, - Point3 = point3 - }; + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(point1, point2, point3); _currentPoint = point3; } @@ -333,22 +330,21 @@ namespace Avalonia.Media private void AddQuadraticBezierCurve(CommandToken commandToken) { var start = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = start; var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = start, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(start, end); _currentPoint = end; } @@ -356,8 +352,8 @@ namespace Avalonia.Media private void AddSmoothCubicBezierCurve(CommandToken commandToken) { var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var end = commandToken.IsRelative ? commandToken.ReadRelativePoint(_currentPoint) @@ -368,10 +364,12 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var bezierSegment = - new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + if (_isOpen == null) + { + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end); _previousControlPoint = point2; @@ -389,13 +387,12 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = _previousControlPoint ?? _currentPoint, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end); _currentPoint = end; } @@ -414,16 +411,12 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var arcSegment = new ArcSegment + if (_isOpen == null) { - Size = size, - RotationAngle = rotationAngle, - IsLargeArc = isLargeArc, - SweepDirection = sweepDirection, - Point = end - }; + CreateFigure(); + } - AddSegment(arcSegment); + _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection); _currentPoint = end; @@ -589,10 +582,7 @@ namespace Avalonia.Media return new Point(origin.X + x, origin.Y + y); } - private static bool ReadCommand( - TextReader reader, - ref Command command, - ref bool relative) + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { ReadWhitespace(reader); diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 1983740375..2622175d60 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -5,6 +5,8 @@ using Avalonia.Platform; namespace Avalonia.Media { + using Avalonia.Visuals.Platform; + /// /// Represents the geometry of an arbitrarily complex shape. /// @@ -35,10 +37,15 @@ namespace Avalonia.Media /// A . public static new StreamGeometry Parse(string s) { - using (var parser = new PathMarkupParser()) - { - return parser.Parse(s); - } + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(s); + } + + return streamGeometry; } /// diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 70b889f817..7521582067 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media /// . /// /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed? - public class StreamGeometryContext : IDisposable + public class StreamGeometryContext : IGeometryContext { private readonly IStreamGeometryContextImpl _impl; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs new file mode 100644 index 0000000000..ac63837428 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs @@ -0,0 +1,66 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Platform +{ + /// + /// Describes a geometry using drawing commands. + /// + public interface IGeometryContext : IDisposable + { + /// + /// Draws an arc to the specified point. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); + + /// + /// Begins a new figure. + /// + /// The starting point for the figure. + /// Whether the figure is filled. + void BeginFigure(Point startPoint, bool isFilled = true); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + void CubicBezierTo(Point point1, Point point2, Point point3); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + void QuadraticBezierTo(Point control, Point endPoint); + + /// + /// Draws a line to the specified point. + /// + /// The destination point. + void LineTo(Point point); + + /// + /// Ends the figure started by . + /// + /// Whether the figure is closed. + void EndFigure(bool isClosed); + + /// + /// Sets the fill rule. + /// + /// The fill rule. + void SetFillRule(FillRule fillRule); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index 386560c6b6..da9505cd2d 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs @@ -1,62 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Media; - namespace Avalonia.Platform { /// /// Describes a geometry using drawing commands. /// - public interface IStreamGeometryContextImpl : IDisposable - { - /// - /// Draws an arc to the specified point. - /// - /// The destination point. - /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. - /// true to draw the arc greater than 180 degrees; otherwise, false. - /// - /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. - /// - void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); - - /// - /// Begins a new figure. - /// - /// The starting point for the figure. - /// Whether the figure is filled. - void BeginFigure(Point startPoint, bool isFilled); - - /// - /// Draws a Bezier curve to the specified point. - /// - /// The first control point used to specify the shape of the curve. - /// The second control point used to specify the shape of the curve. - /// The destination point for the end of the curve. - void CubicBezierTo(Point point1, Point point2, Point point3); - - /// - /// Draws a quadratic Bezier curve to the specified point - /// - /// Control point - /// DestinationPoint - void QuadraticBezierTo(Point control, Point endPoint); - - /// - /// Draws a line to the specified point. - /// - /// The destination point. - void LineTo(Point point); - - /// - /// Ends the figure started by . - /// - /// Whether the figure is closed. - void EndFigure(bool isClosed); - - void SetFillRule(FillRule fillRule); + public interface IStreamGeometryContextImpl : IGeometryContext + { } } diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs new file mode 100644 index 0000000000..cc881094fd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs @@ -0,0 +1,85 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using Avalonia.Platform; +using System; + +namespace Avalonia.Visuals.Platform +{ + public class PathGeometryContext : IGeometryContext + { + private PathFigure _currentFigure; + private PathGeometry _pathGeometry; + + public PathGeometryContext(PathGeometry pathGeometry) + { + _pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry)); + } + + public void Dispose() + { + _pathGeometry = null; + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = point + }; + + _currentFigure.Segments.Add(arcSegment); + } + + public void BeginFigure(Point startPoint, bool isFilled) + { + _currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled }; + + _pathGeometry.Figures.Add(_currentFigure); + } + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 }; + + _currentFigure.Segments.Add(bezierSegment); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint }; + + _currentFigure.Segments.Add(quadraticBezierSegment); + } + + public void LineTo(Point point) + { + var lineSegment = new LineSegment + { + Point = point + }; + + _currentFigure.Segments.Add(lineSegment); + } + + public void EndFigure(bool isClosed) + { + if (_currentFigure != null) + { + _currentFigure.IsClosed = isClosed; + } + + _currentFigure = null; + } + + public void SetFillRule(FillRule fillRule) + { + _pathGeometry.FillRule = fillRule; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs index 6d0027fe23..c4106340b2 100644 --- a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -26,14 +26,20 @@ namespace Avalonia.Benchmarks.Visuals.Media [Benchmark] public void Parse_Large_Path() { - var path = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + - " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + - "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + - " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + - ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + - ".9063 10.1309 30.9062 16.6309 19.9063 Z "; - var parser = new PathMarkupParser(); - var result = parser.Parse(path); + const string PathData = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + + " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + + "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + + " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + + ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + + ".9063 10.1309 30.9062 16.6309 19.9063 Z "; + + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(PathData); + } } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index d7eb6129ac..c8bea43fb0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -2,24 +2,23 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Media; -using Avalonia.Platform; -using Moq; +using Avalonia.Visuals.Platform; using Xunit; namespace Avalonia.Visuals.UnitTests.Media { - using System.Linq; - public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M10 10"); + parser.Parse("M10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.Equal(new Point(10, 10), figure.StartPoint); } @@ -28,13 +27,15 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parses_Line() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10"); + parser.Parse("M0 0L10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; - var segment = figure.Segments.First(); + var segment = figure.Segments[0]; Assert.IsType(segment); @@ -47,11 +48,13 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parses_Close() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10z"); + parser.Parse("M0 0L10 10z"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.True(figure.IsClosed); } @@ -60,11 +63,13 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parses_FillMode_Before_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("F 1M0,0"); + parser.Parse("F 1M0,0"); - Assert.Equal(FillRule.NonZero, geometry.FillRule); + Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } } @@ -74,11 +79,13 @@ namespace Avalonia.Visuals.UnitTests.Media [InlineData("M0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -88,7 +95,7 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new Point(10, 10), lineSegment.Point); - figure = geometry.Figures[1]; + figure = pathGeometry.Figures[1]; segment = figure.Segments[0]; @@ -106,11 +113,13 @@ namespace Avalonia.Visuals.UnitTests.Media [InlineData("m0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -155,7 +164,9 @@ namespace Avalonia.Visuals.UnitTests.Media "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { parser.Parse(pathData); From 42dadae3ecdfeedb6fdde2d5aea59bd76bd6d3c0 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 22:46:40 +0200 Subject: [PATCH 08/88] Merge fix --- src/Avalonia.Controls/AppBuilderBase.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 814c662341..83763c0836 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -218,17 +218,6 @@ namespace Avalonia.Controls return Self; } - private bool CheckSetup { get; set; } = true; - - /// - /// Set this AppBuilder to ignore the setup check. Used for testing purposes. - /// - internal TAppBuilder IgnoreSetupCheck() - { - CheckSetup = false; - return Self; - } - protected virtual bool CheckSetup => true; private void SetupAvaloniaModules() From cd39703645308fe64ed26e43c034971a07d1a947 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 23:52:49 +0200 Subject: [PATCH 09/88] Direct2D1 EndFigure fix --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 2ffff75de3..e8824753b5 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -201,13 +201,23 @@ namespace Avalonia.Media break; } } + + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } } private void CreateFigure() { + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } + _geometryContext.BeginFigure(_currentPoint); - this._isOpen = true; + _isOpen = true; } private void SetFillRule(CommandToken commandToken) From 41fd37151f6e2cc7a947f5c3bea551ddc3f20034 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 22:26:03 +0200 Subject: [PATCH 10/88] IGeometryContext implementation --- src/Avalonia.Visuals/Media/PathGeometry.cs | 23 +- .../Media/PathMarkupParser.cs | 200 +++++++++--------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 15 +- .../Media/StreamGeometryContext.cs | 2 +- .../Platform/IGeometryContext.cs | 66 ++++++ .../Platform/IStreamGeometryContextImpl.cs | 54 +---- .../Platform/PathGeometryContext.cs | 85 ++++++++ .../Visuals/Media/PathMarkupParserTests.cs | 22 +- .../Media/PathMarkupParserTests.cs | 61 +++--- 9 files changed, 336 insertions(+), 192 deletions(-) create mode 100644 src/Avalonia.Visuals/Platform/IGeometryContext.cs create mode 100644 src/Avalonia.Visuals/Platform/PathGeometryContext.cs diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index ecda07ada1..c5cd8036d2 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -5,9 +5,10 @@ using System; using Avalonia.Collections; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Visuals.Platform; namespace Avalonia.Media -{ +{ public class PathGeometry : StreamGeometry { /// @@ -28,7 +29,7 @@ namespace Avalonia.Media static PathGeometry() { - FiguresProperty.Changed.AddClassHandler((s, e) => + FiguresProperty.Changed.AddClassHandler((s, e) => s.OnFiguresChanged(e.NewValue as PathFigures)); } @@ -40,6 +41,24 @@ namespace Avalonia.Media Figures = new PathFigures(); } + /// + /// Parses the specified path data to a . + /// + /// The s. + /// + public static new PathGeometry Parse(string pathData) + { + var pathGeometry = new PathGeometry(); + + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + } + + return pathGeometry; + } + /// /// Gets or sets the figures. /// diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index c3fcf19148..e8824753b5 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Avalonia.Platform; namespace Avalonia.Media { @@ -17,13 +18,6 @@ namespace Avalonia.Media public class PathMarkupParser : IDisposable { private static readonly string s_separatorPattern; - - private Point _currentPoint; - private Point? _previousControlPoint; - private PathGeometry _currentGeometry; - private PathFigure _currentFigure; - private bool _isDisposed; - private static readonly Dictionary s_commands = new Dictionary { @@ -40,11 +34,32 @@ namespace Avalonia.Media { 'Z', Command.Close }, }; + private IGeometryContext _geometryContext; + private Point _currentPoint; + private Point? _previousControlPoint; + private bool? _isOpen; + private bool _isDisposed; + static PathMarkupParser() { s_separatorPattern = CreatesSeparatorPattern(); } + /// + /// Initializes a new instance of the class. + /// + /// The geometry context. + /// geometryContext + public PathMarkupParser(IGeometryContext geometryContext) + { + if (geometryContext == null) + { + throw new ArgumentNullException(nameof(geometryContext)); + } + + _geometryContext = geometryContext; + } + private enum Command { None, @@ -61,13 +76,15 @@ namespace Avalonia.Media Close } - public PathGeometry Parse(string s) + /// + /// Parses the specified path data and writes the result to the geometryContext of this instance. + /// + /// The path data. + public void Parse(string pathData) { - _currentGeometry = new PathGeometry(); - - var tokens = ParseTokens(s); + var tokens = ParseTokens(pathData); - return CreateGeometry(tokens); + CreateGeometry(tokens); } void IDisposable.Dispose() @@ -84,9 +101,7 @@ namespace Avalonia.Media if (disposing) { - _currentFigure = null; - - _currentGeometry = null; + _geometryContext = null; } _isDisposed = true; @@ -118,11 +133,9 @@ namespace Avalonia.Media return center + -dir; } - private PathGeometry CreateGeometry(IEnumerable commandTokens) + private void CreateGeometry(IEnumerable commandTokens) { - _currentGeometry = new PathGeometry(); - - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -189,45 +202,41 @@ namespace Avalonia.Media } } - return _currentGeometry; - } - - private void SetFillRule(CommandToken commandToken) - { - _currentGeometry.FillRule = commandToken.ReadFillRule(); + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } } - private void CloseFigure() + private void CreateFigure() { - if (_currentFigure != null && !_currentFigure.IsClosed) + if (_isOpen != null) { - _currentFigure.IsClosed = true; + _geometryContext.EndFigure(false); } - _previousControlPoint = null; + _geometryContext.BeginFigure(_currentPoint); - _currentFigure = null; + _isOpen = true; } - private void CreateFigure() + private void SetFillRule(CommandToken commandToken) { - _currentFigure = new PathFigure - { - StartPoint = _currentPoint, - IsClosed = false - }; + var fillRule = commandToken.ReadFillRule(); - _currentGeometry.Figures.Add(_currentFigure); + _geometryContext.SetFillRule(fillRule); } - private void AddSegment(PathSegment segment) + private void CloseFigure() { - if (_currentFigure == null) + if (_isOpen == true) { - CreateFigure(); + _geometryContext.EndFigure(true); } - _currentFigure.Segments.Add(segment); + _previousControlPoint = null; + + _isOpen = null; } private void AddMove(CommandToken commandToken) @@ -266,66 +275,64 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddHorizontalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) - : _currentPoint.WithX(commandToken.ReadDouble()); + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddVerticalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) - : _currentPoint.WithY(commandToken.ReadDouble()); + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddCubicBezierCurve(CommandToken commandToken) { var point1 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = point2; var point3 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var bezierSegment = new BezierSegment + if (_isOpen == null) { - Point1 = point1, - Point2 = point2, - Point3 = point3 - }; + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(point1, point2, point3); _currentPoint = point3; } @@ -333,22 +340,21 @@ namespace Avalonia.Media private void AddQuadraticBezierCurve(CommandToken commandToken) { var start = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = start; var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = start, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(start, end); _currentPoint = end; } @@ -356,8 +362,8 @@ namespace Avalonia.Media private void AddSmoothCubicBezierCurve(CommandToken commandToken) { var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var end = commandToken.IsRelative ? commandToken.ReadRelativePoint(_currentPoint) @@ -368,10 +374,12 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var bezierSegment = - new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + if (_isOpen == null) + { + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end); _previousControlPoint = point2; @@ -389,13 +397,12 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = _previousControlPoint ?? _currentPoint, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end); _currentPoint = end; } @@ -414,16 +421,12 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var arcSegment = new ArcSegment + if (_isOpen == null) { - Size = size, - RotationAngle = rotationAngle, - IsLargeArc = isLargeArc, - SweepDirection = sweepDirection, - Point = end - }; + CreateFigure(); + } - AddSegment(arcSegment); + _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection); _currentPoint = end; @@ -589,10 +592,7 @@ namespace Avalonia.Media return new Point(origin.X + x, origin.Y + y); } - private static bool ReadCommand( - TextReader reader, - ref Command command, - ref bool relative) + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { ReadWhitespace(reader); diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 1983740375..2622175d60 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -5,6 +5,8 @@ using Avalonia.Platform; namespace Avalonia.Media { + using Avalonia.Visuals.Platform; + /// /// Represents the geometry of an arbitrarily complex shape. /// @@ -35,10 +37,15 @@ namespace Avalonia.Media /// A . public static new StreamGeometry Parse(string s) { - using (var parser = new PathMarkupParser()) - { - return parser.Parse(s); - } + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(s); + } + + return streamGeometry; } /// diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 70b889f817..7521582067 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media /// . /// /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed? - public class StreamGeometryContext : IDisposable + public class StreamGeometryContext : IGeometryContext { private readonly IStreamGeometryContextImpl _impl; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs new file mode 100644 index 0000000000..ac63837428 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs @@ -0,0 +1,66 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Platform +{ + /// + /// Describes a geometry using drawing commands. + /// + public interface IGeometryContext : IDisposable + { + /// + /// Draws an arc to the specified point. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); + + /// + /// Begins a new figure. + /// + /// The starting point for the figure. + /// Whether the figure is filled. + void BeginFigure(Point startPoint, bool isFilled = true); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + void CubicBezierTo(Point point1, Point point2, Point point3); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + void QuadraticBezierTo(Point control, Point endPoint); + + /// + /// Draws a line to the specified point. + /// + /// The destination point. + void LineTo(Point point); + + /// + /// Ends the figure started by . + /// + /// Whether the figure is closed. + void EndFigure(bool isClosed); + + /// + /// Sets the fill rule. + /// + /// The fill rule. + void SetFillRule(FillRule fillRule); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index 386560c6b6..da9505cd2d 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs @@ -1,62 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Media; - namespace Avalonia.Platform { /// /// Describes a geometry using drawing commands. /// - public interface IStreamGeometryContextImpl : IDisposable - { - /// - /// Draws an arc to the specified point. - /// - /// The destination point. - /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. - /// true to draw the arc greater than 180 degrees; otherwise, false. - /// - /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. - /// - void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); - - /// - /// Begins a new figure. - /// - /// The starting point for the figure. - /// Whether the figure is filled. - void BeginFigure(Point startPoint, bool isFilled); - - /// - /// Draws a Bezier curve to the specified point. - /// - /// The first control point used to specify the shape of the curve. - /// The second control point used to specify the shape of the curve. - /// The destination point for the end of the curve. - void CubicBezierTo(Point point1, Point point2, Point point3); - - /// - /// Draws a quadratic Bezier curve to the specified point - /// - /// Control point - /// DestinationPoint - void QuadraticBezierTo(Point control, Point endPoint); - - /// - /// Draws a line to the specified point. - /// - /// The destination point. - void LineTo(Point point); - - /// - /// Ends the figure started by . - /// - /// Whether the figure is closed. - void EndFigure(bool isClosed); - - void SetFillRule(FillRule fillRule); + public interface IStreamGeometryContextImpl : IGeometryContext + { } } diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs new file mode 100644 index 0000000000..cc881094fd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs @@ -0,0 +1,85 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using Avalonia.Platform; +using System; + +namespace Avalonia.Visuals.Platform +{ + public class PathGeometryContext : IGeometryContext + { + private PathFigure _currentFigure; + private PathGeometry _pathGeometry; + + public PathGeometryContext(PathGeometry pathGeometry) + { + _pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry)); + } + + public void Dispose() + { + _pathGeometry = null; + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = point + }; + + _currentFigure.Segments.Add(arcSegment); + } + + public void BeginFigure(Point startPoint, bool isFilled) + { + _currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled }; + + _pathGeometry.Figures.Add(_currentFigure); + } + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 }; + + _currentFigure.Segments.Add(bezierSegment); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint }; + + _currentFigure.Segments.Add(quadraticBezierSegment); + } + + public void LineTo(Point point) + { + var lineSegment = new LineSegment + { + Point = point + }; + + _currentFigure.Segments.Add(lineSegment); + } + + public void EndFigure(bool isClosed) + { + if (_currentFigure != null) + { + _currentFigure.IsClosed = isClosed; + } + + _currentFigure = null; + } + + public void SetFillRule(FillRule fillRule) + { + _pathGeometry.FillRule = fillRule; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs index 6d0027fe23..c4106340b2 100644 --- a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -26,14 +26,20 @@ namespace Avalonia.Benchmarks.Visuals.Media [Benchmark] public void Parse_Large_Path() { - var path = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + - " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + - "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + - " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + - ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + - ".9063 10.1309 30.9062 16.6309 19.9063 Z "; - var parser = new PathMarkupParser(); - var result = parser.Parse(path); + const string PathData = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + + " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + + "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + + " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + + ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + + ".9063 10.1309 30.9062 16.6309 19.9063 Z "; + + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(PathData); + } } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index d7eb6129ac..c8bea43fb0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -2,24 +2,23 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Media; -using Avalonia.Platform; -using Moq; +using Avalonia.Visuals.Platform; using Xunit; namespace Avalonia.Visuals.UnitTests.Media { - using System.Linq; - public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M10 10"); + parser.Parse("M10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.Equal(new Point(10, 10), figure.StartPoint); } @@ -28,13 +27,15 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parses_Line() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10"); + parser.Parse("M0 0L10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; - var segment = figure.Segments.First(); + var segment = figure.Segments[0]; Assert.IsType(segment); @@ -47,11 +48,13 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parses_Close() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10z"); + parser.Parse("M0 0L10 10z"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.True(figure.IsClosed); } @@ -60,11 +63,13 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parses_FillMode_Before_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("F 1M0,0"); + parser.Parse("F 1M0,0"); - Assert.Equal(FillRule.NonZero, geometry.FillRule); + Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } } @@ -74,11 +79,13 @@ namespace Avalonia.Visuals.UnitTests.Media [InlineData("M0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -88,7 +95,7 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new Point(10, 10), lineSegment.Point); - figure = geometry.Figures[1]; + figure = pathGeometry.Figures[1]; segment = figure.Segments[0]; @@ -106,11 +113,13 @@ namespace Avalonia.Visuals.UnitTests.Media [InlineData("m0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -155,7 +164,9 @@ namespace Avalonia.Visuals.UnitTests.Media "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { parser.Parse(pathData); From c36e2e422251e938c044665b88579fbef94929f1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 18 Jun 2018 00:49:38 +0200 Subject: [PATCH 11/88] removed using --- src/Avalonia.Visuals/Media/StreamGeometry.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 2622175d60..d3cb788486 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -5,8 +5,6 @@ using Avalonia.Platform; namespace Avalonia.Media { - using Avalonia.Visuals.Platform; - /// /// Represents the geometry of an arbitrarily complex shape. /// From 7e8f9fbf619313389065950244924c9e46cf0658 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 19 Jun 2018 21:15:22 +0200 Subject: [PATCH 12/88] Add NullArgumentException for Run with main window --- src/Avalonia.Controls/Application.cs | 8 ++++---- .../Avalonia.Controls.UnitTests/ApplicationTests.cs | 12 +++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index ffe4a9c513..99575771da 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -243,14 +243,14 @@ namespace Avalonia Dispatcher.UIThread.InvokeAsync( () => { - if (mainWindow == null) + if (MainWindow != null) { return; } - if (MainWindow != null) + if (mainWindow == null) { - return; + throw new ArgumentNullException(nameof(mainWindow)); } if (!mainWindow.IsVisible) @@ -283,7 +283,7 @@ namespace Avalonia if (!IsExiting) { OnExit?.Invoke(this, EventArgs.Empty); - } + } } /// diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index 85f95b2b5c..694a6d2278 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using Avalonia.UnitTests; using Xunit; @@ -103,5 +104,14 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(Application.Current.Windows); } } + + [Fact] + public void Throws_ArgumentNullException_On_Run_If_MainWindow_Is_Null() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Assert.Throws(() => { Application.Current.Run(null); }); + } + } } -} +} \ No newline at end of file From 0214939fd3bf865655b50ab9e0c5e592cfb8ef5c Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 19 Jun 2018 21:56:13 +0200 Subject: [PATCH 13/88] Add unit test --- tests/Avalonia.Controls.UnitTests/ApplicationTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index 694a6d2278..9602bb0460 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -105,6 +105,17 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Should_Show_MainWindow_After_Run() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var mainWindow = new Window(); + Application.Current.Run(mainWindow); + Assert.True(mainWindow.IsVisible); + } + } + [Fact] public void Throws_ArgumentNullException_On_Run_If_MainWindow_Is_Null() { From 90fdfae3bd93667abe9bdc92094688c1a8244029 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 20 Jun 2018 00:01:57 +0200 Subject: [PATCH 14/88] Remove invoke call on Application.Run --- src/Avalonia.Controls/Application.cs | 35 ++++++++----------- .../ApplicationTests.cs | 11 ------ 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 99575771da..de27aa94f8 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -240,27 +240,20 @@ namespace Avalonia _mainLoopCancellationTokenSource = new CancellationTokenSource(); - Dispatcher.UIThread.InvokeAsync( - () => - { - if (MainWindow != null) - { - return; - } - - if (mainWindow == null) - { - throw new ArgumentNullException(nameof(mainWindow)); - } - - if (!mainWindow.IsVisible) - { - mainWindow.Show(); - } - - MainWindow = mainWindow; - }, - DispatcherPriority.Send); + if (MainWindow == null) + { + if (mainWindow == null) + { + throw new ArgumentNullException(nameof(mainWindow)); + } + + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + + MainWindow = mainWindow; + } Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index 9602bb0460..694a6d2278 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -105,17 +105,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Should_Show_MainWindow_After_Run() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var mainWindow = new Window(); - Application.Current.Run(mainWindow); - Assert.True(mainWindow.IsVisible); - } - } - [Fact] public void Throws_ArgumentNullException_On_Run_If_MainWindow_Is_Null() { From def7da4bb26386b3f81cc3f54c20f83cbca7370b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 20 Jun 2018 20:58:32 +0800 Subject: [PATCH 15/88] Rename Sample projects to avoid confusion with the CI tests. --- samples/{BindingTest => BindingDemo}/App.config | 0 samples/{BindingTest => BindingDemo}/App.xaml | 0 samples/{BindingTest => BindingDemo}/App.xaml.cs | 0 .../BindingTest.csproj => BindingDemo/BindingDemo.csproj} | 0 samples/{BindingTest => BindingDemo}/MainWindow.xaml | 0 samples/{BindingTest => BindingDemo}/MainWindow.xaml.cs | 0 samples/{BindingTest => BindingDemo}/TestItemView.xaml | 0 samples/{BindingTest => BindingDemo}/TestItemView.xaml.cs | 0 .../ViewModels/DataAnnotationsErrorViewModel.cs | 0 .../ViewModels/ExceptionErrorViewModel.cs | 0 .../ViewModels/IndeiErrorViewModel.cs | 0 .../ViewModels/MainWindowViewModel.cs | 0 .../ViewModels/NestedCommandViewModel.cs | 0 samples/{BindingTest => BindingDemo}/ViewModels/TestItem.cs | 0 samples/{RemoteTest => RemoteDemo}/Program.cs | 0 .../RemoteTest.csproj => RemoteDemo/RemoteDemo.csproj} | 0 samples/{RenderTest => RenderDemo}/App.config | 0 samples/{RenderTest => RenderDemo}/App.xaml | 0 samples/{RenderTest => RenderDemo}/App.xaml.cs | 0 samples/{RenderTest => RenderDemo}/MainWindow.xaml | 0 samples/{RenderTest => RenderDemo}/MainWindow.xaml.cs | 0 samples/{RenderTest => RenderDemo}/Pages/AnimationsPage.xaml | 0 samples/{RenderTest => RenderDemo}/Pages/AnimationsPage.xaml.cs | 0 samples/{RenderTest => RenderDemo}/Pages/ClippingPage.xaml | 0 samples/{RenderTest => RenderDemo}/Pages/ClippingPage.xaml.cs | 0 samples/{RenderTest => RenderDemo}/Pages/DrawingPage.xaml | 0 samples/{RenderTest => RenderDemo}/Pages/DrawingPage.xaml.cs | 0 .../RenderTest.csproj => RenderDemo/RenderDemo.csproj} | 0 samples/{RenderTest => RenderDemo}/SideBar.xaml | 0 .../ViewModels/AnimationsPageViewModel.cs | 0 .../{RenderTest => RenderDemo}/ViewModels/MainWindowViewModel.cs | 0 samples/{VirtualizationTest => VirtualizationDemo}/App.config | 0 samples/{VirtualizationTest => VirtualizationDemo}/App.xaml | 0 samples/{VirtualizationTest => VirtualizationDemo}/App.xaml.cs | 0 .../{VirtualizationTest => VirtualizationDemo}/MainWindow.xaml | 0 .../{VirtualizationTest => VirtualizationDemo}/MainWindow.xaml.cs | 0 samples/{VirtualizationTest => VirtualizationDemo}/Program.cs | 0 .../ViewModels/ItemViewModel.cs | 0 .../ViewModels/MainWindowViewModel.cs | 0 .../VirtualizationDemo.csproj} | 0 40 files changed, 0 insertions(+), 0 deletions(-) rename samples/{BindingTest => BindingDemo}/App.config (100%) rename samples/{BindingTest => BindingDemo}/App.xaml (100%) rename samples/{BindingTest => BindingDemo}/App.xaml.cs (100%) rename samples/{BindingTest/BindingTest.csproj => BindingDemo/BindingDemo.csproj} (100%) rename samples/{BindingTest => BindingDemo}/MainWindow.xaml (100%) rename samples/{BindingTest => BindingDemo}/MainWindow.xaml.cs (100%) rename samples/{BindingTest => BindingDemo}/TestItemView.xaml (100%) rename samples/{BindingTest => BindingDemo}/TestItemView.xaml.cs (100%) rename samples/{BindingTest => BindingDemo}/ViewModels/DataAnnotationsErrorViewModel.cs (100%) rename samples/{BindingTest => BindingDemo}/ViewModels/ExceptionErrorViewModel.cs (100%) rename samples/{BindingTest => BindingDemo}/ViewModels/IndeiErrorViewModel.cs (100%) rename samples/{BindingTest => BindingDemo}/ViewModels/MainWindowViewModel.cs (100%) rename samples/{BindingTest => BindingDemo}/ViewModels/NestedCommandViewModel.cs (100%) rename samples/{BindingTest => BindingDemo}/ViewModels/TestItem.cs (100%) rename samples/{RemoteTest => RemoteDemo}/Program.cs (100%) rename samples/{RemoteTest/RemoteTest.csproj => RemoteDemo/RemoteDemo.csproj} (100%) rename samples/{RenderTest => RenderDemo}/App.config (100%) rename samples/{RenderTest => RenderDemo}/App.xaml (100%) rename samples/{RenderTest => RenderDemo}/App.xaml.cs (100%) rename samples/{RenderTest => RenderDemo}/MainWindow.xaml (100%) rename samples/{RenderTest => RenderDemo}/MainWindow.xaml.cs (100%) rename samples/{RenderTest => RenderDemo}/Pages/AnimationsPage.xaml (100%) rename samples/{RenderTest => RenderDemo}/Pages/AnimationsPage.xaml.cs (100%) rename samples/{RenderTest => RenderDemo}/Pages/ClippingPage.xaml (100%) rename samples/{RenderTest => RenderDemo}/Pages/ClippingPage.xaml.cs (100%) rename samples/{RenderTest => RenderDemo}/Pages/DrawingPage.xaml (100%) rename samples/{RenderTest => RenderDemo}/Pages/DrawingPage.xaml.cs (100%) rename samples/{RenderTest/RenderTest.csproj => RenderDemo/RenderDemo.csproj} (100%) rename samples/{RenderTest => RenderDemo}/SideBar.xaml (100%) rename samples/{RenderTest => RenderDemo}/ViewModels/AnimationsPageViewModel.cs (100%) rename samples/{RenderTest => RenderDemo}/ViewModels/MainWindowViewModel.cs (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/App.config (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/App.xaml (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/App.xaml.cs (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/MainWindow.xaml (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/MainWindow.xaml.cs (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/Program.cs (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/ViewModels/ItemViewModel.cs (100%) rename samples/{VirtualizationTest => VirtualizationDemo}/ViewModels/MainWindowViewModel.cs (100%) rename samples/{VirtualizationTest/VirtualizationTest.csproj => VirtualizationDemo/VirtualizationDemo.csproj} (100%) diff --git a/samples/BindingTest/App.config b/samples/BindingDemo/App.config similarity index 100% rename from samples/BindingTest/App.config rename to samples/BindingDemo/App.config diff --git a/samples/BindingTest/App.xaml b/samples/BindingDemo/App.xaml similarity index 100% rename from samples/BindingTest/App.xaml rename to samples/BindingDemo/App.xaml diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingDemo/App.xaml.cs similarity index 100% rename from samples/BindingTest/App.xaml.cs rename to samples/BindingDemo/App.xaml.cs diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingDemo/BindingDemo.csproj similarity index 100% rename from samples/BindingTest/BindingTest.csproj rename to samples/BindingDemo/BindingDemo.csproj diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml similarity index 100% rename from samples/BindingTest/MainWindow.xaml rename to samples/BindingDemo/MainWindow.xaml diff --git a/samples/BindingTest/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs similarity index 100% rename from samples/BindingTest/MainWindow.xaml.cs rename to samples/BindingDemo/MainWindow.xaml.cs diff --git a/samples/BindingTest/TestItemView.xaml b/samples/BindingDemo/TestItemView.xaml similarity index 100% rename from samples/BindingTest/TestItemView.xaml rename to samples/BindingDemo/TestItemView.xaml diff --git a/samples/BindingTest/TestItemView.xaml.cs b/samples/BindingDemo/TestItemView.xaml.cs similarity index 100% rename from samples/BindingTest/TestItemView.xaml.cs rename to samples/BindingDemo/TestItemView.xaml.cs diff --git a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs similarity index 100% rename from samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs rename to samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs diff --git a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs similarity index 100% rename from samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs rename to samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs diff --git a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs similarity index 100% rename from samples/BindingTest/ViewModels/IndeiErrorViewModel.cs rename to samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs similarity index 100% rename from samples/BindingTest/ViewModels/MainWindowViewModel.cs rename to samples/BindingDemo/ViewModels/MainWindowViewModel.cs diff --git a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs similarity index 100% rename from samples/BindingTest/ViewModels/NestedCommandViewModel.cs rename to samples/BindingDemo/ViewModels/NestedCommandViewModel.cs diff --git a/samples/BindingTest/ViewModels/TestItem.cs b/samples/BindingDemo/ViewModels/TestItem.cs similarity index 100% rename from samples/BindingTest/ViewModels/TestItem.cs rename to samples/BindingDemo/ViewModels/TestItem.cs diff --git a/samples/RemoteTest/Program.cs b/samples/RemoteDemo/Program.cs similarity index 100% rename from samples/RemoteTest/Program.cs rename to samples/RemoteDemo/Program.cs diff --git a/samples/RemoteTest/RemoteTest.csproj b/samples/RemoteDemo/RemoteDemo.csproj similarity index 100% rename from samples/RemoteTest/RemoteTest.csproj rename to samples/RemoteDemo/RemoteDemo.csproj diff --git a/samples/RenderTest/App.config b/samples/RenderDemo/App.config similarity index 100% rename from samples/RenderTest/App.config rename to samples/RenderDemo/App.config diff --git a/samples/RenderTest/App.xaml b/samples/RenderDemo/App.xaml similarity index 100% rename from samples/RenderTest/App.xaml rename to samples/RenderDemo/App.xaml diff --git a/samples/RenderTest/App.xaml.cs b/samples/RenderDemo/App.xaml.cs similarity index 100% rename from samples/RenderTest/App.xaml.cs rename to samples/RenderDemo/App.xaml.cs diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml similarity index 100% rename from samples/RenderTest/MainWindow.xaml rename to samples/RenderDemo/MainWindow.xaml diff --git a/samples/RenderTest/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs similarity index 100% rename from samples/RenderTest/MainWindow.xaml.cs rename to samples/RenderDemo/MainWindow.xaml.cs diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml similarity index 100% rename from samples/RenderTest/Pages/AnimationsPage.xaml rename to samples/RenderDemo/Pages/AnimationsPage.xaml diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml.cs b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs similarity index 100% rename from samples/RenderTest/Pages/AnimationsPage.xaml.cs rename to samples/RenderDemo/Pages/AnimationsPage.xaml.cs diff --git a/samples/RenderTest/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml similarity index 100% rename from samples/RenderTest/Pages/ClippingPage.xaml rename to samples/RenderDemo/Pages/ClippingPage.xaml diff --git a/samples/RenderTest/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs similarity index 100% rename from samples/RenderTest/Pages/ClippingPage.xaml.cs rename to samples/RenderDemo/Pages/ClippingPage.xaml.cs diff --git a/samples/RenderTest/Pages/DrawingPage.xaml b/samples/RenderDemo/Pages/DrawingPage.xaml similarity index 100% rename from samples/RenderTest/Pages/DrawingPage.xaml rename to samples/RenderDemo/Pages/DrawingPage.xaml diff --git a/samples/RenderTest/Pages/DrawingPage.xaml.cs b/samples/RenderDemo/Pages/DrawingPage.xaml.cs similarity index 100% rename from samples/RenderTest/Pages/DrawingPage.xaml.cs rename to samples/RenderDemo/Pages/DrawingPage.xaml.cs diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderDemo/RenderDemo.csproj similarity index 100% rename from samples/RenderTest/RenderTest.csproj rename to samples/RenderDemo/RenderDemo.csproj diff --git a/samples/RenderTest/SideBar.xaml b/samples/RenderDemo/SideBar.xaml similarity index 100% rename from samples/RenderTest/SideBar.xaml rename to samples/RenderDemo/SideBar.xaml diff --git a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs similarity index 100% rename from samples/RenderTest/ViewModels/AnimationsPageViewModel.cs rename to samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs diff --git a/samples/RenderTest/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs similarity index 100% rename from samples/RenderTest/ViewModels/MainWindowViewModel.cs rename to samples/RenderDemo/ViewModels/MainWindowViewModel.cs diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationDemo/App.config similarity index 100% rename from samples/VirtualizationTest/App.config rename to samples/VirtualizationDemo/App.config diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationDemo/App.xaml similarity index 100% rename from samples/VirtualizationTest/App.xaml rename to samples/VirtualizationDemo/App.xaml diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs similarity index 100% rename from samples/VirtualizationTest/App.xaml.cs rename to samples/VirtualizationDemo/App.xaml.cs diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml similarity index 100% rename from samples/VirtualizationTest/MainWindow.xaml rename to samples/VirtualizationDemo/MainWindow.xaml diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs similarity index 100% rename from samples/VirtualizationTest/MainWindow.xaml.cs rename to samples/VirtualizationDemo/MainWindow.xaml.cs diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationDemo/Program.cs similarity index 100% rename from samples/VirtualizationTest/Program.cs rename to samples/VirtualizationDemo/Program.cs diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs similarity index 100% rename from samples/VirtualizationTest/ViewModels/ItemViewModel.cs rename to samples/VirtualizationDemo/ViewModels/ItemViewModel.cs diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs similarity index 100% rename from samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs rename to samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj similarity index 100% rename from samples/VirtualizationTest/VirtualizationTest.csproj rename to samples/VirtualizationDemo/VirtualizationDemo.csproj From 935074431c09d5e4d32d7ad3420172df55958019 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 20 Jun 2018 15:01:17 +0200 Subject: [PATCH 16/88] Normalize whitespaces in path data --- .../Media/PathMarkupParser.cs | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index e8824753b5..46a4de637d 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -82,7 +82,9 @@ namespace Avalonia.Media /// The path data. public void Parse(string pathData) { - var tokens = ParseTokens(pathData); + var normalizedPathData = NormalizeWhiteSpaces(pathData); + + var tokens = ParseTokens(normalizedPathData); CreateGeometry(tokens); } @@ -107,6 +109,42 @@ namespace Avalonia.Media _isDisposed = true; } + private static string NormalizeWhiteSpaces(string s) + { + int length = s.Length, + index = 0, + i = 0; + var source = s.ToCharArray(); + var skip = false; + + for (; i < length; i++) + { + var c = source[i]; + + if (char.IsWhiteSpace(c)) + { + if (skip) + { + continue; + } + + source[index++] = c; + skip = true; + continue; + } + + skip = false; + source[index++] = c; + } + + if (char.IsWhiteSpace(source[index - 1])) + { + index--; + } + + return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index); + } + private static string CreatesSeparatorPattern() { var stringBuilder = new StringBuilder(); @@ -123,7 +161,9 @@ namespace Avalonia.Media private static IEnumerable ParseTokens(string s) { - return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); + var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)); + + return expressions.Select(CommandToken.Parse); } private static Point MirrorControlPoint(Point controlPoint, Point center) @@ -135,7 +175,7 @@ namespace Avalonia.Media private void CreateGeometry(IEnumerable commandTokens) { - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -468,7 +508,7 @@ namespace Avalonia.Media private List Arguments { get; } public static CommandToken Parse(string s) - { + { using (var reader = new StringReader(s)) { var command = Command.None; @@ -590,7 +630,7 @@ namespace Avalonia.Media var y = ReadDouble(); return new Point(origin.X + x, origin.Y + y); - } + } private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { From 977010de9573a11953d30051393c840ba40b835c Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 20 Jun 2018 15:17:52 +0200 Subject: [PATCH 17/88] Added unit tests --- .../Media/PathMarkupParserTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index c8bea43fb0..35ec38789e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -7,6 +7,8 @@ using Xunit; namespace Avalonia.Visuals.UnitTests.Media { + using System.IO; + public class PathMarkupParserTests { [Fact] @@ -140,6 +142,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [Theory] + [InlineData(" M0 0")] [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("M0 0L10 10z")] [InlineData("M50 50 L100 100 L150 50")] @@ -173,5 +176,18 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.True(true); } } + + [Theory] + [InlineData("0 0")] + [InlineData("j")] + public void Throws_InvalidDataException_On_None_Defined_Command(string pathData) + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + Assert.Throws(() => parser.Parse(pathData)); + } + } } } \ No newline at end of file From b35e2456eb461d378e3f0210346426265b1e77e4 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 20 Jun 2018 15:39:34 +0200 Subject: [PATCH 18/88] Code style --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 46a4de637d..a322d404bf 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -83,7 +83,6 @@ namespace Avalonia.Media public void Parse(string pathData) { var normalizedPathData = NormalizeWhiteSpaces(pathData); - var tokens = ParseTokens(normalizedPathData); CreateGeometry(tokens); @@ -129,11 +128,14 @@ namespace Avalonia.Media } source[index++] = c; + skip = true; + continue; } skip = false; + source[index++] = c; } From 7b1c9bde187124a2707d31fe64655353d542b8f3 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 20 Jun 2018 21:48:48 +0800 Subject: [PATCH 19/88] Replace namespaces accordingly; Rename projects on main sln file. --- Avalonia.sln | 8 ++++---- samples/BindingDemo/App.xaml.cs | 2 +- samples/BindingDemo/MainWindow.xaml | 4 ++-- samples/BindingDemo/MainWindow.xaml.cs | 4 ++-- samples/BindingDemo/TestItemView.xaml.cs | 2 +- .../ViewModels/DataAnnotationsErrorViewModel.cs | 2 +- samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs | 2 +- samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs | 2 +- samples/BindingDemo/ViewModels/MainWindowViewModel.cs | 2 +- samples/BindingDemo/ViewModels/NestedCommandViewModel.cs | 2 +- samples/BindingDemo/ViewModels/TestItem.cs | 2 +- samples/RemoteDemo/Program.cs | 2 +- samples/RenderDemo/App.xaml | 2 +- samples/RenderDemo/App.xaml.cs | 2 +- samples/RenderDemo/MainWindow.xaml | 2 +- samples/RenderDemo/MainWindow.xaml.cs | 4 ++-- samples/RenderDemo/Pages/AnimationsPage.xaml.cs | 4 ++-- samples/RenderDemo/Pages/ClippingPage.xaml.cs | 2 +- samples/RenderDemo/Pages/DrawingPage.xaml.cs | 2 +- samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs | 2 +- samples/RenderDemo/ViewModels/MainWindowViewModel.cs | 2 +- samples/VirtualizationDemo/App.xaml.cs | 2 +- samples/VirtualizationDemo/MainWindow.xaml.cs | 4 ++-- samples/VirtualizationDemo/Program.cs | 2 +- samples/VirtualizationDemo/ViewModels/ItemViewModel.cs | 2 +- .../VirtualizationDemo/ViewModels/MainWindowViewModel.cs | 2 +- 26 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 54f6f5e7e7..39dc11f41f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -71,7 +71,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Mark EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingTest", "samples\BindingTest\BindingTest.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}" EndProject @@ -109,7 +109,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationDemo", "samples\VirtualizationDemo\VirtualizationDemo.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}" EndProject @@ -117,7 +117,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "sampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetFrameworkRuntime", "src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj", "{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderTest", "samples\RenderTest\RenderTest.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject @@ -169,7 +169,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTest", "samples\RemoteTest\RemoteTest.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteDemo", "samples\RemoteDemo\RemoteDemo.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}" EndProject diff --git a/samples/BindingDemo/App.xaml.cs b/samples/BindingDemo/App.xaml.cs index ccad1d0ba9..01c52a2a49 100644 --- a/samples/BindingDemo/App.xaml.cs +++ b/samples/BindingDemo/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; using Serilog; -namespace BindingTest +namespace BindingDemo { public class App : Application { diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index 6b80225686..a69fb75742 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -1,7 +1,7 @@ diff --git a/samples/BindingDemo/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs index c1c3c09406..eaa57e1f5f 100644 --- a/samples/BindingDemo/MainWindow.xaml.cs +++ b/samples/BindingDemo/MainWindow.xaml.cs @@ -1,9 +1,9 @@ -using BindingTest.ViewModels; +using BindingDemo.ViewModels; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace BindingTest +namespace BindingDemo { public class MainWindow : Window { diff --git a/samples/BindingDemo/TestItemView.xaml.cs b/samples/BindingDemo/TestItemView.xaml.cs index 32f367ef68..8c0b592f00 100644 --- a/samples/BindingDemo/TestItemView.xaml.cs +++ b/samples/BindingDemo/TestItemView.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace BindingTest +namespace BindingDemo { public class TestItemView : UserControl { diff --git a/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs index 634498c165..e274f9180e 100644 --- a/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class DataAnnotationsErrorViewModel { diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index e6071e0678..2ab6c26e68 100644 --- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -4,7 +4,7 @@ using ReactiveUI; using System; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class ExceptionErrorViewModel : ReactiveObject { diff --git a/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs index b4bb528abb..bb3b4d64e9 100644 --- a/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs @@ -6,7 +6,7 @@ using System; using System.ComponentModel; using System.Collections; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo { diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs index 1116810ccb..858fb5159a 100644 --- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Threading; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class MainWindowViewModel : ReactiveObject { diff --git a/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs index 886ecbed8e..0e9139ab98 100644 --- a/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs +++ b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Input; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class NestedCommandViewModel : ReactiveObject { diff --git a/samples/BindingDemo/ViewModels/TestItem.cs b/samples/BindingDemo/ViewModels/TestItem.cs index 2326a92b7d..5a9f192f58 100644 --- a/samples/BindingDemo/ViewModels/TestItem.cs +++ b/samples/BindingDemo/ViewModels/TestItem.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class TestItem : ReactiveObject { diff --git a/samples/RemoteDemo/Program.cs b/samples/RemoteDemo/Program.cs index f518e77143..0565b676fb 100644 --- a/samples/RemoteDemo/Program.cs +++ b/samples/RemoteDemo/Program.cs @@ -9,7 +9,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Threading; using ControlCatalog; -namespace RemoteTest +namespace RemoteDemo { class Program { diff --git a/samples/RenderDemo/App.xaml b/samples/RenderDemo/App.xaml index c119f54915..aee75cb139 100644 --- a/samples/RenderDemo/App.xaml +++ b/samples/RenderDemo/App.xaml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index fd2b940f6a..0f627961e6 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia; using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; -namespace RenderTest +namespace RenderDemo { public class App : Application { diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index da49054b77..df2b221423 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -1,6 +1,6 @@ diff --git a/samples/RenderDemo/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs index 76a8e81aca..f1f974f7a1 100644 --- a/samples/RenderDemo/MainWindow.xaml.cs +++ b/samples/RenderDemo/MainWindow.xaml.cs @@ -5,10 +5,10 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using RenderTest.ViewModels; +using RenderDemo.ViewModels; using ReactiveUI; -namespace RenderTest +namespace RenderDemo { public class MainWindow : Window { diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml.cs b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs index 2623721393..5b02fd9297 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml.cs +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs @@ -7,9 +7,9 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; -using RenderTest.ViewModels; +using RenderDemo.ViewModels; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class AnimationsPage : UserControl { diff --git a/samples/RenderDemo/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs index 2a79076d4c..5357181838 100644 --- a/samples/RenderDemo/Pages/ClippingPage.xaml.cs +++ b/samples/RenderDemo/Pages/ClippingPage.xaml.cs @@ -7,7 +7,7 @@ using Avalonia.Data; using Avalonia.Markup.Xaml; using Avalonia.Media; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class ClippingPage : UserControl { diff --git a/samples/RenderDemo/Pages/DrawingPage.xaml.cs b/samples/RenderDemo/Pages/DrawingPage.xaml.cs index 3bf9bd545d..3475e1aa07 100644 --- a/samples/RenderDemo/Pages/DrawingPage.xaml.cs +++ b/samples/RenderDemo/Pages/DrawingPage.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class DrawingPage : UserControl { diff --git a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index 17eee547a1..626a3e7c77 100644 --- a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -2,7 +2,7 @@ using ReactiveUI; using Avalonia.Animation; -namespace RenderTest.ViewModels +namespace RenderDemo.ViewModels { public class AnimationsPageViewModel : ReactiveObject { diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index 02a2abeb89..0cb5e1b87b 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,7 @@ using System; using ReactiveUI; -namespace RenderTest.ViewModels +namespace RenderDemo.ViewModels { public class MainWindowViewModel : ReactiveObject { diff --git a/samples/VirtualizationDemo/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs index 14ab5b3f84..b220807443 100644 --- a/samples/VirtualizationDemo/App.xaml.cs +++ b/samples/VirtualizationDemo/App.xaml.cs @@ -4,7 +4,7 @@ using Avalonia; using Avalonia.Markup.Xaml; -namespace VirtualizationTest +namespace VirtualizationDemo { public class App : Application { diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs index 952383dffb..271519b10b 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ b/samples/VirtualizationDemo/MainWindow.xaml.cs @@ -4,9 +4,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using VirtualizationTest.ViewModels; +using VirtualizationDemo.ViewModels; -namespace VirtualizationTest +namespace VirtualizationDemo { public class MainWindow : Window { diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index 097f0cfdc7..98f1f08d6c 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -7,7 +7,7 @@ using Avalonia.Controls; using Avalonia.Logging.Serilog; using Serilog; -namespace VirtualizationTest +namespace VirtualizationDemo { class Program { diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs index 75777012c1..e883cdfeb9 100644 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs @@ -4,7 +4,7 @@ using System; using ReactiveUI; -namespace VirtualizationTest.ViewModels +namespace VirtualizationDemo.ViewModels { internal class ItemViewModel : ReactiveObject { diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 8eab91e06d..eb08ef9656 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -9,7 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI; -namespace VirtualizationTest.ViewModels +namespace VirtualizationDemo.ViewModels { internal class MainWindowViewModel : ReactiveObject { From 7f8dc7766ea500a1046763e73e0640d62973660c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 12:46:29 -0500 Subject: [PATCH 20/88] Span-ified PathMarkupParser v1. --- build/System.Memory.props | 5 + src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + .../Media/PathMarkupParser.cs | 106 +++++++++++++++++- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 build/System.Memory.props diff --git a/build/System.Memory.props b/build/System.Memory.props new file mode 100644 index 0000000000..f3253f8882 --- /dev/null +++ b/build/System.Memory.props @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 0f003a4018..c34752a3ef 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -8,4 +8,5 @@ + \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index e8824753b5..7ef07d7ec8 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -82,7 +82,7 @@ namespace Avalonia.Media /// The path data. public void Parse(string pathData) { - var tokens = ParseTokens(pathData); + var tokens = ParseTokens2(pathData); CreateGeometry(tokens); } @@ -121,6 +121,17 @@ namespace Avalonia.Media return @"(?=[" + stringBuilder + "])"; } + private static IEnumerable ParseTokens2(string s) + { + var commands = new List(); + var span = s.AsSpan(); + while (!span.IsEmpty) + { + commands.Add(CommandToken.Parse(ref span)); + } + return commands; + } + private static IEnumerable ParseTokens(string s) { return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); @@ -494,6 +505,27 @@ namespace Avalonia.Media return new CommandToken(command, isRelative, arguments); } } + + public static CommandToken Parse(ref ReadOnlySpan span) + { + if (!ReadCommand(ref span, out var command, out var isRelative)) + { + throw new InvalidDataException("No path command declared."); + } + + span = span.Slice(1); + span = SkipWhitespace(span); + + var arguments = new List(); + + while (ReadArgument(ref span, out var argument)) + { + arguments.Add(argument.ToString()); + span = ReadSeparator(span); + } + + return new CommandToken(command, isRelative, arguments); + } public FillRule ReadFillRule() { @@ -592,6 +624,28 @@ namespace Avalonia.Media return new Point(origin.X + x, origin.Y + y); } + private static bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + { + span = SkipWhitespace(span); + if (span.IsEmpty) + { + command = default; + relative = false; + return false; + } + + var c = span[0]; + + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) + { + throw new InvalidDataException("Unexpected path command '" + c + "'."); + } + + relative = char.IsLower(c); + + return true; + } + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { ReadWhitespace(reader); @@ -619,6 +673,56 @@ namespace Avalonia.Media return true; } + private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) + { + if (remaining.IsEmpty) + { + argument = ReadOnlySpan.Empty; + return false; + } + + var valid = false; + int i = 0; + if (remaining[i] == '-') + { + i++; + } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + + if (i < remaining.Length && remaining[i] == '.') + { + valid = false; + i++; + } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + + if (!valid) + { + argument = ReadOnlySpan.Empty; + return false; + } + argument = remaining.Slice(0, i); + remaining = remaining.Slice(i); + return true; + } + + private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) + { + span = SkipWhitespace(span); + if (!span.IsEmpty && span[0] == ',') + { + span = span.Slice(1); + } + return SkipWhitespace(span); + } + + private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + { + int i = 0; + for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; + return span.Slice(i); + } + private static void ReadWhitespace(TextReader reader) { int i; From dc9fb565fd35b4feed137f11cacd62b7854c7f47 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 12:50:06 -0500 Subject: [PATCH 21/88] Remove old regex-based parser --- .../Media/PathMarkupParser.cs | 107 +----------------- 1 file changed, 5 insertions(+), 102 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 7ef07d7ec8..c71aed8b66 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -17,7 +17,6 @@ namespace Avalonia.Media /// public class PathMarkupParser : IDisposable { - private static readonly string s_separatorPattern; private static readonly Dictionary s_commands = new Dictionary { @@ -42,7 +41,6 @@ namespace Avalonia.Media static PathMarkupParser() { - s_separatorPattern = CreatesSeparatorPattern(); } /// @@ -82,7 +80,7 @@ namespace Avalonia.Media /// The path data. public void Parse(string pathData) { - var tokens = ParseTokens2(pathData); + var tokens = ParseTokens(pathData); CreateGeometry(tokens); } @@ -107,21 +105,7 @@ namespace Avalonia.Media _isDisposed = true; } - private static string CreatesSeparatorPattern() - { - var stringBuilder = new StringBuilder(); - - foreach (var command in s_commands.Keys) - { - stringBuilder.Append(command); - - stringBuilder.Append(char.ToLower(command)); - } - - return @"(?=[" + stringBuilder + "])"; - } - - private static IEnumerable ParseTokens2(string s) + private static IEnumerable ParseTokens(string s) { var commands = new List(); var span = s.AsSpan(); @@ -132,11 +116,6 @@ namespace Avalonia.Media return commands; } - private static IEnumerable ParseTokens(string s) - { - return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); - } - private static Point MirrorControlPoint(Point controlPoint, Point center) { var dir = controlPoint - center; @@ -146,7 +125,7 @@ namespace Avalonia.Media private void CreateGeometry(IEnumerable commandTokens) { - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -446,15 +425,13 @@ namespace Avalonia.Media private class CommandToken { - private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; - - private CommandToken(Command command, bool isRelative, IEnumerable arguments) + private CommandToken(Command command, bool isRelative, List arguments) { Command = command; IsRelative = isRelative; - Arguments = new List(arguments); + Arguments = arguments; } public Command Command { get; } @@ -477,34 +454,6 @@ namespace Avalonia.Media private int CurrentPosition { get; set; } private List Arguments { get; } - - public static CommandToken Parse(string s) - { - using (var reader = new StringReader(s)) - { - var command = Command.None; - - var isRelative = false; - - if (!ReadCommand(reader, ref command, ref isRelative)) - { - throw new InvalidDataException("No path command declared."); - } - - var commandArguments = reader.ReadToEnd(); - - var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression); - - var arguments = new List(); - - foreach (Match match in argumentMatches) - { - arguments.Add(match.Value); - } - - return new CommandToken(command, isRelative, arguments); - } - } public static CommandToken Parse(ref ReadOnlySpan span) { @@ -646,33 +595,6 @@ namespace Avalonia.Media return true; } - private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) - { - ReadWhitespace(reader); - - var i = reader.Peek(); - - if (i == -1) - { - return false; - } - - var c = (char)i; - - if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } - - command = next; - - relative = char.IsLower(c); - - reader.Read(); - - return true; - } - private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) { if (remaining.IsEmpty) @@ -722,25 +644,6 @@ namespace Avalonia.Media for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; return span.Slice(i); } - - private static void ReadWhitespace(TextReader reader) - { - int i; - - while ((i = reader.Peek()) != -1) - { - var c = (char)i; - - if (char.IsWhiteSpace(c)) - { - reader.Read(); - } - else - { - break; - } - } - } } } } \ No newline at end of file From 6c6f1b48cf62048c9b94c671020558cb1b742ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 15:08:23 -0500 Subject: [PATCH 22/88] Remove unused static constructor. --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index c71aed8b66..e17aafeda3 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -39,10 +39,6 @@ namespace Avalonia.Media private bool? _isOpen; private bool _isDisposed; - static PathMarkupParser() - { - } - /// /// Initializes a new instance of the class. /// From 82802621ead1ff9440b2bb3060697388906b560d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 15:54:48 -0500 Subject: [PATCH 23/88] Update gitignore and add System.Memory package dependency. --- .gitignore | 8 ++++++-- packages.cake | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a9a8fd36b4..583a2b8a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -176,5 +176,9 @@ nuget Avalonia.XBuild.sln project.lock.json .idea/* -**/obj-Skia/* -**/obj-Direct2D1/* + + +################## +## BenchmarkDotNet +################## +BenchmarkDotNet.Artifacts/ diff --git a/packages.cake b/packages.cake index 7e7e722c82..79147b5f99 100644 --- a/packages.cake +++ b/packages.cake @@ -253,7 +253,7 @@ public class Packages } .Deps(new string[]{null, "netcoreapp2.0"}, "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", - "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter") + "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory") .ToArray(), Files = coreLibrariesNuSpecContent .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) From 63ec705748fd13ad3fa33aad762eebd3bf315d18 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 16:37:16 -0500 Subject: [PATCH 24/88] Remove extraneous whitespace in tests. --- tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index 35ec38789e..5074d306fd 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Visuals.UnitTests.Media using (var context = new PathGeometryContext(pathGeometry)) using (var parser = new PathMarkupParser(context)) { - parser.Parse("F 1M0,0"); + parser.Parse("F 1M0,0"); Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } From e2f54537982df5136da906478807a71e5e4fe96d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 16:53:00 -0500 Subject: [PATCH 25/88] Remove LINQ usage. --- .../Media/PathMarkupParser.cs | 51 ++++--------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index e17aafeda3..41cd95d319 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,9 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using Avalonia.Platform; namespace Avalonia.Media @@ -70,17 +67,6 @@ namespace Avalonia.Media Close } - /// - /// Parses the specified path data and writes the result to the geometryContext of this instance. - /// - /// The path data. - public void Parse(string pathData) - { - var tokens = ParseTokens(pathData); - - CreateGeometry(tokens); - } - void IDisposable.Dispose() { Dispose(true); @@ -101,17 +87,6 @@ namespace Avalonia.Media _isDisposed = true; } - private static IEnumerable ParseTokens(string s) - { - var commands = new List(); - var span = s.AsSpan(); - while (!span.IsEmpty) - { - commands.Add(CommandToken.Parse(ref span)); - } - return commands; - } - private static Point MirrorControlPoint(Point controlPoint, Point center) { var dir = controlPoint - center; @@ -119,15 +94,21 @@ namespace Avalonia.Media return center + -dir; } - private void CreateGeometry(IEnumerable commandTokens) + /// + /// Parses the specified path data and writes the result to the geometryContext of this instance. + /// + /// The path data. + public void Parse(string pathData) { + var span = pathData.AsSpan(); _currentPoint = new Point(); - foreach (var commandToken in commandTokens) + while(!span.IsEmpty) { + var commandToken = CommandToken.Parse(ref span); try { - while (true) + do { switch (commandToken.Command) { @@ -169,14 +150,7 @@ namespace Avalonia.Media default: throw new NotSupportedException("Unsupported command"); } - - if (commandToken.HasImplicitCommands) - { - continue; - } - - break; - } + } while (commandToken.HasImplicitCommands); } catch (InvalidDataException) { @@ -235,11 +209,6 @@ namespace Avalonia.Media CreateFigure(); - if (!commandToken.HasImplicitCommands) - { - return; - } - while (commandToken.HasImplicitCommands) { AddLine(commandToken); From c31edafb553c70ea64087248fa84addac77fb1b8 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 17:05:33 -0500 Subject: [PATCH 26/88] Clear up code logic. --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 41cd95d319..b0c7ed7e95 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -213,14 +213,11 @@ namespace Avalonia.Media { AddLine(commandToken); - if (commandToken.IsRelative) + if (!commandToken.IsRelative) { - continue; + _currentPoint = currentPoint; + CreateFigure(); } - - _currentPoint = currentPoint; - - CreateFigure(); } } From 11d3e7f7e3cd26f8c92fb09bc4449b40dcac935f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 17:10:56 -0500 Subject: [PATCH 27/88] Make _isOpen non-nullable since the only two values that were used were true and null. --- .../Media/PathMarkupParser.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index b0c7ed7e95..8f1d8ebc71 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -33,7 +33,7 @@ namespace Avalonia.Media private IGeometryContext _geometryContext; private Point _currentPoint; private Point? _previousControlPoint; - private bool? _isOpen; + private bool _isOpen; private bool _isDisposed; /// @@ -162,7 +162,7 @@ namespace Avalonia.Media } } - if (_isOpen != null) + if (_isOpen) { _geometryContext.EndFigure(false); } @@ -170,7 +170,7 @@ namespace Avalonia.Media private void CreateFigure() { - if (_isOpen != null) + if (_isOpen) { _geometryContext.EndFigure(false); } @@ -189,14 +189,14 @@ namespace Avalonia.Media private void CloseFigure() { - if (_isOpen == true) + if (_isOpen) { _geometryContext.EndFigure(true); } _previousControlPoint = null; - _isOpen = null; + _isOpen = false; } private void AddMove(CommandToken commandToken) @@ -227,7 +227,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -241,7 +241,7 @@ namespace Avalonia.Media ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) : _currentPoint.WithX(commandToken.ReadDouble()); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -255,7 +255,7 @@ namespace Avalonia.Media ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) : _currentPoint.WithY(commandToken.ReadDouble()); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -279,7 +279,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -301,7 +301,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -326,7 +326,7 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -349,7 +349,7 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -373,7 +373,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } From 88bfdf87ea31d9e49ab30ed29c8f8317f8b7c030 Mon Sep 17 00:00:00 2001 From: Nelson Carrillo Date: Wed, 20 Jun 2018 18:52:25 -0400 Subject: [PATCH 28/88] Skia backend cleanup. --- Avalonia.sln | 44 + build.cake | 1 + build/SkiaSharp.props | 4 +- samples/ControlCatalog.NetCore/Program.cs | 4 +- .../Rendering/DeferredRenderer.cs | 3 + .../Rendering/ImmediateRenderer.cs | 1 + .../Rendering/RenderLayers.cs | 16 +- src/Avalonia.Visuals/Vector.cs | 52 +- src/Skia/Avalonia.Skia/BitmapImpl.cs | 142 ---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 783 ++++++++++++------ src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 22 +- .../Avalonia.Skia/FramebufferRenderTarget.cs | 211 +++-- src/Skia/Avalonia.Skia/GeometryImpl.cs | 170 +++- .../Helpers/ImageSavingHelper.cs | 47 ++ .../Helpers/PixelFormatHelper.cs | 35 + src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs | 23 + src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 92 ++ .../Avalonia.Skia/PlatformRenderInterface.cs | 80 +- .../SkiaApplicationExtensions.cs | 28 + src/Skia/Avalonia.Skia/SkiaPlatform.cs | 46 +- src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 6 +- src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 124 +-- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 169 ++++ .../Avalonia.Skia/TransformedGeometryImpl.cs | 62 +- src/Skia/Avalonia.Skia/TypefaceCache.cs | 9 +- src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs | 151 ++++ src/Skia/Avalonia.Skia/readme.md | 34 +- .../Interop/UnmanagedMethods.cs | 28 + src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- .../Avalonia.RenderTests/Media/BitmapTests.cs | 2 +- .../Media/ImageBrushTests.cs | 12 +- .../Media/LinearGradientBrushTests.cs | 12 +- .../Media/RadialGradientBrushTests.cs | 6 +- .../Media/VisualBrushTests.cs | 6 +- .../Avalonia.Skia.UnitTests.csproj | 22 + tests/Avalonia.Skia.UnitTests/HitTesting.cs | 79 ++ .../Properties/AssemblyInfo.cs | 8 + ...Brush_RedBlue_Horizontal_Fill.expected.png | Bin 652 -> 642 bytes ...ntBrush_RedBlue_Vertical_Fill.expected.png | Bin 1959 -> 1998 bytes .../RadialGradientBrush_RedBlue.expected.png | Bin 0 -> 9792 bytes .../VisualBrush_InTree_Visual.expected.png | Bin 0 -> 5905 bytes .../Path/Path_With_PenLineCap.expected.png | Bin 0 -> 1155 bytes 42 files changed, 1831 insertions(+), 705 deletions(-) delete mode 100644 src/Skia/Avalonia.Skia/BitmapImpl.cs create mode 100644 src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs create mode 100644 src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs create mode 100644 src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs create mode 100644 src/Skia/Avalonia.Skia/ImmutableBitmap.cs create mode 100644 src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs create mode 100644 src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs create mode 100644 src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj create mode 100644 tests/Avalonia.Skia.UnitTests/HitTesting.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png create mode 100644 tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png create mode 100644 tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png diff --git a/Avalonia.sln b/Avalonia.sln index 39dc11f41f..39396f3ab8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props build\NetCore.props = build\NetCore.props + build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props @@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2468,6 +2471,46 @@ Global {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2520,6 +2563,7 @@ Global {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2} {4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build.cake b/build.cake index bf3ae41b58..561a33186a 100644 --- a/build.cake +++ b/build.cake @@ -207,6 +207,7 @@ Task("Run-Unit-Tests") RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); if (data.Parameters.IsRunningOnWindows) { RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 04e8a3ad4f..35c979a95e 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index b45a93455e..1f53dedc14 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using Avalonia; +using Avalonia.Skia; namespace ControlCatalog.NetCore { @@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); static void ConsoleSilencer() { diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index fd6b149837..a14923b410 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -117,6 +117,9 @@ namespace Avalonia.Rendering var scene = Interlocked.Exchange(ref _scene, null); scene?.Dispose(); Stop(); + + Layers.Clear(); + RenderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index e830d5c313..2118b66de2 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -133,6 +133,7 @@ namespace Avalonia.Rendering /// public void Dispose() { + _renderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index bafd644603..6a45ecd912 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -11,11 +11,7 @@ namespace Avalonia.Rendering { private List _inner = new List(); private Dictionary _index = new Dictionary(); - - public RenderLayers() - { - } - + public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -51,6 +47,16 @@ namespace Avalonia.Rendering } } + public void Clear() + { + foreach (var layer in _index.Values) + { + layer.Bitmap.Dispose(); + } + + _index.Clear(); + } + public bool TryGetValue(IVisual layerRoot, out RenderLayer value) { return _index.TryGetValue(layerRoot, out value); diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index acde49a84a..c2db17cd86 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; -using System.Xml.Linq; +using JetBrains.Annotations; namespace Avalonia { @@ -122,6 +122,56 @@ namespace Avalonia return new Vector(a._x - b._x, a._y - b._y); } + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(Vector other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && _y == other._y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(Vector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is Vector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(Vector left, Vector right) + { + return left.Equals(right); + } + + public static bool operator !=(Vector left, Vector right) + { + return !left.Equals(right); + } + /// /// Returns the string representation of the point. /// diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs deleted file mode 100644 index ccc5a37105..0000000000 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using Avalonia.Platform; -using Avalonia.Rendering; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl - { - private Vector _dpi; - - public SKBitmap Bitmap { get; private set; } - - public BitmapImpl(SKBitmap bm) - { - Bitmap = bm; - PixelHeight = bm.Height; - PixelWidth = bm.Width; - _dpi = new Vector(96, 96); - } - - static void ReleaseProc(IntPtr address, object ctx) - { - ((IUnmanagedBlob) ctx).Dispose(); - } - - private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; - - public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) - { - PixelHeight = height; - PixelWidth = width; - _dpi = dpi; - var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; - var runtimePlatform = AvaloniaLocator.Current?.GetService(); - var runtime = runtimePlatform?.GetRuntimeInfo(); - if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) - colorType = SKColorType.Bgra8888; - - if (runtimePlatform != null) - { - Bitmap = new SKBitmap(); - var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); - var plat = AvaloniaLocator.Current.GetService(); - var blob = plat.AllocBlob(nfo.BytesSize); - Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); - - } - else - Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); - Bitmap.Erase(SKColor.Empty); - } - - public void Dispose() - { - Bitmap.Dispose(); - } - - public int PixelWidth { get; private set; } - public int PixelHeight { get; private set; } - - class BitmapDrawingContext : DrawingContextImpl - { - private readonly SKSurface _surface; - - public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : this(CreateSurface(bitmap), dpi, visualBrushRenderer) - { - CanUseLcdRendering = false; - } - - private static SKSurface CreateSurface(SKBitmap bitmap) - { - IntPtr length; - var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes); - if (rv == null) - throw new Exception("Unable to create Skia surface"); - return rv; - } - - public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : base(surface.Canvas, dpi, visualBrushRenderer) - { - _surface = surface; - } - - public override void Dispose() - { - base.Dispose(); - _surface.Dispose(); - } - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer); - } - - public void Save(Stream stream) - { - IntPtr length; - using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes)) - using (var data = image.Encode()) - { - data.SaveTo(stream); - } - } - - public void Save(string fileName) - { - using (var stream = File.Create(fileName)) - Save(stream); - } - - class BitmapFramebuffer : ILockedFramebuffer - { - private SKBitmap _bmp; - - public BitmapFramebuffer(SKBitmap bmp) - { - _bmp = bmp; - _bmp.LockPixels(); - } - - public void Dispose() - { - _bmp.UnlockPixels(); - _bmp = null; - } - - public IntPtr Address => _bmp.GetPixels(); - public int Width => _bmp.Width; - public int Height => _bmp.Height; - public int RowBytes => _bmp.RowBytes; - public Vector Dpi { get; } = new Vector(96, 96); - public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); - } - - public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap); - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 22e5652cfb..b7ce6eedc4 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1,57 +1,114 @@ -using Avalonia.Media; -using SkiaSharp; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; +using SkiaSharp; namespace Avalonia.Skia { - internal class DrawingContextImpl : IDrawingContextImpl + /// + /// Skia based drawing context. + /// + public class DrawingContextImpl : IDrawingContextImpl { + private readonly IDisposable[] _disposables; private readonly Vector _dpi; + private readonly Stack _maskStack = new Stack(); + private readonly Stack _opacityStack = new Stack(); private readonly Matrix? _postTransform; - private readonly IDisposable[] _disposables; private readonly IVisualBrushRenderer _visualBrushRenderer; - private Stack maskStack = new Stack(); - protected bool CanUseLcdRendering = true; - public SKCanvas Canvas { get; private set; } - - public DrawingContextImpl( - SKCanvas canvas, - Vector dpi, - IVisualBrushRenderer visualBrushRenderer, - params IDisposable[] disposables) + private double _currentOpacity = 1.0f; + private readonly bool _canTextUseLcdRendering; + private Matrix _currentTransform; + + /// + /// Context create info. + /// + public struct CreateInfo + { + /// + /// Canvas to draw to. + /// + public SKCanvas Canvas; + + /// + /// Dpi of drawings. + /// + public Vector Dpi; + + /// + /// Visual brush renderer. + /// + public IVisualBrushRenderer VisualBrushRenderer; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + + /// + /// Create new drawing context. + /// + /// Create info. + /// Array of elements to dispose after drawing has finished. + public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables) { - _dpi = dpi; - if (dpi.X != 96 || dpi.Y != 96) - _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96); - _visualBrushRenderer = visualBrushRenderer; + _dpi = createInfo.Dpi; + _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; - Canvas = canvas; + _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; + + Canvas = createInfo.Canvas; + + if (Canvas == null) + { + throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + } + + if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) + { + _postTransform = + Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + } + Transform = Matrix.Identity; } + + /// + /// Skia canvas. + /// + public SKCanvas Canvas { get; } + /// public void Clear(Color color) { Canvas.Clear(color.ToSKColor()); } + /// public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { - var impl = (BitmapImpl)source.Item; + var drawableImage = (IDrawableBitmapImpl) source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - using (var paint = new SKPaint() - { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) + + using (var paint = + new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))}) { - Canvas.DrawBitmap(impl.Bitmap, s, d, paint); + drawableImage.Draw(this, s, d, paint); } } + /// public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); @@ -59,17 +116,19 @@ namespace Avalonia.Skia PopOpacityMask(); } + /// public void DrawLine(Pen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } } + /// public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = (GeometryImpl)geometry; + var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) @@ -79,6 +138,7 @@ namespace Avalonia.Skia { Canvas.DrawPath(impl.EffectivePath, fill.Paint); } + if (stroke.Paint != null) { Canvas.DrawPath(impl.EffectivePath, stroke.Paint); @@ -86,227 +146,424 @@ namespace Avalonia.Skia } } - private struct PaintState : IDisposable + /// + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) { - private readonly SKColor _color; - private readonly SKShader _shader; - private readonly SKPaint _paint; + using (var paint = CreatePaint(pen, rect.Size)) + { + var rc = rect.ToSKRect(); - public PaintState(SKPaint paint, SKColor color, SKShader shader) + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } + } + } + + /// + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + using (var paint = CreatePaint(brush, rect.Size)) { - _paint = paint; - _color = color; - _shader = shader; + var rc = rect.ToSKRect(); + + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } } + } - public void Dispose() + /// + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + using (var paint = CreatePaint(foreground, text.Size)) { - _paint.Color = _color; - _paint.Shader = _shader; + var textImpl = (FormattedTextImpl) text; + textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); } } - internal struct PaintWrapper : IDisposable + /// + public IRenderTargetBitmapImpl CreateLayer(Size size) { - //We are saving memory allocations there - //TODO: add more disposable fields if needed - public readonly SKPaint Paint; + var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + var pixelSize = size * normalizedDpi; - private IDisposable _disposable1; - private IDisposable _disposable2; + return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi); + } - public IDisposable ApplyTo(SKPaint paint) - { - var state = new PaintState(paint, paint.Color, paint.Shader); + /// + public void PushClip(Rect clip) + { + Canvas.Save(); + Canvas.ClipRect(clip.ToSKRect()); + } - paint.Color = Paint.Color; - paint.Shader = Paint.Shader; + /// + public void PopClip() + { + Canvas.Restore(); + } - return state; - } + /// + public void PushOpacity(double opacity) + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } - public void AddDisposable(IDisposable disposable) - { - if (_disposable1 == null) - _disposable1 = disposable; - else if (_disposable2 == null) - _disposable2 = disposable; - else - throw new InvalidOperationException(); - } + /// + public void PopOpacity() + { + _currentOpacity = _opacityStack.Pop(); + } - public PaintWrapper(SKPaint paint) + /// + public virtual void Dispose() + { + if (_disposables == null) { - Paint = paint; - _disposable1 = null; - _disposable2 = null; + return; } - public void Dispose() + foreach (var disposable in _disposables) { - Paint?.Dispose(); - _disposable1?.Dispose(); - _disposable2?.Dispose(); + disposable?.Dispose(); } } - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + /// + public void PushGeometryClip(IGeometryImpl clip) { - SKPaint paint = new SKPaint(); - var rv = new PaintWrapper(paint); - paint.IsStroke = false; + Canvas.Save(); + Canvas.ClipPath(((GeometryImpl)clip).EffectivePath); + } - - double opacity = brush.Opacity * _currentOpacity; - paint.IsAntialias = true; + /// + public void PopGeometryClip() + { + Canvas.Restore(); + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + // TODO: This should be disposed + var paint = new SKPaint(); + + Canvas.SaveLayer(paint); + _maskStack.Push(CreatePaint(mask, bounds.Size)); + } - var solid = brush as ISolidColorBrush; - if (solid != null) + /// + public void PopOpacityMask() + { + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { - paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); - return rv; + Canvas.SaveLayer(paint); + using (var paintWrapper = _maskStack.Pop()) + { + Canvas.DrawPaint(paintWrapper.Paint); + } + Canvas.Restore(); } - paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity))); - var gradient = brush as IGradientBrush; - if (gradient != null) + Canvas.Restore(); + } + + /// + public Matrix Transform + { + get { return _currentTransform; } + set { - var tileMode = gradient.SpreadMethod.ToSKShaderTileMode(); - var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); - var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray(); + if (_currentTransform == value) + return; + + _currentTransform = value; + + var transform = value; - var linearGradient = brush as ILinearGradientBrush; - if (linearGradient != null) + if (_postTransform.HasValue) + { + transform *= _postTransform.Value; + } + + Canvas.SetMatrix(transform.ToSKMatrix()); + } + } + + /// + /// Configure paint wrapper for using gradient brush. + /// + /// Paint wrapper. + /// Target size. + /// Gradient brush. + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) + { + var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); + var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); + var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); + + switch (gradientBrush) + { + case ILinearGradientBrush linearGradient: { var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + break; } - else + case IRadialGradientBrush radialGradient: { - var radialGradient = brush as IRadialGradientBrush; - if (radialGradient != null) - { - var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); - var radius = (float)radialGradient.Radius; + var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetSize.Width); - // TODO: There is no SetAlpha in SkiaSharp - //paint.setAlpha(128); - - // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + // TODO: There is no SetAlpha in SkiaSharp + //paint.setAlpha(128); + // would be nice to cache these shaders possibly? + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; } + + break; } + } + } + + /// + /// Configure paint wrapper for using tile brush. + /// + /// Paint wrapper. + /// Target size. + /// Tile brush to use. + /// Tile brush image. + private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) + { + var calc = new TileBrushCalculator(tileBrush, + new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); + + var intermediate = CreateRenderTarget( + (int)calc.IntermediateSize.Width, + (int)calc.IntermediateSize.Height, _dpi); + + paintWrapper.AddDisposable(intermediate); + + using (var context = intermediate.CreateDrawingContext(null)) + { + var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); - return rv; + context.Clear(Colors.Transparent); + context.PushClip(calc.IntermediateClip); + context.Transform = calc.IntermediateTransform; + context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); + context.PopClip(); } - var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; - var tileBrushImage = default(BitmapImpl); + var tileTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.MakeIdentity(); - if (visualBrush != null) + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + + var image = intermediate.SnapshotImage(); + paintWrapper.AddDisposable(image); + + using (var shader = image.ToShader(tileX, tileY, tileTransform)) { - if (_visualBrushRenderer != null) - { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + paintWrapper.Paint.Shader = shader; + } + } - if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) - { - var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + /// + /// Configure paint wrapper to use visual brush. + /// + /// Paint wrapper. + /// Visual brush. + /// Visual brush renderer. + /// Tile brush image. + private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage) + { + if (_visualBrushRenderer == null) + { + throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + } - using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer)) - { - ctx.Clear(Colors.Transparent); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); - } + var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); - tileBrushImage = intermediate; - rv.AddDisposable(tileBrushImage); - } - } - else + if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) + { + var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + + using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + ctx.Clear(Colors.Transparent); + + visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); } + + tileBrushImage = intermediate; + paintWrapper.AddDisposable(tileBrushImage); } - else + } + + /// + /// Creates paint wrapper for given brush. + /// + /// Source brush. + /// Target size. + /// Paint wrapper for given brush. + internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + { + var paint = new SKPaint + { + IsStroke = false, + IsAntialias = true + }; + + var paintWrapper = new PaintWrapper(paint); + + double opacity = brush.Opacity * _currentOpacity; + + if (brush is ISolidColorBrush solid) { - tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item); + paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); + + return paintWrapper; } - if (tileBrush != null && tileBrushImage != null) + paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); + + if (brush is IGradientBrush gradient) { - var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); - var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi); - rv.AddDisposable(bitmap); - using (var context = bitmap.CreateDrawingContext(null)) - { - var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); + ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); - context.Clear(Colors.Transparent); - context.PushClip(calc.IntermediateClip); - context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); - context.PopClip(); - } + return paintWrapper; + } + + var tileBrush = brush as ITileBrush; + var visualBrush = brush as IVisualBrush; + var tileBrushImage = default(IDrawableBitmapImpl); - SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y); - SKShaderTileMode tileX = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - - SKShaderTileMode tileY = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation)) - paint.Shader = shader; + if (visualBrush != null) + { + ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + } + else + { + tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item; } - return rv; + if (tileBrush != null && tileBrushImage != null) + { + ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); + } + + return paintWrapper; } + /// + /// Creates paint wrapper for given pen. + /// + /// Source pen. + /// Target size. + /// private PaintWrapper CreatePaint(Pen pen, Size targetSize) { var rv = CreatePaint(pen.Brush, targetSize); var paint = rv.Paint; paint.IsStroke = true; - paint.StrokeWidth = (float)pen.Thickness; + paint.StrokeWidth = (float) pen.Thickness; - if (pen.StartLineCap == PenLineCap.Round) - paint.StrokeCap = SKStrokeCap.Round; - else if (pen.StartLineCap == PenLineCap.Square) - paint.StrokeCap = SKStrokeCap.Square; - else - paint.StrokeCap = SKStrokeCap.Butt; + // Need to modify dashes due to Skia modifying their lengths + // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots + // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. + float dashLengthModifier; + float gapLengthModifier; - if (pen.LineJoin == PenLineJoin.Miter) - paint.StrokeJoin = SKStrokeJoin.Miter; - else if (pen.LineJoin == PenLineJoin.Round) - paint.StrokeJoin = SKStrokeJoin.Round; - else - paint.StrokeJoin = SKStrokeJoin.Bevel; + switch (pen.StartLineCap) + { + case PenLineCap.Round: + paint.StrokeCap = SKStrokeCap.Round; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + case PenLineCap.Square: + paint.StrokeCap = SKStrokeCap.Square; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + default: + paint.StrokeCap = SKStrokeCap.Butt; + dashLengthModifier = 0.0f; + gapLengthModifier = 0.0f; + break; + } + + switch (pen.LineJoin) + { + case PenLineJoin.Miter: + paint.StrokeJoin = SKStrokeJoin.Miter; + break; + case PenLineJoin.Round: + paint.StrokeJoin = SKStrokeJoin.Round; + break; + default: + paint.StrokeJoin = SKStrokeJoin.Bevel; + break; + } - paint.StrokeMiter = (float)pen.MiterLimit; + paint.StrokeMiter = (float) pen.MiterLimit; if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { - var pe = SKPathEffect.CreateDash( - pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(), - (float)pen.DashStyle.Offset); + var srcDashes = pen.DashStyle.Dashes; + var dashesArray = new float[srcDashes.Count]; + + for (var i = 0; i < srcDashes.Count; ++i) + { + var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier; + + // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale + dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier; + } + + var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset); + paint.PathEffect = pe; rv.AddDisposable(pe); } @@ -314,128 +571,118 @@ namespace Avalonia.Skia return rv; } - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + /// + /// Create new render target compatible with this drawing context. + /// + /// Width. + /// Height. + /// Drawing dpi. + /// Pixel format. + /// + private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null) { - using (var paint = CreatePaint(pen, rect.Size)) + var createInfo = new SurfaceRenderTarget.CreateInfo { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } - } + Width = width, + Height = height, + Dpi = dpi, + Format = format, + DisableTextLcdRendering = !_canTextUseLcdRendering + }; + + return new SurfaceRenderTarget(createInfo); } - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + /// + /// Skia cached paint state. + /// + private struct PaintState : IDisposable { - using (var paint = CreatePaint(brush, rect.Size)) + private readonly SKColor _color; + private readonly SKShader _shader; + private readonly SKPaint _paint; + + public PaintState(SKPaint paint, SKColor color, SKShader shader) { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } + _paint = paint; + _color = color; + _shader = shader; } - } - public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) - { - using (var paint = CreatePaint(foreground, text.Size)) + /// + public void Dispose() { - var textImpl = (FormattedTextImpl)text; - textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering); + _paint.Color = _color; + _paint.Shader = _shader; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) - { - var pixelSize = size * (_dpi / 96); - return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi); - } - - public void PushClip(Rect clip) - { - Canvas.Save(); - Canvas.ClipRect(clip.ToSKRect()); - } - - public void PopClip() - { - Canvas.Restore(); - } - - private double _currentOpacity = 1.0f; - private readonly Stack _opacityStack = new Stack(); - - public void PushOpacity(double opacity) + /// + /// Skia paint wrapper. + /// + internal struct PaintWrapper : IDisposable { - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; - } + //We are saving memory allocations there + public readonly SKPaint Paint; - public void PopOpacity() - { - _currentOpacity = _opacityStack.Pop(); - } + private IDisposable _disposable1; + private IDisposable _disposable2; + private IDisposable _disposable3; - public virtual void Dispose() - { - if(_disposables!=null) - foreach (var disposable in _disposables) - disposable?.Dispose(); - } + public PaintWrapper(SKPaint paint) + { + Paint = paint; - public void PushGeometryClip(IGeometryImpl clip) - { - Canvas.Save(); - Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath); - } + _disposable1 = null; + _disposable2 = null; + _disposable3 = null; + } - public void PopGeometryClip() - { - Canvas.Restore(); - } + public IDisposable ApplyTo(SKPaint paint) + { + var state = new PaintState(paint, paint.Color, paint.Shader); - public void PushOpacityMask(IBrush mask, Rect bounds) - { - Canvas.SaveLayer(new SKPaint()); - maskStack.Push(CreatePaint(mask, bounds.Size)); - } + paint.Color = Paint.Color; + paint.Shader = Paint.Shader; - public void PopOpacityMask() - { - Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn }); - using (var paintWrapper = maskStack.Pop()) - { - Canvas.DrawPaint(paintWrapper.Paint); + return state; } - Canvas.Restore(); - Canvas.Restore(); - } - private Matrix _currentTransform; - - public Matrix Transform - { - get { return _currentTransform; } - set + /// + /// Add new disposable to a wrapper. + /// + /// Disposable to add. + public void AddDisposable(IDisposable disposable) { - if (_currentTransform == value) - return; + if (_disposable1 == null) + { + _disposable1 = disposable; + } + else if (_disposable2 == null) + { + _disposable2 = disposable; + } + else if (_disposable3 == null) + { + _disposable3 = disposable; + } + else + { + Debug.Assert(false); - _currentTransform = value; - var transform = value; - if (_postTransform.HasValue) - transform *= _postTransform.Value; - Canvas.SetMatrix(transform.ToSKMatrix()); + // ReSharper disable once HeuristicUnreachableCode + throw new InvalidOperationException( + "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables."); + } + } + + /// + public void Dispose() + { + Paint?.Dispose(); + _disposable1?.Dispose(); + _disposable2?.Dispose(); + _disposable3?.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 9c18d52cea..d835c83aa6 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -10,6 +10,9 @@ using System.Linq; namespace Avalonia.Skia { + /// + /// Skia formatted text implementation. + /// public class FormattedTextImpl : IFormattedTextImpl { public FormattedTextImpl( @@ -21,7 +24,7 @@ namespace Avalonia.Skia IReadOnlyList spans) { Text = text ?? string.Empty; - + // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); @@ -352,7 +355,7 @@ namespace Avalonia.Skia { float measuredWidth; string subText = textInput.Substring(textIndex, stop - textIndex); - lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2; + lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth); } //Check for white space or line breakers before the lengthBreak @@ -451,7 +454,6 @@ namespace Avalonia.Skia private void BuildRects() { // Build character rects - var fm = _paint.FontMetrics; SKTextAlign align = _paint.TextAlign; for (int li = 0; li < _skiaLines.Count; li++) @@ -559,18 +561,16 @@ namespace Avalonia.Skia string subString; - float widthConstraint = (_constraint.Width != double.PositiveInfinity) - ? (float)_constraint.Width - : -1; - - for (int c = 0; curOff < length; c++) + float widthConstraint = double.IsPositiveInfinity(_constraint.Width) + ? -1 + : (float)_constraint.Width; + + while(curOff < length) { float lineWidth = -1; int measured; int trailingnumber = 0; - - subString = Text.Substring(curOff); - + float constraint = -1; if (_wrapping == TextWrapping.Wrap) diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1956f02d1b..99dbbefd4d 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,82 +1,199 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Skia.Helpers; using SkiaSharp; namespace Avalonia.Skia { + /// + /// Skia render target that renders to a framebuffer surface. No gpu acceleration available. + /// public class FramebufferRenderTarget : IRenderTarget { - private readonly IFramebufferPlatformSurface _surface; + private readonly IFramebufferPlatformSurface _platformSurface; + private SKImageInfo _currentImageInfo; + private IntPtr _currentFramebufferAddress; + private SKSurface _framebufferSurface; + private PixelFormatConversionShim _conversionShim; + private IDisposable _preFramebufferCopyHandler; - public FramebufferRenderTarget(IFramebufferPlatformSurface surface) + /// + /// Create new framebuffer render target using a target surface. + /// + /// Target surface. + public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { - _surface = surface; + _platformSurface = platformSurface; } + /// public void Dispose() { - //Nothing to do here, since we don't own framebuffer + FreeSurface(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var framebuffer = _platformSurface.Lock(); + var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height, + framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); + + CreateSurface(framebufferImageInfo, framebuffer); + + var canvas = _framebufferSurface.Canvas; + + canvas.RestoreToCount(-1); + canvas.Save(); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = framebuffer.Dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + + return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer); + } + + /// + /// Check if two images info are compatible. + /// + /// Current. + /// Desired. + /// True, if images are compatible. + private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo) + { + return currentImageInfo.Width == desiredImageInfo.Width && + currentImageInfo.Height == desiredImageInfo.Height && + currentImageInfo.ColorType == desiredImageInfo.ColorType; + } + + /// + /// Create Skia surface backed by given framebuffer. + /// + /// Desired image info. + /// Backing framebuffer. + private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) + { + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) + { + return; + } + + FreeSurface(); + + _currentFramebufferAddress = framebuffer.Address; + + var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes); + + // If surface cannot be created - try to create a compatibilty shim first + if (surface == null) + { + _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); + _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; + + surface = _conversionShim.Surface; + } + + _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " + + framebuffer.Format + + " or pixel format translator"); + _currentImageInfo = desiredImageInfo; + } + + /// + /// Free Skia surface. + /// + private void FreeSurface() + { + _conversionShim?.Dispose(); + _conversionShim = null; + _preFramebufferCopyHandler = null; + + if (_conversionShim != null) + { + _framebufferSurface?.Dispose(); + } + + _framebufferSurface = null; + _currentFramebufferAddress = IntPtr.Zero; } - class PixelFormatShim : IDisposable + /// + /// Converts non-compatible pixel formats using bitmap copies. + /// + private class PixelFormatConversionShim : IDisposable { - private readonly SKImageInfo _nfo; - private readonly IntPtr _fb; - private readonly int _rowBytes; - private SKBitmap _bitmap; + private readonly SKBitmap _bitmap; + private readonly SKImageInfo _destinationInfo; + private readonly IntPtr _framebufferAddress; - public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes) + public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) { - _nfo = nfo; - _fb = fb; - _rowBytes = rowBytes; + _destinationInfo = destinationInfo; + _framebufferAddress = framebufferAddress; + + // Create bitmap using default platform settings + _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); + + if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) + { + _bitmap.Dispose(); + + throw new Exception( + $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); + } + + Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); - - _bitmap = new SKBitmap(nfo.Width, nfo.Height); - if (!_bitmap.CanCopyTo(nfo.ColorType)) + if (Surface == null) { _bitmap.Dispose(); + throw new Exception( - $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}"); + $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); } + + SurfaceCopyHandler = Disposable.Create(CopySurface); } - public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + /// + /// Skia surface. + /// + public SKSurface Surface { get; } + /// + /// Handler to start conversion via surface copy. + /// + public IDisposable SurfaceCopyHandler { get; } + + /// public void Dispose() { - using (var tmp = _bitmap.Copy(_nfo.ColorType)) - tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes); + Surface.Dispose(); _bitmap.Dispose(); } - - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - var fb = _surface.Lock(); - PixelFormatShim shim = null; - SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(), - SKAlphaType.Premul); - var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ?? - (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes)) - .CreateSurface(); - if (surface == null) - throw new Exception("Unable to create a surface for pixel format " + fb.Format + - " or pixel format translator"); - var canvas = surface.Canvas; - - - canvas.RestoreToCount(0); - canvas.Save(); - canvas.ResetMatrix(); - return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb); + /// + /// Convert and copy surface to a framebuffer. + /// + private void CopySurface() + { + using (var snapshot = Surface.Snapshot()) + { + snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); + } + } } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index fb134b728c..af4cdb8056 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; @@ -5,14 +8,169 @@ using SkiaSharp; namespace Avalonia.Skia { - abstract class GeometryImpl : IGeometryImpl + /// + /// A Skia implementation of . + /// + public abstract class GeometryImpl : IGeometryImpl { + private PathCache _pathCache; + + /// public abstract Rect Bounds { get; } public abstract SKPath EffectivePath { get; } - public abstract bool FillContains(Point point); - public abstract Rect GetRenderBounds(Pen pen); - public abstract IGeometryImpl Intersect(IGeometryImpl geometry); - public abstract bool StrokeContains(Pen pen, Point point); - public abstract ITransformedGeometryImpl WithTransform(Matrix transform); + + /// + public bool FillContains(Point point) + { + return PathContainsCore(EffectivePath, point); + } + + /// + public bool StrokeContains(Pen pen, Point point) + { + // Skia requires to compute stroke path to check for point containment. + // Due to that we are caching using stroke width. + // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic. + + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return PathContainsCore(_pathCache.CachedStrokePath, point); + } + + /// + /// Update path cache for given stroke width. + /// + /// Stroke width. + private void UpdatePathCache(float strokeWidth) + { + var strokePath = new SKPath(); + + // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path. + if (Math.Abs(strokeWidth) < float.Epsilon) + { + _pathCache.Cache(strokePath, strokeWidth, Bounds); + } + else + { + using (var paint = new SKPaint()) + { + paint.IsStroke = true; + paint.StrokeWidth = strokeWidth; + + paint.GetFillPath(EffectivePath, strokePath); + + _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); + } + } + } + + /// + /// Check Skia path if it contains a point. + /// + /// Path to check. + /// Point. + /// True, if point is contained in a path. + private static bool PathContainsCore(SKPath path, Point point) + { + return path.Contains((float)point.X, (float)point.Y); + } + + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect); + + return result == null ? null : new StreamGeometryImpl(result); + } + + /// + public Rect GetRenderBounds(Pen pen) + { + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0); + } + + /// + public ITransformedGeometryImpl WithTransform(Matrix transform) + { + return new TransformedGeometryImpl(this, transform); + } + + /// + /// Invalidate all caches. Call after chaning path contents. + /// + protected void InvalidateCaches() + { + _pathCache.Invalidate(); + } + + private struct PathCache + { + private float _cachedStrokeWidth; + + /// + /// Tolerance for two stroke widths to be deemed equal + /// + public const float Tolerance = float.Epsilon; + + /// + /// Cached contour path. + /// + public SKPath CachedStrokePath { get; private set; } + + /// + /// Cached geometry render bounds. + /// + public Rect CachedGeometryRenderBounds { get; private set; } + + /// + /// Is cached valid for given stroke width. + /// + /// Stroke width to check. + /// True, if CachedStrokePath can be used for given stroke width. + public bool HasCacheFor(float strokeWidth) + { + return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance; + } + + /// + /// Cache path for given stroke width. Takes ownership of a passed path. + /// + /// Path to cache. + /// Stroke width to cache. + /// Render bounds to use. + public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds) + { + if (CachedStrokePath != path) + { + CachedStrokePath?.Dispose(); + } + + CachedStrokePath = path; + CachedGeometryRenderBounds = geometryRenderBounds; + _cachedStrokeWidth = strokeWidth; + } + + /// + /// Invalidate cache state. + /// + public void Invalidate() + { + CachedStrokePath?.Dispose(); + CachedGeometryRenderBounds = Rect.Empty; + _cachedStrokeWidth = default(float); + } + } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs new file mode 100644 index 0000000000..d587a989cc --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with saving images to stream/file. + /// + public static class ImageSavingHelper + { + /// + /// Save Skia image to a file. + /// + /// Image to save + /// Target file. + public static void SaveImage(SKImage image, string fileName) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + + using (var stream = File.Create(fileName)) + { + SaveImage(image, stream); + } + } + + /// + /// Save Skia image to a stream. + /// + /// Image to save + /// Target stream. + public static void SaveImage(SKImage image, Stream stream) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + using (var data = image.Encode()) + { + data.SaveTo(stream); + } + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs new file mode 100644 index 0000000000..307af708af --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with resolving pixel formats to Skia color types. + /// + public static class PixelFormatHelper + { + /// + /// Resolve given format to Skia color type. + /// + /// Format to resolve. + /// Resolved color type. + public static SKColorType ResolveColorType(PixelFormat? format) + { + var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; + + // TODO: This looks like some leftover hack + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + var runtime = runtimePlatform?.GetRuntimeInfo(); + + if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux) + { + colorType = SKColorType.Bgra8888; + } + + return colorType; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs new file mode 100644 index 0000000000..5aa5de2abc --- /dev/null +++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs @@ -0,0 +1,23 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Extended bitmap implementation that allows for drawing it's contents. + /// + internal interface IDrawableBitmapImpl : IBitmapImpl + { + /// + /// Draw bitmap to a drawing context. + /// + /// Drawing context. + /// Source rect. + /// Destination rect. + /// Paint to use. + void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs new file mode 100644 index 0000000000..332b8547bf --- /dev/null +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -0,0 +1,92 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Immutable Skia bitmap. + /// + public class ImmutableBitmap : IDrawableBitmapImpl + { + private readonly SKImage _image; + + /// + /// Create immutable bitmap from given stream. + /// + /// Stream containing encoded data. + public ImmutableBitmap(Stream stream) + { + using (var skiaStream = new SKManagedStream(stream)) + { + _image = SKImage.FromEncodedData(SKData.Create(skiaStream)); + + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } + + PixelWidth = _image.Width; + PixelHeight = _image.Height; + } + } + + /// + /// Create immutable bitmap from given pixel data copy. + /// + /// Width of data pixels. + /// Height of data pixels. + /// Stride of data pixels. + /// Format of data pixels. + /// Data pixels. + public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data) + { + var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul); + + _image = SKImage.FromPixelCopy(imageInfo, data, stride); + + if (_image == null) + { + throw new ArgumentException("Unable to create bitmap from provided data"); + } + + PixelWidth = width; + PixelHeight = height; + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Dispose() + { + _image.Dispose(); + } + + /// + public void Save(string fileName) + { + ImageSavingHelper.SaveImage(_image, fileName); + } + + /// + public void Save(Stream stream) + { + ImageSavingHelper.SaveImage(_image, stream); + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawImage(_image, sourceRect, destRect, paint); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 50e65f45dc..d4e6403dc9 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,21 +1,21 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Platform; -using SkiaSharp; namespace Avalonia.Skia { - public partial class PlatformRenderInterface : IPlatformRenderInterface + /// + /// Skia platform render interface. + /// + public class PlatformRenderInterface : IPlatformRenderInterface { - public IBitmapImpl CreateBitmap(int width, int height) - { - return CreateRenderTargetBitmap(width, height, 96, 96); - } - + /// public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, @@ -27,27 +27,19 @@ namespace Avalonia.Skia return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } + /// public IStreamGeometryImpl CreateStreamGeometry() { return new StreamGeometryImpl(); } - public IBitmapImpl LoadBitmap(System.IO.Stream stream) + /// + public IBitmapImpl LoadBitmap(Stream stream) { - using (var s = new SKManagedStream(stream)) - { - var bitmap = SKBitmap.Decode(s); - if (bitmap != null) - { - return new BitmapImpl(bitmap); - } - else - { - throw new ArgumentException("Unable to load bitmap from provided data"); - } - } + return new ImmutableBitmap(stream); } + /// public IBitmapImpl LoadBitmap(string fileName) { using (var stream = File.OpenRead(fileName)) @@ -56,16 +48,13 @@ namespace Avalonia.Skia } } + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) { - using (var tmp = new SKBitmap()) - { - tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul) - , data, stride); - return new BitmapImpl(tmp.Copy()); - } + return new ImmutableBitmap(width, height, stride, format, data); } + /// public IRenderTargetBitmapImpl CreateRenderTargetBitmap( int width, int height, @@ -73,24 +62,47 @@ namespace Avalonia.Skia double dpiY) { if (width < 1) + { throw new ArgumentException("Width can't be less than 1", nameof(width)); + } + if (height < 1) + { throw new ArgumentException("Height can't be less than 1", nameof(height)); + } + + var dpi = new Vector(dpiX, dpiY); - return new BitmapImpl(width, height, new Vector(dpiX, dpiY)); + var createInfo = new SurfaceRenderTarget.CreateInfo + { + Width = width, + Height = height, + Dpi = dpi, + DisableTextLcdRendering = false + }; + + return new SurfaceRenderTarget(createInfo); } + /// public virtual IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - var fb = surfaces?.OfType().FirstOrDefault(); - if (fb == null) - throw new Exception("Skia backend currently only supports framebuffer render target"); - return new FramebufferRenderTarget(fb); + foreach (var surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + { + return new FramebufferRenderTarget(framebufferSurface); + } + } + + throw new NotSupportedException( + "Don't know how to create a Skia render target from any of provided surfaces"); } + /// public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { - return new BitmapImpl(width, height, new Vector(96, 96), format); + return new WriteableBitmapImpl(width, height, format); } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs new file mode 100644 index 0000000000..0086671880 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Skia; + +// ReSharper disable once CheckNamespace +namespace Avalonia +{ + /// + /// Skia appication extensions. + /// + public static class SkiaApplicationExtensions + { + /// + /// Enable Skia renderer. + /// + /// Builder type. + /// Builder. + /// Preferred backend type. + /// Configure builder. + public static T UseSkia(this T builder) where T : AppBuilderBase, new() + { + builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia"); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index d3083d3d33..06679478cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -1,47 +1,31 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia -{ - public static class SkiaApplicationExtensions - { - public static T UseSkia(this T builder) where T : AppBuilderBase, new() - { - builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia"); - return builder; - } - } -} namespace Avalonia.Skia { + /// + /// Skia platform initializer. + /// public static class SkiaPlatform { - private static bool s_forceSoftwareRendering; - + /// + /// Initialize Skia platform. + /// public static void Initialize() { var renderInterface = new PlatformRenderInterface(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); } - public static bool ForceSoftwareRendering - { - get { return s_forceSoftwareRendering; } - set - { - s_forceSoftwareRendering = value; - - // TODO: I left this property here as place holder. Do we still need the ability to Force software rendering? - // Is it even possible with SkiaSharp? Perhaps kekekes can answer as part of the HW accel work. - // - throw new NotImplementedException(); - } - } + /// + /// Default DPI. + /// + public static Vector DefaultDpi => new Vector(96.0f, 96.0f); } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 6a1aed8d79..9196ace4d8 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,10 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; - -namespace Avalonia +namespace Avalonia.Skia { public static class SkiaSharpExtensions { diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 935d6d5e5b..c19ff79d87 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,90 +1,103 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl + /// + /// A Skia implementation of a . + /// + public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { - Rect _bounds; - SKPath _path; - - public override SKPath EffectivePath => _path; - - public override Rect GetRenderBounds(Pen pen) + private Rect _bounds; + private readonly SKPath _effectivePath; + + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + /// Precomputed path bounds. + public StreamGeometryImpl(SKPath path, Rect bounds) { - return GetRenderBounds(pen?.Thickness ?? 0); + _effectivePath = path; + _bounds = bounds; } - public override Rect Bounds => _bounds; - - public IStreamGeometryImpl Clone() + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) { - return new StreamGeometryImpl - { - _path = _path?.Clone(), - _bounds = Bounds - }; } - public IStreamGeometryContextImpl Open() + /// + /// Initializes a new instance of the class. + /// + public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) { - _path = new SKPath(); - _path.FillType = SKPathFillType.EvenOdd; - - return new StreamContext(this); } + + /// + public override SKPath EffectivePath => _effectivePath; - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } + /// + public override Rect Bounds => _bounds; - public override IGeometryImpl Intersect(IGeometryImpl geometry) + /// + public IStreamGeometryImpl Clone() { - throw new NotImplementedException(); + return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds); } - public override ITransformedGeometryImpl WithTransform(Matrix transform) + /// + public IStreamGeometryContextImpl Open() { - return new TransformedGeometryImpl(this, transform); + return new StreamContext(this); } - private Rect GetRenderBounds(double strokeThickness) + /// + /// Create new empty . + /// + /// Empty + private static SKPath CreateEmptyPath() { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return new SKPath + { + FillType = SKPathFillType.EvenOdd + }; } - class StreamContext : IStreamGeometryContextImpl + /// + /// A Skia implementation of a . + /// + private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private SKPath _path; + private readonly SKPath _path; - Point _currentPoint; + /// + /// Initializes a new instance of the class. + /// Geometry to operate on. + /// public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._path; + _path = _geometryImpl._effectivePath; } - + + /// + /// Will update bounds of passed geometry. public void Dispose() { - SKRect rc; - _path.GetBounds(out rc); - _geometryImpl._bounds = rc.ToAvaloniaRect(); + _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl.InvalidateCaches(); } + /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _path.ArcTo( @@ -95,33 +108,33 @@ namespace Avalonia.Skia sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, (float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void BeginFigure(Point startPoint, bool isFilled) { _path.MoveTo((float)startPoint.X, (float)startPoint.Y); - _currentPoint = startPoint; } + /// public void CubicBezierTo(Point point1, Point point2, Point point3) { _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); - _currentPoint = point3; } + /// public void QuadraticBezierTo(Point point1, Point point2) { _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); - _currentPoint = point2; } + /// public void LineTo(Point point) { _path.LineTo((float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void EndFigure(bool isClosed) { if (isClosed) @@ -130,6 +143,7 @@ namespace Avalonia.Skia } } + /// public void SetFillRule(FillRule fillRule) { _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs new file mode 100644 index 0000000000..88200dcfbe --- /dev/null +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -0,0 +1,169 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia render target that writes to a surface. + /// + public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl + { + private readonly Vector _dpi; + private readonly SKSurface _surface; + private readonly SKCanvas _canvas; + private readonly bool _disableLcdRendering; + + /// + /// Create new surface render target. + /// + /// Create info. + public SurfaceRenderTarget(CreateInfo createInfo) + { + PixelWidth = createInfo.Width; + PixelHeight = createInfo.Height; + _dpi = createInfo.Dpi; + _disableLcdRendering = createInfo.DisableTextLcdRendering; + + _surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format); + + _canvas = _surface?.Canvas; + + if (_surface == null || _canvas == null) + { + throw new InvalidOperationException("Failed to create Skia render target surface"); + } + } + + /// + /// Create backing Skia surface. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKSurface CreateSurface(int width, int height, PixelFormat? format) + { + var imageInfo = MakeImageInfo(width, height, format); + + return SKSurface.Create(imageInfo); + } + + /// + public void Dispose() + { + _canvas.Dispose(); + _surface.Dispose(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + _canvas.RestoreToCount(-1); + _canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = _canvas, + Dpi = _dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = _disableLcdRendering + }; + + return new DrawingContextImpl(createInfo); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Save(string fileName) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public void Save(Stream stream) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } + } + + /// + /// Create Skia image snapshot from a surface. + /// + /// Image snapshot. + public SKImage SnapshotImage() + { + return _surface.Snapshot(); + } + + /// + /// Create image info for given parameters. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format) + { + var colorType = PixelFormatHelper.ResolveColorType(format); + + return new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + } + + /// + /// Create info of a surface render target. + /// + public struct CreateInfo + { + /// + /// Width of a render target. + /// + public int Width; + + /// + /// Height of a render target. + /// + public int Height; + + /// + /// Dpi used when rendering to a surface. + /// + public Vector Dpi; + + /// + /// Pixel format of a render target. + /// + public PixelFormat? Format; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index e14d3f04be..e95069eef3 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -1,59 +1,43 @@ -using System; -using Avalonia.Media; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl + /// + /// A Skia implementation of a . + /// + public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl { + /// + /// Initializes a new instance of the class. + /// + /// Source geometry. + /// Transform of new geometry. public TransformedGeometryImpl(GeometryImpl source, Matrix transform) { SourceGeometry = source; Transform = transform; - EffectivePath = source.EffectivePath.Clone(); - EffectivePath.Transform(transform.ToSKMatrix()); + + var transformedPath = source.EffectivePath.Clone(); + transformedPath.Transform(transform.ToSKMatrix()); + + EffectivePath = transformedPath; + Bounds = transformedPath.TightBounds.ToAvaloniaRect(); } + /// public override SKPath EffectivePath { get; } + /// public IGeometryImpl SourceGeometry { get; } + /// public Matrix Transform { get; } - public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); - - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override Rect GetRenderBounds(Pen pen) - { - return GetRenderBounds(pen.Thickness); - } - - public override IGeometryImpl Intersect(IGeometryImpl geometry) - { - throw new NotImplementedException(); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override ITransformedGeometryImpl WithTransform(Matrix transform) - { - return new TransformedGeometryImpl(this, transform); - } - - public Rect GetRenderBounds(double strokeThickness) - { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); - } + /// + public override Rect Bounds { get; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 24674f3b22..f85dd84055 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,3 +1,7 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media; @@ -5,7 +9,10 @@ using SkiaSharp; namespace Avalonia.Skia { - static class TypefaceCache + /// + /// Cache for Skia typefaces. + /// + internal static class TypefaceCache { public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name); static readonly Dictionary> Cache = new Dictionary>(); diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs new file mode 100644 index 0000000000..ab6c399ff4 --- /dev/null +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -0,0 +1,151 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia based writeable bitmap. + /// + public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl + { + private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; + private readonly SKBitmap _bitmap; + + /// + /// Create new writeable bitmap. + /// + /// Width. + /// Height. + /// Format. + public WriteableBitmapImpl(int width, int height, PixelFormat? format = null) + { + PixelHeight = height; + PixelWidth = width; + + var colorType = PixelFormatHelper.ResolveColorType(format); + + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + + if (runtimePlatform != null) + { + _bitmap = new SKBitmap(); + + var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + var blob = runtimePlatform.AllocBlob(nfo.BytesSize); + + _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob); + } + else + { + _bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); + } + + _bitmap.Erase(SKColor.Empty); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint); + } + + /// + public void Dispose() + { + _bitmap.Dispose(); + } + + /// + public void Save(Stream stream) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Save(string fileName) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap); + + /// + /// Get snapshot as image. + /// + /// Image snapshot. + public SKImage GetSnapshot() + { + return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + } + + /// + /// Release given unmanaged blob. + /// + /// Blob address. + /// Blob. + private static void ReleaseProc(IntPtr address, object ctx) + { + ((IUnmanagedBlob)ctx).Dispose(); + } + + /// + /// Framebuffer for bitmap. + /// + private class BitmapFramebuffer : ILockedFramebuffer + { + private SKBitmap _bitmap; + + /// + /// Create framebuffer from given bitmap. + /// + /// Bitmap. + public BitmapFramebuffer(SKBitmap bitmap) + { + _bitmap = bitmap; + } + + /// + public void Dispose() + { + _bitmap = null; + } + + /// + public IntPtr Address => _bitmap.GetPixels(); + + /// + public int Width => _bitmap.Width; + + /// + public int Height => _bitmap.Height; + + /// + public int RowBytes => _bitmap.RowBytes; + + /// + public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; + + /// + public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/readme.md b/src/Skia/Avalonia.Skia/readme.md index 3defe9c58c..7ed92c5453 100644 --- a/src/Skia/Avalonia.Skia/readme.md +++ b/src/Skia/Avalonia.Skia/readme.md @@ -1,42 +1,22 @@ -TODO: - -BitmapImpl -- constructor from Width/Height -- Save - -StreamGeometryImpl -- Hit testing in Geometry missing as SkiaSharp does not expose this - DrawingContextImpl - Alpha support missing as SkiaSharp does not expose this - Gradient Shader caching? -- TileBrushes - Pen Dash styles Formatted Text Rendering -- minor polish +- Minor polish -RenderTarget -- Figure out a cleaner implementation across all platforms -- HW acceleration +Linux +- Need gpu platform implementation -App Bootstrapping -- Cleanup the testapplications across all platforms -- Add a cleaner Fluent API for the subsystems - - ie. app.UseDirect2D() (via platform specific extension methods) +macOS +- Need gpu platform implementation Android - Not tested at all yet iOS -- Get GLView working again. See HW above - -Win32 -- Cleanup the unmanaged methods (BITMAPINFO) if possible +- Not tested at all yet General -- Cleanup/eliminate obsolete files -- Finish cleanup of the many Test Applications -- Get Skia Unit Tests passing - - +- Get Skia Unit Tests passing (most of the issues are related to antialiasing) \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b2ffc2cec9..f89086ccb7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -321,6 +321,24 @@ namespace Avalonia.Win32.Interop WS_EX_NOACTIVATE = 0x08000000 } + [Flags] + public enum ClassStyles : uint + { + CS_VREDRAW = 0x0001, + CS_HREDRAW = 0x0002, + CS_DBLCLKS = 0x0008, + CS_OWNDC = 0x0020, + CS_CLASSDC = 0x0040, + CS_PARENTDC = 0x0080, + CS_NOCLOSE = 0x0200, + CS_SAVEBITS = 0x0800, + CS_BYTEALIGNCLIENT = 0x1000, + CS_BYTEALIGNWINDOW = 0x2000, + CS_GLOBALCLASS = 0x4000, + CS_IME = 0x00010000, + CS_DROPSHADOW = 0x00020000 + } + public enum WindowsMessage : uint { WM_NULL = 0x0000, @@ -1194,6 +1212,16 @@ namespace Avalonia.Win32.Interop public IntPtr hIconSm; } + [Flags] + public enum OpenFileNameFlags + { + OFN_ALLOWMULTISELECT = 0x00000200, + OFN_EXPLORER = 0x00080000, + OFN_HIDEREADONLY = 0x00000004, + OFN_NOREADONLYRETURN = 0x00008000, + OFN_OVERWRITEPROMPT = 0x00000002 + } + public enum HRESULT : uint { S_FALSE = 0x0001, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9f67f97252..292d8a8138 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -724,7 +724,7 @@ namespace Avalonia.Win32 UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { cbSize = Marshal.SizeOf(), - style = 0, + style = (int)(ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW), // Unique DC helps with performance when using Gpu based rendering lpfnWndProc = _wndProcDelegate, hInstance = UnmanagedMethods.GetModuleHandle(null), hCursor = DefaultCursor, diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 089579a0a0..4cee05a0d9 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Theory] [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), -#if SKIA +#if AVALONIA_SKIA InlineData(PixelFormat.Rgb565) #endif ] diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index 6381bceadc..0107002274 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs @@ -281,12 +281,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest() { Decorator target = new Decorator @@ -309,12 +305,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index 062c7d88f5..656e77fc31 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Horizontal_Fill() { Decorator target = new Decorator @@ -52,12 +48,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Vertical_Fill() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index b3e214f863..0017feb106 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task RadialGradientBrush_RedBlue() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index f69d336271..3a1f7adb2d 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -270,12 +270,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj new file mode 100644 index 0000000000..0473355fcd --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -0,0 +1,22 @@ + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Skia.UnitTests/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs new file mode 100644 index 0000000000..544a09e50f --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class HitTesting + { + [Fact] + public void Hit_Test_Should_Respect_Fill() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Fill = Brushes.Red, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(10, 10), root, null); + var insideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] {root.Child}, insideResult); + } + } + + [Fact] + public void Hit_Test_Should_Respect_Stroke() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Stroke = Brushes.Red, + StrokeThickness = 5, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + var insideResult = root.Renderer.HitTest(new Point(1, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] { root.Child }, insideResult); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a462e5b079 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Reflection; +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png index 744a314256d8d42397f9d111ab4b83364af647d0..89668d19f40847604c17c2ed55e9f75562e9602b 100644 GIT binary patch literal 642 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZk(GggNypR0F{C2y?PW*4CI=DLgA;$xm%P%V`fWzy@=(r$KPpel0W|@E{j0)# z_P35dzH!{F@A~rh&tHAt{{QaY^*QhEg_~{ueJZUi{^pr?+Ov0mpL+Jqz1h0bH_n+C oI4TIRFpUyJJq*C{&rqMzopr03^|LYXATM literal 652 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZk(Ggg$;{KmF{C2y?PYJSLk1!a7hf>l`R^qA+tDgugSk+86`!tV<(_lzfEt0| zM_~KgJEgTYzxBSy$87&@)TjUa-uLC@_s@lwrG1_{_hwD%^V>g9oy*y`$$0kqH)o!m zFMnQmimmT9=N?4?4yHzhQDSI^0V6mSH0(D%#+!a^OTm^)OpPEBPgg&ebxsLQ0G|$z AQUCw| diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png index 3a3d7df5d47bd2009a1d5613de4af07739416a7b..2b882874dcb11d7c79f3c1b73cc29a68952378ff 100644 GIT binary patch literal 1998 zcmYk74^R_V9>>$rP1$hB!vdU{K}g z{0@Y{VCO}KiQmslnHWgs4$OF(r}wQXS$&*t>Syh(Zh^-cH;;!G1HUR>f8$2*#5Gew z*c;)`noBseIF~_j|*ZJ#O}vOS*r;?Ro7!+(CFPFPJtQA1lYI!zbi zgK`H?U1pW2nlc0qagqnKK^#laJvVj#iM}}kON|>6RSt|bl_D)wzORSL18(;aSk|uX zSk%ckvt+7#aVU)0*0PFY$vne%*8iaH1Gs594O=1TuV?%KR&SQ6nsk(Eb@QsG62$&$ z3CUKg>Wp3BwTfW=H-3@o4VEynpRwcv{WZUdfV_#fLFT#8gMPrqP|JPu?Yf0 z<$J*0u~e!$7w!Q;-?hHOu`nOL1DaK9WvbrBX3+c;S0i@T1dN%uJWpUVH+sP6vy^hg z?mhvU6Iad=m}1ZjkezZ6;TJoBW)OcZ$KvyeKWOq4QdMty2&C8L2|1R(eFBwf3u%S) zVFnHN`*AGMK_2SgY4&4W^nD5K6yze9cSlB32MIS2$_xk9Ah}~s9XnI;J39LeR2{d{2kgiDq zO{D;g7XKFLtj}FhAyq|>tcIY^^Pt4C-yl8Le=!8z=F)J#KgW_c=MI|Ye-K#t)jrUq z%E}SG_6}%1xcdu%$!s5EQCW;TplPIU%`6DySh%rJ%>a@OnioX?z4@>_cEv&w0@ROX zsxm1FnjK|YB*&`|H0yf5Ca~o~x(Lpb2a%k=ghF~kCt{kcO}PehF;Elg-elYW41 z3muON#FYd4W*bwT@197IVupsq`26g3u1e{e=F-`d-c^bZeEggfuluYy|Ls4$@@RUt zYjF=mLYi?UJy<_K$Wy=2P1O@jRSt~KUwhtdUVC&FiIaqk{$3!iJt@(szO6}#obGB% zvT5|E4Z}g3@fT(exL`NNOA=?Y{-p8w(`dmR*D!fgJzj&%aoO>EwkmFkG-A^@R0v~P0D>b12@ts{Jg@^#lfw6FFfz%Tn;+ozX|qc`&Y*(QPJLVc;1I<-O&-3 zO|Y8UzkHJtu@pxE#?>xG)13be&--BdUYxAp!rmM&V0AIwEDrFzO?#y{xtJ>hC}x?M zZWlvon3Io_3uOVYnzS!bBCV_zf{JZksLrb#rgZP4DA64M7A%~X=en4}qE}(eGX2Lm zxgj_m*6LdkW~Q)2L&L_+IC(302f#%UCZ?@x1-Mt(JkgO?R>9s>9!{Y|N_!=wuUWbb z)v>pNW{f@qCp)WQZ;s2AMyAawfS`u#T{sz94e8?_wwswYlY)i{xFJI{oLT{eA5kJF zX%Ph7wYj4@ln0tMmsOM~NN^oA@pdCq`1cQCOo~o{ldDkuKEufYoIbVGft2V|*&!J7AGs&0TQUWjl3oMA9rV$? zPrd}zt(<~%$t5c#lAJPvd%VQN9MtF`=vHGmPLAb6dY0QdNH_68vq!!RO=DLpQ~)0gb_LethIK>A#D1HfN30FUhUKy`jTpxJyp5-0Z%J3(`+yO}9BWxOXw-7FU1+r*`5bs|il6wyGjZbY?q!bo1oSsBS|lt!W;kME59o!we|zK{d-B yz?kz5DL5IF4@)Y`A-B}=y=U*k!z)G|hL~r=y2Tx&{XV>t8Ij?!VTO&{5BvcFr+!@k literal 1959 zcmY+F2~-nj5XWQM#nwf|YXbpgDjqj#{W92IB`}i^QpZ`qe`&RMkD&O(r z_~SSn&Uk-6@oIQ7?9T^g@Vl5SQNd$O>}ua-oRTJ+ZusNs$Mtx z%HK6oDrn7n!IED}=VOfIiVHY6 zG$f^}0$o^Gwg_YRI~M}XjgwME=dVF|tO#l@ng>Epn8zeuc=whlbA8~isYvOOEGQ{a z&&8OWa@M_zlZ-0b%mbm~csq{*s0C<=O>iOru z<|-l;F?pO~Z8qE_arAC8*bG&rAtvI~c!16ONW9;*79dZhK%ABH!F#3iQjBT&RtQ4r z@oG_5%oz~s^_Yf+Ot=p@gPWrX+Myc;Z^XMa-0a35ASb~jCg}R^aDWzD>$%z5vn=c* z2|9a#wVCs|S~R2JDhSn`utG!J4qD6gkLwPRc*~zMXl+4@jPh8v3A%W70z*UQ`ocIK zH`hT9Wi-GLosMf&HwWM++jkT6okOq_!XpBqbyx+v;|usP^Nj(i3!?uh zO!m$+v{ke&kjJib5?FlJ<}|0eBD=*?ooTQdX`IqsKd!WB=O_npd7&yDr%~w@{*> zDk?Yn_h}uId)Tmt&i{r9mJYIEI{SCR{u(ZD$kM-=;us_?l{YHC@YUpzWDWL+FDqTLa8Bt%J~Ba+9qGG-CUFzeCpl?JZFTvh6u0KTvc+ zEQB$g>=dSrFmF#`$?-xqOqkgGW(v8DJJ_(n+=o03jKxx3XoT4$9?fBEoF2}4r$$Om zf^<%Cj%&-C@7_ z+1|^3bXi9MYDj=_&K?-EuoqTUm@!30nSaG@OP_ff62pB6i*dj4ClX&?Q4eePeuff> zv6%uV>V5-F;wjhQL>a^x>4>vj0gEwP0H;u}0)%!fO_5UO9b-WKsH+>saAw0Up1fI0 z;=Tec4v2sy+sI5FC9Q zngK%B2INQ#9t%RAK0_qFA-)}odsJ$<-8VfTr@xzzc5u4DaKeH4YLQ!i5#%5}wrI#w z3=U61MiaokDuDZYtkHZs6olsGts&?nG6WU|ano_TYtvvHSXmrF-ykCa+Pi7E;mr*I zvo67j+DX7obvg5yTJ&u5_aO9H7!S>#cM&deiLO^IYQOX&IErc4aeq$lgq)4%RuZ(K z3Ql1Javk9QX#jscvxA`5SFuB|cdzG$-wFWlg<)`0Ijo1vA|WkHEs{LF0_v?jmT10f zA=oTF>qF4fNw_SG(@#Q;0;8K%mT4*XuIf4r+0tModN-p1Rz3=xv-|@Zg^L>8+$9>#f_w~De+jZUNc^3G? zytOxiHYzD8Y5V#d37jP^{=T|c7+UI*xK`QhkN&*Ki@*}unmm0 z6hVAkP;8@83eB}k z??=rU<8uEHV?G<&2C;T0Xb`Yk|@#$Uh_@SeY4|?r!$0*xK){5 zXjQ^9*bnEuhFNQDTiepCaay(EoJa zh3KLiK~xs9-x8QsS$c36?j#Wx{&gY3VxKL#%|NEC_1^A-x&P9mg2Sp(>&U2oDjs#- zVmU3Dj1^>?0xPlaz$`{BMCeA*y%Kzhn$>#(VYxVyvPxL}018$Rq2H@*yP62B?Uyh& zIr=9j6`XYEeT7b}{YCm!9JESz6Ms@DC;{uORHWJ_=WnRa0G4@}poBI1W~%k$i0i|U zpR&huriAIn7o)N~+|8D3RBDwM3}d(cGfixBQwkU5$(_|FN4`B8EKSydi zhrS0TZS`FUfoiLL)V_>qX7;|InqHTu#C)=%OXICoy7mRChHC^9l$;G%_*s{PE}5@NsbjE(QxV6 zut8hAdxyEmtQt%>C%#Quw#gl5XjZEaIX__>a9y{7ar%~pEN{e-ttxPA-I~SAxdtC) zwiV)}@v!@E0{}mVrhpl{&*Gg9aZx=r+JtytnOC!D;V&vk&scu;v|WQ_3n^^ILhYsu zaJO3HCJ)}B{uQl{AEXeOW09{wtyMRRnRD&T8BYc!K71i;0-v8p4I_EC|DA1;^(kj$%4^^zjtDJy^wr8+1y$^H$bDYxiL(dpp~S{}Yzx9sachb` z<4{U{Sz@eb{PApHm5^n@JH+jh{SeCo{;LE07r3-&w+zJt2?oPER&)ymo!W0`_i%Uw zoHwE8jLSH5e})D6ZhKHQku&AAROnFOZ9$GJB{d7(gU@IABZAn3^+CWfp`Pha0`GO159`yT1!skXFV0ZVVQ1rt8jFCQgN+ z^Umb1z@Qq|Y7QgMuuz{8XAV>4?NG4WNq4UxEBSF3ZzzY-`Ib%@sNlK^kEVVPE?+r$ zcXd!AZz3fgwH}4c$&Q-7blyF*Lsd!VbJOc_w-0Bi_c~@40oDQL8|K0cfK($qKwR5n zQQe6{w|G{BPSgN~j~fht@co^mCxG~!-Y>8mNGdx5_}fDEE!_UvjTTR?9&n`r0e6l& zP`VxpY?H~DQ^otv(Z6*x!!!4fpcL~6H#ebk-v zkpxF3V)b?-nD}My;vcFP5#Ub6<^gco5&BiiEx4LhZ<*J6V2nJ-eMvI~4!G_hAcbnO z?tD-seo4!CrJTCjs4m}*a7<&S>(mce7|XD1OO;yZNc)RGa$B@_B_dCi9%=37?P%?m z$Um!}-fz0twzkO4o=1)COKw^Z&-(g-o}fkvttNu8)$@f8_h7&rDCzAr1u3XH4oQ9e z9CW=ewkJw`10kU_2o9vk;a*j-t2bHS-_JJR8t7M?DSG$l78cPZX)DSiVj{UF??Jn7 zi_Y*F_t@aI=={;1`;omkmzGJ_e8Azw?p4Z6f)eAbT1U@5S=ynB0~0ln(57+3QZYL+ zr~0>EGKcrq^KE*~{V_+UL-NMHhNjN5P>HzaAF5qwUhYX6pAPtA)-Q?vsbU*CEUOeA zrhPf>(^Z2E@bGE(zyYfIZ&Ci!$AiFgWohCq&MlrVk_s=St2qBO6_f}B_aHF{82u;d z1}`$Frq~!JP0d)!xu$L|+r04@`K5(l>U)g%mRB&jY1|-Jl#9Uwi|n*EEE~Y3vKz1R z>-X@TQkfXaK$kUX5dG5&v@J`pLU?sYN3%F;{~z{?JP*g+GQzqe2ztj}qar&)Tz11= zuKQ(eqja;ntF(>XvAZ?J{i$f-vYI?8?_zJn(j!PR-TZpRe@wwI3D8liuk> z+abXg+2|2}A=q)f={u`{YSs(5F0@)`QoS8}#U9prmBJXZ#Hzna z@D&_?Hn^l5vEPi5S3d*v);^t5II>bQUg|yrl)cH5O&;VMiPZMLL-6Ycy8RBicw5#i zAtz_OAnLw}v3mjnf*7nCFo8H|od`@v=kIt@XdOum8QRU~0A2tpB<&SqsRscXLW?#* zQ{Cr2^t&hKO&{}Yo;>rj(E8!-P)CKa_YrqY>zKnPrn32;&=VDrS!bFIY?D>I#(Rbi zwHRD*SjkA63LVTmcn|I1_s}|kf-U5abk{E3iil>Oqs?c_3snC)b!q@T3$0`r ze`NbSeQo7Ee2ClK2OV!)gWegf(PEp#LOhtxn_XpP1a!uBE($Qik!V-9j~zHbKzmhm zLZRo}y#^lb8D=)|DQ8UGZp$}=xf6#o`zkbrN@GENz8dmVS#Nj zvdh2+a&G7#6$=&dhC(rGZqVP|V{Z#Se7FxrG{Izx?M;srtZ`ZoHo$dI959QV{6U#} zNvsxj$Uh;9eNPa*bUU(N;D_qP7|^7f^<51sHXZ@Oqk)v zQ-&eX4EK)u1`kr>36faa5;_5^{`nC&&UoqU&rAkPEzF?SAlA@>i3_n9D=>xNfArh0 zk$WRC`>@^F(pkEFaup*fB<%U3hOTBwe|efA7K4Mec7Kk2*2@-DFX7)sCq8@~C@G%p zu2-SdJ&eyNz!bONa`W1+&mW018&Q5o8jYlH^yL3T5}l)>ShXJ|uFJ=$un0 zpa19-AmP!EhJC#&>v=az5uq@Y+7JvWqm$9-XtuQDwuLU?gUQ9oM4a#z0(}l*JQ%~z z2`3$6uMD22TEWFMOM2F>FWb`=K3U09(Y;lJT3AZkS8g3K%VQg1qQ|2O5(1koZTo=> zmDaxs5v1b6D<^|s=CsLDQkr+`+M;!ErcRcZxG8^3QKrNIx2vX8W*J*kw0Tof;cYBUx_NNsITMMz%HV1Kt%hK zXfI}vgx!@P&r|im<76&`aa5Soa5($^(M)FaXY+e$M-pnI&W~f`BwH`l zk2TC2u6T~?&K%3*tT`nxxlQdj_0xi^{R1tf&X_MfC(i=siG=XklshE+M>1jTtoxi7 z4i?77Had+xoN zY3Zn+&J@5bC}Qf^yudZfhkLsFv%k?6r=@mh3@b<$Ki^Z5M(I;{$Ft3m!)%Lerqe3m z`(}~>V!n_)oHy_J7~)H!#uxA()jV(&LaHe8B;bt(8YeWl1&*s@Oo|~EVVUVn|WFbUO31VlJ{a;|G>)TB1pN&NP(JDP6Db&t=dk z58F&zI&K@u(|9OKE183Z__Kp$CllPIuI?eueFY?X!n_6EK3C|HeZbD9oVOICEx)u1 zW|b|3jftpl#O2W|#p<hoMMi@sl5qdp`XSsXTVr|*40duqcDe_&ngCK8v(v~PkACA_CU6T}SZ zkKU`~S4F_-@HydQf}(8pZox|6>fseHD(gk9S0c?2I=)ry82bvEXPwwN(3ZSo54p9{ z?pGsR6KfnfKtrDBBm5$&KSS{wT9MqwuUJDy=qH{B{r#GgQ+Ga*_y?zI@Gyb}sg06~ zv5w{9!5DEV|6Lb$j)t>f2rHq#Ia#{~u?>@$6!lQZ*we?YuR3zZ%T;)O2#A7*>|f5N z?&iMNyV79U+`7}KyHe=00JGa)PHLpYaeKO88mJc34%S8y4^_s3@I@jYHO z^T-8>64!!;sQVl%F%`KBA0Y5k96aaBaibfwM+S%_Va#7c_sc9CH+SO($D3u%y=JZ$ zApILS)IshwP#oEBefkhGa#LMKK5^{^#XX;ijqcWz7#4zNS}!R<0y{ zF6LL+qYs&0Iv9c2%S*qbD8|mYqmJO;(3*doa+&M49zrZ$Z#$_het-z?TS_E<)hE$v zo4(+sZ_cp2V0D*0lDu2Eg^EuLbJJ=VZp3wE&XsXP9Gpu>Q~>D<6j5`(C$;(EtY}aq z89Zzq#5W2x+B#UHYOAgy-wP+RGJ}F2N6lvtb00_s1%m}WL=4Y5{4>Gr#dh-8>x8YYqxkU!GH3Xj?%i>HQV#yUmgCD2Iso3-&`o{ zY-r6>q~=7epklP&c=N*I6n~k@kJ2>&*VWQ|b$iapsxiL2w~+gw>A`Nd62V~(or_S*uY9tAKp}d-!&T~a)wBy@}D;G0fd*Bp7 zBWWvfrdd)^86N4lHh&{ujfK(T;@VW(R#|Q8gt(GxWaXu6p2v6s)^M&=8E_Q(Tz{Ux zjX$}T45|5V7o5`d%bTT5R4`}7aoK+!{N9>+j9djTJKfOhkpC*%)rTX@{?sCUz)Gj9 z-$c}L=m;15ya}sU)kPvZ353+`!%|){Q#%oyws@mF%N;U8>z1*S9gR#Y@}?cOao6g7 z2T7GsqxxFWLO*B(Lo^+f6w=!A2GZxCqp#MqXO1?ge$Qbkl3bN!NOCGaQI2D+&9k>@ zt{A3<>5B0^;_W^MRzh^)XSMyc6QL+cc%0M??5#Spyon7?n^@0kSOhl|d2fc_JlOso zwn7_mkB#gHRY}Vq{_08+I%{u6alu^Jx1n-$+h1RDTxQLHbxvi_IV)G#NOo8{9Lu`= z*+Ko(W{)aoaZ4yGs*RX$acp4U?qlXR5#Q$&JI84IU{$9|5p!AD)Zj|fdJ3cN(nS+!G+ImX~A8q|h9#IcFKbSh@?N*OGa`tmA14(uMQ;X!| zS3E>}KpV}}dxJJPK1|B8Lk$G$*3bX1fW|uD^4%w zeLovwoMSfow1}jtk3cKue~w;>&aQi4sVB@|P!#gRLIgLYxWVP_0ppXe#rO0=+&Ux- zL^FPeG=Q6*+{SrvUi$-C8Ieui9kX_E(T``qSl^LC=?^IZt_g^?BNv{Hm-65LyPaWR z&4=hl)@RcZQ^gXuz25ukYo2}#CdsOq?V(X;KnUig|+XCDAtRYWo z^-<>lZ*K_D4yG}j;zHbhL+eeATcZWZett#Ob)4NYTD#PBf9plkg$MId>-)N@6#Hbo z1m2_8V}i);uiDdBA%s^m@r(bmt@lv##2}8i2tgQx%d2M~D3rY7s12Edw-fQ=ILw z&6&LR`NX&5)6%J%f>Tr@WhD2frVg&v$0wkc0w9ad_blA!Ooh2VpG+*0=+EL26fp)3 z4+a9~t5Ex8&6-mHQwz*Gv|5wAa(7=S=4NQb#1BCRq&+Q^e|Kz)vo%`)UjlrZZAdkp zR_wjCC+K`W|3#Mt|9B-n?G6c>#Y!mRxZNay`xF%ZiiXoPaL!8`uWfzB_Li3rCDf)G z2=!=Fag2Wnk{gD^Mip@e6ebkDDIKKK8~raQ(PDMA>GD0$6A^K;N*B(3`K;SRp(Jb! zW^j@jWy?F!)Nn@jkIS6p_xps95^R(B9#3JZqw%Ps6ND>|IPflY z)-gyGC!=K_IX5{}lEkzba2%;y%ytWx1Vtnuzxq7p_g9GT87%=Bw_&@z;%-NT2_^f| z({PM$o+wWiSl(;Yhk-G_mL;N|htJjpFct<2(`*E~w64A8}Dc#!wv-(*3j(gphLzDB{Rp4MFKK#&eY0S?rd$ct za5JAD@|;DT=WsC6>#k{A?KO6}U7Pv1^^;NAmn@*Nb$iI zyF=WPcBtoU#&X+YG9vzc?xQkc&^gGig25b= zAUboZ6NBR+rw)>_l3zPpz_auBQu&Y;3^Rm=K36JB+4@{V%}4jov7RGUs_(=4L(Wac z^Y2|gX!J9LAUQ}*l2}Q@OF$9|Nnu7o5FC4)gdq$p8RLS8i}D-W_n}eYww&# zl+Sf5BGO)VCEm5FXiiK+HEU&d1k1H0n80xrdL7_gT0vxr(u-_=l&SPRv?NEV}Ca+ z6&wjm*o9x-`>(}II{bf|M|ah4FcHI#cVM+CDHD803He9wQy^PwEAFO=EVXq zxiXbokb^mpq(a$Q-P9mH=pwJ|7+7S{4YY*icOcJ6m zV{}+-)`S{@aeiVs?EKmGFiAYN`g3J#VT?J`Id>fLgEa1+7sJg(`0=0_EnyHB0D11j zaGbC!7m>44>x=KnU8 z%3Tf?yls^36t#9c(xP!Sf0Qi;?;CUZ0)UCXLfI)Xb}{U^?|9_F^u+(bBY!9^iPPEe zUx?)Ke?cS%-@vFzSyhndY|h$iB9JHJrzUqym95oZPGK^UJ&1!x>f+}di=X9iGG531 zSf=WAI`f(H%jI|R7LlLSuD!uET}`v#$10ZQ-VuH1)OZtGE;VEMCfl}}${FGHOn0eX z?A;$h1m|@AHnn2aw)5|Y-O+ys_s#3%hJ-0QlgDi2&nS#3`rFU%jas|0t^FgRM0i4@2G~{l$NG2x`QwPOGBLNpv>T~E$klM z#fQo-`viTYQ6KHLa_8NIs_p>njl^O7ZJ@i3!0{4~&===}5n+o=pLg~}^ z&5ni`+N^5pY4@DIqxbEXx)Du`gVw?L!GtobtIHTfqnaC5sPwn9Cn7xmW#!CXA>2*F zEIO@-ax43?b6$Tce%uj2%ySe}WLJstm{2DlU4)Zr?xO1Z^OPBSERgSlp}YgP&gT*) zMEBh<(BcYy0qYsNhW~!SA>4l3uKx+Z$8Xm9j^8H&hDDC^URHBPW*@suLFRqUYdK*v z1uy1aK;YGT?g~mbGDSVj=%GWhV+I#+&65zIs!BtC*3L};sEO6y(YG3EjjJJ7!?Zhk zMn)38J%VMKoR*&>*`l?emhAtYIF**~Pyi!NIi>!NFh{%!)ZVzxRNMmGDX~sww(Z;Y zNOod^9fSq&{=?rnxo_b7`sF&KvQBMD!m9Oo@u!$*vYi)-SVw;CU0K!(9!_yRR0VoxVf#{+Tn#vN>Uyr7rwGjbS5LEvgMf86WW&oM>oiKZ=tUTRasVwWKwkej5pII$C6<#TXYc@*9 zET7C;(fPaKoh_63V2cEDZ^fi__TjLSps(99{su;g1u5`*l!35yJ~q|DKNVdByzsi_ zqPB=@AaoeNz@Rn*xCK;=9(VK-RA)F1$dF5iGk*AQiN-VEiN;Qekzg(iAlteu9z$HJ zN?tuT)r-sk?{+(WvNpQ-8-d(&TZ3*a0Vq9e4oVB~0M@Z$Gc^GHon7v}Z62geoRcr= z1D^KUCAVZ*1A2vZN3@y)+?akYM!MxgQl#{SF~L1)#v~41N|c@?-a@1mS$vT%?ga)- zs>zQ}#`&d%XZE8$%~>jQftdg#^VZeKPol#WV!q z)Kx|_HBy_c*A*T?;OPos%{N-wnm}Ec`-@B>qjmzj$*qWh@(->_07)TquXwTWg1TK#$=jvup7v*_*H(9ywY~`w z071wBf1ud`(BQLlW;*OtzeBDpF9KqwRJ>R}iU26q9xZ?(1B3t@drv)AV+Ij{rtA3e zO^A(%cg21sd@%_KkazzMe^)4vWr0=08OPyO={plGeon`#e57_U^m0tdpb{NC9nZ&t1v@uAy|x6&S3wC#?;;`Qeg4SlpMLlc0SDs6dO0zC=x$ZISJRvOdS973nqA6j z6=r$l01^X0VWAbP0r(%Ffgb95bdMHg*{n_}@0d$~E^JZ(I!TV$`32A?Y?2kX+c36Q z7IK!0KGt=!pFG0szmr&)gWfr@<-^AZfXkhE6SqZMPv^*q0#`HT)h@1@6=8os^oz_5 zpX&Sxb165G7Hys`a@nhNchEJ&Y=pjhOCSai=mZ055Q7_ug}CH2?KZG#p-hkV& zhYhUC$8qPx4`*g!wnRwpj^wf|_j$slg`8$hu2LIhdWuXxqcS rf3!J&6sk~8{r9pg++wHOs;*B*!`sJSo-6?>FeTqZu!EEXkr)3LstH5G literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3725f2acbc06c0a3f653c1ce22813cfa46fef3b8 GIT binary patch literal 5905 zcmcgwXH*kRmnMJ^sx*N})JQ^m5h00&fYJo%grYCyl_Ca+2uKN4X#!F-RH-HurCE?J zf(l46frKJOAb=o72mz7aLUH-_oc(tH?vLFcbMG@}=G?hu&dfbek`=~Ci2o!%2M33c ziLrsrk?#7}0P-BkH};hUM+$(qF}lK0*#~AFO}KG-7J3{UHK_vouE#hyBtMuK=v@zW zUYew&c-zs*s}Wrq*yk2xnCR7dQS+A?w`R!2pQn}^_b1;^lsbaY--Og17vjLz($I&U3c{o{U2y95?37~&>C^kI z@s4{sYw5de>A;lr$=U;Bu9C@#U-i6U+aH+j7LAM)&-aJy-JK<0m#`J8iXA2r^ln<) zV$@09MFAS}?X9-`YQZ+R=X%ac_B*ilJMY#^<VoZ6e(?6#>?yVUv_A)YIWu5`fK-EH3diF^>;qZW4eZJ_e(t7?4o$3>^1 zjhW;94{DWTTt`a>CZ-y+?fsegISuR2j=wo>N>{f9?f+5#grd<&Mr4an7;(wF1xoAP zY835nHXWTLdU--s0p2Tkl?<=f44dEZlmq#hj~d_Z^auRPHKk|e=h&A{p7D&%tbNF1 z{6A~(-_`g(L6h(PtHf^>J8XXA9UIjm?nE@EJ8}N}!LH8S1Ha~=pX54jpHrGCbV1jY zeub+fQ{~?PyeGLx(V5zp^m@Bjc)FUuidWlDl5#w9<=N$VJzn2WWq%HqtM|8Cal50f zb8D@Coa`Dr92Bhk?UytD?Bgnxax z&!S5?%Q8w+q7A)e5+NZ?mvEb1SnH3%3{C&h7AV}Ixp>@Ills915}onA&n~YfEee!> z?*Xi}Z2Jg{^zF|Z^|$+HS%y)cUEX|&x7uIwO;}rWu^j4;MA43{9*^wwhL4nAKMDb% z8@c_#lRc}7+x@IM$6?aiRebuQMQUj0GvL!&7hs~!p}#QytZ&pMi&pm6XT~v5Z}vX3 ze=SDJe5kJAZHpg&9~hz<);-5D(<8ghXPIlZ_6yg)Mq>8J%E0PmcD{vJ4sEyU(2hr{ zDc>@Pw~U}FXtAm*hkfQw?(X)Nvr;YzXUFiCvO+5utEoDqh%V@vrpW4FLal!eE*#9M z<2nB}EeM$kQw+z&S-2c5c;4&v@(82TMx-!p{(xG)cQg?4&@vKX5%TTgCFrWd2ud+l zgjJ^RWt2xEc$wBE-b~-rD#z#{M_44f?nEKU;fT5 zq);Vxa(j&8ypWmgw)<<$#z@N}X;=yQg7JP)crknq$!zH5IWkkDc#(+T6WC^TJSn_< zW--B`V~slg_I}y{lus~BslztgCOkHEMaax!&juASweB&^P~B(LPlm4tuCx81l=C%H ze-6WvKJ&df;qFu~s`1ni}9kZN~5Nga~*AR{TWv*Lu( z-myPv(NL2YN-8rdRkaHKQ!R{A)%@9qs*ZMn|GFGUPa7|gH*<5HY+RB`WI9d3q;s1e z;Ia?~C|un0VR2CiqoobrZKE=@miX(+TD}o%6r(?`Ws35@{(;eCJ&a6I-kghcGgrc= z1}H5Ue-riARIhzmX??W*jm31Bfhy~eTDF^;E_66X9e|~&Ozn^ehLZiJuh;R^rNcYo z5k->p>3B`zo&28Hs9G}xWUSjr_2rDSvJ!V3HtWlKOCtf$l#A_(DAuSWGG`$Xs|Z>tk^T^*rZ0+|M%*;y z2J&tNKoBWU7z^bs^f)o>x~X_wy%6~c)5FN|$ta0Klv1~aqL^9>+MM&7{m0aF!dK7Z$<%XD+oSr2d8KLGfn(bX8`s-IfQX+53P`V#|HqR3}8 za!H~GU32zL=EmbU74H2#7{z{3Z?3Iuhty4R@|z5x1&VW73(j`$XR7LV3rk$t{fS0r zGBrR(>Q;G9YMqIB+3qk<<~pAsowgnm3yoMO4sB#r_0;9bxftYU^9z=r-h54wc558! zfsD@v#%Q_(EF>V}W%hJ+3QTSa+&Ylez-13eVdT!D%YXYucs*@lf9-(GuXdHFUgE%vvwr1L77VRhR<1vk$ z^ygqO{rqhsMzc}S+A=)3gRidQ_(@1~{kWZt_|?$lc_&Z=+|$MiFM)IQXTD%MU3K#Y962&@K%Qcf>E+ z$8>zBeD7)!?aTVE7J>YY764T<(K?@BDG=MG3u3U+=2cE>5CZjXSpMwfF!(%^uYtn?PGn9Y?++yk1Zdj!r z@(Zgfbmji-G=u}?IrnG>KKTxzaab$(n=8b?+GfIhQUN~-23Ye%tj9iy0>AGU&hU>K zhY~qqTkbN2tkGI(d z2z!CHBlLI7a zm8qlC8lUZ&d_x`+Z4AtgAKBP4>l>KSPIDMU(=(dUA4ch%pBl5V2klaGqf65}=M$L++qpfY#5s6PCVL^tH<3HP{H|==2F0A&@KYmrh(!ffDjmL(`+uZ ziM+~p?1}kT37_B^52oYqn!xVUolhN|_I|?1P~#0HGnJHzsyr?CC>f`=rE!0;h-8LD z;Lkhlr?Z>A***&<`5NRBY1%D;5|9O8d=?0+m;~(AuH=n;!>}=#4LWqr}jZoK=(;{ z=UyLWw^L*En<}~o!U?PEZ+RXi*tZhdL;}TOy{WIT|4MvfoG$L@1T1Z?ZF zNw?>|%pAe#`4snhauYzZQR3pO%C;C;H8lUXFXCuuzA+eV^=*QL+E12Y@4 z|8*IVSBwFxZr$6nXX_s_(M5BLQ(f)x)=n(`w*U37>+-`=>6#Ve2-*i zs|kJY3Mb6P?=FAZInXv-cVF<}(sUkITt{sJ$5>omd#E^s4D}O(c+c&;C_(WgHjL#* za(7w#6ewU$LBN**h(3r!UznoTkYKHf%#r|Knv+MG+hl}p6e1QVG99g;i*m<5_~cAa zfa#9K=Vv3kldc4JkR+@s#U>|H14amKB2KNJcU# zX_3+5YKf>GjA>zhh!)YT5ML!|v_@b|Ryl7X>l?m@I?|AhMQX$yv2(2&0R`(X@k*Z1 zO@%bV#7=RnPj`D*#@|F5hRB6O|=`JTqWZ9+5`T2P})cycG0E6?cVA z+9mlEP##_mQlGxXM}Zd@w=jT#$dmM%201aTt)YTw61LMm{F-GC52DT@Dv+52G~1v@ zblN0f3GdT7KVGk<+p^oAv`k9z{%zDbw%N;nR<_z`{wR(?dzPB!rcDP;P*o*S=wM9Y ziV>%UQ=AXu3rtxIYr`+7bIm3v!#Vqlx|0z?#t_C)A@*fN8XXkvH>GIjX?3`NAu47e zL?g3!eK!vktCsOjH%{!Go(baOOg)vGR!azt=R%fbAh`7mi0rHAmjQv-Wo1$T89m01 zNM}7!>t(TR0z&g0b}1!jo(VKc(&l4U8@S*85z2R?TA%9XkeBHv(Ke$(9PD?RSHg6Z zW#b|fO~6J`p6V$l>WhWm+p#MO5^-x=$`A6~B%XOO)^8=eP`-z?EM#d#-B*S4+qRUr zQRKqUUzz5VUjkWj9ykk#5UlU2b%&Vd)Gda^G) zN%s`;v!gIWp?&RX>5ZDsL&--oCDI#n)P>v`zFmEPOXMt=eX#_ovu zNYs&0=PqURX2JCSvgckBJ7De)yn~pvW^t+merPI~#>=t0gGEvGAFVCp@F}5&8E3_w zN4w^XCF1u~6GdGLi|0JjdCv2{(I+Zz421@PyV#LxX@Sg4aQVH2|;{QEYlV4 zI%`sUy|x#zem?yz+8_?wbsmf^y^o10BEYj?!eTR|h5pRDk~5?{6EqctzLkw}NTrPx zdsFX0vz4)F=fz_Kl3H+1mDed_zJ$@<>b(yEWMEaqt5tK-#xO2v`^herA?lPRPBwdIi#oXdnQE{4vhCRtb{*3#zw1vGF zzCk6^2CZn*N*P~L);zNj2FWVtt;7tW_NPM(Q)O(S=_xnb>SVj~lFyD-Mk;iUP9>A|?T zT6dA@=sQ3JzbvKkGP%Zk2u(pfAywH&f6_%juxN5~uq^0bMgLya zMhB-hkQ>t1E)bTkPJ4z79Imhe8k{>{J;BX;DDCwgku@oAY^d@lA*J5W05(*~{}l+{ zy|{d1G@*2o+34rW9zLS<{>|?GKcj$x+_FDhfU9-VM%xNb!1@J z*w6hZkrl|*^>lFzsfc@fcVS*?s6^XCKHq>2FBQJ-H3}`=2Odm_kdbq4YOq#d;WQ6u zeaIKUa*%^XsIi&l+Wq2LCU3L$Z#AF0F(MRJMfd&p*>N!&ha? z=hHWI+IFi6Mg7dn5j#{X%$a+|edo*RUv9b_T5M$-7r*uXu{ina_n)rr;(DF)=Fuln zo8nTXGk#qr>YTY9EKCy}6ofcjm>NA41UOlg8XZ&w5P}EqdDw6?nSFElbXa1`MpL22 z3+JYJusC_Ip2*R%Elf#}W9?E8mP7U59##C^zpB6JzU`fRpNihhKKxd>bhT%4r8rp;nX#c1#@*LIshHz5UoAYAzM@Ef!yYdbzy~$*_#d4`KxR(>wfeM zX!mA?G+|ENvq?L$l@`v82*=qvRM@vPM5M&m=>;`3Jffudm8s7>Gy@t zTlY)jdoM6-^85Ez?%4bE`Ns47+b>+cp1gVT)2vb^$7sz7vph1gRY7E?DTqw>Rxq6i z^q@nQ(!yT<<7t9RI$O+MowWnGdW#~?Sc5pRT6yJuTOQpvolE}x<-`?vw^$$D+;b;H z{^>R2`t&;a)#?Av{jvP^_n}zTs^uro`%PPYm6IiRDaih{Q$gg?s~~c&E+_)EqnUvb zX!dG<^K5XGtuX(s(_}L7LL0V}gcNLVdY^O~W+eKn?C*1wj#d`rc$?D+47M#&oen_X z1H&$=>&Zptk2!I2(X~^e`I~2~OlPdG*fF=Na*vaDe(zbLT=k75sCt8kUw?Q-*FOns zY3uzpTi5N|x9>et^0&{sNXV%+9xCf)*MGQEWAyFnL$MvFr%gW6AF*YNP^U%4+kGBK v7!T_R6&^6Xt}Mvz6C&}5fuVv&UHw1P$(b*Hr Date: Wed, 20 Jun 2018 18:52:04 -0500 Subject: [PATCH 29/88] Span-ification v2. Remove CommandToken and just operate directly on the single span. Memory allocation is now down to less than 5KB. --- .../Media/PathMarkupParser.cs | 534 ++++++++---------- 1 file changed, 238 insertions(+), 296 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 8f1d8ebc71..384c96a7d6 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -105,61 +105,64 @@ namespace Avalonia.Media while(!span.IsEmpty) { - var commandToken = CommandToken.Parse(ref span); - try + if(!ReadCommand(ref span, out var command, out var relative)) { - do - { - switch (commandToken.Command) - { - case Command.None: - break; - case Command.FillRule: - SetFillRule(commandToken); - break; - case Command.Move: - AddMove(commandToken); - break; - case Command.Line: - AddLine(commandToken); - break; - case Command.HorizontalLine: - AddHorizontalLine(commandToken); - break; - case Command.VerticalLine: - AddVerticalLine(commandToken); - break; - case Command.CubicBezierCurve: - AddCubicBezierCurve(commandToken); - break; - case Command.QuadraticBezierCurve: - AddQuadraticBezierCurve(commandToken); - break; - case Command.SmoothCubicBezierCurve: - AddSmoothCubicBezierCurve(commandToken); - break; - case Command.SmoothQuadraticBezierCurve: - AddSmoothQuadraticBezierCurve(commandToken); - break; - case Command.Arc: - AddArc(commandToken); - break; - case Command.Close: - CloseFigure(); - break; - default: - throw new NotSupportedException("Unsupported command"); - } - } while (commandToken.HasImplicitCommands); - } - catch (InvalidDataException) - { - break; + return; } - catch (NotSupportedException) + + bool initialCommand = true; + + do { - break; - } + if (!initialCommand) + { + span = ReadSeparator(span); + } + + switch (command) + { + case Command.None: + break; + case Command.FillRule: + SetFillRule(ref span); + break; + case Command.Move: + AddMove(ref span, relative); + break; + case Command.Line: + AddLine(ref span, relative); + break; + case Command.HorizontalLine: + AddHorizontalLine(ref span, relative); + break; + case Command.VerticalLine: + AddVerticalLine(ref span, relative); + break; + case Command.CubicBezierCurve: + AddCubicBezierCurve(ref span, relative); + break; + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(ref span, relative); + break; + case Command.SmoothCubicBezierCurve: + AddSmoothCubicBezierCurve(ref span, relative); + break; + case Command.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(ref span, relative); + break; + case Command.Arc: + AddArc(ref span, relative); + break; + case Command.Close: + CloseFigure(); + break; + default: + throw new NotSupportedException("Unsupported command"); + } + + initialCommand = false; + } while (PeekArgument(span)); + } if (_isOpen) @@ -180,11 +183,28 @@ namespace Avalonia.Media _isOpen = true; } - private void SetFillRule(CommandToken commandToken) + private void SetFillRule(ref ReadOnlySpan span) { - var fillRule = commandToken.ReadFillRule(); + if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1) + { + throw new InvalidDataException("Invalid fill rule."); + } + + FillRule rule; - _geometryContext.SetFillRule(fillRule); + switch (fillRule[0]) + { + case '0': + rule = FillRule.EvenOdd; + break; + case '1': + rule = FillRule.NonZero; + break; + default: + throw new InvalidDataException("Invalid fill rule"); + } + + _geometryContext.SetFillRule(rule); } private void CloseFigure() @@ -199,21 +219,22 @@ namespace Avalonia.Media _isOpen = false; } - private void AddMove(CommandToken commandToken) + private void AddMove(ref ReadOnlySpan span, bool relative) { - var currentPoint = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var currentPoint = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); _currentPoint = currentPoint; CreateFigure(); - while (commandToken.HasImplicitCommands) + while (PeekArgument(span)) { - AddLine(commandToken); + span = ReadSeparator(span); + AddLine(ref span, relative); - if (!commandToken.IsRelative) + if (!relative) { _currentPoint = currentPoint; CreateFigure(); @@ -221,11 +242,11 @@ namespace Avalonia.Media } } - private void AddLine(CommandToken commandToken) + private void AddLine(ref ReadOnlySpan span, bool relative) { - _currentPoint = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + _currentPoint = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -235,11 +256,11 @@ namespace Avalonia.Media _geometryContext.LineTo(_currentPoint); } - private void AddHorizontalLine(CommandToken commandToken) + private void AddHorizontalLine(ref ReadOnlySpan span, bool relative) { - _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) - : _currentPoint.WithX(commandToken.ReadDouble()); + _currentPoint = relative + ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y) + : _currentPoint.WithX(ReadDouble(ref span)); if (!_isOpen) { @@ -249,11 +270,11 @@ namespace Avalonia.Media _geometryContext.LineTo(_currentPoint); } - private void AddVerticalLine(CommandToken commandToken) + private void AddVerticalLine(ref ReadOnlySpan span, bool relative) { - _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) - : _currentPoint.WithY(commandToken.ReadDouble()); + _currentPoint = relative + ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span)) + : _currentPoint.WithY(ReadDouble(ref span)); if (!_isOpen) { @@ -263,21 +284,25 @@ namespace Avalonia.Media _geometryContext.LineTo(_currentPoint); } - private void AddCubicBezierCurve(CommandToken commandToken) + private void AddCubicBezierCurve(ref ReadOnlySpan span, bool relative) { - var point1 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var point1 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); - var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + span = ReadSeparator(span); + + var point2 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); _previousControlPoint = point2; - var point3 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + span = ReadSeparator(span); + + var point3 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -289,17 +314,19 @@ namespace Avalonia.Media _currentPoint = point3; } - private void AddQuadraticBezierCurve(CommandToken commandToken) + private void AddQuadraticBezierCurve(ref ReadOnlySpan span, bool relative) { - var start = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var start = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); _previousControlPoint = start; - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + span = ReadSeparator(span); + + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -311,15 +338,17 @@ namespace Avalonia.Media _currentPoint = end; } - private void AddSmoothCubicBezierCurve(CommandToken commandToken) + private void AddSmoothCubicBezierCurve(ref ReadOnlySpan span, bool relative) { - var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var point2 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + span = ReadSeparator(span); - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (_previousControlPoint != null) { @@ -338,11 +367,11 @@ namespace Avalonia.Media _currentPoint = end; } - private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) + private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan span, bool relative) { - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (_previousControlPoint != null) { @@ -359,19 +388,25 @@ namespace Avalonia.Media _currentPoint = end; } - private void AddArc(CommandToken commandToken) + private void AddArc(ref ReadOnlySpan span, bool relative) { - var size = commandToken.ReadSize(); + var size = ReadSize(ref span); - var rotationAngle = commandToken.ReadDouble(); + span = ReadSeparator(span); - var isLargeArc = commandToken.ReadBool(); + var rotationAngle = ReadDouble(ref span); + span = ReadSeparator(span); + var isLargeArc = ReadBool(ref span); - var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + span = ReadSeparator(span); + + var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + + span = ReadSeparator(span); - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -385,227 +420,134 @@ namespace Avalonia.Media _previousControlPoint = null; } - private class CommandToken + private static bool PeekArgument(ReadOnlySpan span) { - private CommandToken(Command command, bool isRelative, List arguments) - { - Command = command; - - IsRelative = isRelative; - - Arguments = arguments; - } - - public Command Command { get; } + span = SkipWhitespace(span); - public bool IsRelative { get; } + return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0])); + } - public bool HasImplicitCommands + private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) + { + remaining = SkipWhitespace(remaining); + if (remaining.IsEmpty) { - get - { - if (CurrentPosition == 0 && Arguments.Count > 0) - { - return true; - } - - return CurrentPosition < Arguments.Count - 1; - } + argument = ReadOnlySpan.Empty; + return false; } - private int CurrentPosition { get; set; } - - private List Arguments { get; } - - public static CommandToken Parse(ref ReadOnlySpan span) + var valid = false; + int i = 0; + if (remaining[i] == '-') { - if (!ReadCommand(ref span, out var command, out var isRelative)) - { - throw new InvalidDataException("No path command declared."); - } - - span = span.Slice(1); - span = SkipWhitespace(span); - - var arguments = new List(); - - while (ReadArgument(ref span, out var argument)) - { - arguments.Add(argument.ToString()); - span = ReadSeparator(span); - } - - return new CommandToken(command, isRelative, arguments); + i++; } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; - public FillRule ReadFillRule() + if (i < remaining.Length && remaining[i] == '.') { - if (CurrentPosition == Arguments.Count) - { - throw new InvalidDataException("Invalid fill rule"); - } - - var value = Arguments[CurrentPosition]; - - CurrentPosition++; - - switch (value) - { - case "0": - { - return FillRule.EvenOdd; - } - - case "1": - { - return FillRule.NonZero; - } - - default: - throw new InvalidDataException("Invalid fill rule"); - } + valid = false; + i++; } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; - public bool ReadBool() + if (!valid) { - if (CurrentPosition == Arguments.Count) - { - throw new InvalidDataException("Invalid boolean value"); - } - - var value = Arguments[CurrentPosition]; - - CurrentPosition++; - - switch (value) - { - case "1": - { - return true; - } - - case "0": - { - return false; - } - - default: - throw new InvalidDataException("Invalid boolean value"); - } + argument = ReadOnlySpan.Empty; + return false; } + argument = remaining.Slice(0, i); + remaining = remaining.Slice(i); + return true; + } - public double ReadDouble() - { - if (CurrentPosition == Arguments.Count) - { - throw new InvalidDataException("Invalid double value"); - } - - var value = Arguments[CurrentPosition]; - - CurrentPosition++; - - return double.Parse(value, CultureInfo.InvariantCulture); - } - public Size ReadSize() + private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) + { + span = SkipWhitespace(span); + if (!span.IsEmpty && span[0] == ',') { - var width = ReadDouble(); - - var height = ReadDouble(); - - return new Size(width, height); + span = span.Slice(1); } + return span; + } - public Point ReadPoint() - { - var x = ReadDouble(); - - var y = ReadDouble(); - - return new Point(x, y); - } + private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + { + int i = 0; + for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; + return span.Slice(i); + } - public Point ReadRelativePoint(Point origin) + private bool ReadBool(ref ReadOnlySpan span) + { + if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1) { - var x = ReadDouble(); - - var y = ReadDouble(); - - return new Point(origin.X + x, origin.Y + y); + throw new InvalidDataException("Invalid bool rule."); } - - private static bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + + switch (boolValue[0]) { - span = SkipWhitespace(span); - if (span.IsEmpty) - { - command = default; - relative = false; + case '0': return false; - } - - var c = span[0]; - - if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } - - relative = char.IsLower(c); - - return true; + case '1': + return true; + default: + throw new InvalidDataException("Invalid bool rule"); } + } - private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) + private double ReadDouble(ref ReadOnlySpan span) + { + if (!ReadArgument(ref span, out var doubleValue)) { - if (remaining.IsEmpty) - { - argument = ReadOnlySpan.Empty; - return false; - } + throw new InvalidDataException("Invalid double value"); + } - var valid = false; - int i = 0; - if (remaining[i] == '-') - { - i++; - } - for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture); + } - if (i < remaining.Length && remaining[i] == '.') - { - valid = false; - i++; - } - for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + private Size ReadSize(ref ReadOnlySpan span) + { + var width = ReadDouble(ref span); + span = ReadSeparator(span); + var height = ReadDouble(ref span); + return new Size(width, height); + } - if (!valid) - { - argument = ReadOnlySpan.Empty; - return false; - } - argument = remaining.Slice(0, i); - remaining = remaining.Slice(i); - return true; - } + private Point ReadPoint(ref ReadOnlySpan span) + { + var x = ReadDouble(ref span); + span = ReadSeparator(span); + var y = ReadDouble(ref span); + return new Point(x, y); + } - private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) + private Point ReadRelativePoint(ref ReadOnlySpan span, Point origin) + { + var x = ReadDouble(ref span); + span = ReadSeparator(span); + var y = ReadDouble(ref span); + return new Point(origin.X + x, origin.Y + y); + } + + private bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + { + span = SkipWhitespace(span); + if (span.IsEmpty) { - span = SkipWhitespace(span); - if (!span.IsEmpty && span[0] == ',') - { - span = span.Slice(1); - } - return SkipWhitespace(span); + command = default; + relative = false; + return false; } - - private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + var c = span[0]; + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) { - int i = 0; - for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; - return span.Slice(i); + throw new InvalidDataException("Unexpected path command '" + c + "'."); } + relative = char.IsLower(c); + span = span.Slice(1); + return true; } } } \ No newline at end of file From d3a0507f35c6951daa1cc06c4fe1c9f4e7664af5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Dec 2017 01:07:14 +0100 Subject: [PATCH 30/88] Implement custom observable for classes. Use a custom `ClassObserver` for observing changes to a control's classes. This saves shaves off about 15% memory use in ControlCatalog after cyling through all tabs. --- .../Styling/TypeNameAndClassSelector.cs | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index fb32913e7e..5d48496e8c 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; using System.Text; +using Avalonia.Collections; namespace Avalonia.Styling { @@ -122,14 +124,7 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = Observable.FromEventPattern< - NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventArgs>( - x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x) - .StartWith((EventPattern)null) - .Select(_ => Matches(control.Classes)) - .DistinctUntilChanged(); + var observable = new ClassObserver(control.Classes, _classes.Value); return new SelectorMatch(observable); } else @@ -204,5 +199,64 @@ namespace Avalonia.Styling return builder.ToString(); } + + private class ClassObserver : IObservable + { + readonly IList _match; + readonly List> _observers = new List>(); + IAvaloniaReadOnlyList _classes; + + public ClassObserver(IAvaloniaReadOnlyList classes, IList match) + { + _classes = classes; + _match = match; + } + + public IDisposable Subscribe(IObserver observer) + { + if (_observers.Count == 0) + { + _classes.CollectionChanged += ClassesChanged; + } + + _observers.Add(observer); + observer.OnNext(GetResult()); + + return Disposable.Create(() => + { + _observers.Remove(observer); + if (_observers.Count == 0) + { + _classes.CollectionChanged -= ClassesChanged; + } + }); + } + + private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Move) + { + foreach (var observer in _observers) + { + observer.OnNext(GetResult()); + } + } + } + + private bool GetResult() + { + int remaining = _match.Count; + + foreach (var c in _classes) + { + if (_match.Contains(c)) + { + --remaining; + } + } + + return remaining == 0; + } + } } } From 6d3381dd6e8992f7dd39b4c3f439f98f531775e5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Dec 2017 13:56:45 +0100 Subject: [PATCH 31/88] Make ActivatedObservable a custom observable. --- .../Styling/ActivatedObservable.cs | 123 +++++++++++++++--- 1 file changed, 102 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs index 2cd324fff4..1d9af6ed41 100644 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ b/src/Avalonia.Styling/Styling/ActivatedObservable.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive; -using System.Reactive.Linq; +using System.Collections.Generic; +using System.Reactive.Disposables; namespace Avalonia.Styling { @@ -17,8 +17,17 @@ namespace Avalonia.Styling /// value. When the activator produces false it will produce /// . /// - internal class ActivatedObservable : ObservableBase, IDescription + internal class ActivatedObservable : IObservable, IDescription { + private static readonly object NotSent = new object(); + private readonly Listener _listener; + private List> _observers; + private IDisposable _activatorSubscription; + private IDisposable _sourceSubscription; + private bool? _active; + private object _value; + private object _last = NotSent; + /// /// Initializes a new instance of the class. /// @@ -36,6 +45,7 @@ namespace Avalonia.Styling Activator = activator; Description = description; Source = source; + _listener = new Listener(this); } /// @@ -53,25 +63,96 @@ namespace Avalonia.Styling /// public IObservable Source { get; } - /// - /// Notifies the provider that an observer is to receive notifications. - /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) + public IDisposable Subscribe(IObserver observer) + { + var subscribe = _observers == null; + + _observers = _observers ?? new List>(); + _observers.Add(observer); + + if (subscribe) + { + _sourceSubscription = Source.Subscribe(_listener); + _activatorSubscription = Activator.Subscribe(_listener); + } + + return Disposable.Create(() => + { + _observers.Remove(observer); + + if (_observers.Count == 0) + { + _activatorSubscription.Dispose(); + _sourceSubscription.Dispose(); + _activatorSubscription = null; + _sourceSubscription = null; + } + }); + } + + private void NotifyCompleted() + { + foreach (var observer in _observers) + { + observer.OnCompleted(); + } + + _observers = null; + } + + private void NotifyError(Exception error) { - Contract.Requires(observer != null); - - var sourceCompleted = Source.LastOrDefaultAsync().Select(_ => Unit.Default); - var activatorCompleted = Activator.LastOrDefaultAsync().Select(_ => Unit.Default); - var completed = sourceCompleted.Merge(activatorCompleted); - - return Activator - .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) - .Select(x => x.Active ? x.Value : AvaloniaProperty.UnsetValue) - .DistinctUntilChanged() - .TakeUntil(completed) - .Subscribe(observer); + foreach (var observer in _observers) + { + observer.OnError(error); + } + + _observers = null; + } + + private void Update() + { + if (_active.HasValue) + { + var v = _active.Value ? _value : AvaloniaProperty.UnsetValue; + + if (!Equals(v, _last)) + { + foreach (var observer in _observers) + { + observer.OnNext(v); + } + + _last = v; + } + } + } + + private class Listener : IObserver, IObserver + { + private readonly ActivatedObservable _parent; + + public Listener(ActivatedObservable parent) + { + _parent = parent; + } + + void IObserver.OnCompleted() => _parent.NotifyCompleted(); + void IObserver.OnCompleted() => _parent.NotifyCompleted(); + void IObserver.OnError(Exception error) => _parent.NotifyError(error); + void IObserver.OnError(Exception error) => _parent.NotifyError(error); + + void IObserver.OnNext(bool value) + { + _parent._active = value; + _parent.Update(); + } + + void IObserver.OnNext(object value) + { + _parent._value = value; + _parent.Update(); + } } } } From de35eb9fee049179e41076de2fc441454e415959 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Dec 2017 15:56:22 +0100 Subject: [PATCH 32/88] Updated ActivatedSubject. Can be expressed more cleanly given the new `ActivatedObject`. --- .../Styling/ActivatedObservable.cs | 46 +++++++++------ .../Styling/ActivatedSubject.cs | 59 ++++++++++--------- .../ActivatedSubjectTests.cs | 10 +++- 3 files changed, 66 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs index 1d9af6ed41..3c58bb5f9d 100644 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ b/src/Avalonia.Styling/Styling/ActivatedObservable.cs @@ -24,8 +24,6 @@ namespace Avalonia.Styling private List> _observers; private IDisposable _activatorSubscription; private IDisposable _sourceSubscription; - private bool? _active; - private object _value; private object _last = NotSent; /// @@ -58,11 +56,21 @@ namespace Avalonia.Styling /// public string Description { get; } + /// + /// Gets a value indicating whether the observable is active. + /// + public bool? IsActive { get; private set; } + /// /// Gets an observable which produces the . /// public IObservable Source { get; } + /// + /// Gets the value that will be produced when is true. + /// + public object Value { get; private set; } + public IDisposable Subscribe(IObserver observer) { var subscribe = _observers == null; @@ -90,7 +98,7 @@ namespace Avalonia.Styling }); } - private void NotifyCompleted() + protected virtual void NotifyCompleted() { foreach (var observer in _observers) { @@ -100,7 +108,7 @@ namespace Avalonia.Styling _observers = null; } - private void NotifyError(Exception error) + protected virtual void NotifyError(Exception error) { foreach (var observer in _observers) { @@ -110,11 +118,23 @@ namespace Avalonia.Styling _observers = null; } + protected virtual void NotifyValue(object value) + { + Value = value; + Update(); + } + + protected virtual void NotifyActive(bool active) + { + IsActive = active; + Update(); + } + private void Update() { - if (_active.HasValue) + if (IsActive.HasValue) { - var v = _active.Value ? _value : AvaloniaProperty.UnsetValue; + var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; if (!Equals(v, _last)) { @@ -141,18 +161,8 @@ namespace Avalonia.Styling void IObserver.OnCompleted() => _parent.NotifyCompleted(); void IObserver.OnError(Exception error) => _parent.NotifyError(error); void IObserver.OnError(Exception error) => _parent.NotifyError(error); - - void IObserver.OnNext(bool value) - { - _parent._active = value; - _parent.Update(); - } - - void IObserver.OnNext(object value) - { - _parent._value = value; - _parent.Update(); - } + void IObserver.OnNext(bool value) => _parent.NotifyActive(value); + void IObserver.OnNext(object value) => _parent.NotifyValue(value); } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs index 4c2e8dde85..bcbf30649a 100644 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ b/src/Avalonia.Styling/Styling/ActivatedSubject.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; using System.Reactive.Subjects; namespace Avalonia.Styling @@ -19,9 +18,8 @@ namespace Avalonia.Styling /// internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription { - private bool? _active; private bool _completed; - private object _value; + private object _pushValue; /// /// Initializes a new instance of the class. @@ -35,7 +33,6 @@ namespace Avalonia.Styling string description) : base(activator, source, description) { - Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted); } /// @@ -46,53 +43,57 @@ namespace Avalonia.Styling get { return (ISubject)base.Source; } } - /// - /// Notifies all subscribed observers about the end of the sequence. - /// public void OnCompleted() { - if (_active.Value && !_completed) + Source.OnCompleted(); + } + + public void OnError(Exception error) + { + Source.OnError(error); + } + + public void OnNext(object value) + { + _pushValue = value; + + if (IsActive == true && !_completed) { - Source.OnCompleted(); + Source.OnNext(_pushValue); } } - /// - /// Notifies all subscribed observers with the exception. - /// - /// The exception to send to all subscribed observers. - /// is null. - public void OnError(Exception error) + protected override void NotifyCompleted() { - if (_active.Value && !_completed) + base.NotifyCompleted(); + + if (!_completed) { - Source.OnError(error); + Source.OnCompleted(); + _completed = true; } } - /// - /// Notifies all subscribed observers with the value. - /// - /// The value to send to all subscribed observers. - public void OnNext(object value) + protected override void NotifyError(Exception error) { - _value = value; + base.NotifyError(error); - if (_active.Value && !_completed) + if (!_completed) { - Source.OnNext(value); + Source.OnError(error); + _completed = true; } } - private void ActivatorChanged(bool active) + protected override void NotifyActive(bool active) { - bool first = !_active.HasValue; + bool first = !IsActive.HasValue; - _active = active; + base.NotifyActive(active); if (!first) { - Source.OnNext(active ? _value : AvaloniaProperty.UnsetValue); + Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); } } diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs index cc42e3f840..03f91d97a1 100644 --- a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs @@ -17,6 +17,7 @@ namespace Avalonia.Styling.UnitTests var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); + target.Subscribe(); target.OnNext("bar"); Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); activator.OnNext(true); @@ -36,6 +37,7 @@ namespace Avalonia.Styling.UnitTests var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); + target.Subscribe(); activator.OnCompleted(); Assert.True(source.Completed); @@ -47,10 +49,14 @@ namespace Avalonia.Styling.UnitTests var activator = new BehaviorSubject(false); var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); + var targetError = default(Exception); + var error = new Exception(); - activator.OnError(new Exception()); + target.Subscribe(_ => { }, e => targetError = e); + activator.OnError(error); - Assert.NotNull(source.Error); + Assert.Same(error, source.Error); + Assert.Same(error, targetError); } private class TestSubject : ISubject From cdba9b856d93d597e8192e00236763acab4952ee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Dec 2017 18:25:57 +0100 Subject: [PATCH 33/88] Make ActivatedValue a custom Observable. And make `ActivatedObservable` inherit from it. --- .../Styling/ActivatedObservable.cs | 125 ++------------- .../Styling/ActivatedValue.cs | 147 +++++++++++++++--- 2 files changed, 139 insertions(+), 133 deletions(-) diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs index 3c58bb5f9d..4933daed31 100644 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ b/src/Avalonia.Styling/Styling/ActivatedObservable.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Reactive.Disposables; namespace Avalonia.Styling { @@ -17,14 +15,9 @@ namespace Avalonia.Styling /// value. When the activator produces false it will produce /// . /// - internal class ActivatedObservable : IObservable, IDescription + internal class ActivatedObservable : ActivatedValue, IDescription { - private static readonly object NotSent = new object(); - private readonly Listener _listener; - private List> _observers; - private IDisposable _activatorSubscription; private IDisposable _sourceSubscription; - private object _last = NotSent; /// /// Initializes a new instance of the class. @@ -36,133 +29,49 @@ namespace Avalonia.Styling IObservable activator, IObservable source, string description) + : base(activator, AvaloniaProperty.UnsetValue, description) { - Contract.Requires(activator != null); Contract.Requires(source != null); - Activator = activator; - Description = description; Source = source; - _listener = new Listener(this); } - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - - /// - /// Gets a value indicating whether the observable is active. - /// - public bool? IsActive { get; private set; } - /// /// Gets an observable which produces the . /// public IObservable Source { get; } - /// - /// Gets the value that will be produced when is true. - /// - public object Value { get; private set; } - - public IDisposable Subscribe(IObserver observer) - { - var subscribe = _observers == null; - - _observers = _observers ?? new List>(); - _observers.Add(observer); - - if (subscribe) - { - _sourceSubscription = Source.Subscribe(_listener); - _activatorSubscription = Activator.Subscribe(_listener); - } - - return Disposable.Create(() => - { - _observers.Remove(observer); - - if (_observers.Count == 0) - { - _activatorSubscription.Dispose(); - _sourceSubscription.Dispose(); - _activatorSubscription = null; - _sourceSubscription = null; - } - }); - } + protected override ActivatorListener CreateListener() => new ValueListener(this); - protected virtual void NotifyCompleted() + protected override void Deinitialize() { - foreach (var observer in _observers) - { - observer.OnCompleted(); - } - - _observers = null; + base.Deinitialize(); + _sourceSubscription.Dispose(); + _sourceSubscription = null; } - protected virtual void NotifyError(Exception error) + protected override void Initialize() { - foreach (var observer in _observers) - { - observer.OnError(error); - } - - _observers = null; + base.Initialize(); + _sourceSubscription = Source.Subscribe((ValueListener)Listener); } protected virtual void NotifyValue(object value) { Value = value; - Update(); } - protected virtual void NotifyActive(bool active) + private class ValueListener : ActivatorListener, IObserver { - IsActive = active; - Update(); - } - - private void Update() - { - if (IsActive.HasValue) - { - var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; - - if (!Equals(v, _last)) - { - foreach (var observer in _observers) - { - observer.OnNext(v); - } - - _last = v; - } - } - } - - private class Listener : IObserver, IObserver - { - private readonly ActivatedObservable _parent; - - public Listener(ActivatedObservable parent) + public ValueListener(ActivatedObservable parent) + : base(parent) { - _parent = parent; } + protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - void IObserver.OnCompleted() => _parent.NotifyCompleted(); - void IObserver.OnCompleted() => _parent.NotifyCompleted(); - void IObserver.OnError(Exception error) => _parent.NotifyError(error); - void IObserver.OnError(Exception error) => _parent.NotifyError(error); - void IObserver.OnNext(bool value) => _parent.NotifyActive(value); - void IObserver.OnNext(object value) => _parent.NotifyValue(value); + void IObserver.OnCompleted() => Parent.NotifyCompleted(); + void IObserver.OnError(Exception error) => Parent.NotifyError(error); + void IObserver.OnNext(object value) => Parent.NotifyValue(value); } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs index 3b9324c059..683121628c 100644 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ b/src/Avalonia.Styling/Styling/ActivatedValue.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive; -using System.Reactive.Linq; +using System.Collections.Generic; +using System.Reactive.Disposables; namespace Avalonia.Styling { @@ -16,12 +16,13 @@ namespace Avalonia.Styling /// will produce the current value. When the activator /// produces false it will produce . /// - internal class ActivatedValue : ObservableBase, IDescription + internal class ActivatedValue : IObservable, IDescription { - /// - /// The activator. - /// - private readonly IObservable _activator; + private static readonly object NotSent = new object(); + private List> _observers = new List>(); + private IDisposable _activatorSubscription; + private object _value; + private object _last = NotSent; /// /// Initializes a new instance of the class. @@ -34,39 +35,135 @@ namespace Avalonia.Styling object value, string description) { - _activator = activator; + Contract.Requires(activator != null); + + Activator = activator; Value = value; Description = description; + Listener = CreateListener(); } /// - /// Gets the activated value. + /// Gets the activator observable. /// - public object Value - { - get; - } + public IObservable Activator { get; } /// /// Gets a description of the binding. /// - public string Description - { - get; - } + public string Description { get; } + + /// + /// Gets a value indicating whether the activator is active. + /// + public bool? IsActive { get; private set; } /// - /// Notifies the provider that an observer is to receive notifications. + /// Gets the value that will be produced when is true. /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) + public object Value + { + get => _value; + protected set + { + _value = value; + PublishValue(); + } + } + + protected ActivatorListener Listener { get; } + + public virtual IDisposable Subscribe(IObserver observer) + { + _observers.Add(observer); + + if (_observers.Count == 1) + { + Initialize(); + } + + return Disposable.Create(() => + { + _observers.Remove(observer); + + if (_observers.Count == 0) + { + Deinitialize(); + } + }); + } + + protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); + + protected virtual void Deinitialize() { - Contract.Requires(observer != null); + _activatorSubscription.Dispose(); + _activatorSubscription = null; + } + + protected virtual void Initialize() + { + _activatorSubscription = Activator.Subscribe(Listener); + } + + protected virtual void NotifyCompleted() + { + foreach (var observer in _observers) + { + observer.OnCompleted(); + } + + Deinitialize(); + _observers = null; + } + + protected virtual void NotifyError(Exception error) + { + foreach (var observer in _observers) + { + observer.OnError(error); + } + + Deinitialize(); + _observers = null; + } + + protected virtual void NotifyActive(bool active) + { + IsActive = active; + PublishValue(); + } + + private void PublishValue() + { + if (IsActive.HasValue) + { + var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; + + if (!Equals(v, _last)) + { + foreach (var observer in _observers) + { + observer.OnNext(v); + } + + _last = v; + } + } + } + + protected class ActivatorListener : IObserver + { + public ActivatorListener(ActivatedValue parent) + { + Parent = parent; + } + + protected ActivatedValue Parent { get; } - return _activator - .Select(active => active ? Value : AvaloniaProperty.UnsetValue) - .Subscribe(observer); + void IObserver.OnCompleted() => Parent.NotifyCompleted(); + void IObserver.OnError(Exception error) => Parent.NotifyError(error); + void IObserver.OnNext(bool value) => Parent.NotifyActive(value); } } } From fcbde80682939636c7aa49da99018252dfa1b1c2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Dec 2017 19:35:30 +0100 Subject: [PATCH 34/88] Added a couple of tests. --- src/Avalonia.Styling/Styling/ActivatedValue.cs | 4 ++++ .../ActivatedObservableTests.cs | 7 ++++--- .../ActivatedValueTests.cs | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs index 683121628c..a68ae1e435 100644 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ b/src/Avalonia.Styling/Styling/ActivatedValue.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Avalonia.Threading; namespace Avalonia.Styling { @@ -75,6 +76,9 @@ namespace Avalonia.Styling public virtual IDisposable Subscribe(IObserver observer) { + Contract.Requires(observer != null); + Dispatcher.UIThread.VerifyAccess(); + _observers.Add(observer); if (_observers.Count == 1) diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs index d5d1d3b01c..7773d4767a 100644 --- a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs @@ -54,15 +54,16 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Should_Complete_When_Activator_Completes() + public void Should_Error_When_Source_Errors() { var activator = new BehaviorSubject(false); var source = new BehaviorSubject(1); var target = new ActivatedObservable(activator, source, string.Empty); + var error = new Exception(); var completed = false; - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); + target.Subscribe(_ => { }, x => completed = true); + source.OnError(error); Assert.True(completed); } diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs index e2cc470a1e..92a7c1bd1f 100644 --- a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs @@ -40,6 +40,20 @@ namespace Avalonia.Styling.UnitTests Assert.True(completed); } + [Fact] + public void Should_Error_When_Activator_Errors() + { + var activator = new BehaviorSubject(false); + var target = new ActivatedValue(activator, 1, string.Empty); + var error = new Exception(); + var completed = false; + + target.Subscribe(_ => { }, x => completed = true); + activator.OnError(error); + + Assert.True(completed); + } + [Fact] public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed() { From 29eafc6be226ab7932fd5abe616b9f43ba48c4ff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Dec 2017 22:15:35 +0000 Subject: [PATCH 35/88] Added LightweightObservableBase class. And use it for `ActivatedValue`. --- .../Reactive/LightweightObservableBase.cs | 115 ++++++++++++++++++ .../Styling/ActivatedObservable.cs | 6 +- .../Styling/ActivatedSubject.cs | 36 +++--- .../Styling/ActivatedValue.cs | 76 +++--------- 4 files changed, 153 insertions(+), 80 deletions(-) create mode 100644 src/Avalonia.Base/Reactive/LightweightObservableBase.cs diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs new file mode 100644 index 0000000000..3ae6e5916b --- /dev/null +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + /// + /// Lightweight base class for observable implementations. + /// + /// The observable type. + /// + /// is rather heavyweight in terms of allocations and memory + /// usage. This class provides a more lightweight base for some internal observable types + /// in the Avalonia framework. + /// + public abstract class LightweightObservableBase : IObservable + { + private Exception _error; + private List> _observers = new List>(); + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + Dispatcher.UIThread.VerifyAccess(); + + if (_observers == null) + { + if (_error != null) + { + observer.OnError(_error); + } + else + { + observer.OnCompleted(); + } + + return Disposable.Empty; + } + + lock (_observers) + { + _observers.Add(observer); + } + + if (_observers.Count == 1) + { + Initialize(); + } + + Subscribed(observer); + + return Disposable.Create(() => + { + _observers?.Remove(observer); + + if (_observers?.Count == 0) + { + Deinitialize(); + _observers.TrimExcess(); + } + }); + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + + protected void PublishNext(T value) + { + lock (_observers) + { + foreach (var observer in _observers) + { + observer.OnNext(value); + } + } + } + + protected void PublishCompleted() + { + lock (_observers) + { + foreach (var observer in _observers) + { + observer.OnCompleted(); + } + + _observers = null; + } + + Deinitialize(); + } + + protected void PublishError(Exception error) + { + lock (_observers) + { + foreach (var observer in _observers) + { + observer.OnError(error); + } + + _observers = null; + } + + _error = error; + Deinitialize(); + } + + protected virtual void Subscribed(IObserver observer) + { + } + } +} diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs index 4933daed31..5b2774943a 100644 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ b/src/Avalonia.Styling/Styling/ActivatedObservable.cs @@ -9,7 +9,7 @@ namespace Avalonia.Styling /// An observable which is switched on or off according to an activator observable. /// /// - /// An has two inputs: an activator observable a + /// An has two inputs: an activator observable and a /// observable which produces the activated value. When the activator /// produces true, the will produce the current activated /// value. When the activator produces false it will produce @@ -69,8 +69,8 @@ namespace Avalonia.Styling } protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - void IObserver.OnCompleted() => Parent.NotifyCompleted(); - void IObserver.OnError(Exception error) => Parent.NotifyError(error); + void IObserver.OnCompleted() => Parent.CompletedReceived(); + void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); void IObserver.OnNext(object value) => Parent.NotifyValue(value); } } diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs index bcbf30649a..a8446c4bfb 100644 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ b/src/Avalonia.Styling/Styling/ActivatedSubject.cs @@ -10,11 +10,9 @@ namespace Avalonia.Styling /// A subject which is switched on or off according to an activator observable. /// /// - /// An has two inputs: an activator observable and either an - /// or a observable which produces the - /// activated value. When the activator produces true, the will - /// produce the current activated value. When the activator produces false it will produce - /// . + /// An extends to + /// be an . When the object is active then values + /// received via will be passed to the source subject. /// internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription { @@ -63,37 +61,37 @@ namespace Avalonia.Styling } } - protected override void NotifyCompleted() + protected override void ActiveChanged(bool active) { - base.NotifyCompleted(); + bool first = !IsActive.HasValue; - if (!_completed) + base.ActiveChanged(active); + + if (!first) { - Source.OnCompleted(); - _completed = true; + Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); } } - protected override void NotifyError(Exception error) + protected override void CompletedReceived() { - base.NotifyError(error); + base.CompletedReceived(); if (!_completed) { - Source.OnError(error); + Source.OnCompleted(); _completed = true; } } - protected override void NotifyActive(bool active) + protected override void ErrorReceived(Exception error) { - bool first = !IsActive.HasValue; + base.ErrorReceived(error); - base.NotifyActive(active); - - if (!first) + if (!_completed) { - Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); + Source.OnError(error); + _completed = true; } } diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs index a68ae1e435..5c69c2c9aa 100644 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ b/src/Avalonia.Styling/Styling/ActivatedValue.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Reactive.Disposables; -using Avalonia.Threading; +using Avalonia.Reactive; namespace Avalonia.Styling { @@ -17,10 +15,9 @@ namespace Avalonia.Styling /// will produce the current value. When the activator /// produces false it will produce . /// - internal class ActivatedValue : IObservable, IDescription + internal class ActivatedValue : LightweightObservableBase, IDescription { private static readonly object NotSent = new object(); - private List> _observers = new List>(); private IDisposable _activatorSubscription; private object _value; private object _last = NotSent; @@ -74,68 +71,35 @@ namespace Avalonia.Styling protected ActivatorListener Listener { get; } - public virtual IDisposable Subscribe(IObserver observer) + protected virtual void ActiveChanged(bool active) { - Contract.Requires(observer != null); - Dispatcher.UIThread.VerifyAccess(); - - _observers.Add(observer); - - if (_observers.Count == 1) - { - Initialize(); - } - - return Disposable.Create(() => - { - _observers.Remove(observer); - - if (_observers.Count == 0) - { - Deinitialize(); - } - }); + IsActive = active; + PublishValue(); } + protected virtual void CompletedReceived() => PublishCompleted(); + protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); - protected virtual void Deinitialize() + protected override void Deinitialize() { _activatorSubscription.Dispose(); _activatorSubscription = null; } - protected virtual void Initialize() - { - _activatorSubscription = Activator.Subscribe(Listener); - } + protected virtual void ErrorReceived(Exception error) => PublishError(error); - protected virtual void NotifyCompleted() + protected override void Initialize() { - foreach (var observer in _observers) - { - observer.OnCompleted(); - } - - Deinitialize(); - _observers = null; + _activatorSubscription = Activator.Subscribe(Listener); } - protected virtual void NotifyError(Exception error) + protected override void Subscribed(IObserver observer) { - foreach (var observer in _observers) + if (IsActive == true) { - observer.OnError(error); + observer.OnNext(Value); } - - Deinitialize(); - _observers = null; - } - - protected virtual void NotifyActive(bool active) - { - IsActive = active; - PublishValue(); } private void PublishValue() @@ -146,11 +110,7 @@ namespace Avalonia.Styling if (!Equals(v, _last)) { - foreach (var observer in _observers) - { - observer.OnNext(v); - } - + PublishNext(v); _last = v; } } @@ -165,9 +125,9 @@ namespace Avalonia.Styling protected ActivatedValue Parent { get; } - void IObserver.OnCompleted() => Parent.NotifyCompleted(); - void IObserver.OnError(Exception error) => Parent.NotifyError(error); - void IObserver.OnNext(bool value) => Parent.NotifyActive(value); + void IObserver.OnCompleted() => Parent.CompletedReceived(); + void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); + void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); } } } From dadc30d84ebfbb257c0a2625937efc781b525b49 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Dec 2017 00:26:36 +0000 Subject: [PATCH 36/88] Use lightweight observable for ExpressionObserver. --- .../Data/Core/ExpressionObserver.cs | 105 ++++++++---------- .../Reactive/LightweightObservableBase.cs | 69 ++++++++---- .../Styling/ActivatedValue.cs | 4 +- 3 files changed, 98 insertions(+), 80 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 14bc09f5b7..3a25407133 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -4,18 +4,19 @@ using System; using System.Collections.Generic; using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Data.Core { /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : ObservableBase, IDescription + public class ExpressionObserver : LightweightObservableBase, + IDescription, + IObserver { /// /// An ordered collection of property accessor plugins that can be used to customize @@ -54,9 +55,10 @@ namespace Avalonia.Data.Core private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private readonly Subject _finished; - private readonly object _root; - private IObservable _result; + private IDisposable _nodeSubscription; + private object _root; + private IDisposable _rootSubscription; + private WeakReference _value; /// /// Initializes a new instance of the class. @@ -107,7 +109,6 @@ namespace Avalonia.Data.Core Expression = expression; Description = description ?? expression; _node = Parse(expression, enableDataValidation); - _finished = new Subject(); _root = rootObservable; } @@ -135,8 +136,6 @@ namespace Avalonia.Data.Core Expression = expression; Description = description ?? expression; _node = Parse(expression, enableDataValidation); - _finished = new Subject(); - _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } @@ -203,27 +202,42 @@ namespace Avalonia.Data.Core } } - /// - protected override IDisposable SubscribeCore(IObserver observer) + void IObserver.OnNext(object value) { - if (_result == null) - { - var source = (IObservable)_node; + var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; + broken?.Commit(Description); + _value = new WeakReference(value); + PublishNext(value); + } - if (_finished != null) - { - source = source.TakeUntil(_finished); - } + void IObserver.OnCompleted() + { + } - _result = Observable.Using(StartRoot, _ => source) - .Select(ToWeakReference) - .Publish(UninitializedValue) - .RefCount() - .Where(x => x != UninitializedValue) - .Select(Translate); - } + void IObserver.OnError(Exception error) + { + } + + protected override void Initialize() + { + _value = null; + _nodeSubscription = _node.Subscribe(this); + StartRoot(); + } + + protected override void Deinitialize() + { + _rootSubscription?.Dispose(); + _nodeSubscription?.Dispose(); + _rootSubscription = _nodeSubscription = null; + } - return _result.Subscribe(observer); + protected override void Subscribed(IObserver observer, bool first) + { + if (!first && _value != null && _value.TryGetTarget(out var value)) + { + observer.OnNext(value); + } } private static ExpressionNode Parse(string expression, bool enableDataValidation) @@ -238,42 +252,19 @@ namespace Avalonia.Data.Core } } - private static object ToWeakReference(object o) + private void StartRoot() { - return o is BindingNotification ? o : new WeakReference(o); - } - - private object Translate(object o) - { - if (o is WeakReference weak) + if (_root is IObservable observable) { - return weak.Target; + _rootSubscription = observable.Subscribe( + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + x => PublishCompleted(), + () => PublishCompleted()); } - else if (BindingNotification.ExtractError(o) is MarkupBindingChainException broken) - { - broken.Commit(Description); - } - - return o; - } - - private IDisposable StartRoot() - { - switch (_root) + else { - case IObservable observable: - return observable.Subscribe( - x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), - _ => _finished.OnNext(Unit.Default), - () => _finished.OnNext(Unit.Default)); - case WeakReference weak: - _node.Target = weak; - break; - default: - throw new AvaloniaInternalException("The ExpressionObserver._root member should only be either an observable or WeakReference."); + _node.Target = (WeakReference)_root; } - - return Disposable.Empty; } } } diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 3ae6e5916b..ced2c03b01 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -39,26 +39,34 @@ namespace Avalonia.Reactive return Disposable.Empty; } + var first = _observers.Count == 0; + lock (_observers) { _observers.Add(observer); } - if (_observers.Count == 1) + if (first) { Initialize(); } - Subscribed(observer); + Subscribed(observer, first); return Disposable.Create(() => { - _observers?.Remove(observer); - - if (_observers?.Count == 0) + if (_observers != null) { - Deinitialize(); - _observers.TrimExcess(); + lock (_observers) + { + _observers?.Remove(observer); + + if (_observers?.Count == 0) + { + Deinitialize(); + _observers.TrimExcess(); + } + } } }); } @@ -68,9 +76,16 @@ namespace Avalonia.Reactive protected void PublishNext(T value) { - lock (_observers) + if (_observers != null) { - foreach (var observer in _observers) + IObserver[] observers; + + lock (_observers) + { + observers = _observers.ToArray(); + } + + foreach (var observer in observers) { observer.OnNext(value); } @@ -79,36 +94,48 @@ namespace Avalonia.Reactive protected void PublishCompleted() { - lock (_observers) + if (_observers != null) { - foreach (var observer in _observers) + IObserver[] observers; + + lock (_observers) + { + observers = _observers.ToArray(); + _observers = null; + } + + foreach (var observer in observers) { observer.OnCompleted(); } - _observers = null; + Deinitialize(); } - - Deinitialize(); } protected void PublishError(Exception error) { - lock (_observers) + if (_observers != null) { - foreach (var observer in _observers) + IObserver[] observers; + + lock (_observers) + { + observers = _observers.ToArray(); + _observers = null; + } + + foreach (var observer in observers) { observer.OnError(error); } - _observers = null; + _error = error; + Deinitialize(); } - - _error = error; - Deinitialize(); } - protected virtual void Subscribed(IObserver observer) + protected virtual void Subscribed(IObserver observer, bool first) { } } diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs index 5c69c2c9aa..908d89b751 100644 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ b/src/Avalonia.Styling/Styling/ActivatedValue.cs @@ -94,9 +94,9 @@ namespace Avalonia.Styling _activatorSubscription = Activator.Subscribe(Listener); } - protected override void Subscribed(IObserver observer) + protected override void Subscribed(IObserver observer, bool first) { - if (IsActive == true) + if (IsActive == true && !first) { observer.OnNext(Value); } From 0f1664a84672f49a85f26a7e65718c631d8c81c4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Dec 2017 21:05:17 +0000 Subject: [PATCH 37/88] Use custom rx for observing properties. --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 46 ++----------------- .../Reactive/AvaloniaObservable.cs | 42 ----------------- .../Reactive/AvaloniaPropertyObservable.cs | 46 +++++++++++++++++++ 3 files changed, 51 insertions(+), 83 deletions(-) delete mode 100644 src/Avalonia.Base/Reactive/AvaloniaObservable.cs create mode 100644 src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 1da2ecb942..71e6b9474f 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -41,27 +41,7 @@ namespace Avalonia Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaObservable( - observer => - { - EventHandler handler = (s, e) => - { - if (e.Property == property) - { - observer.OnNext(e.NewValue); - } - }; - - observer.OnNext(o.GetValue(property)); - - o.PropertyChanged += handler; - - return Disposable.Create(() => - { - o.PropertyChanged -= handler; - }); - }, - GetDescription(o, property)); + return new AvaloniaPropertyObservable(o, property); } /// @@ -79,7 +59,7 @@ namespace Avalonia Contract.Requires(o != null); Contract.Requires(property != null); - return o.GetObservable((AvaloniaProperty)property).Cast(); + return new AvaloniaPropertyObservable(o, property); } /// @@ -100,25 +80,9 @@ namespace Avalonia Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaObservable>( - observer => - { - EventHandler handler = (s, e) => - { - if (e.Property == property) - { - observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue)); - } - }; - - o.PropertyChanged += handler; - - return Disposable.Create(() => - { - o.PropertyChanged -= handler; - }); - }, - GetDescription(o, property)); + return new AvaloniaPropertyObservable(o, property) + .Buffer(2, 1) + .Select(x => Tuple.Create(x[0], x[1])); } /// diff --git a/src/Avalonia.Base/Reactive/AvaloniaObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaObservable.cs deleted file mode 100644 index b59d8e5226..0000000000 --- a/src/Avalonia.Base/Reactive/AvaloniaObservable.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using System.Reactive.Disposables; - -namespace Avalonia.Reactive -{ - /// - /// An with an additional description. - /// - /// The type of the elements in the sequence. - public class AvaloniaObservable : ObservableBase, IDescription - { - private readonly Func, IDisposable> _subscribe; - - /// - /// Initializes a new instance of the class. - /// - /// The subscribe function. - /// The description of the observable. - public AvaloniaObservable(Func, IDisposable> subscribe, string description) - { - Contract.Requires(subscribe != null); - - _subscribe = subscribe; - Description = description; - } - - /// - /// Gets the description of the observable. - /// - public string Description { get; } - - /// - protected override IDisposable SubscribeCore(IObserver observer) - { - return _subscribe(observer) ?? Disposable.Empty; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs new file mode 100644 index 0000000000..c19423b623 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Reactive +{ + public class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + private T _value; + + public AvaloniaPropertyObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = target; + _property = property; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + _value = (T)_target.GetValue(_property); + _target.PropertyChanged += PropertyChanged; + } + + protected override void Deinitialize() + { + _target.PropertyChanged -= PropertyChanged; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_value); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + _value = (T)e.NewValue; + PublishNext(_value); + } + } + } +} From 34474af8350f0312aa85f287ddace77db8a05e16 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Dec 2017 11:54:42 +0000 Subject: [PATCH 38/88] Make AvaloniaPropertyObservable use weak refs. And remove `WeakPropertyChangedObservable`/`GetWeakObservable`. --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 23 ++--- .../Plugins/AvaloniaPropertyAccessorPlugin.cs | 2 +- .../Reactive/AvaloniaPropertyObservable.cs | 17 ++-- .../Reactive/WeakPropertyChangedObservable.cs | 85 ------------------- 4 files changed, 19 insertions(+), 108 deletions(-) delete mode 100644 src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 71e6b9474f..75e56c7186 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -36,6 +36,9 @@ namespace Avalonia /// An observable which fires immediately with the current value of the property on the /// object and subsequently each time the property value changes. /// + /// + /// The subscription to is created using a weak reference. + /// public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); @@ -54,6 +57,9 @@ namespace Avalonia /// An observable which fires immediately with the current value of the property on the /// object and subsequently each time the property value changes. /// + /// + /// The subscription to is created using a weak reference. + /// public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); @@ -130,23 +136,6 @@ namespace Avalonia o.GetObservable(property)); } - /// - /// Gets a weak observable for a . - /// - /// The object. - /// The property. - /// An observable. - public static IObservable GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property) - { - Contract.Requires(o != null); - Contract.Requires(property != null); - - return new WeakPropertyChangedObservable( - new WeakReference(o), - property, - GetDescription(o, property)); - } - /// /// Binds a property on an to an . /// diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 8cbcaa8233..48edb218dc 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -153,7 +153,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore(IObserver observer) { - _subscription = Instance?.GetWeakObservable(_property).Subscribe(observer); + _subscription = Instance?.GetObservable(_property).Subscribe(observer); } } } diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index c19423b623..0e07065d6c 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -1,10 +1,11 @@ using System; +using Avalonia.Utilities; namespace Avalonia.Reactive { public class AvaloniaPropertyObservable : LightweightObservableBase, IDescription { - private readonly IAvaloniaObject _target; + private readonly WeakReference _target; private readonly AvaloniaProperty _property; private T _value; @@ -12,7 +13,7 @@ namespace Avalonia.Reactive IAvaloniaObject target, AvaloniaProperty property) { - _target = target; + _target = new WeakReference(target); _property = property; } @@ -20,13 +21,19 @@ namespace Avalonia.Reactive protected override void Initialize() { - _value = (T)_target.GetValue(_property); - _target.PropertyChanged += PropertyChanged; + if (_target.TryGetTarget(out var target)) + { + _value = (T)target.GetValue(_property); + target.PropertyChanged += PropertyChanged; + } } protected override void Deinitialize() { - _target.PropertyChanged -= PropertyChanged; + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } } protected override void Subscribed(IObserver observer, bool first) diff --git a/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs b/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs deleted file mode 100644 index aa5f553865..0000000000 --- a/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Utilities; - -namespace Avalonia.Reactive -{ - internal class WeakPropertyChangedObservable : ObservableBase, - IWeakSubscriber, IDescription - { - private WeakReference _sourceReference; - private readonly AvaloniaProperty _property; - private readonly Subject _changed = new Subject(); - - private int _count; - - public WeakPropertyChangedObservable( - WeakReference source, - AvaloniaProperty property, - string description) - { - _sourceReference = source; - _property = property; - Description = description; - } - - public string Description { get; } - - public void OnEvent(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == _property) - { - _changed.OnNext(e.NewValue); - } - } - - protected override IDisposable SubscribeCore(IObserver observer) - { - IAvaloniaObject instance; - - if (_sourceReference.TryGetTarget(out instance)) - { - if (_count++ == 0) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.PropertyChanged), - this); - } - - observer.OnNext(instance.GetValue(_property)); - - return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed) - .Subscribe(observer); - } - else - { - _changed.OnCompleted(); - observer.OnCompleted(); - return Disposable.Empty; - } - } - - private void DecrementCount() - { - if (--_count == 0) - { - IAvaloniaObject instance; - - if (_sourceReference.TryGetTarget(out instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.PropertyChanged), - this); - } - } - } - } -} From 3150d3f10aa9b22f06b0f9ecee16dc8f60f680f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Dec 2017 12:29:01 +0000 Subject: [PATCH 39/88] Added GetPropertyChangedObservable. Replaced `GetObservableWithHistory` with `GetPropertyChangedObservable`. --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 18 ++++---- .../AvaloniaPropertyChangedObservable.cs | 46 +++++++++++++++++++ .../Reactive/AvaloniaPropertyObservable.cs | 3 +- .../Mixins/ContentControlMixin.cs | 40 ++++++---------- 4 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 75e56c7186..393482cccf 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -69,26 +69,24 @@ namespace Avalonia } /// - /// Gets an observable for a . + /// Gets an observable that listens for property changed events for an + /// . /// /// The object. - /// The type of the property. /// The property. /// - /// An observable which when subscribed pushes the old and new values of the property each - /// time it is changed. Note that the observable returned from this method does not fire - /// with the current value of the property immediately. + /// An observable which when subscribed pushes the property changed event args + /// each time a event is raised + /// for the specified property. /// - public static IObservable> GetObservableWithHistory( + public static IObservable GetPropertyChangedObservable( this IAvaloniaObject o, - AvaloniaProperty property) + AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaPropertyObservable(o, property) - .Buffer(2, 1) - .Select(x => Tuple.Create(x[0], x[1])); + return new AvaloniaPropertyChangedObservable(o, property); } /// diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs new file mode 100644 index 0000000000..5ef0d25133 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyChangedObservable : + LightweightObservableBase, + IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyChangedObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(e); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 0e07065d6c..4385ab13ef 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -1,9 +1,8 @@ using System; -using Avalonia.Utilities; namespace Avalonia.Reactive { - public class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index daaf3fd5cf..95193c0432 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -49,11 +49,9 @@ namespace Avalonia.Controls.Mixins Contract.Requires(content != null); Contract.Requires(logicalChildrenSelector != null); - EventHandler templateApplied = (s, ev) => + void TemplateApplied(object s, RoutedEventArgs ev) { - var sender = s as TControl; - - if (sender != null) + if (s is TControl sender) { var e = (TemplateAppliedEventArgs)ev; var presenter = (IControl)e.NameScope.Find(presenterName); @@ -64,12 +62,12 @@ namespace Avalonia.Controls.Mixins var logicalChildren = logicalChildrenSelector(sender); var subscription = presenter - .GetObservableWithHistory(ContentPresenter.ChildProperty) - .Subscribe(child => UpdateLogicalChild( + .GetPropertyChangedObservable(ContentPresenter.ChildProperty) + .Subscribe(c => UpdateLogicalChild( sender, - logicalChildren, - child.Item1, - child.Item2)); + logicalChildren, + c.OldValue, + c.NewValue)); UpdateLogicalChild( sender, @@ -80,18 +78,16 @@ namespace Avalonia.Controls.Mixins subscriptions.Value.Add(sender, subscription); } } - }; + } TemplatedControl.TemplateAppliedEvent.AddClassHandler( typeof(TControl), - templateApplied, + TemplateApplied, RoutingStrategies.Direct); content.Changed.Subscribe(e => { - var sender = e.Sender as TControl; - - if (sender != null) + if (e.Sender is TControl sender) { var logicalChildren = logicalChildrenSelector(sender); UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue); @@ -100,9 +96,7 @@ namespace Avalonia.Controls.Mixins Control.TemplatedParentProperty.Changed.Subscribe(e => { - var sender = e.Sender as TControl; - - if (sender != null) + if (e.Sender is TControl sender) { var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl; logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent); @@ -111,13 +105,9 @@ namespace Avalonia.Controls.Mixins TemplatedControl.TemplateProperty.Changed.Subscribe(e => { - var sender = e.Sender as TControl; - - if (sender != null) + if (e.Sender is TControl sender) { - IDisposable subscription; - - if (subscriptions.Value.TryGetValue(sender, out subscription)) + if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription)) { subscription.Dispose(); subscriptions.Value.Remove(sender); @@ -134,9 +124,7 @@ namespace Avalonia.Controls.Mixins { if (oldValue != newValue) { - var child = oldValue as IControl; - - if (child != null) + if (oldValue is IControl child) { logicalChildren.Remove(child); } From 360a2603c3285482f7d981c417d4b66e6cfafab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Sat, 26 May 2018 00:36:39 +0200 Subject: [PATCH 40/88] Fix LightweightObservableBase's concurrency, save 1 allocation. --- .../Reactive/LightweightObservableBase.cs | 122 +++++++++++++----- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index ced2c03b01..a2786d63da 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reactive; using System.Reactive.Disposables; +using System.Threading; using Avalonia.Threading; namespace Avalonia.Reactive @@ -25,25 +26,35 @@ namespace Avalonia.Reactive Contract.Requires(observer != null); Dispatcher.UIThread.VerifyAccess(); - if (_observers == null) + var first = false; + + for (; ; ) { - if (_error != null) - { - observer.OnError(_error); - } - else + if (Volatile.Read(ref _observers) == null) { - observer.OnCompleted(); - } + if (_error != null) + { + observer.OnError(_error); + } + else + { + observer.OnCompleted(); + } - return Disposable.Empty; - } + return Disposable.Empty; + } - var first = _observers.Count == 0; + lock (this) + { + if (_observers == null) + { + continue; + } - lock (_observers) - { - _observers.Add(observer); + first = _observers.Count == 0; + _observers.Add(observer); + break; + } } if (first) @@ -53,22 +64,57 @@ namespace Avalonia.Reactive Subscribed(observer, first); - return Disposable.Create(() => + return new RemoveObserver(this, observer); + } + + void Remove(IObserver observer) + { + if (Volatile.Read(ref _observers) != null) { - if (_observers != null) + lock (this) { - lock (_observers) + var observers = _observers; + + if (observers != null) { - _observers?.Remove(observer); + observers.Remove(observer); - if (_observers?.Count == 0) + if (observers.Count == 0) + { + observers.TrimExcess(); + } + else { - Deinitialize(); - _observers.TrimExcess(); + return; } + } else + { + return; } } - }); + + Deinitialize(); + } + } + + sealed class RemoveObserver : IDisposable + { + LightweightObservableBase _parent; + + IObserver _observer; + + public RemoveObserver(LightweightObservableBase parent, IObserver observer) + { + _parent = parent; + Volatile.Write(ref _observer, observer); + } + + public void Dispose() + { + var observer = _observer; + Interlocked.Exchange(ref _parent, null)?.Remove(observer); + _observer = null; + } } protected abstract void Initialize(); @@ -76,12 +122,16 @@ namespace Avalonia.Reactive protected void PublishNext(T value) { - if (_observers != null) + if (Volatile.Read(ref _observers) != null) { IObserver[] observers; - lock (_observers) + lock (this) { + if (_observers == null) + { + return; + } observers = _observers.ToArray(); } @@ -94,14 +144,18 @@ namespace Avalonia.Reactive protected void PublishCompleted() { - if (_observers != null) + if (Volatile.Read(ref _observers) != null) { IObserver[] observers; - lock (_observers) + lock (this) { + if (_observers == null) + { + return; + } observers = _observers.ToArray(); - _observers = null; + Volatile.Write(ref _observers, null); } foreach (var observer in observers) @@ -115,14 +169,21 @@ namespace Avalonia.Reactive protected void PublishError(Exception error) { - if (_observers != null) + if (Volatile.Read(ref _observers) != null) { + IObserver[] observers; - lock (_observers) + lock (this) { + if (_observers == null) + { + return; + } + + _error = error; observers = _observers.ToArray(); - _observers = null; + Volatile.Write(ref _observers, null); } foreach (var observer in observers) @@ -130,7 +191,6 @@ namespace Avalonia.Reactive observer.OnError(error); } - _error = error; Deinitialize(); } } From 9b0aff8cc1c0a1ed18333355459394aee75b8268 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 01:14:49 +0200 Subject: [PATCH 41/88] Use LightweightObservableBase in TypeNameAndClassSelector --- .../Styling/TypeNameAndClassSelector.cs | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 5d48496e8c..93d4e14f27 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -4,12 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Reflection; using System.Text; using Avalonia.Collections; +using Avalonia.Reactive; namespace Avalonia.Styling { @@ -200,10 +198,9 @@ namespace Avalonia.Styling return builder.ToString(); } - private class ClassObserver : IObservable + private class ClassObserver : LightweightObservableBase { readonly IList _match; - readonly List> _observers = new List>(); IAvaloniaReadOnlyList _classes; public ClassObserver(IAvaloniaReadOnlyList classes, IList match) @@ -212,34 +209,19 @@ namespace Avalonia.Styling _match = match; } - public IDisposable Subscribe(IObserver observer) - { - if (_observers.Count == 0) - { - _classes.CollectionChanged += ClassesChanged; - } + protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; + protected override void Initialize() => _classes.CollectionChanged += ClassesChanged; - _observers.Add(observer); + protected override void Subscribed(IObserver observer, bool first) + { observer.OnNext(GetResult()); - - return Disposable.Create(() => - { - _observers.Remove(observer); - if (_observers.Count == 0) - { - _classes.CollectionChanged -= ClassesChanged; - } - }); } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action != NotifyCollectionChangedAction.Move) { - foreach (var observer in _observers) - { - observer.OnNext(GetResult()); - } + PublishNext(GetResult()); } } From de325ec501f2104ea2870e92dd5ea2ef792fec5d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 01:44:59 +0200 Subject: [PATCH 42/88] Use LightweightObservableBase for WeakCollectionChangedObservable --- .../NotifyCollectionChangedExtensions.cs | 59 ++++++------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index d295cb91ce..3a355bcb48 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -2,13 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Collections @@ -43,9 +39,8 @@ namespace Avalonia.Collections Contract.Requires(collection != null); Contract.Requires(handler != null); - return - collection.GetWeakCollectionChangedObservable() - .Subscribe(e => handler.Invoke(collection, e)); + return collection.GetWeakCollectionChangedObservable() + .Subscribe(e => handler(collection, e)); } /// @@ -63,18 +58,13 @@ namespace Avalonia.Collections Contract.Requires(collection != null); Contract.Requires(handler != null); - return - collection.GetWeakCollectionChangedObservable() - .Subscribe(handler); + return collection.GetWeakCollectionChangedObservable().Subscribe(handler); } - private class WeakCollectionChangedObservable : ObservableBase, + private class WeakCollectionChangedObservable : LightweightObservableBase, IWeakSubscriber { private WeakReference _sourceReference; - private readonly Subject _changed = new Subject(); - - private int _count; public WeakCollectionChangedObservable(WeakReference source) { @@ -83,43 +73,28 @@ namespace Avalonia.Collections public void OnEvent(object sender, NotifyCollectionChangedEventArgs e) { - _changed.OnNext(e); + PublishNext(e); } - protected override IDisposable SubscribeCore(IObserver observer) + protected override void Initialize() { if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) { - if (_count++ == 0) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.CollectionChanged), - this); - } - - return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed) - .Subscribe(observer); - } - else - { - _changed.OnCompleted(); - observer.OnCompleted(); - return Disposable.Empty; + WeakSubscriptionManager.Subscribe( + instance, + nameof(instance.CollectionChanged), + this); } } - private void DecrementCount() + protected override void Deinitialize() { - if (--_count == 0) + if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) { - if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakSubscriptionManager.Unsubscribe( + instance, + nameof(instance.CollectionChanged), + this); } } } From a3dea23560c04c1d603f91af1d6fe65e0e949254 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 02:28:12 +0200 Subject: [PATCH 43/88] Use LightweightObservableBase for BindingExpression. --- .../Data/Core/BindingExpression.cs | 43 ++++++++++++++++--- .../Data/Core/BindingExpressionTests.cs | 15 +++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 4b41d1568c..911c621fb7 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Data.Converters; using Avalonia.Logging; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core @@ -15,13 +16,14 @@ namespace Avalonia.Data.Core /// Binds to an expression on an object using a type value converter to convert the values /// that are send and received. /// - public class BindingExpression : ISubject, IDescription + public class BindingExpression : LightweightObservableBase, ISubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; private readonly BindingPriority _priority; - private readonly Subject _errors = new Subject(); + InnerListener _innerListener; + WeakReference _value; /// /// Initializes a new instance of the class. @@ -139,7 +141,7 @@ namespace Avalonia.Data.Core "IValueConverter should not return non-errored BindingNotification."); } - _errors.OnNext(notification); + PublishNext(notification); if (_fallbackValue != AvaloniaProperty.UnsetValue) { @@ -170,12 +172,18 @@ namespace Avalonia.Data.Core } } - /// - public IDisposable Subscribe(IObserver observer) + protected override void Initialize() => _innerListener = new InnerListener(this); + protected override void Deinitialize() => _innerListener.Dispose(); + + protected override void Subscribed(IObserver observer, bool first) { - return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); + if (!first && _value != null && _value.TryGetTarget(out var val) == true) + { + observer.OnNext(val); + } } + /// private object ConvertValue(object value) { var notification = value as BindingNotification; @@ -301,5 +309,28 @@ namespace Avalonia.Data.Core return a; } + + public class InnerListener : IObserver, IDisposable + { + private readonly BindingExpression _owner; + private readonly IDisposable _dispose; + + public InnerListener(BindingExpression owner) + { + _owner = owner; + _dispose = owner._inner.Subscribe(this); + } + + public void Dispose() => _dispose.Dispose(); + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + + public void OnNext(object value) + { + var converted = _owner.ConvertValue(value); + _owner._value = new WeakReference(converted); + _owner.PublishNext(converted); + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 6b71d28e22..81acd6a087 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -337,6 +337,21 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public void Second_Subscription_Should_Fire_Immediately() + { + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + object result = null; + + target.Subscribe(); + target.Subscribe(x => result = x); + + Assert.Equal("foo", result); + + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _stringValue; From 365aa42b4ab1396561f94f148fe024f494a9c4e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 13:10:25 +0200 Subject: [PATCH 44/88] Use custom rx for binding update signal. --- .../Data/Core/BindingExpression.cs | 2 +- .../SingleSubscriberObservableBase.cs | 79 +++++++++++++++++++ .../Styling/StyleActivator.cs | 4 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 44 ++++++++--- 4 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 911c621fb7..c4ffa839e0 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -14,7 +14,7 @@ namespace Avalonia.Data.Core { /// /// Binds to an expression on an object using a type value converter to convert the values - /// that are send and received. + /// that are sent and received. /// public class BindingExpression : LightweightObservableBase, ISubject, IDescription { diff --git a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs new file mode 100644 index 0000000000..af48df177e --- /dev/null +++ b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs @@ -0,0 +1,79 @@ +using System; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + public abstract class SingleSubscriberObservableBase : IObservable, IDisposable + { + private Exception _error; + private IObserver _observer; + private bool _completed; + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + Dispatcher.UIThread.VerifyAccess(); + + if (_observer != null) + { + throw new InvalidOperationException("The observable can only be subscribed once."); + } + + if (_error != null) + { + observer.OnError(_error); + } + else if (_completed) + { + observer.OnCompleted(); + } + else + { + _observer = observer; + Subscribed(); + } + + return this; + } + + void IDisposable.Dispose() + { + Unsubscribed(); + _observer = null; + } + + protected abstract void Unsubscribed(); + + protected void PublishNext(T value) + { + if (_observer != null) + { + _observer?.OnNext(value); + } + } + + protected void PublishCompleted() + { + if (_observer != null) + { + _observer?.OnCompleted(); + _completed = true; + Unsubscribed(); + _observer = null; + } + } + + protected void PublishError(Exception error) + { + if (_observer != null) + { + _observer?.OnError(error); + _error = error; + Unsubscribed(); + _observer = null; + } + } + + protected abstract void Subscribed(); + } +} diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs index 5bdea9f42e..63945037d8 100644 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ b/src/Avalonia.Styling/Styling/StyleActivator.cs @@ -48,8 +48,8 @@ namespace Avalonia.Styling else { return inputs.CombineLatest() - .Select(values => values.Any(x => x)) - .DistinctUntilChanged(); + .Select(values => values.Any(x => x)) + .DistinctUntilChanged(); } } } diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 96fc2986e8..ef5ebb9844 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -5,11 +5,10 @@ using System; using System.Linq; using System.Reactive; using System.Reactive.Linq; -using System.Reflection; -using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Data @@ -190,13 +189,10 @@ namespace Avalonia.Data if (!targetIsDataContext) { - var update = target.GetObservable(StyledElement.DataContextProperty) - .Skip(1) - .Select(_ => Unit.Default); var result = new ExpressionObserver( () => target.GetValue(StyledElement.DataContextProperty), path, - update, + new UpdateSignal(target, StyledElement.DataContextProperty), enableDataValidation); return result; @@ -278,14 +274,10 @@ namespace Avalonia.Data { Contract.Requires(target != null); - var update = target.GetObservable(StyledElement.TemplatedParentProperty) - .Skip(1) - .Select(_ => Unit.Default); - var result = new ExpressionObserver( () => target.GetValue(StyledElement.TemplatedParentProperty), path, - update, + new UpdateSignal(target, StyledElement.TemplatedParentProperty), enableDataValidation); return result; @@ -306,5 +298,35 @@ namespace Avalonia.Data Observable.Return((object)null); }).Switch(); } + + private class UpdateSignal : SingleSubscriberObservableBase + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + + public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) + { + _target = target; + _property = property; + } + + protected override void Subscribed() + { + _target.PropertyChanged += PropertyChanged; + } + + protected override void Unsubscribed() + { + _target.PropertyChanged -= PropertyChanged; + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(Unit.Default); + } + } + } } } From b488ca179a0b72aa074c24d7948fe78f380af69a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 13:32:05 +0200 Subject: [PATCH 45/88] Use custom class for direct binding subscriptions. --- src/Avalonia.Base/AvaloniaObject.cs | 53 ++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48e72db126..baca1494be 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -10,6 +10,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; @@ -38,7 +39,7 @@ namespace Avalonia /// Maintains a list of direct property binding subscriptions so that the binding source /// doesn't get collected. /// - private List _directBindings; + private List _directBindings; /// /// Event handler for implementation. @@ -359,25 +360,12 @@ namespace Avalonia property, description); - IDisposable subscription = null; - if (_directBindings == null) { - _directBindings = new List(); + _directBindings = new List(); } - subscription = source - .Select(x => CastOrDefault(x, property.PropertyType)) - .Do(_ => { }, () => _directBindings.Remove(subscription)) - .Subscribe(x => SetDirectValue(property, x)); - - _directBindings.Add(subscription); - - return Disposable.Create(() => - { - subscription.Dispose(); - _directBindings.Remove(subscription); - }); + return new DirectBindingSubscription(this, property, source); } else { @@ -908,5 +896,38 @@ namespace Avalonia value, priority); } + + private class DirectBindingSubscription : IObserver, IDisposable + { + readonly AvaloniaObject _owner; + readonly AvaloniaProperty _property; + IDisposable _subscription; + + public DirectBindingSubscription( + AvaloniaObject owner, + AvaloniaProperty property, + IObservable source) + { + _owner = owner; + _property = property; + _owner._directBindings.Add(this); + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription.Dispose(); + _owner._directBindings.Remove(this); + } + + public void OnCompleted() => Dispose(); + public void OnError(Exception error) => Dispose(); + + public void OnNext(object value) + { + var castValue = CastOrDefault(value, _property.PropertyType); + _owner.SetDirectValue(_property, castValue); + } + } } } From a8d5dc1da4e49d57d241bc21beac4df7412d4b70 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 14:13:58 +0200 Subject: [PATCH 46/88] Use custom rx for ResourceObservable. --- .../Controls/ResourceProviderExtensions.cs | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs index 1f25fa132d..1586c61185 100644 --- a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Reactive; using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -55,11 +56,39 @@ namespace Avalonia.Controls public static IObservable GetResourceObservable(this IResourceNode target, string key) { - return Observable.FromEventPattern( - x => target.ResourcesChanged += x, - x => target.ResourcesChanged -= x) - .StartWith((EventPattern)null) - .Select(x => target.FindResource(key)); + return new ResourceObservable(target, key); + } + + private class ResourceObservable : LightweightObservableBase + { + private readonly IResourceNode _target; + private readonly string _key; + + public ResourceObservable(IResourceNode target, string key) + { + _target = target; + _key = key; + } + + protected override void Initialize() + { + _target.ResourcesChanged += ResourcesChanged; + } + + protected override void Deinitialize() + { + _target.ResourcesChanged -= ResourcesChanged; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_target.FindResource(_key)); + } + + private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + PublishNext(_target.FindResource(_key)); + } } } } From 26b3efd8d402b4a182e20152f9d7f5f6d84cc1a9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 15:27:53 +0200 Subject: [PATCH 47/88] Use custom rx for control locators. --- .../LogicalTree/ControlLocator.cs | 162 ++++++++++++------ .../VisualTree/VisualLocator.cs | 67 +++++--- 2 files changed, 147 insertions(+), 82 deletions(-) diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 2858d11d9d..7186143bf9 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Linq; using System.Reflection; using Avalonia.Controls; +using Avalonia.Reactive; namespace Avalonia.LogicalTree { @@ -23,75 +24,122 @@ namespace Avalonia.LogicalTree /// The name of the control to find. public static IObservable Track(ILogical relativeTo, string name) { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.AttachedToLogicalTree -= x) - .Select(x => ((ILogical)x.Sender).FindNameScope()) - .StartWith(relativeTo.FindNameScope()); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree -= x) - .Select(x => (INameScope)null); - - return attached.Merge(detached).Select(nameScope => + return new ControlTracker(relativeTo, name); + } + + public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) + { + return new ControlTracker(relativeTo, ancestorLevel, ancestorType); + } + + private class ControlTracker : LightweightObservableBase + { + private readonly ILogical _relativeTo; + private readonly string _name; + private readonly int _ancestorLevel; + private readonly Type _ancestorType; + INameScope _nameScope; + ILogical _value; + + public ControlTracker(ILogical relativeTo, string name) + { + _relativeTo = relativeTo; + _name = name; + } + + public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType) { - if (nameScope != null) + _relativeTo = relativeTo; + _ancestorLevel = ancestorLevel; + _ancestorType = ancestorType; + } + + protected override void Initialize() + { + Update(); + _relativeTo.AttachedToLogicalTree += Attached; + _relativeTo.DetachedFromLogicalTree += Detached; + } + + protected override void Deinitialize() + { + _relativeTo.AttachedToLogicalTree -= Attached; + _relativeTo.DetachedFromLogicalTree -= Detached; + + if (_nameScope != null) { - var registered = Observable.FromEventPattern( - x => nameScope.Registered += x, - x => nameScope.Registered -= x) - .Where(x => x.EventArgs.Name == name) - .Select(x => x.EventArgs.Element) - .OfType(); - var unregistered = Observable.FromEventPattern( - x => nameScope.Unregistered += x, - x => nameScope.Unregistered -= x) - .Where(x => x.EventArgs.Name == name) - .Select(_ => (ILogical)null); - return registered - .StartWith(nameScope.Find(name)) - .Merge(unregistered); + _nameScope.Registered -= Registered; + _nameScope.Unregistered -= Unregistered; } - else + + _value = null; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_value); + } + + private void Attached(object sender, LogicalTreeAttachmentEventArgs e) + { + Update(); + PublishNext(_value); + } + + private void Detached(object sender, LogicalTreeAttachmentEventArgs e) + { + if (_nameScope != null) { - return Observable.Return(null); + _nameScope.Registered -= Registered; + _nameScope.Unregistered -= Unregistered; } - }).Switch(); - } - public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) - { - return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => + _value = null; + PublishNext(null); + } + + private void Registered(object sender, NameScopeEventArgs e) { - if (isAttachedToTree) + if (e.Name == _name && e.Element is ILogical logical) { - return relativeTo.GetLogicalAncestors() - .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(ancestorLevel); + _value = logical; + PublishNext(logical); } - else + } + + private void Unregistered(object sender, NameScopeEventArgs e) + { + if (e.Name == _name) { - return null; + _value = null; + PublishNext(null); } - }); - } + } - private static IObservable TrackAttachmentToTree(ILogical relativeTo) - { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.AttachedToLogicalTree -= x) - .Select(x => true) - .StartWith(relativeTo.IsAttachedToLogicalTree); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree -= x) - .Select(x => false); - - var attachmentStatus = attached.Merge(detached); - return attachmentStatus; + private void Update() + { + if (_name != null) + { + _nameScope = _relativeTo.FindNameScope(); + + if (_nameScope != null) + { + _nameScope.Registered += Registered; + _nameScope.Unregistered += Unregistered; + _value = _nameScope.Find(_name); + } + else + { + _value = null; + } + } + else + { + _value = _relativeTo.GetLogicalAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); + } + } } } } diff --git a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs index 6fcb6c0db1..eaca9ed9d7 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Reflection; -using System.Text; +using Avalonia.Reactive; namespace Avalonia.VisualTree { @@ -11,36 +10,54 @@ namespace Avalonia.VisualTree { public static IObservable Track(IVisual relativeTo, int ancestorLevel, Type ancestorType = null) { - return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => + return new VisualTracker(relativeTo, ancestorLevel, ancestorType); + } + + private class VisualTracker : LightweightObservableBase + { + private readonly IVisual _relativeTo; + private readonly int _ancestorLevel; + private readonly Type _ancestorType; + + public VisualTracker(IVisual relativeTo, int ancestorLevel, Type ancestorType) + { + _relativeTo = relativeTo; + _ancestorLevel = ancestorLevel; + _ancestorType = ancestorType; + } + + protected override void Initialize() + { + _relativeTo.AttachedToVisualTree += AttachedDetached; + _relativeTo.DetachedFromVisualTree += AttachedDetached; + } + + protected override void Deinitialize() { - if (isAttachedToTree) + _relativeTo.AttachedToVisualTree -= AttachedDetached; + _relativeTo.DetachedFromVisualTree -= AttachedDetached; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(GetResult()); + } + + private void AttachedDetached(object sender, VisualTreeAttachmentEventArgs e) => PublishNext(GetResult()); + + private IVisual GetResult() + { + if (_relativeTo.IsAttachedToVisualTree) { - return relativeTo.GetVisualAncestors() - .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(ancestorLevel); + return _relativeTo.GetVisualAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); } else { return null; } - }); - } - - private static IObservable TrackAttachmentToTree(IVisual relativeTo) - { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToVisualTree += x, - x => relativeTo.AttachedToVisualTree -= x) - .Select(x => true) - .StartWith(relativeTo.IsAttachedToVisualTree); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromVisualTree += x, - x => relativeTo.DetachedFromVisualTree -= x) - .Select(x => false); - - var attachmentStatus = attached.Merge(detached); - return attachmentStatus; + } } } } From 6720bb9633d120663952fa07c3db1019ead539da Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 16:31:16 +0200 Subject: [PATCH 48/88] Updated ignore list in ncrunch. --- .ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject | 5 +++++ .ncrunch/BindingDemo.net461.v3.ncrunchproject | 5 +++++ .ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject | 5 +++++ .ncrunch/Previewer.v3.ncrunchproject | 5 +++++ .ncrunch/RemoteDemo.v3.ncrunchproject | 5 +++++ .ncrunch/RenderDemo.net461.v3.ncrunchproject | 5 +++++ .ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject | 5 +++++ .ncrunch/VirtualizationDemo.net461.v3.ncrunchproject | 5 +++++ .ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject | 5 +++++ 9 files changed, 45 insertions(+) create mode 100644 .ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject create mode 100644 .ncrunch/BindingDemo.net461.v3.ncrunchproject create mode 100644 .ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject create mode 100644 .ncrunch/Previewer.v3.ncrunchproject create mode 100644 .ncrunch/RemoteDemo.v3.ncrunchproject create mode 100644 .ncrunch/RenderDemo.net461.v3.ncrunchproject create mode 100644 .ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject create mode 100644 .ncrunch/VirtualizationDemo.net461.v3.ncrunchproject create mode 100644 .ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject b/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/BindingDemo.net461.v3.ncrunchproject b/.ncrunch/BindingDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Previewer.v3.ncrunchproject b/.ncrunch/Previewer.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Previewer.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RemoteDemo.v3.ncrunchproject b/.ncrunch/RemoteDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RemoteDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.net461.v3.ncrunchproject b/.ncrunch/RenderDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From ef5be70c4b6451a1f142b33dfd6b154b7c3d0ddd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 21:13:48 +0200 Subject: [PATCH 49/88] Don't double-check nulls. --- .../Reactive/SingleSubscriberObservableBase.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs index af48df177e..cd8ce2cd80 100644 --- a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs +++ b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs @@ -46,17 +46,14 @@ namespace Avalonia.Reactive protected void PublishNext(T value) { - if (_observer != null) - { - _observer?.OnNext(value); - } + _observer?.OnNext(value); } protected void PublishCompleted() { if (_observer != null) { - _observer?.OnCompleted(); + _observer.OnCompleted(); _completed = true; Unsubscribed(); _observer = null; @@ -67,7 +64,7 @@ namespace Avalonia.Reactive { if (_observer != null) { - _observer?.OnError(error); + _observer.OnError(error); _error = error; Unsubscribed(); _observer = null; From 4f0764ea6bee6d4f24f445491b3169ba4830fcd9 Mon Sep 17 00:00:00 2001 From: Robert Hencke Date: Sat, 23 Jun 2018 19:29:49 -0400 Subject: [PATCH 50/88] Make Delete on macOS delete correctly. On a Mac keyboard, the Delete key functions as the Backspace key does on a PC keyboard. Map VK_Delete to Key.Back (VK_ForwardDelete correctly maps to Key.Delete) --- src/OSX/Avalonia.MonoMac/KeyTransform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OSX/Avalonia.MonoMac/KeyTransform.cs b/src/OSX/Avalonia.MonoMac/KeyTransform.cs index 6d4b58031e..4abd5a24f1 100644 --- a/src/OSX/Avalonia.MonoMac/KeyTransform.cs +++ b/src/OSX/Avalonia.MonoMac/KeyTransform.cs @@ -200,7 +200,7 @@ namespace Avalonia.MonoMac [kVK_Return] = Key.Return, [kVK_Tab] = Key.Tab, [kVK_Space] = Key.Space, - [kVK_Delete] = Key.Delete, + [kVK_Delete] = Key.Back, [kVK_Escape] = Key.Escape, [kVK_Command] = Key.LWin, [kVK_Shift] = Key.LeftShift, From 0c078c9dec3086b262dcd1bde391106625e106eb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 18:23:08 +0200 Subject: [PATCH 51/88] Don't use rx for ExpressionNodes. `ExpressionNode`s were always single-subscriber and making them use `IObservable<>` meant that we had to have extra allocations in order to return `IDisposable`s. Instead of using `IObservable` use a simpler `Subscribe`/`Unsubscribe` pattern. This saves a bunch more memory. --- .../Data/Core/EmptyExpressionNode.cs | 5 - src/Avalonia.Base/Data/Core/ExpressionNode.cs | 120 +++++++++--------- .../Data/Core/ExpressionObserver.cs | 35 ++--- src/Avalonia.Base/Data/Core/IndexerNode.cs | 11 +- .../Plugins/AvaloniaPropertyAccessorPlugin.cs | 10 +- .../Data/Core/Plugins/DataValidatiorBase.cs | 10 +- .../Core/Plugins/ExceptionValidationPlugin.cs | 5 +- .../Data/Core/Plugins/IPropertyAccessor.cs | 14 +- .../Core/Plugins/IndeiValidationPlugin.cs | 19 ++- .../Plugins/InpcPropertyAccessorPlugin.cs | 16 +-- .../Data/Core/Plugins/MethodAccessorPlugin.cs | 8 +- .../Data/Core/Plugins/PropertyAccessorBase.cs | 68 +++++----- .../Data/Core/Plugins/PropertyError.cs | 11 +- .../Data/Core/PropertyAccessorNode.cs | 23 ++-- src/Avalonia.Base/Data/Core/StreamNode.cs | 17 ++- .../Plugins/IndeiValidationPluginTests.cs | 5 +- 16 files changed, 193 insertions(+), 184 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 93e0d5947a..c4166b44e5 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -9,10 +9,5 @@ namespace Avalonia.Data.Core internal class EmptyExpressionNode : ExpressionNode { public override string Description => "."; - - protected override IObservable StartListeningCore(WeakReference reference) - { - return Observable.Return(reference.Target); - } } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ac7e97a4b1..600cd68d60 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -2,22 +2,18 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Data; namespace Avalonia.Data.Core { - internal abstract class ExpressionNode : ISubject + internal abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); private WeakReference _target = UnsetReference; - private IDisposable _valueSubscription; - private IObserver _observer; + private Action _subscriber; + private bool _listening; protected WeakReference LastValue { get; private set; } @@ -33,92 +29,66 @@ namespace Avalonia.Data.Core var oldTarget = _target?.Target; var newTarget = value.Target; - var running = _valueSubscription != null; if (!ReferenceEquals(oldTarget, newTarget)) { - _valueSubscription?.Dispose(); - _valueSubscription = null; + if (_listening) + { + StopListening(); + } + _target = value; - if (running) + if (_subscriber != null) { - _valueSubscription = StartListening(); + StartListening(); } } } } - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action subscriber) { - if (_observer != null) + if (_subscriber != null) { throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); } - _observer = observer; - var nextSubscription = Next?.Subscribe(this); - _valueSubscription = StartListening(); - - return Disposable.Create(() => - { - _valueSubscription?.Dispose(); - _valueSubscription = null; - LastValue = null; - nextSubscription?.Dispose(); - _observer = null; - }); + _subscriber = subscriber; + Next?.Subscribe(NextValueChanged); + StartListening(); } - void IObserver.OnCompleted() + public void Unsubscribe() { - throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); - } + Next?.Unsubscribe(); - void IObserver.OnError(Exception error) - { - throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); + if (_listening) + { + StopListening(); + } + + LastValue = null; + _subscriber = null; } - void IObserver.OnNext(object value) + protected virtual void StartListeningCore(WeakReference reference) { - NextValueChanged(value); + ValueChanged(reference.Target); } - protected virtual IObservable StartListeningCore(WeakReference reference) + protected virtual void StopListeningCore() { - return Observable.Return(reference.Target); } protected virtual void NextValueChanged(object value) { var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException; bindingBroken?.AddNode(Description); - _observer.OnNext(value); - } - - private IDisposable StartListening() - { - var target = _target.Target; - IObservable source; - - if (target == null) - { - source = Observable.Return(TargetNullNotification()); - } - else if (target == AvaloniaProperty.UnsetValue) - { - source = Observable.Empty(); - } - else - { - source = StartListeningCore(_target); - } - - return source.Subscribe(ValueChanged); + _subscriber(value); } - private void ValueChanged(object value) + protected void ValueChanged(object value) { var notification = value as BindingNotification; @@ -131,24 +101,50 @@ namespace Avalonia.Data.Core } else { - _observer.OnNext(value); + _subscriber(value); } } else { LastValue = new WeakReference(notification.Value); + if (Next != null) { Next.Target = new WeakReference(notification.Value); } - + if (Next == null || notification.Error != null) { - _observer.OnNext(value); + _subscriber(value); } } } + private void StartListening() + { + var target = _target.Target; + + if (target == null) + { + ValueChanged(TargetNullNotification()); + _listening = false; + } + else if (target != AvaloniaProperty.UnsetValue) + { + StartListeningCore(_target); + _listening = true; + } + else + { + _listening = false; + } + } + + private void StopListening() + { + StopListeningCore(); + } + private BindingNotification TargetNullNotification() { return new BindingNotification( diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 3a25407133..3a061206bf 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -14,9 +14,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, - IDescription, - IObserver + public class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize @@ -55,7 +53,6 @@ namespace Avalonia.Data.Core private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private IDisposable _nodeSubscription; private object _root; private IDisposable _rootSubscription; private WeakReference _value; @@ -202,34 +199,18 @@ namespace Avalonia.Data.Core } } - void IObserver.OnNext(object value) - { - var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; - broken?.Commit(Description); - _value = new WeakReference(value); - PublishNext(value); - } - - void IObserver.OnCompleted() - { - } - - void IObserver.OnError(Exception error) - { - } - protected override void Initialize() { _value = null; - _nodeSubscription = _node.Subscribe(this); + _node.Subscribe(ValueChanged); StartRoot(); } protected override void Deinitialize() { _rootSubscription?.Dispose(); - _nodeSubscription?.Dispose(); - _rootSubscription = _nodeSubscription = null; + _rootSubscription = null; + _node.Unsubscribe(); } protected override void Subscribed(IObserver observer, bool first) @@ -266,5 +247,13 @@ namespace Avalonia.Data.Core _node.Target = (WeakReference)_root; } } + + private void ValueChanged(object value) + { + var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; + broken?.Commit(Description); + _value = new WeakReference(value); + PublishNext(value); + } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNode.cs b/src/Avalonia.Base/Data/Core/IndexerNode.cs index 633d3558ee..afdfe90c4c 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNode.cs @@ -17,6 +17,8 @@ namespace Avalonia.Data.Core { internal class IndexerNode : SettableNode { + private IDisposable _subscription; + public IndexerNode(IList arguments) { Arguments = arguments; @@ -24,7 +26,7 @@ namespace Avalonia.Data.Core public override string Description => "[" + string.Join(",", Arguments) + "]"; - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { var target = reference.Target; var incc = target as INotifyCollectionChanged; @@ -49,7 +51,12 @@ namespace Avalonia.Data.Core .Select(_ => GetValue(target))); } - return Observable.Merge(inputs).StartWith(GetValue(target)); + _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription.Dispose(); } protected override bool SetTargetValueCore(object value, BindingPriority priority) diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 48edb218dc..a163c07f87 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -145,15 +145,15 @@ namespace Avalonia.Data.Core.Plugins return false; } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() { - _subscription?.Dispose(); - _subscription = null; + _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); } - protected override void SubscribeCore(IObserver observer) + protected override void UnsubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(observer); + _subscription?.Dispose(); + _subscription = null; } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs index bd429f04d6..03ab7712bd 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs @@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins /// The value. void IObserver.OnNext(object value) => InnerValueChanged(value); - /// - protected override void Dispose(bool disposing) => _inner.Dispose(); - /// /// Begins listening to the inner . /// - protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this); + protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged); + + /// + protected override void UnsubscribeCore() => _inner.Dispose(); /// /// Called when the inner notifies with a new value. @@ -74,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins protected virtual void InnerValueChanged(object value) { var notification = value as BindingNotification ?? new BindingNotification(value); - Observer.OnNext(notification); + PublishValue(notification); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 35f9f7e59a..4507b32e0c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Data; using System; using System.Reflection; @@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins } catch (TargetInvocationException ex) { - Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); + PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); } catch (Exception ex) { - Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); + PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError)); } return false; diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs index d7dda57a72..33ea5bba08 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { @@ -10,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins /// Defines an accessor to a property on an object returned by a /// /// - public interface IPropertyAccessor : IObservable, IDisposable + public interface IPropertyAccessor : IDisposable { /// /// Gets the type of the property. @@ -38,5 +37,16 @@ namespace Avalonia.Data.Core.Plugins /// True if the property was set; false if the property could not be set. /// bool SetValue(object value, BindingPriority priority); + + /// + /// Subscribes to the value of the member. + /// + /// A method that receives the values. + void Subscribe(Action listener); + + /// + /// Unsubscribes to the value of the member. + /// + void Unsubscribe(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 436046f3fa..4d6fc01229 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Avalonia.Data; using Avalonia.Utilities; namespace Avalonia.Data.Core.Plugins @@ -40,43 +39,43 @@ namespace Avalonia.Data.Core.Plugins { if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) { - Observer.OnNext(CreateBindingNotification(Value)); + PublishValue(CreateBindingNotification(Value)); } } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() { - base.Dispose(disposing); - var target = _reference.Target as INotifyDataErrorInfo; if (target != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Subscribe( target, nameof(target.ErrorsChanged), this); } + + base.SubscribeCore(); } - protected override void SubscribeCore(IObserver observer) + protected override void UnsubscribeCore() { var target = _reference.Target as INotifyDataErrorInfo; if (target != null) { - WeakSubscriptionManager.Subscribe( + WeakSubscriptionManager.Unsubscribe( target, nameof(target.ErrorsChanged), this); } - base.SubscribeCore(observer); + base.UnsubscribeCore(); } protected override void InnerValueChanged(object value) { - base.InnerValueChanged(CreateBindingNotification(value)); + PublishValue(CreateBindingNotification(value)); } private BindingNotification CreateBindingNotification(object value) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index ba4e60eb74..dab32b639a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -103,7 +103,13 @@ namespace Avalonia.Data.Core.Plugins } } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() { var inpc = _reference.Target as INotifyPropertyChanged; @@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins } } - protected override void SubscribeCore(IObserver observer) - { - SendCurrentValue(); - SubscribeToChanges(); - } - private void SendCurrentValue() { try { var value = Value; - Observer.OnNext(value); + PublishValue(value); } catch { } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index b2b3a107fa..cf0abc6f35 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -74,14 +74,18 @@ namespace Avalonia.Data.Core.Plugins public override bool SetValue(object value, BindingPriority priority) => false; - protected override void SubscribeCore(IObserver observer) + protected override void SubscribeCore() { try { - Observer.OnNext(Value); + PublishValue(Value); } catch { } } + + protected override void UnsubscribeCore() + { + } } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs index 9cc78369a7..e840b2c5c9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs @@ -2,67 +2,75 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { /// /// Defines a default base implementation for a . /// - /// - /// is an observable that will only be subscribed to one time. - /// In addition, the subscription can be disposed by calling on the - /// property accessor itself - this prevents needing to hold two references for a subscription. - /// public abstract class PropertyAccessorBase : IPropertyAccessor { + private Action _listener; + /// public abstract Type PropertyType { get; } /// public abstract object Value { get; } - /// - /// Stops the subscription. - /// - public void Dispose() => Dispose(true); + /// + public void Dispose() + { + if (_listener != null) + { + Unsubscribe(); + } + } /// public abstract bool SetValue(object value, BindingPriority priority); - /// - /// The currently subscribed observer. - /// - protected IObserver Observer { get; private set; } - /// - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) { - Contract.Requires(observer != null); + Contract.Requires(listener != null); - if (Observer != null) + if (_listener != null) { throw new InvalidOperationException( - "A property accessor can be subscribed to only once."); + "A member accessor can be subscribed to only once."); } - Observer = observer; - SubscribeCore(observer); - return this; + _listener = listener; + SubscribeCore(); } + public void Unsubscribe() + { + if (_listener == null) + { + throw new InvalidOperationException( + "The member accessor was not subscribed."); + } + + UnsubscribeCore(); + _listener = null; + } + + /// + /// Publishes a value to the listener. + /// + /// The value. + protected void PublishValue(object value) => _listener?.Invoke(value); + /// - /// Stops listening to the property. + /// When overridden in a derived class, begins listening to the member. /// - /// - /// True if the method was called, false if the object is being - /// finalized. - /// - protected virtual void Dispose(bool disposing) => Observer = null; + protected abstract void SubscribeCore(); /// - /// When overridden in a derived class, begins listening to the property. + /// When overridden in a derived class, stops listening to the member. /// - protected abstract void SubscribeCore(IObserver observer); + protected abstract void UnsubscribeCore(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs index 647adc36cb..eb2400807a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs @@ -1,6 +1,4 @@ using System; -using System.Reactive.Disposables; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { @@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins return false; } - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) + { + listener(_error); + } + + public void Unsubscribe() { - observer.OnNext(_error); - return Disposable.Empty; } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 9d657b3144..2565a34322 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -3,9 +3,7 @@ using System; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Data; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -39,7 +37,7 @@ namespace Avalonia.Data.Core return false; } - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); var accessor = plugin?.Start(reference, PropertyName); @@ -55,17 +53,14 @@ namespace Avalonia.Data.Core } } - // Ensure that _accessor is set for the duration of the subscription. - return Observable.Using( - () => - { - _accessor = accessor; - return Disposable.Create(() => - { - _accessor = null; - }); - }, - _ => accessor); + accessor.Subscribe(ValueChanged); + _accessor = accessor; + } + + protected override void StopListeningCore() + { + _accessor.Dispose(); + _accessor = null; } } } diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 187c79af49..415def4d30 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -2,30 +2,37 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Globalization; -using Avalonia.Data; using System.Reactive.Linq; namespace Avalonia.Data.Core { internal class StreamNode : ExpressionNode { + private IDisposable _subscription; + public override string Description => "^"; - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - return plugin.Start(reference); + _subscription = plugin.Start(reference).Subscribe(ValueChanged); + return; } } // TODO: Improve error. - return Observable.Return(new BindingNotification( + ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 45c084014b..383030cb6c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core.Plugins; using Xunit; @@ -58,9 +57,9 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); - var sub = validator.Subscribe(_ => { }); + validator.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); - sub.Dispose(); + validator.Unsubscribe(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } From 6d0e46134919956ba20a115218b5ba7f43fa933c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Jun 2018 10:46:50 +0200 Subject: [PATCH 52/88] Throw if no matching property accessor found. This shouldn't happen normally as `InpcPropertyAcessorPlugin` matches everything. --- src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 2565a34322..e9831eb047 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -53,6 +53,12 @@ namespace Avalonia.Data.Core } } + if (accessor == null) + { + throw new NotSupportedException( + $"Could not find a matching property accessor for {PropertyName}."); + } + accessor.Subscribe(ValueChanged); _accessor = accessor; } From af7e139ed4cb7e06850ccef07d6cc8e934ca061d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Jun 2018 00:33:24 +0200 Subject: [PATCH 53/88] Added failing test for #1698. --- .../SelectorTests_Class.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index b41c21fbf4..75599925b7 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; @@ -8,6 +9,7 @@ using Moq; using Avalonia.Controls; using Avalonia.Styling; using Xunit; +using System.Collections.Generic; namespace Avalonia.Styling.UnitTests { @@ -117,6 +119,28 @@ namespace Avalonia.Styling.UnitTests Assert.False(await activator.Take(1)); } + [Fact] + public void Only_Notifies_When_Result_Changes() + { + // Test for #1698 + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).Class("foo"); + var activator = target.Match(control).ObservableResult; + var result = new List(); + + using (activator.Subscribe(x => result.Add(x))) + { + control.Classes.Add("bar"); + control.Classes.Remove("foo"); + } + + Assert.Equal(new[] { true, false }, result); + } + public class Control1 : TestControlBase { } From 114b393813d38683ebc4ea013555ed5f7fa6956b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Jun 2018 00:33:55 +0200 Subject: [PATCH 54/88] Only publish distinct values from ClassObserver. Fixes #1698. --- .../Styling/TypeNameAndClassSelector.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 93d4e14f27..94c0b75c6e 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -202,6 +202,7 @@ namespace Avalonia.Styling { readonly IList _match; IAvaloniaReadOnlyList _classes; + bool _value; public ClassObserver(IAvaloniaReadOnlyList classes, IList match) { @@ -210,18 +211,29 @@ namespace Avalonia.Styling } protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; - protected override void Initialize() => _classes.CollectionChanged += ClassesChanged; + + protected override void Initialize() + { + _value = GetResult(); + _classes.CollectionChanged += ClassesChanged; + } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(GetResult()); + observer.OnNext(_value); } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action != NotifyCollectionChangedAction.Move) { - PublishNext(GetResult()); + var value = GetResult(); + + if (value != _value) + { + PublishNext(GetResult()); + _value = value; + } } } From 617dd6a5ab9718043440e4cccbece7ae8e566a01 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 26 Jun 2018 15:43:33 +0100 Subject: [PATCH 55/88] fix win32 clipboard settext async. --- src/Windows/Avalonia.Win32/ClipboardImpl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 3dae8e37d9..a908c9e1e2 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -57,6 +57,9 @@ namespace Avalonia.Win32 } await OpenClipboard(); + + UnmanagedMethods.EmptyClipboard(); + try { var hGlobal = Marshal.StringToHGlobalUni(text); From b3cfc22e27cb18e9c758ab9f3fc551c3577ec80e Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 28 Jun 2018 19:55:25 +0200 Subject: [PATCH 56/88] Add scientific notation support for double values --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 15 +++++++++++++++ .../Media/PathMarkupParserTests.cs | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 384c96a7d6..bfb6ec6450 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -451,6 +451,21 @@ namespace Avalonia.Media } for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; + if (i < remaining.Length) + { + // scientific notation + if (remaining[i] == 'E') + { + valid = false; + i++; + if (remaining[i] == '-' || remaining[i] == '+') + { + i++; + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; + } + } + } + if (!valid) { argument = ReadOnlySpan.Empty; diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index 5074d306fd..6c456a9321 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -142,6 +142,10 @@ namespace Avalonia.Visuals.UnitTests.Media } [Theory] + [InlineData("F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 " + + "9.99999999997669E-05,10 9.99999999997669E-05,0 3.0001,0 3.0001,0.234 9.0001,3.834 9.0001,0 " + + "12.0001,0 12.0001,8.062C12.1861,8.043 12.3821,8.031 12.5941,8.031 15.3481,8.031 15.7961,9.826 " + + "15.9201,11L16.0001,16 9.0001,16 9.0001,12.562 9.0001,11z")] // issue #1708 [InlineData(" M0 0")] [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("M0 0L10 10z")] From 9ccf63d51bd4fe548d920219f256a0df2620766d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jun 2018 23:45:10 +0200 Subject: [PATCH 57/88] Add failing test for clearing templated child's parent. --- .../Primitives/TemplatedControlTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index cd71717619..166586ace1 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -160,6 +160,24 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(target, child.GetLogicalParent()); } + [Fact] + public void Changing_Template_Should_Clear_Old_Templated_Childs_Parent() + { + var target = new TemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator()) + }; + + target.ApplyTemplate(); + + var child = (Decorator)target.GetVisualChildren().Single(); + + target.Template = new FuncControlTemplate(_ => new Canvas()); + target.ApplyTemplate(); + + Assert.Null(child.Parent); + } + [Fact] public void Nested_Templated_Control_Should_Not_Have_Template_Applied() { From 95fe6f4cdfa9ca67be64037f956ad0a70c3c567c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jun 2018 23:46:03 +0200 Subject: [PATCH 58/88] Clear templated child's parent when template detached. --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 8514104c91..296134ca48 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -247,6 +247,7 @@ namespace Avalonia.Controls.Primitives foreach (var child in this.GetTemplateChildren()) { child.SetValue(TemplatedParentProperty, null); + ((ISetLogicalParent)child).SetParent(null); } VisualChildren.Clear(); From 9c1c8749ddd3e11a717901726f7e61119a8c9e64 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 29 Jun 2018 00:50:15 +0200 Subject: [PATCH 59/88] Added failing test for #1709. --- .../ItemsControlTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 4da803353e..9ef1e9f0d2 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -315,6 +315,26 @@ namespace Avalonia.Controls.UnitTests Assert.Same(before, after); } + [Fact] + public void Should_Clear_Containers_When_ItemsPresenter_Changes() + { + var target = new ItemsControl + { + Items = new[] { "foo", "bar" }, + Template = GetTemplate(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Equal(2, target.ItemContainerGenerator.Containers.Count()); + + target.Template = GetTemplate(); + target.ApplyTemplate(); + + Assert.Empty(target.ItemContainerGenerator.Containers); + } + [Fact] public void Empty_Class_Should_Initially_Be_Applied() { From cf14976dcce4ce011a664a83d9ea57184706f183 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 29 Jun 2018 00:50:28 +0200 Subject: [PATCH 60/88] Fix #1709. --- src/Avalonia.Controls/ItemsControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 5119096965..3cb997f615 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -155,6 +155,7 @@ namespace Avalonia.Controls void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { Presenter = presenter; + ItemContainerGenerator.Clear(); } /// From 2f7a578c38eddc108028ea2aff34e7e4e9d68932 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 29 Jun 2018 00:51:03 +0200 Subject: [PATCH 61/88] Added null check for panel. Problem reared its head when #1709 was fixed. --- .../Primitives/SelectingItemsControl.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2e668fda95..a7b8981583 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives var panel = (InputElement)Presenter.Panel; - foreach (var container in e.Containers) + if (panel != null) { - if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) + foreach (var container in e.Containers) { - KeyboardNavigation.SetTabOnceActiveElement(panel, null); - break; + if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) + { + KeyboardNavigation.SetTabOnceActiveElement(panel, null); + break; + } } } } From ac8342ebf89d35f1b0030edc566325b40668e057 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 29 Jun 2018 06:34:54 +0200 Subject: [PATCH 62/88] Allow lower case e for scientific notation --- .../Media/PathMarkupParser.cs | 2 +- .../Media/PathMarkupParserTests.cs | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index bfb6ec6450..b6232c6803 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -454,7 +454,7 @@ namespace Avalonia.Media if (i < remaining.Length) { // scientific notation - if (remaining[i] == 'E') + if (remaining[i] == 'E' || remaining[i] == 'e') { valid = false; i++; diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index 6c456a9321..4d3de034d8 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -7,6 +7,7 @@ using Xunit; namespace Avalonia.Visuals.UnitTests.Media { + using System.Globalization; using System.IO; public class PathMarkupParserTests @@ -139,7 +140,26 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new Point(30, 30), lineSegment.Point); } - } + } + + [Fact] + public void Parses_Scientific_Notation_Double() + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse("M -1.01725E-005 -1.01725e-005"); + + var figure = pathGeometry.Figures[0]; + + Assert.Equal( + new Point( + double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture), + double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture)), + figure.StartPoint); + } + } [Theory] [InlineData("F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 " From 370bfe3c82b7e467a68ba44c74cb54679e597d34 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 29 Jun 2018 12:31:53 +0200 Subject: [PATCH 63/88] Support double values that start with a dot for implicit commands --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 2 +- tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index b6232c6803..656526890a 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -424,7 +424,7 @@ namespace Avalonia.Media { span = SkipWhitespace(span); - return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0])); + return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0])); } private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index 4d3de034d8..8dca52e6a7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -162,6 +162,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [Theory] + [InlineData("M5.5.5 5.5.5 5.5.5")] [InlineData("F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 " + "9.99999999997669E-05,10 9.99999999997669E-05,0 3.0001,0 3.0001,0.234 9.0001,3.834 9.0001,0 " + "12.0001,0 12.0001,8.062C12.1861,8.043 12.3821,8.031 12.5941,8.031 15.3481,8.031 15.7961,9.826 " From 2a14f4d6a55ef4ab4f49f39d8b7837cbd226bbcd Mon Sep 17 00:00:00 2001 From: Sannan Khan Date: Fri, 29 Jun 2018 16:33:33 +0500 Subject: [PATCH 64/88] Moved SetWindowStartupLocation() to end of Show() function --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index c19c69ce73..e0fefef0b7 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -373,7 +373,6 @@ namespace Avalonia.Controls AddWindow(this); EnsureInitialized(); - SetWindowStartupLocation(); IsVisible = true; LayoutManager.Instance.ExecuteInitialLayoutPass(this); @@ -382,6 +381,7 @@ namespace Avalonia.Controls PlatformImpl?.Show(); Renderer?.Start(); } + SetWindowStartupLocation(); } /// From 064c89bd42583b3d6103be1b2fee4db12e29f6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 15:17:42 +0200 Subject: [PATCH 65/88] Refactored Cake script to enable running Impl tasks without dependencies --- build.cake | 140 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 49 deletions(-) diff --git a/build.cake b/build.cake index 561a33186a..e739d9375e 100644 --- a/build.cake +++ b/build.cake @@ -95,10 +95,10 @@ Teardown((context, buildContext) => }); /////////////////////////////////////////////////////////////////////////////// -// TASKS +// TASKS IMPLEMENTATIONS /////////////////////////////////////////////////////////////////////////////// -Task("Clean") +Task("Clean-Impl") .Does(data => { CleanDirectories(data.Parameters.BuildDirs); @@ -108,9 +108,7 @@ Task("Clean") CleanDirectory(data.Parameters.BinRoot); }); -Task("Restore-NuGet-Packages") - .IsDependentOn("Clean") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) +Task("Restore-NuGet-Packages-Impl") .Does(data => { var maxRetryCount = 5; @@ -148,8 +146,7 @@ void DotNetCoreBuild(Parameters parameters) DotNetCoreBuild(parameters.MSBuildSolution, settings); } -Task("Build") - .IsDependentOn("Restore-NuGet-Packages") +Task("Build-Impl") .Does(data => { if(data.Parameters.IsRunningOnWindows) @@ -171,7 +168,6 @@ Task("Build") } }); - void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) { if(!project.EndsWith(".csproj")) @@ -194,9 +190,7 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) } } -Task("Run-Unit-Tests") - .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests) +Task("Run-Unit-Tests-Impl") .Does(data => { RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); @@ -214,24 +208,18 @@ Task("Run-Unit-Tests") } }); -Task("Run-Designer-Tests") - .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests) +Task("Run-Designer-Tests-Impl") .Does(data => { RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); }); -Task("Run-Render-Tests") - .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) +Task("Run-Render-Tests-Impl") .Does(data => { RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); }); -Task("Run-Leak-Tests") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .IsDependentOn("Build") +Task("Run-Leak-Tests-Impl") .Does(() => { var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); @@ -251,21 +239,13 @@ Task("Run-Leak-Tests") } }); -Task("Run-Tests") - .IsDependentOn("Run-Unit-Tests") - .IsDependentOn("Run-Render-Tests") - .IsDependentOn("Run-Designer-Tests") - .IsDependentOn("Run-Leak-Tests"); - -Task("Copy-Files") - .IsDependentOn("Run-Tests") +Task("Copy-Files-Impl") .Does(data => { CopyFiles(data.Packages.BinFiles, data.Parameters.BinRoot); }); -Task("Zip-Files") - .IsDependentOn("Copy-Files") +Task("Zip-Files-Impl") .Does(data => { Zip(data.Parameters.BinRoot, data.Parameters.ZipCoreArtifacts); @@ -279,9 +259,7 @@ Task("Zip-Files") GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); }); -Task("Create-NuGet-Packages") - .IsDependentOn("Run-Tests") - .IsDependentOn("Inspect") +Task("Create-NuGet-Packages-Impl") .Does(data => { foreach(var nuspec in data.Packages.NuspecNuGetSettings) @@ -290,13 +268,7 @@ Task("Create-NuGet-Packages") } }); -Task("Publish-MyGet") - .IsDependentOn("Create-NuGet-Packages") - .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) - .WithCriteria((context, data) => !data.Parameters.IsPullRequest) - .WithCriteria((context, data) => data.Parameters.IsMainRepo) - .WithCriteria((context, data) => data.Parameters.IsMasterBranch) - .WithCriteria((context, data) => data.Parameters.IsMyGetRelease) +Task("Publish-MyGet-Impl") .Does(data => { var apiKey = EnvironmentVariable("MYGET_API_KEY"); @@ -324,12 +296,7 @@ Task("Publish-MyGet") Information("Publish-MyGet Task failed, but continuing with next Task..."); }); -Task("Publish-NuGet") - .IsDependentOn("Create-NuGet-Packages") - .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) - .WithCriteria((context, data) => !data.Parameters.IsPullRequest) - .WithCriteria((context, data) => data.Parameters.IsMainRepo) - .WithCriteria((context, data) => data.Parameters.IsNuGetRelease) +Task("Publish-NuGet-Impl") .Does(data => { var apiKey = EnvironmentVariable("NUGET_API_KEY"); @@ -357,9 +324,7 @@ Task("Publish-NuGet") Information("Publish-NuGet Task failed, but continuing with next Task..."); }); -Task("Inspect") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) - .IsDependentOn("Restore-NuGet-Packages") +Task("Inspect-Impl") .Does(() => { var badIssues = new []{"PossibleNullReferenceException"}; @@ -394,6 +359,83 @@ Task("Inspect") throw new Exception("Issues found"); }); +/////////////////////////////////////////////////////////////////////////////// +// TASKS DEPENDENCY TREE +/////////////////////////////////////////////////////////////////////////////// + +Task("Clean"); + .IsDependentOn("Clean-Impl"); + +Task("Restore-NuGet-Packages") + .IsDependentOn("Restore-NuGet-Packages-Impl"); + .IsDependentOn("Clean") + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows); + +Task("Build") + .IsDependentOn("Build-Impl"); + .IsDependentOn("Restore-NuGet-Packages"); + +Task("Run-Unit-Tests") + .IsDependentOn("Run-Unit-Tests-Impl"); + .IsDependentOn("Build") + .WithCriteria((context, data) => !data.Parameters.SkipTests); + +Task("Run-Designer-Tests") + .IsDependentOn("Run-Designer-Tests-Impl"); + .IsDependentOn("Build") + .WithCriteria((context, data) => !data.Parameters.SkipTests); + +Task("Run-Render-Tests") + .IsDependentOn("Run-Render-Tests-Impl"); + .IsDependentOn("Build") + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows); + +Task("Run-Leak-Tests") + .IsDependentOn("Run-Leak-Tests-Impl"); + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .IsDependentOn("Build"); + +Task("Run-Tests") + .IsDependentOn("Run-Unit-Tests") + .IsDependentOn("Run-Render-Tests") + .IsDependentOn("Run-Designer-Tests") + .IsDependentOn("Run-Leak-Tests"); + +Task("Copy-Files") + .IsDependentOn("Copy-Files-Impl"); + .IsDependentOn("Run-Tests"); + +Task("Zip-Files") + .IsDependentOn("Zip-Files-Impl"); + .IsDependentOn("Copy-Files"); + +Task("Create-NuGet-Packages") + .IsDependentOn("Create-NuGet-Packages-Impl"); + .IsDependentOn("Run-Tests"); + .IsDependentOn("Inspect"); + +Task("Publish-MyGet") + .IsDependentOn("Publish-MyGet-Impl"); + .IsDependentOn("Create-NuGet-Packages"); + .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) + .WithCriteria((context, data) => !data.Parameters.IsPullRequest) + .WithCriteria((context, data) => data.Parameters.IsMainRepo) + .WithCriteria((context, data) => data.Parameters.IsMasterBranch) + .WithCriteria((context, data) => data.Parameters.IsMyGetRelease); + +Task("Publish-NuGet") + .IsDependentOn("Publish-NuGet-Impl"); + .IsDependentOn("Create-NuGet-Packages") + .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) + .WithCriteria((context, data) => !data.Parameters.IsPullRequest) + .WithCriteria((context, data) => data.Parameters.IsMainRepo) + .WithCriteria((context, data) => data.Parameters.IsNuGetRelease); + +Task("Inspect") + .IsDependentOn("Inspect-Impl"); + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .IsDependentOn("Restore-NuGet-Packages"); + /////////////////////////////////////////////////////////////////////////////// // TARGETS /////////////////////////////////////////////////////////////////////////////// From 4de1d156bfbe2599377f52e37efa952dba50abd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 15:26:16 +0200 Subject: [PATCH 66/88] Fix errors --- build.cake | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/build.cake b/build.cake index e739d9375e..d60e358184 100644 --- a/build.cake +++ b/build.cake @@ -363,35 +363,35 @@ Task("Inspect-Impl") // TASKS DEPENDENCY TREE /////////////////////////////////////////////////////////////////////////////// -Task("Clean"); +Task("Clean") .IsDependentOn("Clean-Impl"); Task("Restore-NuGet-Packages") - .IsDependentOn("Restore-NuGet-Packages-Impl"); + .IsDependentOn("Restore-NuGet-Packages-Impl") .IsDependentOn("Clean") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows); Task("Build") - .IsDependentOn("Build-Impl"); + .IsDependentOn("Build-Impl") .IsDependentOn("Restore-NuGet-Packages"); Task("Run-Unit-Tests") - .IsDependentOn("Run-Unit-Tests-Impl"); + .IsDependentOn("Run-Unit-Tests-Impl") .IsDependentOn("Build") .WithCriteria((context, data) => !data.Parameters.SkipTests); Task("Run-Designer-Tests") - .IsDependentOn("Run-Designer-Tests-Impl"); + .IsDependentOn("Run-Designer-Tests-Impl") .IsDependentOn("Build") .WithCriteria((context, data) => !data.Parameters.SkipTests); Task("Run-Render-Tests") - .IsDependentOn("Run-Render-Tests-Impl"); + .IsDependentOn("Run-Render-Tests-Impl") .IsDependentOn("Build") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows); Task("Run-Leak-Tests") - .IsDependentOn("Run-Leak-Tests-Impl"); + .IsDependentOn("Run-Leak-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .IsDependentOn("Build"); @@ -402,21 +402,21 @@ Task("Run-Tests") .IsDependentOn("Run-Leak-Tests"); Task("Copy-Files") - .IsDependentOn("Copy-Files-Impl"); + .IsDependentOn("Copy-Files-Impl") .IsDependentOn("Run-Tests"); Task("Zip-Files") - .IsDependentOn("Zip-Files-Impl"); + .IsDependentOn("Zip-Files-Impl") .IsDependentOn("Copy-Files"); Task("Create-NuGet-Packages") - .IsDependentOn("Create-NuGet-Packages-Impl"); - .IsDependentOn("Run-Tests"); + .IsDependentOn("Create-NuGet-Packages-Impl") + .IsDependentOn("Run-Tests") .IsDependentOn("Inspect"); Task("Publish-MyGet") - .IsDependentOn("Publish-MyGet-Impl"); - .IsDependentOn("Create-NuGet-Packages"); + .IsDependentOn("Publish-MyGet-Impl") + .IsDependentOn("Create-NuGet-Packages") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) @@ -424,7 +424,7 @@ Task("Publish-MyGet") .WithCriteria((context, data) => data.Parameters.IsMyGetRelease); Task("Publish-NuGet") - .IsDependentOn("Publish-NuGet-Impl"); + .IsDependentOn("Publish-NuGet-Impl") .IsDependentOn("Create-NuGet-Packages") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) @@ -432,7 +432,7 @@ Task("Publish-NuGet") .WithCriteria((context, data) => data.Parameters.IsNuGetRelease); Task("Inspect") - .IsDependentOn("Inspect-Impl"); + .IsDependentOn("Inspect-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .IsDependentOn("Restore-NuGet-Packages"); From c9fa15fce82edc376617cb853f9ea92eeefc71a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 15:27:21 +0200 Subject: [PATCH 67/88] Fix dependency order --- build.cake | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/build.cake b/build.cake index d60e358184..7bac795633 100644 --- a/build.cake +++ b/build.cake @@ -367,33 +367,33 @@ Task("Clean") .IsDependentOn("Clean-Impl"); Task("Restore-NuGet-Packages") - .IsDependentOn("Restore-NuGet-Packages-Impl") .IsDependentOn("Clean") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows); + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .IsDependentOn("Restore-NuGet-Packages-Impl"); Task("Build") - .IsDependentOn("Build-Impl") - .IsDependentOn("Restore-NuGet-Packages"); + .IsDependentOn("Restore-NuGet-Packages") + .IsDependentOn("Build-Impl"); Task("Run-Unit-Tests") - .IsDependentOn("Run-Unit-Tests-Impl") .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests); + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .IsDependentOn("Run-Unit-Tests-Impl"); Task("Run-Designer-Tests") - .IsDependentOn("Run-Designer-Tests-Impl") .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests); + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .IsDependentOn("Run-Designer-Tests-Impl"); Task("Run-Render-Tests") - .IsDependentOn("Run-Render-Tests-Impl") .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows); + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .IsDependentOn("Run-Render-Tests-Impl"); Task("Run-Leak-Tests") - .IsDependentOn("Run-Leak-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .IsDependentOn("Build"); + .IsDependentOn("Build") + .IsDependentOn("Run-Leak-Tests-Impl"); Task("Run-Tests") .IsDependentOn("Run-Unit-Tests") @@ -402,39 +402,39 @@ Task("Run-Tests") .IsDependentOn("Run-Leak-Tests"); Task("Copy-Files") - .IsDependentOn("Copy-Files-Impl") - .IsDependentOn("Run-Tests"); + .IsDependentOn("Run-Tests") + .IsDependentOn("Copy-Files-Impl"); Task("Zip-Files") - .IsDependentOn("Zip-Files-Impl") - .IsDependentOn("Copy-Files"); + .IsDependentOn("Copy-Files") + .IsDependentOn("Zip-Files-Impl"); Task("Create-NuGet-Packages") - .IsDependentOn("Create-NuGet-Packages-Impl") .IsDependentOn("Run-Tests") - .IsDependentOn("Inspect"); + .IsDependentOn("Inspect") + .IsDependentOn("Create-NuGet-Packages-Impl"); Task("Publish-MyGet") - .IsDependentOn("Publish-MyGet-Impl") .IsDependentOn("Create-NuGet-Packages") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) .WithCriteria((context, data) => data.Parameters.IsMasterBranch) - .WithCriteria((context, data) => data.Parameters.IsMyGetRelease); + .WithCriteria((context, data) => data.Parameters.IsMyGetRelease) + .IsDependentOn("Publish-MyGet-Impl"); Task("Publish-NuGet") - .IsDependentOn("Publish-NuGet-Impl") .IsDependentOn("Create-NuGet-Packages") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) - .WithCriteria((context, data) => data.Parameters.IsNuGetRelease); + .WithCriteria((context, data) => data.Parameters.IsNuGetRelease) + .IsDependentOn("Publish-NuGet-Impl"); Task("Inspect") - .IsDependentOn("Inspect-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) - .IsDependentOn("Restore-NuGet-Packages"); + .IsDependentOn("Restore-NuGet-Packages") + .IsDependentOn("Inspect-Impl"); /////////////////////////////////////////////////////////////////////////////// // TARGETS From 9da1c65eec57af931cd41a3403e5a2503938cdb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 15:53:31 +0200 Subject: [PATCH 68/88] Check first for criteria --- build.cake | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.cake b/build.cake index 7bac795633..0007e83c61 100644 --- a/build.cake +++ b/build.cake @@ -367,8 +367,8 @@ Task("Clean") .IsDependentOn("Clean-Impl"); Task("Restore-NuGet-Packages") - .IsDependentOn("Clean") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .IsDependentOn("Clean") .IsDependentOn("Restore-NuGet-Packages-Impl"); Task("Build") @@ -376,18 +376,18 @@ Task("Build") .IsDependentOn("Build-Impl"); Task("Run-Unit-Tests") - .IsDependentOn("Build") .WithCriteria((context, data) => !data.Parameters.SkipTests) + .IsDependentOn("Build") .IsDependentOn("Run-Unit-Tests-Impl"); Task("Run-Designer-Tests") - .IsDependentOn("Build") .WithCriteria((context, data) => !data.Parameters.SkipTests) + .IsDependentOn("Build") .IsDependentOn("Run-Designer-Tests-Impl"); Task("Run-Render-Tests") - .IsDependentOn("Build") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .IsDependentOn("Build") .IsDependentOn("Run-Render-Tests-Impl"); Task("Run-Leak-Tests") @@ -415,20 +415,20 @@ Task("Create-NuGet-Packages") .IsDependentOn("Create-NuGet-Packages-Impl"); Task("Publish-MyGet") - .IsDependentOn("Create-NuGet-Packages") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) .WithCriteria((context, data) => data.Parameters.IsMasterBranch) .WithCriteria((context, data) => data.Parameters.IsMyGetRelease) + .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-MyGet-Impl"); Task("Publish-NuGet") - .IsDependentOn("Create-NuGet-Packages") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) .WithCriteria((context, data) => data.Parameters.IsNuGetRelease) + .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-NuGet-Impl"); Task("Inspect") From 3bcaa2996aa9576691263ab20afc4d859764f125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:18:15 +0200 Subject: [PATCH 69/88] Moved criteria to Impl tasks --- build.cake | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/build.cake b/build.cake index 0007e83c61..dca58e54b3 100644 --- a/build.cake +++ b/build.cake @@ -109,6 +109,7 @@ Task("Clean-Impl") }); Task("Restore-NuGet-Packages-Impl") + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .Does(data => { var maxRetryCount = 5; @@ -191,6 +192,7 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) } Task("Run-Unit-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests) .Does(data => { RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); @@ -209,17 +211,20 @@ Task("Run-Unit-Tests-Impl") }); Task("Run-Designer-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests) .Does(data => { RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); }); Task("Run-Render-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .Does(data => { RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); }); Task("Run-Leak-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .Does(() => { var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); @@ -269,6 +274,11 @@ Task("Create-NuGet-Packages-Impl") }); Task("Publish-MyGet-Impl") + .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) + .WithCriteria((context, data) => !data.Parameters.IsPullRequest) + .WithCriteria((context, data) => data.Parameters.IsMainRepo) + .WithCriteria((context, data) => data.Parameters.IsMasterBranch) + .WithCriteria((context, data) => data.Parameters.IsMyGetRelease) .Does(data => { var apiKey = EnvironmentVariable("MYGET_API_KEY"); @@ -297,6 +307,10 @@ Task("Publish-MyGet-Impl") }); Task("Publish-NuGet-Impl") + .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) + .WithCriteria((context, data) => !data.Parameters.IsPullRequest) + .WithCriteria((context, data) => data.Parameters.IsMainRepo) + .WithCriteria((context, data) => data.Parameters.IsNuGetRelease) .Does(data => { var apiKey = EnvironmentVariable("NUGET_API_KEY"); @@ -325,6 +339,7 @@ Task("Publish-NuGet-Impl") }); Task("Inspect-Impl") + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .Does(() => { var badIssues = new []{"PossibleNullReferenceException"}; @@ -367,7 +382,6 @@ Task("Clean") .IsDependentOn("Clean-Impl"); Task("Restore-NuGet-Packages") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .IsDependentOn("Clean") .IsDependentOn("Restore-NuGet-Packages-Impl"); @@ -376,22 +390,18 @@ Task("Build") .IsDependentOn("Build-Impl"); Task("Run-Unit-Tests") - .WithCriteria((context, data) => !data.Parameters.SkipTests) .IsDependentOn("Build") .IsDependentOn("Run-Unit-Tests-Impl"); Task("Run-Designer-Tests") - .WithCriteria((context, data) => !data.Parameters.SkipTests) .IsDependentOn("Build") .IsDependentOn("Run-Designer-Tests-Impl"); Task("Run-Render-Tests") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .IsDependentOn("Build") .IsDependentOn("Run-Render-Tests-Impl"); Task("Run-Leak-Tests") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .IsDependentOn("Build") .IsDependentOn("Run-Leak-Tests-Impl"); @@ -415,24 +425,14 @@ Task("Create-NuGet-Packages") .IsDependentOn("Create-NuGet-Packages-Impl"); Task("Publish-MyGet") - .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) - .WithCriteria((context, data) => !data.Parameters.IsPullRequest) - .WithCriteria((context, data) => data.Parameters.IsMainRepo) - .WithCriteria((context, data) => data.Parameters.IsMasterBranch) - .WithCriteria((context, data) => data.Parameters.IsMyGetRelease) .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-MyGet-Impl"); Task("Publish-NuGet") - .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) - .WithCriteria((context, data) => !data.Parameters.IsPullRequest) - .WithCriteria((context, data) => data.Parameters.IsMainRepo) - .WithCriteria((context, data) => data.Parameters.IsNuGetRelease) .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-NuGet-Impl"); Task("Inspect") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .IsDependentOn("Restore-NuGet-Packages") .IsDependentOn("Inspect-Impl"); From 84be6a7d35a676f3fdaffb20f90a07078a217a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:32:02 +0200 Subject: [PATCH 70/88] Check for NetCoreOnly platform --- build.cake | 3 ++- packages.cake | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.cake b/build.cake index dca58e54b3..839f91b135 100644 --- a/build.cake +++ b/build.cake @@ -110,6 +110,7 @@ Task("Clean-Impl") Task("Restore-NuGet-Packages-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") .Does(data => { var maxRetryCount = 5; @@ -150,7 +151,7 @@ void DotNetCoreBuild(Parameters parameters) Task("Build-Impl") .Does(data => { - if(data.Parameters.IsRunningOnWindows) + if(data.Parameters.IsRunningOnWindows && data.Parameters.Platform != "NetCoreOnly") { MSBuild(data.Parameters.MSBuildSolution, settings => { settings.SetConfiguration(data.Parameters.Configuration); diff --git a/packages.cake b/packages.cake index 79147b5f99..ffc6e7d257 100644 --- a/packages.cake +++ b/packages.cake @@ -491,7 +491,10 @@ public class Packages NuspecNuGetSettings.AddRange(nuspecNuGetSettingsCore); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsDesktop); - NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); + + if (parameters.Platform != "NetCoreOnly") { + NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); + } NuspecNuGetSettings.ForEach((nuspec) => SetNuGetNuspecCommonProperties(nuspec)); From 81bd1c1acc01e763077c7218a025d8ea33db9096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:34:46 +0200 Subject: [PATCH 71/88] More NetCoreOnly checks --- build.cake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.cake b/build.cake index 839f91b135..8e58eaf4b1 100644 --- a/build.cake +++ b/build.cake @@ -205,7 +205,7 @@ Task("Run-Unit-Tests-Impl") RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); - if (data.Parameters.IsRunningOnWindows) + if (data.Parameters.IsRunningOnWindows && data.Parameters.Platform != "NetCoreOnly") { RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); } @@ -219,6 +219,7 @@ Task("Run-Designer-Tests-Impl") Task("Run-Render-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") .Does(data => { RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); @@ -226,6 +227,7 @@ Task("Run-Render-Tests-Impl") Task("Run-Leak-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") .Does(() => { var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); @@ -341,6 +343,7 @@ Task("Publish-NuGet-Impl") Task("Inspect-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") .Does(() => { var badIssues = new []{"PossibleNullReferenceException"}; From 40eb37c9777d20bb642d466204bb753d6a62ed19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:42:19 +0200 Subject: [PATCH 72/88] Filter core files --- packages.cake | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages.cake b/packages.cake index ffc6e7d257..525ba539f9 100644 --- a/packages.cake +++ b/packages.cake @@ -1,3 +1,6 @@ +using System; +using System.Collections; +using System.Collections.Generic; using System.Xml.Linq; public class Packages @@ -9,9 +12,7 @@ public class Packages public string SkiaSharpVersion {get; private set; } public string SkiaSharpLinuxVersion {get; private set; } public Dictionary>> PackageVersions{get; private set;} - - - + class DependencyBuilder : List { Packages _parent; @@ -223,6 +224,21 @@ public class Packages } }; + IList coreFiles; + + if (parameters.Platform != "NetCoreOnly") { + coreFiles = coreLibrariesNuSpecContent + .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) + .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) + .Concat(toolsContent) + .ToList(); + } else { + coreFiles = coreLibrariesNuSpecContent + .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) + .Concat(toolsContent) + .ToList(); + } + var nuspecNuGetSettingsCore = new [] { /////////////////////////////////////////////////////////////////////////////// @@ -255,11 +271,7 @@ public class Packages "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory") .ToArray(), - Files = coreLibrariesNuSpecContent - .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) - .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) - .Concat(toolsContent) - .ToList(), + Files = coreFiles, BasePath = context.Directory("./"), OutputDirectory = parameters.NugetRoot }, From a0196a3ddc130ca95831b9ebe827d62f86d0a9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:46:15 +0200 Subject: [PATCH 73/88] Filter tools content --- packages.cake | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages.cake b/packages.cake index 525ba539f9..3071ddf2a0 100644 --- a/packages.cake +++ b/packages.cake @@ -213,26 +213,27 @@ public class Packages }; }); - var toolsContent = new[] { - new NuSpecContent{ - Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.Designer.HostApp.dll")).FullPath, - Target = "tools/netcoreapp2.0/previewer" - }, - new NuSpecContent{ - Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp.NetFx/bin/" + parameters.DirSuffix + "/Avalonia.Designer.HostApp.exe")).FullPath, - Target = "tools/net461/previewer" - } + var toolHostApp = new NuSpecContent{ + Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.Designer.HostApp.dll")).FullPath, + Target = "tools/netcoreapp2.0/previewer" + }; + + var toolHostAppNetFx = new NuSpecContent{ + Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp.NetFx/bin/" + parameters.DirSuffix + "/Avalonia.Designer.HostApp.exe")).FullPath, + Target = "tools/net461/previewer" }; IList coreFiles; if (parameters.Platform != "NetCoreOnly") { + var toolsContent = new[] { toolHostApp, toolHostAppNetFx }; coreFiles = coreLibrariesNuSpecContent .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) .Concat(toolsContent) .ToList(); } else { + var toolsContent = new[] { toolHostApp }; coreFiles = coreLibrariesNuSpecContent .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) .Concat(toolsContent) From 8661dcc48f6e99659d90a97d316814ea493f3ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:57:26 +0200 Subject: [PATCH 74/88] Enable Direct2D1 for NetCoreOnly platform --- Avalonia.sln | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Avalonia.sln b/Avalonia.sln index 39396f3ab8..75bca0bca8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -398,6 +398,7 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.ActiveCfg = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.Build.0 = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -407,6 +408,7 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|NetCoreOnly.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.Build.0 = Release|Any CPU {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU From 21ab8c36662f96f8dd26e57c3e4663304ce4066f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 16:57:45 +0200 Subject: [PATCH 75/88] Do not include interop for NetCoreOnly --- packages.cake | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages.cake b/packages.cake index 3071ddf2a0..f45e53aeaf 100644 --- a/packages.cake +++ b/packages.cake @@ -464,22 +464,6 @@ public class Packages BasePath = context.Directory("./"), OutputDirectory = parameters.NugetRoot }, - new NuGetPackSettings() - { - Id = "Avalonia.Win32.Interoperability", - Dependencies = new [] - { - new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version }, - new NuSpecDependency() { Id = "SharpDX.Direct3D9", Version = SharpDXDirect3D9Version }, - }, - Files = new [] - { - new NuSpecContent { Source = "Avalonia.Win32.Interop/bin/" + parameters.DirSuffix + "/Avalonia.Win32.Interop.dll", Target = "lib/net45" } - }, - BasePath = context.Directory("./src/Windows"), - OutputDirectory = parameters.NugetRoot - }, /////////////////////////////////////////////////////////////////////////////// // Avalonia.LinuxFramebuffer /////////////////////////////////////////////////////////////////////////////// @@ -500,12 +484,30 @@ public class Packages } }; + var nuspecNuGetSettingInterop = new NuGetPackSettings() + { + Id = "Avalonia.Win32.Interoperability", + Dependencies = new [] + { + new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version }, + new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version }, + new NuSpecDependency() { Id = "SharpDX.Direct3D9", Version = SharpDXDirect3D9Version }, + }, + Files = new [] + { + new NuSpecContent { Source = "Avalonia.Win32.Interop/bin/" + parameters.DirSuffix + "/Avalonia.Win32.Interop.dll", Target = "lib/net45" } + }, + BasePath = context.Directory("./src/Windows"), + OutputDirectory = parameters.NugetRoot + }; + NuspecNuGetSettings = new List(); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsCore); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsDesktop); if (parameters.Platform != "NetCoreOnly") { + NuspecNuGetSettings.Add(nuspecNuGetSettingInterop); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); } From e787ff5a5a8b96e64e52e562fb9744830ecbac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 17:01:39 +0200 Subject: [PATCH 76/88] Zip nupkg --- build.cake | 2 ++ parameters.cake | 2 ++ 2 files changed, 4 insertions(+) diff --git a/build.cake b/build.cake index 8e58eaf4b1..941f640544 100644 --- a/build.cake +++ b/build.cake @@ -258,6 +258,8 @@ Task("Zip-Files-Impl") { Zip(data.Parameters.BinRoot, data.Parameters.ZipCoreArtifacts); + Zip(data.Parameters.NugetRoot, data.Parameters.ZipNuGetArtifacts); + Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, data.Parameters.ZipTargetControlCatalogDesktopDirs, GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + diff --git a/parameters.cake b/parameters.cake index ffd472cbd4..939ac6227e 100644 --- a/parameters.cake +++ b/parameters.cake @@ -34,6 +34,7 @@ public class Parameters public DirectoryPathCollection BuildDirs { get; private set; } public string FileZipSuffix { get; private set; } public FilePath ZipCoreArtifacts { get; private set; } + public FilePath ZipNuGetArtifacts { get; private set; } public DirectoryPath ZipSourceControlCatalogDesktopDirs { get; private set; } public FilePath ZipTargetControlCatalogDesktopDirs { get; private set; } @@ -111,6 +112,7 @@ public class Parameters FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot.CombineWithFilePath("Avalonia-" + FileZipSuffix); + ZipNuGetArtifacts = ZipRoot.CombineWithFilePath("Avalonia-NuGet-" + FileZipSuffix); ZipSourceControlCatalogDesktopDirs = (DirectoryPath)context.Directory("./samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461"); ZipTargetControlCatalogDesktopDirs = ZipRoot.CombineWithFilePath("ControlCatalog.Desktop-" + FileZipSuffix); } From 5cd9ede2893235cca9f1ec8e7ed27b7247200dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 17:29:42 +0200 Subject: [PATCH 77/88] Fix Zip-Files task --- build.cake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.cake b/build.cake index 941f640544..34ad461819 100644 --- a/build.cake +++ b/build.cake @@ -423,6 +423,7 @@ Task("Copy-Files") Task("Zip-Files") .IsDependentOn("Copy-Files") + .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Zip-Files-Impl"); Task("Create-NuGet-Packages") @@ -450,9 +451,9 @@ Task("Package") .IsDependentOn("Create-NuGet-Packages"); Task("AppVeyor") - .IsDependentOn("Zip-Files") .IsDependentOn("Publish-MyGet") - .IsDependentOn("Publish-NuGet"); + .IsDependentOn("Publish-NuGet") + .IsDependentOn("Zip-Files"); Task("Travis") .IsDependentOn("Run-Tests"); From 03628cc0bb10f0d3505eb581829b1a930b6cf060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 29 Jun 2018 17:37:45 +0200 Subject: [PATCH 78/88] Do not copy when building NetCoreOnly platform --- build.cake | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/build.cake b/build.cake index 34ad461819..ab20f7fc7f 100644 --- a/build.cake +++ b/build.cake @@ -260,13 +260,15 @@ Task("Zip-Files-Impl") Zip(data.Parameters.NugetRoot, data.Parameters.ZipNuGetArtifacts); - Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, - data.Parameters.ZipTargetControlCatalogDesktopDirs, - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); + if (data.Parameters.Platform != "NetCoreOnly") { + Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, + data.Parameters.ZipTargetControlCatalogDesktopDirs, + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); + } }); Task("Create-NuGet-Packages-Impl") From 30c9ef8dd46dc6000b2fc414d333de01179e8ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 12:38:32 +0200 Subject: [PATCH 79/88] Simplified task graph --- build.cake | 49 ++++++++++--------------------------------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/build.cake b/build.cake index ab20f7fc7f..66a30edb59 100644 --- a/build.cake +++ b/build.cake @@ -386,53 +386,28 @@ Task("Inspect-Impl") // TASKS DEPENDENCY TREE /////////////////////////////////////////////////////////////////////////////// -Task("Clean") - .IsDependentOn("Clean-Impl"); - -Task("Restore-NuGet-Packages") - .IsDependentOn("Clean") - .IsDependentOn("Restore-NuGet-Packages-Impl"); - Task("Build") - .IsDependentOn("Restore-NuGet-Packages") + .IsDependentOn("Clean-Impl") + .IsDependentOn("Restore-NuGet-Packages-Impl") .IsDependentOn("Build-Impl"); -Task("Run-Unit-Tests") - .IsDependentOn("Build") - .IsDependentOn("Run-Unit-Tests-Impl"); - -Task("Run-Designer-Tests") - .IsDependentOn("Build") - .IsDependentOn("Run-Designer-Tests-Impl"); - -Task("Run-Render-Tests") - .IsDependentOn("Build") - .IsDependentOn("Run-Render-Tests-Impl"); - -Task("Run-Leak-Tests") +Task("Run-Tests") .IsDependentOn("Build") + .IsDependentOn("Run-Unit-Tests-Impl") + .IsDependentOn("Run-Render-Tests-Impl") + .IsDependentOn("Run-Designer-Tests-Impl") .IsDependentOn("Run-Leak-Tests-Impl"); -Task("Run-Tests") - .IsDependentOn("Run-Unit-Tests") - .IsDependentOn("Run-Render-Tests") - .IsDependentOn("Run-Designer-Tests") - .IsDependentOn("Run-Leak-Tests"); - -Task("Copy-Files") +Task("Create-NuGet-Packages") .IsDependentOn("Run-Tests") - .IsDependentOn("Copy-Files-Impl"); + .IsDependentOn("Inspect-Impl") + .IsDependentOn("Create-NuGet-Packages-Impl"); Task("Zip-Files") - .IsDependentOn("Copy-Files") .IsDependentOn("Create-NuGet-Packages") + .IsDependentOn("Copy-Files-Impl") .IsDependentOn("Zip-Files-Impl"); -Task("Create-NuGet-Packages") - .IsDependentOn("Run-Tests") - .IsDependentOn("Inspect") - .IsDependentOn("Create-NuGet-Packages-Impl"); - Task("Publish-MyGet") .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-MyGet-Impl"); @@ -441,10 +416,6 @@ Task("Publish-NuGet") .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-NuGet-Impl"); -Task("Inspect") - .IsDependentOn("Restore-NuGet-Packages") - .IsDependentOn("Inspect-Impl"); - /////////////////////////////////////////////////////////////////////////////// // TARGETS /////////////////////////////////////////////////////////////////////////////// From 98e08fa63bdbb6449cb76d0d3abe7f550f9689d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 12:42:02 +0200 Subject: [PATCH 80/88] Removed unused task graph --- build.cake | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/build.cake b/build.cake index 66a30edb59..2b40a0e301 100644 --- a/build.cake +++ b/build.cake @@ -386,13 +386,10 @@ Task("Inspect-Impl") // TASKS DEPENDENCY TREE /////////////////////////////////////////////////////////////////////////////// -Task("Build") +Task("Run-Tests") .IsDependentOn("Clean-Impl") .IsDependentOn("Restore-NuGet-Packages-Impl") - .IsDependentOn("Build-Impl"); - -Task("Run-Tests") - .IsDependentOn("Build") + .IsDependentOn("Build-Impl") .IsDependentOn("Run-Unit-Tests-Impl") .IsDependentOn("Run-Render-Tests-Impl") .IsDependentOn("Run-Designer-Tests-Impl") From 7bfe322cddff7e9300989a864a0fa96dc453fbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 12:47:29 +0200 Subject: [PATCH 81/88] Formatted code to be consistent --- build.cake | 138 +++++++++++++++++++++++++----------------------- packages.cake | 4 +- parameters.cake | 8 +-- 3 files changed, 74 insertions(+), 76 deletions(-) diff --git a/build.cake b/build.cake index 2b40a0e301..092334b9f8 100644 --- a/build.cake +++ b/build.cake @@ -6,6 +6,7 @@ #addin "nuget:?package=NuGet.Core&version=2.14.0" #tool "nuget:?package=NuGet.CommandLine&version=4.3.0" #tool "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2017.1.20170613.162720" + /////////////////////////////////////////////////////////////////////////////// // TOOLS /////////////////////////////////////////////////////////////////////////////// @@ -194,59 +195,62 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) Task("Run-Unit-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests) - .Does(data => { - RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); - if (data.Parameters.IsRunningOnWindows && data.Parameters.Platform != "NetCoreOnly") - { - RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); - } - }); + .Does(data => +{ + RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); + if (data.Parameters.IsRunningOnWindows && data.Parameters.Platform != "NetCoreOnly") + { + RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); + } +}); Task("Run-Designer-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests) - .Does(data => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); - }); + .Does(data => +{ + RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); +}); Task("Run-Render-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") - .Does(data => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); - }); + .Does(data => +{ + RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); + RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); +}); Task("Run-Leak-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") .Does(() => +{ + var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); + var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings { - var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); - var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings - { - Arguments = new ProcessArgumentBuilder() - .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) - .Append("--propagate-exit-code") - .Append("--") - .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"), - Timeout = 120000 - }); - - if (leakTestsExitCode != 0) - { - throw new Exception("Leak Tests failed"); - } + Arguments = new ProcessArgumentBuilder() + .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) + .Append("--propagate-exit-code") + .Append("--") + .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"), + Timeout = 120000 }); + if (leakTestsExitCode != 0) + { + throw new Exception("Leak Tests failed"); + } +}); + Task("Copy-Files-Impl") .Does(data => { @@ -349,38 +353,38 @@ Task("Inspect-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") .Does(() => - { - var badIssues = new []{"PossibleNullReferenceException"}; - var whitelist = new []{"tests", "src\\android", "src\\ios", - "src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"}; - Information("Running code inspections"); - - var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"), - new ProcessSettings - { - Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln", - RedirectStandardOutput = true - }); +{ + var badIssues = new []{"PossibleNullReferenceException"}; + var whitelist = new []{"tests", "src\\android", "src\\ios", + "src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"}; + Information("Running code inspections"); + + var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"), + new ProcessSettings + { + Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln", + RedirectStandardOutput = true + }); - Information("Analyzing report"); - var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml")); - var failBuild = false; - foreach(var xml in doc.Descendants("Issue")) + Information("Analyzing report"); + var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml")); + var failBuild = false; + foreach(var xml in doc.Descendants("Issue")) + { + var typeId = xml.Attribute("TypeId").Value.ToString(); + if(badIssues.Contains(typeId)) { - var typeId = xml.Attribute("TypeId").Value.ToString(); - if(badIssues.Contains(typeId)) - { - var file = xml.Attribute("File").Value.ToString().ToLower(); - if(whitelist.Any(wh => file.StartsWith(wh))) - continue; - var line = xml.Attribute("Line").Value.ToString(); - Error(typeId + " - " + file + " on line " + line); - failBuild = true; - } + var file = xml.Attribute("File").Value.ToString().ToLower(); + if(whitelist.Any(wh => file.StartsWith(wh))) + continue; + var line = xml.Attribute("Line").Value.ToString(); + Error(typeId + " - " + file + " on line " + line); + failBuild = true; } - if(failBuild) - throw new Exception("Issues found"); - }); + } + if(failBuild) + throw new Exception("Issues found"); +}); /////////////////////////////////////////////////////////////////////////////// // TASKS DEPENDENCY TREE diff --git a/packages.cake b/packages.cake index f45e53aeaf..8a1cf8453c 100644 --- a/packages.cake +++ b/packages.cake @@ -16,6 +16,7 @@ public class Packages class DependencyBuilder : List { Packages _parent; + public DependencyBuilder(Packages parent) { _parent = parent; @@ -25,8 +26,7 @@ public class Packages { return _parent.PackageVersions[name].First().Item1; } - - + public DependencyBuilder Dep(string name, params string[] fws) { if(fws.Length == 0) diff --git a/parameters.cake b/parameters.cake index 939ac6227e..328eed81cf 100644 --- a/parameters.cake +++ b/parameters.cake @@ -8,8 +8,7 @@ public class Parameters public string AssemblyInfoPath { get; private set; } public string ReleasePlatform { get; private set; } public string ReleaseConfiguration { get; private set; } - public string MSBuildSolution { get; private set; } - public string XBuildSolution { get; private set; } + public string MSBuildSolution { get; private set; } public bool IsPlatformAnyCPU { get; private set; } public bool IsPlatformX86 { get; private set; } public bool IsPlatformX64 { get; private set; } @@ -54,7 +53,6 @@ public class Parameters ReleasePlatform = "Any CPU"; ReleaseConfiguration = "Release"; MSBuildSolution = "./Avalonia.sln"; - XBuildSolution = "./Avalonia.XBuild.sln"; // PARAMETERS IsPlatformAnyCPU = StringComparer.OrdinalIgnoreCase.Equals(Platform, "Any CPU"); @@ -72,7 +70,6 @@ public class Parameters IsReleasable = StringComparer.OrdinalIgnoreCase.Equals(ReleasePlatform, Platform) && StringComparer.OrdinalIgnoreCase.Equals(ReleaseConfiguration, Configuration); IsMyGetRelease = !IsTagged && IsReleasable; - // VERSION Version = context.Argument("force-nuget-version", context.ParseAssemblyInfo(AssemblyInfoPath).AssemblyVersion); @@ -104,12 +101,9 @@ public class Parameters NugetRoot = ArtifactsDir.Combine("nuget"); ZipRoot = ArtifactsDir.Combine("zip"); BinRoot = ArtifactsDir.Combine("bin"); - BuildDirs = context.GetDirectories("**/bin") + context.GetDirectories("**/obj"); - DirSuffix = Configuration; DirSuffixIOS = "iPhone" + "/" + Configuration; - FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot.CombineWithFilePath("Avalonia-" + FileZipSuffix); ZipNuGetArtifacts = ZipRoot.CombineWithFilePath("Avalonia-NuGet-" + FileZipSuffix); From 3cd1e567ed0865b0610ef8597f6bbd206bdcb3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 12:53:05 +0200 Subject: [PATCH 82/88] Added IsPlatformNetCoreOnly parameter --- build.cake | 14 +++++++------- parameters.cake | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/build.cake b/build.cake index 092334b9f8..3d2dfe6e51 100644 --- a/build.cake +++ b/build.cake @@ -111,7 +111,7 @@ Task("Clean-Impl") Task("Restore-NuGet-Packages-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) - .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(data => { var maxRetryCount = 5; @@ -152,7 +152,7 @@ void DotNetCoreBuild(Parameters parameters) Task("Build-Impl") .Does(data => { - if(data.Parameters.IsRunningOnWindows && data.Parameters.Platform != "NetCoreOnly") + if(data.Parameters.IsRunningOnWindows && !data.Parameters.IsPlatformNetCoreOnly) { MSBuild(data.Parameters.MSBuildSolution, settings => { settings.SetConfiguration(data.Parameters.Configuration); @@ -207,7 +207,7 @@ Task("Run-Unit-Tests-Impl") RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); - if (data.Parameters.IsRunningOnWindows && data.Parameters.Platform != "NetCoreOnly") + if (data.Parameters.IsRunningOnWindows && !data.Parameters.IsPlatformNetCoreOnly) { RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); } @@ -222,7 +222,7 @@ Task("Run-Designer-Tests-Impl") Task("Run-Render-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(data => { RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); @@ -231,7 +231,7 @@ Task("Run-Render-Tests-Impl") Task("Run-Leak-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(() => { var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); @@ -264,7 +264,7 @@ Task("Zip-Files-Impl") Zip(data.Parameters.NugetRoot, data.Parameters.ZipNuGetArtifacts); - if (data.Parameters.Platform != "NetCoreOnly") { + if (!data.Parameters.IsPlatformNetCoreOnly) { Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, data.Parameters.ZipTargetControlCatalogDesktopDirs, GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + @@ -351,7 +351,7 @@ Task("Publish-NuGet-Impl") Task("Inspect-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) - .WithCriteria((context, data) => data.Parameters.Platform != "NetCoreOnly") + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(() => { var badIssues = new []{"PossibleNullReferenceException"}; diff --git a/parameters.cake b/parameters.cake index 328eed81cf..e595b8159f 100644 --- a/parameters.cake +++ b/parameters.cake @@ -12,6 +12,7 @@ public class Parameters public bool IsPlatformAnyCPU { get; private set; } public bool IsPlatformX86 { get; private set; } public bool IsPlatformX64 { get; private set; } + public bool IsPlatformNetCoreOnly { get; private set; } public bool IsLocalBuild { get; private set; } public bool IsRunningOnUnix { get; private set; } public bool IsRunningOnWindows { get; private set; } @@ -58,6 +59,7 @@ public class Parameters IsPlatformAnyCPU = StringComparer.OrdinalIgnoreCase.Equals(Platform, "Any CPU"); IsPlatformX86 = StringComparer.OrdinalIgnoreCase.Equals(Platform, "x86"); IsPlatformX64 = StringComparer.OrdinalIgnoreCase.Equals(Platform, "x64"); + IsPlatformNetCoreOnly = StringComparer.OrdinalIgnoreCase.Equals(Platform, "NetCoreOnly"); IsLocalBuild = buildSystem.IsLocalBuild; IsRunningOnUnix = context.IsRunningOnUnix(); IsRunningOnWindows = context.IsRunningOnWindows(); From 608194cb1418c6519533d1d3fc878f050af373ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 12:53:40 +0200 Subject: [PATCH 83/88] Changed comments --- build.cake | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/build.cake b/build.cake index 3d2dfe6e51..6585da1e72 100644 --- a/build.cake +++ b/build.cake @@ -96,7 +96,7 @@ Teardown((context, buildContext) => }); /////////////////////////////////////////////////////////////////////////////// -// TASKS IMPLEMENTATIONS +// TASKS /////////////////////////////////////////////////////////////////////////////// Task("Clean-Impl") @@ -387,7 +387,7 @@ Task("Inspect-Impl") }); /////////////////////////////////////////////////////////////////////////////// -// TASKS DEPENDENCY TREE +// TARGETS /////////////////////////////////////////////////////////////////////////////// Task("Run-Tests") @@ -417,10 +417,6 @@ Task("Publish-NuGet") .IsDependentOn("Create-NuGet-Packages") .IsDependentOn("Publish-NuGet-Impl"); -/////////////////////////////////////////////////////////////////////////////// -// TARGETS -/////////////////////////////////////////////////////////////////////////////// - Task("Package") .IsDependentOn("Create-NuGet-Packages"); From efd119e6d4f55488dd80a87cce95f1ba60bc3b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 12:56:11 +0200 Subject: [PATCH 84/88] Unified tasks with criteria --- build.cake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.cake b/build.cake index 6585da1e72..6b03f9fe52 100644 --- a/build.cake +++ b/build.cake @@ -221,7 +221,8 @@ Task("Run-Designer-Tests-Impl") }); Task("Run-Render-Tests-Impl") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(data => { @@ -230,7 +231,8 @@ Task("Run-Render-Tests-Impl") }); Task("Run-Leak-Tests-Impl") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(() => { From fcf54c4939cf39a94105d2b7437db98905c48b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 13:07:48 +0200 Subject: [PATCH 85/88] Even more simplification of tasks graph --- build.cake | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/build.cake b/build.cake index 6b03f9fe52..28dc503e35 100644 --- a/build.cake +++ b/build.cake @@ -401,31 +401,17 @@ Task("Run-Tests") .IsDependentOn("Run-Designer-Tests-Impl") .IsDependentOn("Run-Leak-Tests-Impl"); -Task("Create-NuGet-Packages") +Task("Package") .IsDependentOn("Run-Tests") .IsDependentOn("Inspect-Impl") - .IsDependentOn("Create-NuGet-Packages-Impl"); - -Task("Zip-Files") - .IsDependentOn("Create-NuGet-Packages") - .IsDependentOn("Copy-Files-Impl") - .IsDependentOn("Zip-Files-Impl"); - -Task("Publish-MyGet") - .IsDependentOn("Create-NuGet-Packages") - .IsDependentOn("Publish-MyGet-Impl"); - -Task("Publish-NuGet") - .IsDependentOn("Create-NuGet-Packages") - .IsDependentOn("Publish-NuGet-Impl"); - -Task("Package") - .IsDependentOn("Create-NuGet-Packages"); + .IsDependentOn("Create-NuGet-Packages-Impl") Task("AppVeyor") - .IsDependentOn("Publish-MyGet") - .IsDependentOn("Publish-NuGet") - .IsDependentOn("Zip-Files"); + .IsDependentOn("Package") + .IsDependentOn("Publish-MyGet-Impl") + .IsDependentOn("Publish-NuGet-Impl") + .IsDependentOn("Copy-Files-Impl") + .IsDependentOn("Zip-Files-Impl"); Task("Travis") .IsDependentOn("Run-Tests"); From 98642a97b717b3afa62630dcbdc7a29629126b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 13:09:57 +0200 Subject: [PATCH 86/88] Changed task dependency order for AppVeyor task --- build.cake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.cake b/build.cake index 28dc503e35..deac33de09 100644 --- a/build.cake +++ b/build.cake @@ -408,10 +408,10 @@ Task("Package") Task("AppVeyor") .IsDependentOn("Package") - .IsDependentOn("Publish-MyGet-Impl") - .IsDependentOn("Publish-NuGet-Impl") .IsDependentOn("Copy-Files-Impl") - .IsDependentOn("Zip-Files-Impl"); + .IsDependentOn("Zip-Files-Impl") + .IsDependentOn("Publish-MyGet-Impl") + .IsDependentOn("Publish-NuGet-Impl"); Task("Travis") .IsDependentOn("Run-Tests"); From ff6676a2b0e1e027a990bfd6295c44c45184464c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 13:11:37 +0200 Subject: [PATCH 87/88] Fixed error --- build.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cake b/build.cake index deac33de09..78c166a3bc 100644 --- a/build.cake +++ b/build.cake @@ -404,7 +404,7 @@ Task("Run-Tests") Task("Package") .IsDependentOn("Run-Tests") .IsDependentOn("Inspect-Impl") - .IsDependentOn("Create-NuGet-Packages-Impl") + .IsDependentOn("Create-NuGet-Packages-Impl"); Task("AppVeyor") .IsDependentOn("Package") From 213ec8ce07c806f8cd7d1a0ea16474ccaa55da90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 30 Jun 2018 14:46:39 +0200 Subject: [PATCH 88/88] Use IsPlatformNetCoreOnly property --- packages.cake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages.cake b/packages.cake index 8a1cf8453c..51d9c48159 100644 --- a/packages.cake +++ b/packages.cake @@ -225,7 +225,7 @@ public class Packages IList coreFiles; - if (parameters.Platform != "NetCoreOnly") { + if (!parameters.IsPlatformNetCoreOnly) { var toolsContent = new[] { toolHostApp, toolHostAppNetFx }; coreFiles = coreLibrariesNuSpecContent .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) @@ -506,7 +506,7 @@ public class Packages NuspecNuGetSettings.AddRange(nuspecNuGetSettingsCore); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsDesktop); - if (parameters.Platform != "NetCoreOnly") { + if (!parameters.IsPlatformNetCoreOnly) { NuspecNuGetSettings.Add(nuspecNuGetSettingInterop); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); }