From ab06e45488b94f2d767def229460283a50281e83 Mon Sep 17 00:00:00 2001 From: RMBGAME Date: Fri, 19 Nov 2021 20:08:06 +0800 Subject: [PATCH 001/213] TrayIcon should be re-added when the Explorer is restarted --- .../Interop/UnmanagedMethods.cs | 85 ++++++++++--------- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 12 ++- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 938f4222e0..9e514461d7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -903,9 +903,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData); - + public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); - + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetDC(IntPtr hWnd); @@ -996,7 +996,7 @@ namespace Avalonia.Win32.Interop public static uint GetWindowLong(IntPtr hWnd, int nIndex) { - if(IntPtr.Size == 4) + if (IntPtr.Size == 4) { return GetWindowLong32b(hWnd, nIndex); } @@ -1023,7 +1023,7 @@ namespace Avalonia.Win32.Interop return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32(); } } - + public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle) { if (IntPtr.Size == 4) @@ -1057,14 +1057,14 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); - - + + [DllImport("user32.dll")] public static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect); [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd); - + [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr hWnd); @@ -1091,22 +1091,25 @@ namespace Avalonia.Win32.Interop [DllImport("user32")] public static extern IntPtr GetMessageExtraInfo(); - + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")] public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx); [DllImport("user32.dll")] public static extern void RegisterTouchWindow(IntPtr hWnd, int flags); - + [DllImport("user32.dll")] public static extern bool ReleaseCapture(); + [DllImport("user32.dll", SetLastError = true)] + public static extern uint RegisterWindowMessage(string lpString); + [DllImport("user32.dll")] public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetActiveWindow(); - + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetActiveWindow(IntPtr hWnd); @@ -1282,7 +1285,7 @@ namespace Avalonia.Win32.Interop [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LoadLibrary(string fileName); - + [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, int flags); @@ -1326,7 +1329,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags); - + [DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi); @@ -1334,14 +1337,14 @@ namespace Avalonia.Win32.Interop [DllImport("user32")] public static extern unsafe bool GetTouchInputInfo( IntPtr hTouchInput, - uint cInputs, + uint cInputs, TOUCHINPUT* pInputs, - int cbSize + int cbSize ); - + [DllImport("user32")] public static extern bool CloseTouchInputHandle(IntPtr hTouchInput); - + [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); @@ -1350,7 +1353,7 @@ namespace Avalonia.Win32.Interop public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines, IntPtr lpvBits, [In] ref BITMAPINFOHEADER lpbmi, uint fuColorUse); - + [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle(IntPtr hObject); @@ -1365,27 +1368,27 @@ namespace Avalonia.Win32.Interop [DllImport("gdi32.dll")] public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); - + [DllImport("gdi32.dll")] public static extern int DescribePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); [DllImport("gdi32.dll")] public static extern int SetPixelFormat(IntPtr hdc, int iPixelFormat, ref PixelFormatDescriptor pfd); - - + + [DllImport("gdi32.dll")] public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd); - + [DllImport("gdi32.dll")] public static extern bool SwapBuffers(IntPtr hdc); [DllImport("opengl32.dll")] public static extern IntPtr wglCreateContext(IntPtr hdc); - + [DllImport("opengl32.dll")] public static extern bool wglDeleteContext(IntPtr context); - + [DllImport("opengl32.dll")] public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); @@ -1406,9 +1409,9 @@ namespace Avalonia.Win32.Interop uint dwMaximumSizeLow, string lpName); - [DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); - + [DllImport("msvcrt.dll", EntryPoint = "memcpy", SetLastError = false, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); @@ -1447,10 +1450,10 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern void DwmFlush(); - + [DllImport("dwmapi.dll")] public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); - + [DllImport("dwmapi.dll")] public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); @@ -1507,8 +1510,8 @@ namespace Avalonia.Win32.Interop throw new Exception("RtlGetVersion failed!"); } } - - [DllImport("kernel32", EntryPoint="WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] + + [DllImport("kernel32", EntryPoint = "WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable); public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF); @@ -1516,7 +1519,7 @@ namespace Avalonia.Win32.Interop internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable) { int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable); - if(result == WAIT_FAILED) + if (result == WAIT_FAILED) { throw new Win32Exception(); } @@ -1558,7 +1561,7 @@ namespace Avalonia.Win32.Interop DrawLeftBorder = 0x20, DrawTopBorder = 0x40, DrawRightBorder = 0x80, - DrawBottomBorder = 0x100, + DrawBottomBorder = 0x100, } [StructLayout(LayoutKind.Sequential)] @@ -1626,9 +1629,9 @@ namespace Avalonia.Win32.Interop MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI - } + } - public enum ClipboardFormat + public enum ClipboardFormat { /// /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text. @@ -1679,7 +1682,7 @@ namespace Avalonia.Win32.Interop public int X; public int Y; } - + public struct SIZE { public int X; @@ -1880,7 +1883,7 @@ namespace Avalonia.Win32.Interop OFN_NOREADONLYRETURN = 0x00008000, OFN_OVERWRITEPROMPT = 0x00000002 } - + public enum HRESULT : uint { S_FALSE = 0x0001, @@ -2198,13 +2201,13 @@ namespace Avalonia.Win32.Interop internal interface IDropTarget { [PreserveSig] - UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)][In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)][In] int grfKeyState, [MarshalAs(UnmanagedType.U8)][In] long pt, [In][Out] ref DropEffect pdwEffect); [PreserveSig] - UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)][In] int grfKeyState, [MarshalAs(UnmanagedType.U8)][In] long pt, [In][Out] ref DropEffect pdwEffect); [PreserveSig] UnmanagedMethods.HRESULT DragLeave(); [PreserveSig] - UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)][In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)][In] int grfKeyState, [MarshalAs(UnmanagedType.U8)][In] long pt, [In][Out] ref DropEffect pdwEffect); } [ComImport] @@ -2213,9 +2216,9 @@ namespace Avalonia.Win32.Interop internal interface IDropSource { [PreserveSig] - int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState); + int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)][In] int grfKeyState); [PreserveSig] - int GiveFeedback([MarshalAs(UnmanagedType.U4)] [In] int dwEffect); + int GiveFeedback([MarshalAs(UnmanagedType.U4)][In] int dwEffect); } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 23395dd9b5..c28ec94fe8 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -24,6 +24,7 @@ namespace Avalonia.Win32 private readonly Win32NativeToManagedMenuExporter _exporter; private static readonly Dictionary s_trayIcons = new Dictionary(); private bool _disposedValue; + private static readonly uint WM_TASKBARCREATED = UnmanagedMethods.RegisterWindowMessage("TaskbarCreated"); public TrayIconImpl() { @@ -44,6 +45,15 @@ namespace Avalonia.Win32 { s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); } + + if (msg == WM_TASKBARCREATED) + { + foreach (var tray in s_trayIcons.Values) + { + tray.UpdateIcon(true); + tray.UpdateIcon(); + } + } } public void SetIcon(IWindowIconImpl? icon) @@ -145,7 +155,7 @@ namespace Avalonia.Win32 private enum CustomWindowsMessage : uint { WM_TRAYICON = WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024, } private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable From 6bb6c0e5c92716521aac4915904f509450966397 Mon Sep 17 00:00:00 2001 From: RMBGAME Date: Fri, 19 Nov 2021 21:55:35 +0800 Subject: [PATCH 002/213] Only re-add visible TrayIcon --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index c28ec94fe8..86732539f1 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -50,8 +50,11 @@ namespace Avalonia.Win32 { foreach (var tray in s_trayIcons.Values) { - tray.UpdateIcon(true); - tray.UpdateIcon(); + if (tray._iconAdded) + { + tray.UpdateIcon(true); + tray.UpdateIcon(); + } } } } From 213cc3429b7d91a2e84c1c2978a28d82ccb2f1ab Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:27:34 +0300 Subject: [PATCH 003/213] Visual now uses WeakEvent too --- src/Avalonia.Visuals/Visual.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 324b253a0f..41c9f76dbc 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -91,11 +91,17 @@ namespace Avalonia /// public static readonly StyledProperty ZIndexProperty = AvaloniaProperty.Register(nameof(ZIndex)); + + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); private Rect _bounds; private TransformedBounds? _transformedBounds; private IRenderRoot? _visualRoot; private IVisual? _visualParent; + private WeakEventSubscriber? _affectsRenderWeakSubscriber; /// /// Initializes static members of the class. @@ -352,12 +358,21 @@ namespace Avalonia { if (e.OldValue is IAffectsRender oldValue) { - WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); } if (e.NewValue is IAffectsRender newValue) { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new WeakEventSubscriber(); + sender._affectsRenderWeakSubscriber.Event += delegate + { + sender.InvalidateVisual(); + }; + } + InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); } sender.InvalidateVisual(); @@ -608,8 +623,6 @@ namespace Avalonia OnVisualParentChanged(old, value); } - private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual(); - /// /// Called when the collection changes. /// From 7e1d9dbc72eb9c6371f8bf33800caab19916cf20 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:28:49 +0300 Subject: [PATCH 004/213] Optimized WeakEvent (9332ms to 5ms using #6660 bench) --- src/Avalonia.Base/Utilities/WeakEvent.cs | 85 ++++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 0b32015a8a..21c165afae 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (!_subscriptions.TryGetValue(target, out var subscription)) _subscriptions.Add(target, subscription = new Subscription(this, target)); - subscription.Add(new WeakReference>(subscriber)); + subscription.Add(subscriber); } public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) @@ -51,11 +52,61 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly TSender _target; private readonly Action _compact; - private WeakReference>?[] _data = - new WeakReference>[16]; + struct Entry + { + WeakReference>? _reference; + int _hashCode; + + public Entry(IWeakEventSubscriber r) + { + if (r == null) + { + _reference = null; + _hashCode = 0; + return; + } + + _hashCode = r.GetHashCode(); + _reference = new WeakReference>(r); + } + + public bool IsEmpty + { + get + { + if (_reference == null) + return false; + if (_reference.TryGetTarget(out var target)) + return true; + _reference = null; + return false; + } + } + + public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) + { + if (_reference == null) + { + target = null!; + return false; + } + return _reference.TryGetTarget(out target); + } + + public bool Equals(IWeakEventSubscriber r) + { + if (_reference == null || r.GetHashCode() != _hashCode) + return false; + return _reference.TryGetTarget(out var target) && target == r; + } + } + + private Entry[] _data = + new Entry[16]; private int _count; private readonly Action _unsubscribe; private bool _compactScheduled; + private int _removedSinceLastCompact; public Subscription(WeakEvent ev, TSender target) { @@ -71,17 +122,17 @@ public class WeakEvent : WeakEvent where TEventArgs : Event _ev._subscriptions.Remove(_target); } - public void Add(WeakReference> s) + public void Add(IWeakEventSubscriber s) { if (_count == _data.Length) { //Extend capacity - var extendedData = new WeakReference>?[_data.Length * 2]; + var extendedData = new Entry[_data.Length * 2]; Array.Copy(_data, extendedData, _data.Length); _data = extendedData; } - _data[_count] = s; + _data[_count] = new(s); _count++; } @@ -93,16 +144,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { var reference = _data[c]; - if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + if (reference.Equals(s)) { - _data[c] = null; + _data[c] = default; removed = true; + break; } } if (removed) { + _removedSinceLastCompact++; ScheduleCompact(); + + if (_removedSinceLastCompact > 500) + Compact(); } } @@ -116,18 +172,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { + if(!_compactScheduled || _removedSinceLastCompact == 0) + return; _compactScheduled = false; + _removedSinceLastCompact = 0; int empty = -1; for (var c = 0; c < _count; c++) { - var r = _data[c]; + ref var r = ref _data[c]; //Mark current index as first empty - if (r == null && empty == -1) + if (r.IsEmpty && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (r != null && empty != -1) + if (!r.IsEmpty && empty != -1) { - _data[c] = null; + _data[c] = default; _data[empty] = r; empty++; } @@ -145,7 +204,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event for (var c = 0; c < _count; c++) { var r = _data[c]; - if (r?.TryGetTarget(out var sub) == true) + if (r.TryGetTarget(out var sub)) sub!.OnEvent(_target, _ev, eventArgs); else needCompact = true; From dca2ca5940f0c768529f97b89535ed172ef44403 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:29:14 +0300 Subject: [PATCH 005/213] Don't implement IWeakEventSubscriber directly on public types --- .../Utilities/IWeakEventSubscriber.cs | 10 +++++++ .../Repeater/ItemsRepeater.cs | 30 ++++++++++--------- src/Avalonia.Controls/TopLevel.cs | 16 +++++----- src/Avalonia.Visuals/Media/Pen.cs | 25 +++++++++------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index e48c0cb111..2a24376592 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -9,4 +9,14 @@ namespace Avalonia.Utilities; public interface IWeakEventSubscriber where TEventArgs : EventArgs { void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} + +public class WeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + public event Action? Event; + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + + } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index b40cf26df5..6d2f4144eb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -68,6 +68,7 @@ namespace Avalonia.Controls private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + private WeakEventSubscriber _layoutWeakSubscriber = new(); /// /// Initializes a new instance of the class. @@ -77,6 +78,15 @@ namespace Avalonia.Controls _viewManager = new ViewManager(this); _viewportManager = new ViewportManager(this); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + + _layoutWeakSubscriber.Event += (_, ev, e) => + { + if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + }; + OnLayoutChanged(null, Layout); } @@ -723,8 +733,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -742,8 +752,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -793,15 +803,7 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) - InvalidateArrange(); - else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) - InvalidateMeasure(); - } - + private VirtualizingLayoutContext GetLayoutContext() { if (_layoutContext == null) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index eaee5bdb50..76de113290 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -33,8 +33,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, - ITextInputMethodRoot, - IWeakEventSubscriber + ITextInputMethodRoot { /// /// Defines the property. @@ -90,6 +89,7 @@ namespace Avalonia.Controls private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; + private WeakEventSubscriber _resourcesChangesSubscriber; /// /// Initializes static members of the class. @@ -184,7 +184,12 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - ResourcesChangedWeakEvent.Subscribe(applicationResources, this); + _resourcesChangesSubscriber = new(); + _resourcesChangesSubscriber.Event += (_, __, e) => + { + ((ILogical)this).NotifyResourcesChanged(e); + }; + ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber); } impl.LostFocus += PlatformImpl_LostFocus; @@ -289,11 +294,6 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } - /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 65ba851100..f0a0d24248 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber + public sealed class Pen : AvaloniaObject, IPen { /// /// Defines the property. @@ -48,7 +48,8 @@ namespace Avalonia.Media private EventHandler? _invalidated; private IAffectsRender? _subscribedToBrush; private IAffectsRender? _subscribedToDashes; - + private WeakEventSubscriber? _weakSubscriber; + /// /// Initializes a new instance of the class. /// @@ -207,13 +208,23 @@ namespace Avalonia.Media { if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(field, this); + if (_weakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); field = null; } if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { - InvalidatedWeakEvent.Subscribe(affectsRender, this); + if (_weakSubscriber == null) + { + _weakSubscriber = new WeakEventSubscriber(); + _weakSubscriber.Event += (_, ev, __) => + { + if (ev == InvalidatedWeakEvent) + _invalidated?.Invoke(this, EventArgs.Empty); + }; + } + InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber); field = affectsRender; } } @@ -223,11 +234,5 @@ namespace Avalonia.Media UpdateSubscription(ref _subscribedToBrush, Brush); UpdateSubscription(ref _subscribedToDashes, DashStyle); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if (ev == InvalidatedWeakEvent) - _invalidated?.Invoke(this, EventArgs.Empty); - } } } From bb9f5e75f33f460dcb729678b2c3a8a41fc81fb0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:55:46 +0300 Subject: [PATCH 006/213] Fixed WeakEvent Compact --- src/Avalonia.Base/Utilities/WeakEvent.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 21c165afae..5d5a1cb55b 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -75,7 +75,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event get { if (_reference == null) - return false; + return true; if (_reference.TryGetTarget(out var target)) return true; _reference = null; @@ -179,15 +179,15 @@ public class WeakEvent : WeakEvent where TEventArgs : Event int empty = -1; for (var c = 0; c < _count; c++) { - ref var r = ref _data[c]; + ref var currentRef = ref _data[c]; //Mark current index as first empty - if (r.IsEmpty && empty == -1) + if (currentRef.IsEmpty && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (!r.IsEmpty && empty != -1) + if (!currentRef.IsEmpty && empty != -1) { - _data[c] = default; - _data[empty] = r; + _data[empty] = currentRef; + currentRef = default; empty++; } } From c73fcd25a9cfcaa17e7295b6b01080c585fe3c78 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 20:09:24 +0300 Subject: [PATCH 007/213] Fixed tests --- src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs | 2 +- src/Avalonia.Base/Utilities/WeakEvent.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index 2a24376592..57060853c8 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -17,6 +17,6 @@ public class WeakEventSubscriber : IWeakEventSubscriber void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) { - + Event?.Invoke(sender, ev, e); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 5d5a1cb55b..bcee7f89f7 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -76,10 +76,10 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (_reference == null) return true; - if (_reference.TryGetTarget(out var target)) - return true; + if (_reference.TryGetTarget(out _)) + return false; _reference = null; - return false; + return true; } } From a4fa74977f2a9c7c8cb6fc1f8b86c88f00543463 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 20:17:56 +0300 Subject: [PATCH 008/213] Removed immediate compact since it makes things worse --- src/Avalonia.Base/Utilities/WeakEvent.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index bcee7f89f7..b27ea9f455 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -106,7 +106,6 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private int _count; private readonly Action _unsubscribe; private bool _compactScheduled; - private int _removedSinceLastCompact; public Subscription(WeakEvent ev, TSender target) { @@ -154,11 +153,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if (removed) { - _removedSinceLastCompact++; ScheduleCompact(); - - if (_removedSinceLastCompact > 500) - Compact(); } } @@ -172,10 +167,9 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { - if(!_compactScheduled || _removedSinceLastCompact == 0) + if(!_compactScheduled) return; _compactScheduled = false; - _removedSinceLastCompact = 0; int empty = -1; for (var c = 0; c < _count; c++) { From 8103f2a0b1c7dbe5f0c620229442ce8a7fe619d3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 23:47:12 +0300 Subject: [PATCH 009/213] Use Dictionary for more than 8 WeakEvent subscribers --- src/Avalonia.Base/Utilities/WeakEvent.cs | 85 ++----- src/Avalonia.Base/Utilities/WeakHashList.cs | 236 ++++++++++++++++++++ 2 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/WeakHashList.cs diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index b27ea9f455..1335d7e9b8 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -101,11 +101,10 @@ public class WeakEvent : WeakEvent where TEventArgs : Event } } - private Entry[] _data = - new Entry[16]; - private int _count; private readonly Action _unsubscribe; + private readonly WeakHashList> _list = new(); private bool _compactScheduled; + private bool _destroyed; public Subscription(WeakEvent ev, TSender target) { @@ -117,49 +116,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Destroy() { + if(_destroyed) + return; + _destroyed = true; _unsubscribe(); _ev._subscriptions.Remove(_target); } - public void Add(IWeakEventSubscriber s) - { - if (_count == _data.Length) - { - //Extend capacity - var extendedData = new Entry[_data.Length * 2]; - Array.Copy(_data, extendedData, _data.Length); - _data = extendedData; - } - - _data[_count] = new(s); - _count++; - } + public void Add(IWeakEventSubscriber s) => _list.Add(s); public void Remove(IWeakEventSubscriber s) { - var removed = false; - - for (int c = 0; c < _count; ++c) - { - var reference = _data[c]; - - if (reference.Equals(s)) - { - _data[c] = default; - removed = true; - break; - } - } - - if (removed) - { + _list.Remove(s); + if(_list.IsEmpty) + Destroy(); + else if(_list.NeedCompact && _compactScheduled) ScheduleCompact(); - } } void ScheduleCompact() { - if(_compactScheduled) + if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); @@ -170,42 +147,24 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if(!_compactScheduled) return; _compactScheduled = false; - int empty = -1; - for (var c = 0; c < _count; c++) - { - ref var currentRef = ref _data[c]; - //Mark current index as first empty - if (currentRef.IsEmpty && empty == -1) - empty = c; - //If current element isn't null and we have an empty one - if (!currentRef.IsEmpty && empty != -1) - { - _data[empty] = currentRef; - currentRef = default; - empty++; - } - } - - if (empty != -1) - _count = empty; - if (_count == 0) + _list.Compact(); + if (_list.IsEmpty) Destroy(); } void OnEvent(object? sender, TEventArgs eventArgs) { - var needCompact = false; - for (var c = 0; c < _count; c++) + var alive = _list.GetAlive(); + if(alive == null) + Destroy(); + else { - var r = _data[c]; - if (r.TryGetTarget(out var sub)) - sub!.OnEvent(_target, _ev, eventArgs); - else - needCompact = true; + foreach(var item in alive) + item.OnEvent(_target, _ev, eventArgs); + WeakHashList>.ReturnToSharedPool(alive); + if(_list.NeedCompact && !_compactScheduled) + ScheduleCompact(); } - - if (needCompact) - ScheduleCompact(); } } diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs new file mode 100644 index 0000000000..32668872da --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Utilities; + +internal class WeakHashList where T : class +{ + private struct Key + { + public WeakReference? Weak; + public T? Strong; + public int HashCode; + + public static Key MakeStrong(T r) => new() + { + HashCode = r.GetHashCode(), + Strong = r + }; + + public static Key MakeWeak(T r) => new() + { + HashCode = r.GetHashCode(), + Weak = new WeakReference(r) + }; + + public override int GetHashCode() => HashCode; + } + + class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (x.HashCode != y.HashCode) + return false; + if (x.Strong != null) + { + if (y.Strong != null) + return x.Strong == y.Strong; + if (y.Weak == null) + return false; + return y.Weak.TryGetTarget(out var weakTarget) && weakTarget == x.Strong; + } + else if (y.Strong != null) + { + if (x.Weak == null) + return false; + return x.Weak.TryGetTarget(out var weakTarget) && weakTarget == y.Strong; + } + else + { + if (x.Weak == null || x.Weak.TryGetTarget(out var xTarget) == false) + return y.Weak?.TryGetTarget(out _) != true; + return y.Weak?.TryGetTarget(out var yTarget) == true && xTarget == yTarget; + } + } + + public int GetHashCode(Key obj) => obj.HashCode; + public static KeyComparer Instance = new(); + } + + Dictionary? _dic; + WeakReference?[]? _arr; + int _arrCount; + + public bool IsEmpty => _dic == null || _dic.Count == 0; + public bool NeedCompact { get; private set; } + + public void Add(T item) + { + if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + if (_dic.TryGetValue(strongKey, out var cnt)) + _dic[strongKey] = cnt + 1; + else + _dic[Key.MakeWeak(item)] = 1; + return; + } + + if (_arr == null) + _arr = new WeakReference[8]; + + if (_arrCount < _arr.Length) + { + _arr[_arrCount] = new WeakReference(item); + _arrCount++; + return; + } + + // Check if something is dead + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]!.TryGetTarget(out _) == false) + { + _arr[c] = new WeakReference(item); + return; + } + } + + _dic = new Dictionary(KeyComparer.Instance); + foreach (var existing in _arr) + { + if (existing!.TryGetTarget(out var target)) + Add(target); + } + _arr = null; + + } + + public void Remove(T item) + { + if (_arr != null) + { + for (var c = 0; c < _arr.Length; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true && target == item) + { + _arr[c] = null; + Compact(); + return; + } + } + } + else if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + + if (_dic.TryGetValue(strongKey, out var cnt)) + { + if (cnt > 1) + { + _dic[strongKey] = cnt - 1; + return; + } + } + + _dic.Remove(strongKey); + } + } + + private void ArrCompact() + { + if (_arr != null) + { + int empty = -1; + for (var c = 0; c < _arrCount; c++) + { + var r = _arr[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _arr[c] = null; + _arr[empty] = r; + empty++; + } + } + + if (empty != -1) + _arrCount = empty; + } + } + + public void Compact() + { + if (_dic != null) + { + PooledList? toRemove = null; + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out _) != true) + (toRemove ??= new PooledList()).Add(kvp.Key); + } + + if (toRemove != null) + { + foreach (var k in toRemove) + _dic.Remove(k); + toRemove.Dispose(); + } + } + } + + private static readonly Stack> s_listPool = new(); + + public static void ReturnToSharedPool(PooledList list) + { + list.Clear(); + s_listPool.Push(list); + } + + public PooledList? GetAlive(Func>? factory = null) + { + PooledList? pooled = null; + if (_arr != null) + { + bool needCompact = false; + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())).Add(target!); + else + { + _arr[c] = null; + needCompact = true; + } + } + if(needCompact) + ArrCompact(); + return pooled; + } + if (_dic != null) + { + + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())) + .Add(target!); + else + NeedCompact = true; + } + } + + return pooled; + } +} \ No newline at end of file From 27ecb055086d437516ec6685fc22016d77ebed94 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 17 Jan 2022 11:38:20 +0300 Subject: [PATCH 010/213] Use PooledList.Span for enumeration --- src/Avalonia.Base/Utilities/WeakEvent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 1335d7e9b8..e72606bf70 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -159,7 +159,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event Destroy(); else { - foreach(var item in alive) + foreach(var item in alive.Span) item.OnEvent(_target, _ev, eventArgs); WeakHashList>.ReturnToSharedPool(alive); if(_list.NeedCompact && !_compactScheduled) From e1e6789f7824c6a21f4d8c7288cf2cffe672ee00 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:05:29 -0400 Subject: [PATCH 011/213] Add ColorPicker base enums --- .../ColorPicker/ColorPickerHsvChannel.cs | 33 +++++++++++++ .../ColorSpectrum/IncrementAmount.cs | 23 ++++++++++ .../ColorSpectrum/IncrementDirection.cs | 23 ++++++++++ .../ColorPicker/ColorSpectrumChannels.cs | 46 +++++++++++++++++++ .../ColorPicker/ColorSpectrumShape.cs | 26 +++++++++++ 5 files changed, 151 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs b/src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs new file mode 100644 index 0000000000..9e6e7cb89e --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs @@ -0,0 +1,33 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +namespace Avalonia.Controls +{ + /// + /// Defines a specific HSV color model channel. + /// + public enum ColorPickerHsvChannel + { + /// + /// The Hue channel. + /// + Hue, + + /// + /// The Saturation channel. + /// + Saturation, + + /// + /// The Value channel. + /// + Value, + + /// + /// The Alpha channel. + /// + Alpha + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs new file mode 100644 index 0000000000..fd749cd7dc --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs @@ -0,0 +1,23 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +namespace Avalonia.Controls.Primitives +{ + /// + /// Defines a relative amount that a color channel should be incremented. + /// + internal enum IncrementAmount + { + /// + /// A smaller change in value. + /// + Small, + + /// + /// A larger change in value. + /// + Large, + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs new file mode 100644 index 0000000000..8002a44fc9 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs @@ -0,0 +1,23 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +namespace Avalonia.Controls.Primitives +{ + /// + /// Defines the direction a color channel should be incremented. + /// + internal enum IncrementDirection + { + /// + /// Decreasing in value towards zero. + /// + Lower, + + /// + /// Increasing in value towards positive infinity. + /// + Higher, + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs new file mode 100644 index 0000000000..2b3f711634 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs @@ -0,0 +1,46 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// Defines the two HSV color channels displayed by a . + /// Order of the color channels is important. + /// + public enum ColorSpectrumChannels + { + /// + /// The Hue and Value channels. + /// + HueValue, + + /// + /// The Value and Hue channels. + /// + ValueHue, + + /// + /// The Hue and Saturation channels. + /// + HueSaturation, + + /// + /// The Saturation and Hue channels. + /// + SaturationHue, + + /// + /// The Saturation and Value channels. + /// + SaturationValue, + + /// + /// The Value and Saturation channels. + /// + ValueSaturation, + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs new file mode 100644 index 0000000000..0319d4a6c8 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs @@ -0,0 +1,26 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// Defines the shape of a . + /// + public enum ColorSpectrumShape + { + /// + /// The spectrum is in the shape of a rectangular or square box. + /// Note that more colors are visible to the user in Box shape. + /// + Box, + + /// + /// The spectrum is in the shape of an ellipse or circle. + /// + Ring, + }; +} From bf9b356a29655e74b3a8fd7ade8a094f34952054 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:06:27 -0400 Subject: [PATCH 012/213] Add HsvColor.FromRgb() overload that works with normalized double values --- src/Avalonia.Visuals/Media/HsvColor.cs | 42 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 8b2f10d088..a18f1032f2 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -173,7 +173,7 @@ namespace Avalonia.Media } /// - /// Converts the given HSV color to it's RGB color equivalent. + /// Converts the given HSV color to its RGB color equivalent. /// /// The color in the HSV color model. /// A new RGB equivalent to the given HSVA values. @@ -183,7 +183,7 @@ namespace Avalonia.Media } /// - /// Converts the given HSVA color channel values to it's RGB color equivalent. + /// Converts the given HSVA color channel values to its RGB color equivalent. /// /// The hue channel value in the HSV color model in the range from 0..360. /// The saturation channel value in the HSV color model in the range from 0..1. @@ -321,7 +321,7 @@ namespace Avalonia.Media } /// - /// Converts the given RGB color to it's HSV color equivalent. + /// Converts the given RGB color to its HSV color equivalent. /// /// The color in the RGB color model. /// A new equivalent to the given RGBA values. @@ -331,7 +331,7 @@ namespace Avalonia.Media } /// - /// Converts the given RGBA color channel values to it's HSV color equivalent. + /// Converts the given RGBA color channel values to its HSV color equivalent. /// /// The red channel value in the RGB color model. /// The green channel value in the RGB color model. @@ -343,18 +343,40 @@ namespace Avalonia.Media byte green, byte blue, byte alpha = 0xFF) + { + // Normalize RGBA channel values into the 0..1 range + return HsvColor.FromRgb( + (red / 255.0), + (green / 255.0), + (blue / 255.0), + (alpha / 255.0)); + } + + // TODO: Mark the below method Internal and make Internals visible to Avalonia.Controls... + + /// + /// Converts the given RGBA color channel values to its HSV color equivalent. + /// + /// + /// Warning: No bounds checks or clamping is done on the input channel values. + /// This method is for internal-use only and the caller must ensure bounds. + /// + /// The red channel value in the RGB color model within the range 0..1. + /// The green channel value in the RGB color model within the range 0..1. + /// The blue channel value in the RGB color model within the range 0..1. + /// The alpha channel value in the RGB color model within the range 0..1. + /// A new equivalent to the given RGBA values. + public static HsvColor FromRgb( + double r, + double g, + double b, + double a = 1.0) { // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp // This was used because it is the best documented and likely most optimized for performance // Alpha channel support was added - // Normalize RGBA channel values into the 0..1 range used by this algorithm - double r = red / 255.0; - double g = green / 255.0; - double b = blue / 255.0; - double a = alpha / 255.0; - double hue; double saturation; double value; From 653abb3b3187e2b6f8753ad3b97616d590aa483f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:07:23 -0400 Subject: [PATCH 013/213] Add higher performance, mutable RGB and HSV structs These are only for internal use with the ColorSpectrum --- .../ColorPicker/ColorSpectrum/Hsv.cs | 87 +++++++++++++++++ .../ColorPicker/ColorSpectrum/Rgb.cs | 94 +++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs new file mode 100644 index 0000000000..6d9f8440bf --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs @@ -0,0 +1,87 @@ +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains and allows modification of Hue, Saturation and Value channel values. + /// + /// + /// The is a specialized struct optimized for permanence and memory: + /// + /// This is not a read-only struct like and allows editing the fields + /// Removes the alpha component unnecessary in core calculations + /// No channel value bounds checks or clamping is done. + /// + /// + internal struct Hsv + { + /// + /// The Hue channel value in the range from 0..359. + /// + public double H; + + /// + /// The Saturation channel value in the range from 0..1. + /// + public double S; + + /// + /// The Value channel value in the range from 0..1. + /// + public double V; + + /// + /// Initializes a new instance of the struct. + /// + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + public Hsv(double h, double s, double v) + { + H = h; + S = s; + V = v; + } + + /// + /// Initializes a new instance of the struct. + /// + /// An existing to convert to . + public Hsv(HsvColor hsvColor) + { + H = hsvColor.H; + S = hsvColor.S; + V = hsvColor.V; + } + + /// + /// Converts this struct into a standard . + /// + /// The Alpha channel value in the range from 0..1. + /// A new representing this struct. + public HsvColor ToHsvColor(double alpha = 1.0) + { + // Clamping is done automatically in the constructor + return HsvColor.FromAhsv(alpha, H, S, V); + } + + /// + /// Returns the color model equivalent of this color. + /// + /// The equivalent color. + public Rgb ToRgb() + { + // Instantiating a Color is unfortunately necessary to use existing conversions + // Clamping is done internally in the conversion method + Color color = HsvColor.ToRgb(H, S, V); + + return new Rgb(color); + } + } +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs new file mode 100644 index 0000000000..c2358b7bee --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs @@ -0,0 +1,94 @@ +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains and allows modification of Red, Green and Blue channel values. + /// + /// + /// The is a specialized struct optimized for permanence and memory: + /// + /// This is not a read-only struct like and allows editing the fields + /// Removes the alpha component unnecessary in core calculations + /// Normalizes RGB channel values in the range of 0..1 to simplify calculations. + /// No channel value bounds checks or clamping is done. + /// + /// + internal struct Rgb + { + /// + /// The Red channel value in the range from 0..1. + /// + public double R; + + /// + /// The Green channel value in the range from 0..1. + /// + public double G; + + /// + /// The Blue channel value in the range from 0..1. + /// + public double B; + + /// + /// Initializes a new instance of the struct. + /// + /// The Red channel value in the range from 0..1. + /// The Green channel value in the range from 0..1. + /// The Blue channel value in the range from 0..1. + public Rgb(double r, double g, double b) + { + R = r; + G = g; + B = b; + } + + /// + /// Initializes a new instance of the struct. + /// + /// An existing to convert to . + public Rgb(Color color) + { + R = color.R / 255.0; + G = color.G / 255.0; + B = color.B / 255.0; + } + + /// + /// Converts this struct into a standard . + /// + /// The Alpha channel value in the range from 0..1. + /// A new representing this struct. + public Color ToColor(double alpha = 1.0) + { + return Color.FromArgb( + (byte)MathUtilities.Clamp(alpha * 255.0, 0x00, 0xFF), + (byte)MathUtilities.Clamp(R * 255.0, 0x00, 0xFF), + (byte)MathUtilities.Clamp(G * 255.0, 0x00, 0xFF), + (byte)MathUtilities.Clamp(B * 255.0, 0x00, 0xFF)); + } + + /// + /// Returns the color model equivalent of this color. + /// + /// The equivalent color. + public Hsv ToHsv() + { + // Instantiating an HsvColor is unfortunately necessary to use existing conversions + HsvColor hsvColor = HsvColor.FromRgb( + MathUtilities.Clamp(R, 0.0, 1.0), + MathUtilities.Clamp(G, 0.0, 1.0), + MathUtilities.Clamp(B, 0.0, 1.0)); + + return new Hsv(hsvColor); + } + } +} From 2e9d392ef91ca27bd94567dd30926a7ec9ce3321 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:07:45 -0400 Subject: [PATCH 014/213] Add new ColorChangedEventArgs --- .../ColorPicker/ColorChangedEventArgs.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs new file mode 100644 index 0000000000..93ba1a4db1 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs @@ -0,0 +1,59 @@ +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Holds the details of a ColorChanged event. + /// + /// + /// HSV color information is intentionally not provided. + /// Use to obtain it. + /// + public class ColorChangedEventArgs : EventArgs + { + private Color _OldColor; + private Color _NewColor; + + /// + /// Initializes a new instance of the class. + /// + public ColorChangedEventArgs() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The old/original color from before the change event. + /// The new/updated color that triggered the change event. + public ColorChangedEventArgs(Color oldColor, Color newColor) + { + _OldColor = oldColor; + _NewColor = newColor; + } + + /// + /// Gets the old/original color from before the change event. + /// + public Color OldColor + { + get => _OldColor; + internal set => _OldColor = value; + } + + /// + /// Gets the new/updated color that triggered the change event. + /// + public Color NewColor + { + get => _NewColor; + internal set => _NewColor = value; + } + } +} From 0f3409064864cffc65996d2ac8a29ce4a4d225f5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:08:17 -0400 Subject: [PATCH 015/213] Add base ColorHelpers porting from WinUI --- .../ColorPicker/ColorSpectrum/ColorHelpers.cs | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs new file mode 100644 index 0000000000..3d380392aa --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs @@ -0,0 +1,388 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.Controls.Primitives +{ + internal static class ColorHelpers + { + public const int CheckerSize = 4; + + public static bool ToDisplayNameExists + { + get => false; + } + + public static string ToDisplayName(Color color) + { + return string.Empty; + } + + public static Hsv IncrementColorChannel( + Hsv originalHsv, + ColorPickerHsvChannel channel, + IncrementDirection direction, + IncrementAmount amount, + bool shouldWrap, + double minBound, + double maxBound) + { + Hsv newHsv = originalHsv; + + if (amount == IncrementAmount.Small || !ToDisplayNameExists) + { + // In order to avoid working with small values that can incur rounding issues, + // we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1. + newHsv.S *= 100; + newHsv.V *= 100; + + // Note: *valueToIncrement replaced with ref local variable for C#, must be initialized + ref double valueToIncrement = ref newHsv.H; + double incrementAmount = 0.0; + + // If we're adding a small increment, then we'll just add or subtract 1. + // If we're adding a large increment, then we want to snap to the next + // or previous major value - for hue, this is every increment of 30; + // for saturation and value, this is every increment of 10. + switch (channel) + { + case ColorPickerHsvChannel.Hue: + valueToIncrement = ref newHsv.H; + incrementAmount = amount == IncrementAmount.Small ? 1 : 30; + break; + + case ColorPickerHsvChannel.Saturation: + valueToIncrement = ref newHsv.S; + incrementAmount = amount == IncrementAmount.Small ? 1 : 10; + break; + + case ColorPickerHsvChannel.Value: + valueToIncrement = ref newHsv.V; + incrementAmount = amount == IncrementAmount.Small ? 1 : 10; + break; + + default: + throw new InvalidOperationException("Invalid ColorPickerHsvChannel."); + } + + double previousValue = valueToIncrement; + + valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); + + // If the value has reached outside the bounds, we were previous at the boundary, and we should wrap, + // then we'll place the selection on the other side of the spectrum. + // Otherwise, we'll place it on the boundary that was exceeded. + if (valueToIncrement < minBound) + { + valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; + } + + if (valueToIncrement > maxBound) + { + valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; + } + + // We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range. + newHsv.S /= 100; + newHsv.V /= 100; + } + else + { + // While working with named colors, we're going to need to be working in actual HSV units, + // so we'll divide the min bound and max bound by 100 in the case of saturation or value, + // since we'll have received units between 0-100 and we need them within 0-1. + if (channel == ColorPickerHsvChannel.Saturation || + channel == ColorPickerHsvChannel.Value) + { + minBound /= 100; + maxBound /= 100; + } + + newHsv = FindNextNamedColor(originalHsv, channel, direction, shouldWrap, minBound, maxBound); + } + + return newHsv; + } + + public static Hsv FindNextNamedColor( + Hsv originalHsv, + ColorPickerHsvChannel channel, + IncrementDirection direction, + bool shouldWrap, + double minBound, + double maxBound) + { + // There's no easy way to directly get the next named color, so what we'll do + // is just iterate in the direction that we want to find it until we find a color + // in that direction that has a color name different than our current color name. + // Once we find a new color name, then we'll iterate across that color name until + // we find its bounds on the other side, and then select the color that is exactly + // in the middle of that color's bounds. + Hsv newHsv = originalHsv; + + string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); + string newColorName = originalColorName; + + // Note: *newValue replaced with ref local variable for C#, must be initialized + double originalValue = 0.0; + ref double newValue = ref newHsv.H; + double incrementAmount = 0.0; + + switch (channel) + { + case ColorPickerHsvChannel.Hue: + originalValue = originalHsv.H; + newValue = ref newHsv.H; + incrementAmount = 1; + break; + + case ColorPickerHsvChannel.Saturation: + originalValue = originalHsv.S; + newValue = ref newHsv.S; + incrementAmount = 0.01; + break; + + case ColorPickerHsvChannel.Value: + originalValue = originalHsv.V; + newValue = ref newHsv.V; + incrementAmount = 0.01; + break; + + default: + throw new InvalidOperationException("Invalid ColorPickerHsvChannel."); + } + + bool shouldFindMidPoint = true; + + while (newColorName == originalColorName) + { + double previousValue = newValue; + newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; + + bool justWrapped = false; + + // If we've hit a boundary, then either we should wrap or we shouldn't. + // If we should, then we'll perform that wrapping if we were previously up against + // the boundary that we've now hit. Otherwise, we'll stop at that boundary. + if (newValue > maxBound) + { + if (shouldWrap) + { + newValue = minBound; + justWrapped = true; + } + else + { + newValue = maxBound; + shouldFindMidPoint = false; + newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + break; + } + } + else if (newValue < minBound) + { + if (shouldWrap) + { + newValue = maxBound; + justWrapped = true; + } + else + { + newValue = minBound; + shouldFindMidPoint = false; + newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + break; + } + } + + if (!justWrapped && + previousValue != originalValue && + Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) + { + // If we've wrapped all the way back to the start and have failed to find a new color name, + // then we'll just quit - there isn't a new color name that we're going to find. + shouldFindMidPoint = false; + break; + } + + newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + } + + if (shouldFindMidPoint) + { + Hsv startHsv = newHsv; + Hsv currentHsv = startHsv; + double startEndOffset = 0; + string currentColorName = newColorName; + + // Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized + ref double startValue = ref startHsv.H; + ref double currentValue = ref currentHsv.H; + double wrapIncrement = 0; + + switch (channel) + { + case ColorPickerHsvChannel.Hue: + startValue = ref startHsv.H; + currentValue = ref currentHsv.H; + wrapIncrement = 360.0; + break; + + case ColorPickerHsvChannel.Saturation: + startValue = ref startHsv.S; + currentValue = ref currentHsv.S; + wrapIncrement = 1.0; + break; + + case ColorPickerHsvChannel.Value: + startValue = ref startHsv.V; + currentValue = ref currentHsv.V; + wrapIncrement = 1.0; + break; + + default: + throw new InvalidOperationException("Invalid ColorPickerHsvChannel."); + } + + while (newColorName == currentColorName) + { + currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; + + // If we've hit a boundary, then either we should wrap or we shouldn't. + // If we should, then we'll perform that wrapping if we were previously up against + // the boundary that we've now hit. Otherwise, we'll stop at that boundary. + if (currentValue > maxBound) + { + if (shouldWrap) + { + currentValue = minBound; + startEndOffset = maxBound - minBound; + } + else + { + currentValue = maxBound; + break; + } + } + else if (currentValue < minBound) + { + if (shouldWrap) + { + currentValue = maxBound; + startEndOffset = minBound - maxBound; + } + else + { + currentValue = minBound; + break; + } + } + + currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); + } + + newValue = (startValue + currentValue + startEndOffset) / 2; + + // Dividing by 2 may have gotten us halfway through a single step, so we'll + // remove that half-step if it exists. + double leftoverValue = Math.Abs(newValue); + + while (leftoverValue > incrementAmount) + { + leftoverValue -= incrementAmount; + } + + newValue -= leftoverValue; + + while (newValue < minBound) + { + newValue += wrapIncrement; + } + + while (newValue > maxBound) + { + newValue -= wrapIncrement; + } + } + + return newHsv; + } + + public static double IncrementAlphaChannel( + double originalAlpha, + IncrementDirection direction, + IncrementAmount amount, + bool shouldWrap, + double minBound, + double maxBound) + { + // In order to avoid working with small values that can incur rounding issues, + // we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1. + originalAlpha *= 100; + + const double smallIncrementAmount = 1; + const double largeIncrementAmount = 10; + + if (amount == IncrementAmount.Small) + { + originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount; + } + else + { + if (direction == IncrementDirection.Lower) + { + originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; + } + else + { + originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; + } + } + + // If the value has reached outside the bounds and we should wrap, then we'll place the selection + // on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded. + if (originalAlpha < minBound) + { + originalAlpha = shouldWrap ? maxBound : minBound; + } + + if (originalAlpha > maxBound) + { + originalAlpha = shouldWrap ? minBound : maxBound; + } + + // We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range. + return originalAlpha / 100; + } + + public static WriteableBitmap CreateBitmapFromPixelData( + int pixelWidth, + int pixelHeight, + List bgraPixelData) + { + Vector dpi = new Vector(96, 96); // Standard may need to change on some devices + + WriteableBitmap bitmap = new WriteableBitmap( + new PixelSize(pixelWidth, pixelHeight), + dpi, + PixelFormat.Bgra8888, + AlphaFormat.Premul); + + // Warning: This is highly questionable + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + } + + return bitmap; + } + } +} From fb63032d40bea28ef5703a05814c230d6025a093 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:08:42 -0400 Subject: [PATCH 016/213] Add ColorSpectrum porting from WinUI --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 205 +++ .../ColorSpectrum/ColorSpectrum.cs | 1615 +++++++++++++++++ 2 files changed, 1820 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs new file mode 100644 index 0000000000..e95c63da33 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -0,0 +1,205 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorSpectrum + { + /// + /// Gets or sets the currently selected color in the RGB color model. + /// + /// + /// For control authors use instead to avoid loss + /// of precision and color drifting. + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); + + /// + /// Gets or sets the two HSV color channels displayed by the spectrum. + /// + /// + /// Internally, the uses the HSV color model. + /// + public ColorSpectrumChannels Channels + { + get => GetValue(ChannelsProperty); + set => SetValue(ChannelsProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ChannelsProperty = + AvaloniaProperty.Register( + nameof(Channels), + ColorSpectrumChannels.HueSaturation); + + /// + /// Gets or sets the currently selected color in the HSV color model. + /// + /// + /// This should be used in all cases instead of the property. + /// Internally, the uses the HSV color model and using + /// this property will avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register(nameof(HsvColor), new HsvColor(1, 0, 0, 1)); + + /// + /// Gets or sets the maximum value of the Hue channel in the range from 0..359. + /// This property must be greater than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MaxHue + { + get => GetValue(MaxHueProperty); + set => SetValue(MaxHueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register(nameof(MaxHue), 359); + + /// + /// Gets or sets the maximum value of the Saturation channel in the range from 0..100. + /// This property must be greater than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MaxSaturation + { + get => GetValue(MaxSaturationProperty); + set => SetValue(MaxSaturationProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register(nameof(MaxSaturation), 100); + + /// + /// Gets or sets the maximum value of the Value channel in the range from 0..100. + /// This property must be greater than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register(nameof(MaxValue), 100); + + /// + /// Gets or sets the minimum value of the Hue channel in the range from 0..359. + /// This property must be less than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MinHue + { + get => GetValue(MinHueProperty); + set => SetValue(MinHueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register(nameof(MinHue), 0); + + /// + /// Gets or sets the minimum value of the Saturation channel in the range from 0..100. + /// This property must be less than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MinSaturation + { + get => GetValue(MinSaturationProperty); + set => SetValue(MinSaturationProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register(nameof(MinSaturation), 0); + + /// + /// Gets or sets the minimum value of the Value channel in the range from 0..100. + /// This property must be less than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MinValue + { + get => GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register(nameof(MinValue), 0); + + /// + /// Gets or sets the displayed shape of the spectrum. + /// + public ColorSpectrumShape Shape + { + get => GetValue(ShapeProperty); + set => SetValue(ShapeProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShapeProperty = + AvaloniaProperty.Register( + nameof(Shape), + ColorSpectrumShape.Box); + } +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs new file mode 100644 index 0000000000..e4096bcfda --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -0,0 +1,1615 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// A two dimensional spectrum for color selection. + /// + public partial class ColorSpectrum : TemplatedControl + { + /// + /// Event for when the selected color changes within the spectrum. + /// + public event EventHandler? ColorChanged; + + private bool _updatingColor = false; + private bool _updatingHsvColor = false; + private bool _isPointerOver = false; + private bool _isPointerPressed = false; + private bool _shouldShowLargeSelection = false; + private List _hsvValues = new List(); + + private IDisposable? _layoutRootDisposable; + + // XAML template parts + private Grid? _layoutRoot; + private Grid? _sizingGrid; + + private Rectangle? _spectrumRectangle; + private Ellipse? _spectrumEllipse; + private Rectangle? _spectrumOverlayRectangle; + private Ellipse? _spectrumOverlayEllipse; + + private Canvas? _inputTarget; + private Panel? _selectionEllipsePanel; + + private ToolTip? _colorNameToolTip; + + // Put the spectrum images in a bitmap, which is then given to an ImageBrush. + private WriteableBitmap? _hueRedBitmap; + private WriteableBitmap? _hueYellowBitmap; + private WriteableBitmap? _hueGreenBitmap; + private WriteableBitmap? _hueCyanBitmap; + private WriteableBitmap? _hueBlueBitmap; + private WriteableBitmap? _huePurpleBitmap; + + private WriteableBitmap? _saturationMinimumBitmap; + private WriteableBitmap? _saturationMaximumBitmap; + + private WriteableBitmap? _valueBitmap; + + // Fields used by UpdateEllipse() to ensure that it's using the data + // associated with the last call to CreateBitmapsAndColorMap(), + // in order to function properly while the asynchronous bitmap creation + // is in progress. + private ColorSpectrumShape _shapeFromLastBitmapCreation = ColorSpectrumShape.Box; + private ColorSpectrumChannels _componentsFromLastBitmapCreation = ColorSpectrumChannels.HueSaturation; + private double _imageWidthFromLastBitmapCreation = 0.0; + private double _imageHeightFromLastBitmapCreation = 0.0; + private int _minHueFromLastBitmapCreation = 0; + private int _maxHueFromLastBitmapCreation = 0; + private int _minSaturationFromLastBitmapCreation = 0; + private int _maxSaturationFromLastBitmapCreation = 0; + private int _minValueFromLastBitmapCreation = 0; + private int _maxValueFromLastBitmapCreation = 0; + + private Color _oldColor = Color.FromArgb(255, 255, 255, 255); + private HsvColor _oldHsvColor = HsvColor.FromAhsv(0.0f, 0.0f, 1.0f, 1.0f); + + /// + /// Initializes a new instance of the class. + /// + public ColorSpectrum() + { + _shapeFromLastBitmapCreation = Shape; + _componentsFromLastBitmapCreation = Channels; + _imageWidthFromLastBitmapCreation = 0; + _imageHeightFromLastBitmapCreation = 0; + _minHueFromLastBitmapCreation = MinHue; + _maxHueFromLastBitmapCreation = MaxHue; + _minSaturationFromLastBitmapCreation = MinSaturation; + _maxSaturationFromLastBitmapCreation = MaxSaturation; + _minValueFromLastBitmapCreation = MinValue; + _maxValueFromLastBitmapCreation = MaxValue; + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + UnregisterEvents(); + + _layoutRoot = e.NameScope.Find("LayoutRoot"); + _sizingGrid = e.NameScope.Find("SizingGrid"); + _spectrumRectangle = e.NameScope.Find("SpectrumRectangle"); + _spectrumEllipse = e.NameScope.Find("SpectrumEllipse"); + _spectrumOverlayRectangle = e.NameScope.Find("SpectrumOverlayRectangle"); + _spectrumOverlayEllipse = e.NameScope.Find("SpectrumOverlayEllipse"); + _inputTarget = e.NameScope.Find("InputTarget"); + _selectionEllipsePanel = e.NameScope.Find("SelectionEllipsePanel"); + _colorNameToolTip = e.NameScope.Find("ColorNameToolTip"); + + if (_layoutRoot != null) + { + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => OnLayoutRootSizeChanged()); + } + + if (_inputTarget != null) + { + _inputTarget.PointerEnter += OnInputTargetPointerEnter; + _inputTarget.PointerLeave += OnInputTargetPointerLeave; + _inputTarget.PointerPressed += OnInputTargetPointerPressed; + _inputTarget.PointerMoved += OnInputTargetPointerMoved; + _inputTarget.PointerReleased += OnInputTargetPointerReleased; + } + + if (ColorHelpers.ToDisplayNameExists) + { + if (_colorNameToolTip != null) + { + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + } + } + + if (_selectionEllipsePanel != null) + { + // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 + //m_selectionEllipsePanel.RegisterPropertyChangedCallback(FrameworkElement.FlowDirectionProperty, OnSelectionEllipseFlowDirectionChanged); + } + + // If we haven't yet created our bitmaps, do so now. + if (_hsvValues.Count == 0) + { + CreateBitmapsAndColorMap(); + } + + UpdateEllipse(); + UpdateVisualState(useTransitions: false); + } + + /// + /// Explicitly unregisters all events connected in OnApplyTemplate(). + /// + private void UnregisterEvents() + { + _layoutRootDisposable?.Dispose(); + _layoutRootDisposable = null; + + if (_inputTarget != null) + { + _inputTarget.PointerEnter -= OnInputTargetPointerEnter; + _inputTarget.PointerLeave -= OnInputTargetPointerLeave; + _inputTarget.PointerPressed -= OnInputTargetPointerPressed; + _inputTarget.PointerMoved -= OnInputTargetPointerMoved; + _inputTarget.PointerReleased -= OnInputTargetPointerReleased; + } + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + var key = e.Key; + + if (key != Key.Left && + key != Key.Right && + key != Key.Up && + key != Key.Down) + { + base.OnKeyDown(e); + return; + } + + bool isControlDown = e.KeyModifiers.HasFlag(KeyModifiers.Control); + + ColorPickerHsvChannel incrementChannel = ColorPickerHsvChannel.Hue; + + if (key == Key.Left || + key == Key.Right) + { + switch (Channels) + { + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.HueValue: + incrementChannel = ColorPickerHsvChannel.Hue; + break; + + case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumChannels.SaturationValue: + incrementChannel = ColorPickerHsvChannel.Saturation; + break; + + case ColorSpectrumChannels.ValueHue: + case ColorSpectrumChannels.ValueSaturation: + incrementChannel = ColorPickerHsvChannel.Value; + break; + } + } + else if (key == Key.Up || + key == Key.Down) + { + switch (Channels) + { + case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumChannels.ValueHue: + incrementChannel = ColorPickerHsvChannel.Hue; + break; + + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.ValueSaturation: + incrementChannel = ColorPickerHsvChannel.Saturation; + break; + + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.SaturationValue: + incrementChannel = ColorPickerHsvChannel.Value; + break; + } + } + + double minBound = 0.0; + double maxBound = 0.0; + + switch (incrementChannel) + { + case ColorPickerHsvChannel.Hue: + minBound = MinHue; + maxBound = MaxHue; + break; + + case ColorPickerHsvChannel.Saturation: + minBound = MinSaturation; + maxBound = MaxSaturation; + break; + + case ColorPickerHsvChannel.Value: + minBound = MinValue; + maxBound = MaxValue; + break; + } + + // The order of saturation and value in the spectrum is reversed - the max value is at the bottom while the min value is at the top - + // so we want left and up to be lower for hue, but higher for saturation and value. + // This will ensure that the icon always moves in the direction of the key press. + IncrementDirection direction = + (incrementChannel == ColorPickerHsvChannel.Hue && (key == Key.Left || key == Key.Up)) || + (incrementChannel != ColorPickerHsvChannel.Hue && (key == Key.Right || key == Key.Down)) ? + IncrementDirection.Lower : + IncrementDirection.Higher; + + IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; + + HsvColor hsvColor = HsvColor; + UpdateColor(ColorHelpers.IncrementColorChannel( + new Hsv(hsvColor), + incrementChannel, + direction, + amount, + shouldWrap: true, + minBound, + maxBound)); + + e.Handled = true; + + return; + } + + /// + protected override void OnGotFocus(GotFocusEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + if (ColorHelpers.ToDisplayNameExists) + { + //colorNameToolTip.IsOpen = true; + } + } + + UpdateVisualState(useTransitions: true); + } + + /// + protected override void OnLostFocus(RoutedEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + if (ColorHelpers.ToDisplayNameExists) + { + //colorNameToolTip.IsOpen = false; + } + } + + UpdateVisualState(useTransitions: true); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == ColorProperty) + { + OnColorChanged(change); + } + else if (change.Property == HsvColorProperty) + { + OnHsvColorChanged(change); + } + else if ( + change.Property == MinHueProperty || + change.Property == MaxHueProperty) + { + OnMinMaxHueChanged(); + } + else if ( + change.Property == MinSaturationProperty || + change.Property == MaxSaturationProperty) + { + OnMinMaxSaturationChanged(); + } + else if ( + change.Property == MinValueProperty || + change.Property == MaxValueProperty) + { + OnMinMaxValueChanged(); + } + else if (change.Property == ShapeProperty) + { + OnShapeChanged(); + } + else if (change.Property == ChannelsProperty) + { + OnComponentsChanged(); + } + + base.OnPropertyChanged(change); + } + + private void OnColorChanged(AvaloniaPropertyChangedEventArgs change) + { + // If we're in the process of internally updating the color, + // then we don't want to respond to the Color property changing. + if (!_updatingColor) + { + Color color = Color; + + _updatingHsvColor = true; + Hsv newHsv = (new Rgb(color)).ToHsv(); + HsvColor = newHsv.ToHsvColor(color.A / 255.0); + _updatingHsvColor = false; + + UpdateEllipse(); + UpdateBitmapSources(); + } + + _oldColor = change.OldValue.GetValueOrDefault(); + } + + private void OnHsvColorChanged(AvaloniaPropertyChangedEventArgs change) + { + // If we're in the process of internally updating the HSV color, + // then we don't want to respond to the HsvColor property changing. + if (!_updatingHsvColor) + { + SetColor(); + } + + _oldHsvColor = change.OldValue.GetValueOrDefault(); + } + + private void SetColor() + { + HsvColor hsvColor = HsvColor; + + _updatingColor = true; + Rgb newRgb = (new Hsv(hsvColor)).ToRgb(); + + Color = newRgb.ToColor(hsvColor.A); + + _updatingColor = false; + + UpdateEllipse(); + UpdateBitmapSources(); + RaiseColorChanged(); + } + + public void RaiseColorChanged() + { + Color newColor = Color; + + if (_oldColor.A != newColor.A || + _oldColor.R != newColor.R || + _oldColor.G != newColor.G || + _oldColor.B != newColor.B) + { + var colorChangedEventArgs = new ColorChangedEventArgs(); + + colorChangedEventArgs.OldColor = _oldColor; + colorChangedEventArgs.NewColor = newColor; + + ColorChanged?.Invoke(this, colorChangedEventArgs); + + if (ColorHelpers.ToDisplayNameExists) + { + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); + } + } + } + } + + protected void OnMinMaxHueChanged() + { + int minHue = MinHue; + int maxHue = MaxHue; + + if (minHue < 0 || minHue > 359) + { + throw new ArgumentException("MinHue must be between 0 and 359."); + } + else if (maxHue < 0 || maxHue > 359) + { + throw new ArgumentException("MaxHue must be between 0 and 359."); + } + + ColorSpectrumChannels channels = Channels; + + // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.SaturationValue && + channels != ColorSpectrumChannels.ValueSaturation) + { + CreateBitmapsAndColorMap(); + } + } + + protected void OnMinMaxSaturationChanged() + { + int minSaturation = MinSaturation; + int maxSaturation = MaxSaturation; + + if (minSaturation < 0 || minSaturation > 100) + { + throw new ArgumentException("MinSaturation must be between 0 and 100."); + } + else if (maxSaturation < 0 || maxSaturation > 100) + { + throw new ArgumentException("MaxSaturation must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueValue && + channels != ColorSpectrumChannels.ValueHue) + { + CreateBitmapsAndColorMap(); + } + } + + private void OnMinMaxValueChanged() + { + int minValue = MinValue; + int maxValue = MaxValue; + + if (minValue < 0 || minValue > 100) + { + throw new ArgumentException("MinValue must be between 0 and 100."); + } + else if (maxValue < 0 || maxValue > 100) + { + throw new ArgumentException("MaxValue must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueSaturation && + channels != ColorSpectrumChannels.SaturationHue) + { + CreateBitmapsAndColorMap(); + } + } + + private void OnShapeChanged() + { + CreateBitmapsAndColorMap(); + } + + private void OnComponentsChanged() + { + CreateBitmapsAndColorMap(); + } + + private void UpdateVisualState(bool useTransitions) + { + //if (m_isPointerPressed) + //{ + // VisualStateManager.GoToState(this, m_shouldShowLargeSelection ? "PressedLarge" : "Pressed", useTransitions); + //} + //else if (m_isPointerOver) + //{ + // VisualStateManager.GoToState(this, "PointerOver", useTransitions); + //} + //else + //{ + // VisualStateManager.GoToState(this, "Normal", useTransitions); + //} + + //VisualStateManager.GoToState(this, m_shapeFromLastBitmapCreation == ColorSpectrumShape.Box ? "BoxSelected" : "RingSelected", useTransitions); + //VisualStateManager.GoToState(this, SelectionEllipseShouldBeLight() ? "SelectionEllipseLight" : "SelectionEllipseDark", useTransitions); + + //if (IsEnabled && FocusState != FocusState.Unfocused) + //{ + // if (FocusState == FocusState.Pointer) + // { + // VisualStateManager.GoToState(this, "PointerFocused", useTransitions); + // } + // else + // { + // VisualStateManager.GoToState(this, "Focused", useTransitions); + // } + //} + //else + //{ + // VisualStateManager.GoToState(this, "Unfocused", useTransitions); + //} + } + + private void UpdateColor(Hsv newHsv) + { + _updatingColor = true; + _updatingHsvColor = true; + + Rgb newRgb = newHsv.ToRgb(); + double alpha = HsvColor.A; + + Color = newRgb.ToColor(alpha); + HsvColor = newHsv.ToHsvColor(alpha); + + UpdateEllipse(); + UpdateVisualState(useTransitions: true); + + _updatingHsvColor = false; + _updatingColor = false; + + RaiseColorChanged(); + } + + private void UpdateColorFromPoint(PointerPoint point) + { + // If we haven't initialized our HSV value array yet, then we should just ignore any user input - + // we don't yet know what to do with it. + if (_hsvValues.Count == 0) + { + return; + } + + double xPosition = point.Position.X; + double yPosition = point.Position.Y; + double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; + double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2)); + + var shape = Shape; + + // If the point is outside the circle, we should bring it back into the circle. + if (distanceFromRadius > radius && shape == ColorSpectrumShape.Ring) + { + xPosition = (radius / distanceFromRadius) * (xPosition - radius) + radius; + yPosition = (radius / distanceFromRadius) * (yPosition - radius) + radius; + } + + // Now we need to find the index into the array of HSL values at each point in the spectrum m_image. + int x = (int)Math.Round(xPosition); + int y = (int)Math.Round(yPosition); + int width = (int)Math.Round(_imageWidthFromLastBitmapCreation); + + if (x < 0) + { + x = 0; + } + else if (x >= _imageWidthFromLastBitmapCreation) + { + x = (int)Math.Round(_imageWidthFromLastBitmapCreation) - 1; + } + + if (y < 0) + { + y = 0; + } + else if (y >= _imageHeightFromLastBitmapCreation) + { + y = (int)Math.Round(_imageHeightFromLastBitmapCreation) - 1; + } + + // The gradient image contains two dimensions of HSL information, but not the third. + // We should keep the third where it already was. + // Note: This can sometimes cause a crash -- possibly due to differences in c# rounding. Therefore, index is now clamped. + Hsv hsvAtPoint = _hsvValues[MathUtilities.Clamp((y * width + x), 0, _hsvValues.Count - 1)]; + + var channels = Channels; + var hsvColor = HsvColor; + + switch (channels) + { + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.ValueHue: + hsvAtPoint.S = hsvColor.S; + break; + + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.SaturationHue: + hsvAtPoint.V = hsvColor.V; + break; + + case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumChannels.SaturationValue: + hsvAtPoint.H = hsvColor.H; + break; + } + + UpdateColor(hsvAtPoint); + } + + private void UpdateEllipse() + { + var selectionEllipsePanel = _selectionEllipsePanel; + + if (selectionEllipsePanel == null) + { + return; + } + + // If we don't have an image size yet, we shouldn't be showing the ellipse. + if (_imageWidthFromLastBitmapCreation == 0 || + _imageHeightFromLastBitmapCreation == 0) + { + selectionEllipsePanel.IsVisible = false; + return; + } + else + { + selectionEllipsePanel.IsVisible = true; + } + + double xPosition; + double yPosition; + + Hsv hsvColor = new Hsv(HsvColor); + + hsvColor.H = MathUtilities.Clamp(hsvColor.H, (double)_minHueFromLastBitmapCreation, (double)_maxHueFromLastBitmapCreation); + hsvColor.S = MathUtilities.Clamp(hsvColor.S, _minSaturationFromLastBitmapCreation / 100.0, _maxSaturationFromLastBitmapCreation / 100.0); + hsvColor.V = MathUtilities.Clamp(hsvColor.V, _minValueFromLastBitmapCreation / 100.0, _maxValueFromLastBitmapCreation / 100.0); + + if (_shapeFromLastBitmapCreation == ColorSpectrumShape.Box) + { + double xPercent = 0; + double yPercent = 0; + + double hPercent = (hsvColor.H - _minHueFromLastBitmapCreation) / (_maxHueFromLastBitmapCreation - _minHueFromLastBitmapCreation); + double sPercent = (hsvColor.S * 100.0 - _minSaturationFromLastBitmapCreation) / (_maxSaturationFromLastBitmapCreation - _minSaturationFromLastBitmapCreation); + double vPercent = (hsvColor.V * 100.0 - _minValueFromLastBitmapCreation) / (_maxValueFromLastBitmapCreation - _minValueFromLastBitmapCreation); + + // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, + // we inverted the direction of that axis in order to put more hue on the outside of the ring, + // so we need to do similarly here when positioning the ellipse. + if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) + { + sPercent = 1 - sPercent; + } + else + { + vPercent = 1 - vPercent; + } + + switch (_componentsFromLastBitmapCreation) + { + case ColorSpectrumChannels.HueValue: + xPercent = hPercent; + yPercent = vPercent; + break; + + case ColorSpectrumChannels.HueSaturation: + xPercent = hPercent; + yPercent = sPercent; + break; + + case ColorSpectrumChannels.ValueHue: + xPercent = vPercent; + yPercent = hPercent; + break; + + case ColorSpectrumChannels.ValueSaturation: + xPercent = vPercent; + yPercent = sPercent; + break; + + case ColorSpectrumChannels.SaturationHue: + xPercent = sPercent; + yPercent = hPercent; + break; + + case ColorSpectrumChannels.SaturationValue: + xPercent = sPercent; + yPercent = vPercent; + break; + } + + xPosition = _imageWidthFromLastBitmapCreation * xPercent; + yPosition = _imageHeightFromLastBitmapCreation * yPercent; + } + else + { + double thetaValue = 0; + double rValue = 0; + + double hThetaValue = + _maxHueFromLastBitmapCreation != _minHueFromLastBitmapCreation ? + 360 * (hsvColor.H - _minHueFromLastBitmapCreation) / (_maxHueFromLastBitmapCreation - _minHueFromLastBitmapCreation) : + 0; + double sThetaValue = + _maxSaturationFromLastBitmapCreation != _minSaturationFromLastBitmapCreation ? + 360 * (hsvColor.S * 100.0 - _minSaturationFromLastBitmapCreation) / (_maxSaturationFromLastBitmapCreation - _minSaturationFromLastBitmapCreation) : + 0; + double vThetaValue = + _maxValueFromLastBitmapCreation != _minValueFromLastBitmapCreation ? + 360 * (hsvColor.V * 100.0 - _minValueFromLastBitmapCreation) / (_maxValueFromLastBitmapCreation - _minValueFromLastBitmapCreation) : + 0; + double hRValue = _maxHueFromLastBitmapCreation != _minHueFromLastBitmapCreation ? + (hsvColor.H - _minHueFromLastBitmapCreation) / (_maxHueFromLastBitmapCreation - _minHueFromLastBitmapCreation) - 1 : + 0; + double sRValue = _maxSaturationFromLastBitmapCreation != _minSaturationFromLastBitmapCreation ? + (hsvColor.S * 100.0 - _minSaturationFromLastBitmapCreation) / (_maxSaturationFromLastBitmapCreation - _minSaturationFromLastBitmapCreation) - 1 : + 0; + double vRValue = _maxValueFromLastBitmapCreation != _minValueFromLastBitmapCreation ? + (hsvColor.V * 100.0 - _minValueFromLastBitmapCreation) / (_maxValueFromLastBitmapCreation - _minValueFromLastBitmapCreation) - 1 : + 0; + + // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, + // we inverted the direction of that axis in order to put more hue on the outside of the ring, + // so we need to do similarly here when positioning the ellipse. + if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumChannels.ValueHue) + { + sThetaValue = 360 - sThetaValue; + sRValue = -sRValue - 1; + } + else + { + vThetaValue = 360 - vThetaValue; + vRValue = -vRValue - 1; + } + + switch (_componentsFromLastBitmapCreation) + { + case ColorSpectrumChannels.HueValue: + thetaValue = hThetaValue; + rValue = vRValue; + break; + + case ColorSpectrumChannels.HueSaturation: + thetaValue = hThetaValue; + rValue = sRValue; + break; + + case ColorSpectrumChannels.ValueHue: + thetaValue = vThetaValue; + rValue = hRValue; + break; + + case ColorSpectrumChannels.ValueSaturation: + thetaValue = vThetaValue; + rValue = sRValue; + break; + + case ColorSpectrumChannels.SaturationHue: + thetaValue = sThetaValue; + rValue = hRValue; + break; + + case ColorSpectrumChannels.SaturationValue: + thetaValue = sThetaValue; + rValue = vRValue; + break; + } + + double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; + + xPosition = (Math.Cos((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; + yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; + } + + Canvas.SetLeft(selectionEllipsePanel, xPosition - (selectionEllipsePanel.Width / 2)); + Canvas.SetTop(selectionEllipsePanel, yPosition - (selectionEllipsePanel.Height / 2)); + + // We only want to bother with the color name tool tip if we can provide color names. + if (ColorHelpers.ToDisplayNameExists) + { + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, + // so toggling IsEnabled induces it to do that without incurring any visual glitches. + colorNameToolTip.IsEnabled = false; + colorNameToolTip.IsEnabled = true; + } + } + + UpdateVisualState(useTransitions: true); + } + + private void OnLayoutRootSizeChanged() + { + CreateBitmapsAndColorMap(); + } + + private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) + { + _isPointerOver = true; + UpdateVisualState(useTransitions: true); + args.Handled = true; + } + + private void OnInputTargetPointerLeave(object? sender, PointerEventArgs args) + { + _isPointerOver = false; + UpdateVisualState(useTransitions: true); + args.Handled = true; + } + + private void OnInputTargetPointerPressed(object? sender, PointerPressedEventArgs args) + { + var inputTarget = _inputTarget; + + Focus(); + + _isPointerPressed = true; + _shouldShowLargeSelection = + // TODO: After Pen PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7412 + // args.Pointer.Type == PointerType.Pen || + args.Pointer.Type == PointerType.Touch; + + args.Pointer.Capture(inputTarget); + UpdateColorFromPoint(args.GetCurrentPoint(inputTarget)); + UpdateVisualState(useTransitions: true); + UpdateEllipse(); + + args.Handled = true; + } + + private void OnInputTargetPointerMoved(object? sender, PointerEventArgs args) + { + if (!_isPointerPressed) + { + return; + } + + UpdateColorFromPoint(args.GetCurrentPoint(_inputTarget)); + args.Handled = true; + } + + private void OnInputTargetPointerReleased(object? sender, PointerReleasedEventArgs args) + { + _isPointerPressed = false; + _shouldShowLargeSelection = false; + + args.Pointer.Capture(null); + UpdateVisualState(useTransitions: true); + UpdateEllipse(); + + args.Handled = true; + } + + // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 + //private void OnSelectionEllipseFlowDirectionChanged(DependencyObject o, DependencyProperty p) + //{ + // UpdateEllipse(); + //} + + private async void CreateBitmapsAndColorMap() + { + if (_layoutRoot == null || + _sizingGrid == null || + _inputTarget == null || + _spectrumRectangle == null || + _spectrumEllipse == null || + _spectrumOverlayRectangle == null || + _spectrumOverlayEllipse == null + /*|| SharedHelpers.IsInDesignMode*/) + { + return; + } + + var layoutRoot = _layoutRoot; + var sizingGrid = _sizingGrid; + var inputTarget = _inputTarget; + var spectrumRectangle = _spectrumRectangle; + var spectrumEllipse = _spectrumEllipse; + var spectrumOverlayRectangle = _spectrumOverlayRectangle; + var spectrumOverlayEllipse = _spectrumOverlayEllipse; + + // We want ColorSpectrum to always be a square, so we'll take the smaller of the dimensions + // and size the sizing grid to that. + double minDimension = Math.Min(layoutRoot.Bounds.Width, layoutRoot.Bounds.Height); + + if (minDimension == 0) + { + return; + } + + sizingGrid.Width = minDimension; + sizingGrid.Height = minDimension; + + if (sizingGrid.Clip is RectangleGeometry clip) + { + clip.Rect = new Rect(0, 0, minDimension, minDimension); + } + + inputTarget.Width = minDimension; + inputTarget.Height = minDimension; + spectrumRectangle.Width = minDimension; + spectrumRectangle.Height = minDimension; + spectrumEllipse.Width = minDimension; + spectrumEllipse.Height = minDimension; + spectrumOverlayRectangle.Width = minDimension; + spectrumOverlayRectangle.Height = minDimension; + spectrumOverlayEllipse.Width = minDimension; + spectrumOverlayEllipse.Height = minDimension; + + HsvColor hsvColor = HsvColor; + int minHue = MinHue; + int maxHue = MaxHue; + int minSaturation = MinSaturation; + int maxSaturation = MaxSaturation; + int minValue = MinValue; + int maxValue = MaxValue; + ColorSpectrumShape shape = Shape; + ColorSpectrumChannels channels = Channels; + + // If min >= max, then by convention, min is the only number that a property can have. + if (minHue >= maxHue) + { + maxHue = minHue; + } + + if (minSaturation >= maxSaturation) + { + maxSaturation = minSaturation; + } + + if (minValue >= maxValue) + { + maxValue = minValue; + } + + Hsv hsv = new Hsv(hsvColor); + + // The middle 4 are only needed and used in the case of hue as the third dimension. + // Saturation and luminosity need only a min and max. + List bgraMinPixelData = new List(); + List bgraMiddle1PixelData = new List(); + List bgraMiddle2PixelData = new List(); + List bgraMiddle3PixelData = new List(); + List bgraMiddle4PixelData = new List(); + List bgraMaxPixelData = new List(); + List newHsvValues = new List(); + + var pixelCount = (int)(Math.Round(minDimension) * Math.Round(minDimension)); + var pixelDataSize = pixelCount * 4; + bgraMinPixelData.Capacity = pixelDataSize; + + // We'll only save pixel data for the middle bitmaps if our third dimension is hue. + if (channels == ColorSpectrumChannels.ValueSaturation || + channels == ColorSpectrumChannels.SaturationValue) + { + bgraMiddle1PixelData.Capacity = pixelDataSize; + bgraMiddle2PixelData.Capacity = pixelDataSize; + bgraMiddle3PixelData.Capacity = pixelDataSize; + bgraMiddle4PixelData.Capacity = pixelDataSize; + } + + bgraMaxPixelData.Capacity = pixelDataSize; + newHsvValues.Capacity = pixelCount; + + int minDimensionInt = (int)Math.Round(minDimension); + + await Task.Run(() => + { + // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes, + // the ColorSpectrum will visually change to accommodate that value. For example, if the ColorSpectrum handles hue and luminosity, + // and the saturation externally goes from 1.0 to 0.5, then the ColorSpectrum will visually change to look more washed out + // to represent that third dimension's new value. + // Internally, however, we don't want to regenerate the ColorSpectrum bitmap every single time this happens, since that's very expensive. + // In order to make it so that we don't have to, we implement an optimization where, rather than having only one bitmap, + // we instead have multiple that we blend together using opacity to create the effect that we want. + // In the case where the third dimension is saturation or luminosity, we only need two: one bitmap at the minimum value + // of the third dimension, and one bitmap at the maximum. Then we set the second's opacity at whatever the value of + // the third dimension is - e.g., a saturation of 0.5 implies an opacity of 50%. + // In the case where the third dimension is hue, we need six: one bitmap corresponding to red, yellow, green, cyan, blue, and purple. + // We'll then blend between whichever colors our hue exists between - e.g., an orange color would use red and yellow with an opacity of 50%. + // This optimization does incur slightly more startup time initially since we have to generate multiple bitmaps at once instead of only one, + // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap. + if (shape == ColorSpectrumShape.Box) + { + for (int x = minDimensionInt - 1; x >= 0; --x) + { + for (int y = minDimensionInt - 1; y >= 0; --y) + { + FillPixelForBox( + x, y, hsv, minDimensionInt, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, + newHsvValues); + } + } + } + else + { + for (int y = 0; y < minDimensionInt; ++y) + { + for (int x = 0; x < minDimensionInt; ++x) + { + FillPixelForRing( + x, y, minDimensionInt / 2.0, hsv, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, + newHsvValues); + } + } + } + }); + + Dispatcher.UIThread.Post(() => + { + int pixelWidth = (int)Math.Round(minDimension); + int pixelHeight = (int)Math.Round(minDimension); + + ColorSpectrumChannels channels2 = Channels; + + WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); + WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); + + switch (channels2) + { + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.ValueHue: + _saturationMinimumBitmap = minBitmap; + _saturationMaximumBitmap = maxBitmap; + break; + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.SaturationHue: + _valueBitmap = maxBitmap; + break; + case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumChannels.SaturationValue: + _hueRedBitmap = minBitmap; + _hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); + _hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); + _hueCyanBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle3PixelData); + _hueBlueBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle4PixelData); + _huePurpleBitmap = maxBitmap; + break; + } + + _shapeFromLastBitmapCreation = Shape; + _componentsFromLastBitmapCreation = Channels; + _imageWidthFromLastBitmapCreation = minDimension; + _imageHeightFromLastBitmapCreation = minDimension; + _minHueFromLastBitmapCreation = MinHue; + _maxHueFromLastBitmapCreation = MaxHue; + _minSaturationFromLastBitmapCreation = MinSaturation; + _maxSaturationFromLastBitmapCreation = MaxSaturation; + _minValueFromLastBitmapCreation = MinValue; + _maxValueFromLastBitmapCreation = MaxValue; + + _hsvValues = newHsvValues; + + UpdateBitmapSources(); + UpdateEllipse(); + }); + } + + private void FillPixelForBox( + double x, + double y, + Hsv baseHsv, + double minDimension, + ColorSpectrumChannels channels, + double minHue, + double maxHue, + double minSaturation, + double maxSaturation, + double minValue, + double maxValue, + List bgraMinPixelData, + List bgraMiddle1PixelData, + List bgraMiddle2PixelData, + List bgraMiddle3PixelData, + List bgraMiddle4PixelData, + List bgraMaxPixelData, + List newHsvValues) + { + double hMin = minHue; + double hMax = maxHue; + double sMin = minSaturation / 100.0; + double sMax = maxSaturation / 100.0; + double vMin = minValue / 100.0; + double vMax = maxValue / 100.0; + + Hsv hsvMin = baseHsv; + Hsv hsvMiddle1 = baseHsv; + Hsv hsvMiddle2 = baseHsv; + Hsv hsvMiddle3 = baseHsv; + Hsv hsvMiddle4 = baseHsv; + Hsv hsvMax = baseHsv; + + double xPercent = (minDimension - 1 - x) / (minDimension - 1); + double yPercent = (minDimension - 1 - y) / (minDimension - 1); + + switch (channels) + { + case ColorSpectrumChannels.HueValue: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.HueSaturation: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.ValueHue: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.ValueSaturation: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + + case ColorSpectrumChannels.SaturationHue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.SaturationValue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + } + + // If saturation is an axis in the spectrum with hue, or value is an axis, then we want + // that axis to go from maximum at the top to minimum at the bottom, + // or maximum at the outside to minimum at the inside in the case of the ring configuration, + // so we'll invert the number before assigning the HSL value to the array. + // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue + // in the case of the ring configuration. + if (channels == ColorSpectrumChannels.HueSaturation || + channels == ColorSpectrumChannels.SaturationHue) + { + hsvMin.S = sMax - hsvMin.S + sMin; + hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; + hsvMiddle2.S = sMax - hsvMiddle2.S + sMin; + hsvMiddle3.S = sMax - hsvMiddle3.S + sMin; + hsvMiddle4.S = sMax - hsvMiddle4.S + sMin; + hsvMax.S = sMax - hsvMax.S + sMin; + } + else + { + hsvMin.V = vMax - hsvMin.V + vMin; + hsvMiddle1.V = vMax - hsvMiddle1.V + vMin; + hsvMiddle2.V = vMax - hsvMiddle2.V + vMin; + hsvMiddle3.V = vMax - hsvMiddle3.V + vMin; + hsvMiddle4.V = vMax - hsvMiddle4.V + vMin; + hsvMax.V = vMax - hsvMax.V + vMin; + } + + newHsvValues.Add(hsvMin); + + Rgb rgbMin = hsvMin.ToRgb(); + bgraMinPixelData.Add((byte)Math.Round(rgbMin.B * 255.0)); // b + bgraMinPixelData.Add((byte)Math.Round(rgbMin.G * 255.0)); // g + bgraMinPixelData.Add((byte)Math.Round(rgbMin.R * 255.0)); // r + bgraMinPixelData.Add(255); // a - ignored + + // We'll only save pixel data for the middle bitmaps if our third dimension is hue. + if (channels == ColorSpectrumChannels.ValueSaturation || + channels == ColorSpectrumChannels.SaturationValue) + { + Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255.0)); // b + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.G * 255.0)); // g + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.R * 255.0)); // r + bgraMiddle1PixelData.Add(255); // a - ignored + + Rgb rgbMiddle2 = hsvMiddle2.ToRgb(); + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.B * 255.0)); // b + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.G * 255.0)); // g + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.R * 255.0)); // r + bgraMiddle2PixelData.Add(255); // a - ignored + + Rgb rgbMiddle3 = hsvMiddle3.ToRgb(); + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.B * 255.0)); // b + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.G * 255.0)); // g + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.R * 255.0)); // r + bgraMiddle3PixelData.Add(255); // a - ignored + + Rgb rgbMiddle4 = hsvMiddle4.ToRgb(); + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.B * 255.0)); // b + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.G * 255.0)); // g + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.R * 255.0)); // r + bgraMiddle4PixelData.Add(255); // a - ignored + } + + Rgb rgbMax = hsvMax.ToRgb(); + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.B * 255.0)); // b + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.G * 255.0)); // g + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.R * 255.0)); // r + bgraMaxPixelData.Add(255); // a - ignored + } + + private void FillPixelForRing( + double x, + double y, + double radius, + Hsv baseHsv, + ColorSpectrumChannels channels, + double minHue, + double maxHue, + double minSaturation, + double maxSaturation, + double minValue, + double maxValue, + List bgraMinPixelData, + List bgraMiddle1PixelData, + List bgraMiddle2PixelData, + List bgraMiddle3PixelData, + List bgraMiddle4PixelData, + List bgraMaxPixelData, + List newHsvValues) + { + double hMin = minHue; + double hMax = maxHue; + double sMin = minSaturation / 100.0; + double sMax = maxSaturation / 100.0; + double vMin = minValue / 100.0; + double vMax = maxValue / 100.0; + + double distanceFromRadius = Math.Sqrt(Math.Pow(x - radius, 2) + Math.Pow(y - radius, 2)); + + double xToUse = x; + double yToUse = y; + + // If we're outside the ring, then we want the pixel to appear as blank. + // However, to avoid issues with rounding errors, we'll act as though this point + // is on the edge of the ring for the purposes of returning an HSL value. + // That way, hit testing on the edges will always return the correct value. + if (distanceFromRadius > radius) + { + xToUse = (radius / distanceFromRadius) * (x - radius) + radius; + yToUse = (radius / distanceFromRadius) * (y - radius) + radius; + distanceFromRadius = radius; + } + + Hsv hsvMin = baseHsv; + Hsv hsvMiddle1 = baseHsv; + Hsv hsvMiddle2 = baseHsv; + Hsv hsvMiddle3 = baseHsv; + Hsv hsvMiddle4 = baseHsv; + Hsv hsvMax = baseHsv; + + double r = 1 - distanceFromRadius / radius; + + double theta = Math.Atan2((radius - yToUse), (radius - xToUse)) * 180.0 / Math.PI; + theta += 180.0; + theta = Math.Floor(theta); + + while (theta > 360) + { + theta -= 360; + } + + double thetaPercent = theta / 360; + + switch (channels) + { + case ColorSpectrumChannels.HueValue: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.HueSaturation: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.ValueHue: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.ValueSaturation: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + + case ColorSpectrumChannels.SaturationHue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.SaturationValue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + } + + // If saturation is an axis in the spectrum with hue, or value is an axis, then we want + // that axis to go from maximum at the top to minimum at the bottom, + // or maximum at the outside to minimum at the inside in the case of the ring configuration, + // so we'll invert the number before assigning the HSL value to the array. + // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue + // in the case of the ring configuration. + if (channels == ColorSpectrumChannels.HueSaturation || + channels == ColorSpectrumChannels.SaturationHue) + { + hsvMin.S = sMax - hsvMin.S + sMin; + hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; + hsvMiddle2.S = sMax - hsvMiddle2.S + sMin; + hsvMiddle3.S = sMax - hsvMiddle3.S + sMin; + hsvMiddle4.S = sMax - hsvMiddle4.S + sMin; + hsvMax.S = sMax - hsvMax.S + sMin; + } + else + { + hsvMin.V = vMax - hsvMin.V + vMin; + hsvMiddle1.V = vMax - hsvMiddle1.V + vMin; + hsvMiddle2.V = vMax - hsvMiddle2.V + vMin; + hsvMiddle3.V = vMax - hsvMiddle3.V + vMin; + hsvMiddle4.V = vMax - hsvMiddle4.V + vMin; + hsvMax.V = vMax - hsvMax.V + vMin; + } + + newHsvValues.Add(hsvMin); + + Rgb rgbMin = hsvMin.ToRgb(); + bgraMinPixelData.Add((byte)Math.Round(rgbMin.B * 255)); // b + bgraMinPixelData.Add((byte)Math.Round(rgbMin.G * 255)); // g + bgraMinPixelData.Add((byte)Math.Round(rgbMin.R * 255)); // r + bgraMinPixelData.Add(255); // a + + // We'll only save pixel data for the middle bitmaps if our third dimension is hue. + if (channels == ColorSpectrumChannels.ValueSaturation || + channels == ColorSpectrumChannels.SaturationValue) + { + Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255)); // b + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.G * 255)); // g + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.R * 255)); // r + bgraMiddle1PixelData.Add(255); // a + + Rgb rgbMiddle2 = hsvMiddle2.ToRgb(); + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.B * 255)); // b + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.G * 255)); // g + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.R * 255)); // r + bgraMiddle2PixelData.Add(255); // a + + Rgb rgbMiddle3 = hsvMiddle3.ToRgb(); + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.B * 255)); // b + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.G * 255)); // g + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.R * 255)); // r + bgraMiddle3PixelData.Add(255); // a + + Rgb rgbMiddle4 = hsvMiddle4.ToRgb(); + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.B * 255)); // b + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.G * 255)); // g + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.R * 255)); // r + bgraMiddle4PixelData.Add(255); // a + } + + Rgb rgbMax = hsvMax.ToRgb(); + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.B * 255)); // b + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.G * 255)); // g + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.R * 255)); // r + bgraMaxPixelData.Add(255); // a + } + + private void UpdateBitmapSources() + { + var spectrumOverlayRectangle = _spectrumOverlayRectangle; + var spectrumOverlayEllipse = _spectrumOverlayEllipse; + + var spectrumRectangle = _spectrumRectangle; + var spectrumEllipse = _spectrumEllipse; + + if (spectrumOverlayRectangle == null || + spectrumOverlayEllipse == null || + spectrumRectangle == null || + spectrumEllipse == null) + { + return; + } + + HsvColor hsvColor = HsvColor; + ColorSpectrumChannels channels = Channels; + + // We'll set the base image and the overlay image based on which component is our third dimension. + // If it's saturation or luminosity, then the base image is that dimension at its minimum value, + // while the overlay image is that dimension at its maximum value. + // If it's hue, then we'll figure out where in the color wheel we are, and then use the two + // colors on either side of our position as our base image and overlay image. + // For example, if our hue is orange, then the base image would be red and the overlay image yellow. + switch (channels) + { + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.ValueHue: + { + if (_saturationMinimumBitmap == null || + _saturationMaximumBitmap == null) + { + return; + } + + ImageBrush spectrumBrush; + ImageBrush spectrumOverlayBrush; + + spectrumBrush = new ImageBrush(_saturationMinimumBitmap); + spectrumOverlayBrush = new ImageBrush(_saturationMaximumBitmap); + spectrumOverlayRectangle.Opacity = hsvColor.S; + spectrumOverlayEllipse.Opacity = hsvColor.S; + spectrumRectangle.Fill = spectrumBrush; + spectrumEllipse.Fill = spectrumBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + } + break; + + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.SaturationHue: + { + if (_valueBitmap == null) + { + return; + } + + ImageBrush spectrumBrush; + ImageBrush spectrumOverlayBrush; + + spectrumBrush = new ImageBrush(_valueBitmap); + spectrumOverlayBrush = new ImageBrush(_valueBitmap); + spectrumOverlayRectangle.Opacity = 1.0; + spectrumOverlayEllipse.Opacity = 1.0; + spectrumRectangle.Fill = spectrumBrush; + spectrumEllipse.Fill = spectrumBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + } + break; + + case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumChannels.SaturationValue: + { + if (_hueRedBitmap == null || + _hueYellowBitmap == null || + _hueGreenBitmap == null || + _hueCyanBitmap == null || + _hueBlueBitmap == null || + _huePurpleBitmap == null) + { + return; + } + + ImageBrush spectrumBrush; + ImageBrush spectrumOverlayBrush; + + double sextant = hsvColor.H / 60.0; + + if (sextant < 1) + { + spectrumBrush = new ImageBrush(_hueRedBitmap); + spectrumOverlayBrush = new ImageBrush(_hueYellowBitmap); + } + else if (sextant >= 1 && sextant < 2) + { + spectrumBrush = new ImageBrush(_hueYellowBitmap); + spectrumOverlayBrush = new ImageBrush(_hueGreenBitmap); + } + else if (sextant >= 2 && sextant < 3) + { + spectrumBrush = new ImageBrush(_hueGreenBitmap); + spectrumOverlayBrush = new ImageBrush(_hueCyanBitmap); + } + else if (sextant >= 3 && sextant < 4) + { + spectrumBrush = new ImageBrush(_hueCyanBitmap); + spectrumOverlayBrush = new ImageBrush(_hueBlueBitmap); + } + else if (sextant >= 4 && sextant < 5) + { + spectrumBrush = new ImageBrush(_hueBlueBitmap); + spectrumOverlayBrush = new ImageBrush(_huePurpleBitmap); + } + else + { + spectrumBrush = new ImageBrush(_huePurpleBitmap); + spectrumOverlayBrush = new ImageBrush(_hueRedBitmap); + } + + spectrumOverlayRectangle.Opacity = sextant - (int)sextant; + spectrumOverlayEllipse.Opacity = sextant - (int)sextant; + spectrumRectangle.Fill = spectrumBrush; + spectrumEllipse.Fill = spectrumBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + } + break; + } + } + + private bool SelectionEllipseShouldBeLight() + { + // The selection ellipse should be light if and only if the chosen color + // contrasts more with black than it does with white. + // To find how much something contrasts with white, we use the equation + // for relative luminance, which is given by + // + // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg + // + // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } + // + // If L is closer to 1, then the color is closer to white; if it is closer to 0, + // then the color is closer to black. This is based on the fact that the human + // eye perceives green to be much brighter than red, which in turn is perceived to be + // brighter than blue. + // + // If the third dimension is value, then we won't be updating the spectrum's displayed colors, + // so in that case we should use a value of 1 when considering the backdrop + // for the selection ellipse. + Color displayedColor; + + if (Channels == ColorSpectrumChannels.HueSaturation || + Channels == ColorSpectrumChannels.SaturationHue) + { + HsvColor hsvColor = HsvColor; + Rgb color = (new Hsv(hsvColor.H, hsvColor.S, 1.0)).ToRgb(); + displayedColor = color.ToColor(hsvColor.A); + } + else + { + displayedColor = Color; + } + + double rg = displayedColor.R <= 10 ? displayedColor.R / 3294.0 : Math.Pow(displayedColor.R / 269.0 + 0.0513, 2.4); + double gg = displayedColor.G <= 10 ? displayedColor.G / 3294.0 : Math.Pow(displayedColor.G / 269.0 + 0.0513, 2.4); + double bg = displayedColor.B <= 10 ? displayedColor.B / 3294.0 : Math.Pow(displayedColor.B / 269.0 + 0.0513, 2.4); + + return ((0.2126 * rg + 0.7152 * gg + 0.0722 * bg) <= 0.5); + } + } +} From d12ee97e78f9ec1f1a15d0d61906d70317d62f94 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:09:03 -0400 Subject: [PATCH 017/213] Fix missing base.OnApplyTemplate() in SplitButton --- src/Avalonia.Controls/SplitButton/SplitButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index f1d07b2679..c21b70645b 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -227,6 +227,8 @@ namespace Avalonia.Controls /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + UnregisterEvents(); UnregisterFlyoutEvents(Flyout); From 2edd7e4157ae3929fb3341f766785664395f4044 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 00:49:13 -0400 Subject: [PATCH 018/213] Add initial ColorSpectrum Fluent style/template --- .../ColorSpectrum/ColorSpectrum.cs | 41 ++-- .../Controls/ColorSpectrum.xaml | 183 ++++++++++++++++++ .../Controls/FluentControls.xaml | 5 +- 3 files changed, 208 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index e4096bcfda..47b113b0de 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -104,15 +104,15 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); - _layoutRoot = e.NameScope.Find("LayoutRoot"); - _sizingGrid = e.NameScope.Find("SizingGrid"); - _spectrumRectangle = e.NameScope.Find("SpectrumRectangle"); - _spectrumEllipse = e.NameScope.Find("SpectrumEllipse"); - _spectrumOverlayRectangle = e.NameScope.Find("SpectrumOverlayRectangle"); - _spectrumOverlayEllipse = e.NameScope.Find("SpectrumOverlayEllipse"); - _inputTarget = e.NameScope.Find("InputTarget"); - _selectionEllipsePanel = e.NameScope.Find("SelectionEllipsePanel"); - _colorNameToolTip = e.NameScope.Find("ColorNameToolTip"); + _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); + _sizingGrid = e.NameScope.Find("PART_SizingGrid"); + _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); + _spectrumEllipse = e.NameScope.Find("PART_SpectrumEllipse"); + _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); + _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); + _inputTarget = e.NameScope.Find("PART_InputTarget"); + _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); + _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); if (_layoutRoot != null) { @@ -149,7 +149,7 @@ namespace Avalonia.Controls.Primitives } UpdateEllipse(); - UpdateVisualState(useTransitions: false); + UpdatePseudoClasses(); } /// @@ -290,7 +290,7 @@ namespace Avalonia.Controls.Primitives } } - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); } /// @@ -305,7 +305,7 @@ namespace Avalonia.Controls.Primitives } } - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); } /// @@ -508,7 +508,10 @@ namespace Avalonia.Controls.Primitives CreateBitmapsAndColorMap(); } - private void UpdateVisualState(bool useTransitions) + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + private void UpdatePseudoClasses() { //if (m_isPointerPressed) //{ @@ -555,7 +558,7 @@ namespace Avalonia.Controls.Primitives HsvColor = newHsv.ToHsvColor(alpha); UpdateEllipse(); - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); _updatingHsvColor = false; _updatingColor = false; @@ -822,7 +825,7 @@ namespace Avalonia.Controls.Primitives } } - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); } private void OnLayoutRootSizeChanged() @@ -833,14 +836,14 @@ namespace Avalonia.Controls.Primitives private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) { _isPointerOver = true; - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); args.Handled = true; } private void OnInputTargetPointerLeave(object? sender, PointerEventArgs args) { _isPointerOver = false; - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); args.Handled = true; } @@ -858,7 +861,7 @@ namespace Avalonia.Controls.Primitives args.Pointer.Capture(inputTarget); UpdateColorFromPoint(args.GetCurrentPoint(inputTarget)); - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); UpdateEllipse(); args.Handled = true; @@ -881,7 +884,7 @@ namespace Avalonia.Controls.Primitives _shouldShowLargeSelection = false; args.Pointer.Capture(null); - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); UpdateEllipse(); args.Handled = true; diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml new file mode 100644 index 0000000000..c4ca09d439 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 16a6cc9c14..921826bab1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -9,6 +9,7 @@ + @@ -59,8 +60,8 @@ - - + + From edbd3de9a550673b6c20cbccc57a9cbfbb9225be Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 00:52:35 -0400 Subject: [PATCH 019/213] Add initial ColorPickerPage to ControlCatalog --- samples/ControlCatalog/MainView.xaml | 3 +++ .../ControlCatalog/Pages/ColorPickerPage.xaml | 23 +++++++++++++++++++ .../Pages/ColorPickerPage.xaml.cs | 19 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 samples/ControlCatalog/Pages/ColorPickerPage.xaml create mode 100644 samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index facce2aa82..b6f118a721 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -43,6 +43,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml new file mode 100644 index 0000000000..31faf5ff09 --- /dev/null +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs new file mode 100644 index 0000000000..6e017e381f --- /dev/null +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public partial class ColorPickerPage : UserControl + { + public ColorPickerPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} From 5ac00ef54d981a5e5dc23aca5ca0153c29001ee7 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 10:46:15 -0400 Subject: [PATCH 020/213] Add new EnumValueEqualsConverter --- .../Converters/EnumValueEqualsConverter.cs | 54 +++++++++++++++++++ .../Converters/MarginMultiplierConverter.cs | 1 - 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs new file mode 100644 index 0000000000..1a33a82ca4 --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter that checks if an enum value is equal to the given parameter enum value. + /// + public class EnumValueEqualsConverter : IValueConverter + { + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + // Note: Unlike string comparisons, null/empty is not supported + // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal + if (value != null && + parameter != null) + { + Type type = value.GetType(); + + if (type.IsEnum) + { + var valueStr = value?.ToString(); + var paramStr = parameter?.ToString(); + + if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + /* + // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below + // This is a little more type safe + if (type.IsEnum && + Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && + Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) + { + return valueEnum == paramEnum; + } + */ + } + + return false; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs index b0c30ea11f..7931b63d8e 100644 --- a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs +++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs @@ -35,7 +35,6 @@ namespace Avalonia.Controls.Converters Bottom ? Indent * thicknessDepth.Bottom : 0); } return new Thickness(0); - } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) From e828d42952825975b9ab48e03b7c2d5cbace17b9 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 10:49:23 -0400 Subject: [PATCH 021/213] Implement Box/Ring shape states in the template --- .../ColorSpectrum/ColorSpectrum.cs | 1 - .../Controls/ColorSpectrum.xaml | 49 +++++-------------- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 47b113b0de..ef07e56f10 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -526,7 +526,6 @@ namespace Avalonia.Controls.Primitives // VisualStateManager.GoToState(this, "Normal", useTransitions); //} - //VisualStateManager.GoToState(this, m_shapeFromLastBitmapCreation == ColorSpectrumShape.Box ? "BoxSelected" : "RingSelected", useTransitions); //VisualStateManager.GoToState(this, SelectionEllipseShouldBeLight() ? "SelectionEllipseLight" : "SelectionEllipseDark", useTransitions); //if (IsEnabled && FocusState != FocusState.Unfocused) diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index c4ca09d439..0d6ed59c68 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -1,11 +1,13 @@  + x:CompileBindings="True" + xmlns:converters="using:Avalonia.Controls.Converters"> + + + + + + + + @@ -112,11 +110,19 @@ + + + + + + + - - From 7fb48a8a2578d65c5757cdd4108fe96b2c09d8a0 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 13:41:20 -0400 Subject: [PATCH 032/213] Sync with WinUI at 09267531b807707138addc9bef3c8e9fa340615a --- .../ColorSpectrum/ColorSpectrum.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 775a85dfec..3b8d781275 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -191,6 +191,8 @@ namespace Avalonia.Controls.Primitives HsvChannel incrementChannel = HsvChannel.Hue; + bool isSaturationValue = false; + if (key == Key.Left || key == Key.Right) { @@ -201,8 +203,10 @@ namespace Avalonia.Controls.Primitives incrementChannel = HsvChannel.Hue; break; - case ColorSpectrumChannels.SaturationHue: case ColorSpectrumChannels.SaturationValue: + isSaturationValue = true; + goto case ColorSpectrumChannels.SaturationHue; + case ColorSpectrumChannels.SaturationHue: incrementChannel = HsvChannel.Saturation; break; @@ -227,8 +231,10 @@ namespace Avalonia.Controls.Primitives incrementChannel = HsvChannel.Saturation; break; - case ColorSpectrumChannels.HueValue: case ColorSpectrumChannels.SaturationValue: + isSaturationValue = true; + goto case ColorSpectrumChannels.HueValue; + case ColorSpectrumChannels.HueValue: incrementChannel = HsvChannel.Value; break; } @@ -264,6 +270,23 @@ namespace Avalonia.Controls.Primitives IncrementDirection.Lower : IncrementDirection.Higher; + // Image is flipped in RightToLeft, so we need to invert direction in that case. + // The combination saturation and value is also flipped, so we need to invert in that case too. + // If both are false, we don't need to invert. + // If both are true, we would invert twice, so not invert at all. + if ((FlowDirection == FlowDirection.RightToLeft) != isSaturationValue && + (key == Key.Left || key == Key.Right)) + { + if (direction == IncrementDirection.Higher) + { + direction = IncrementDirection.Lower; + } + else + { + direction = IncrementDirection.Higher; + } + } + IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; HsvColor hsvColor = HsvColor; @@ -404,10 +427,18 @@ namespace Avalonia.Controls.Primitives { Color newColor = Color; - if (_oldColor.A != newColor.A || + bool colorChanged = + _oldColor.A != newColor.A || _oldColor.R != newColor.R || _oldColor.G != newColor.G || - _oldColor.B != newColor.B) + _oldColor.B != newColor.B; + + bool areBothColorsBlack = + (_oldColor.R == newColor.R && newColor.R == 0) || + (_oldColor.G == newColor.G && newColor.G == 0) || + (_oldColor.B == newColor.B && newColor.B == 0); + + if (colorChanged || areBothColorsBlack) { var colorChangedEventArgs = new ColorChangedEventArgs(); @@ -745,7 +776,7 @@ namespace Avalonia.Controls.Primitives // we inverted the direction of that axis in order to put more hue on the outside of the ring, // so we need to do similarly here when positioning the ellipse. if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || - _componentsFromLastBitmapCreation == ColorSpectrumChannels.ValueHue) + _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) { sThetaValue = 360 - sThetaValue; sRValue = -sRValue - 1; From baec9c52fc03d9d41a26d5cbe48b050831809166 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 18:31:52 -0400 Subject: [PATCH 033/213] Fix ToolTip handling --- .../ColorSpectrum/ColorSpectrum.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 3b8d781275..42e45c61eb 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -308,12 +308,10 @@ namespace Avalonia.Controls.Primitives protected override void OnGotFocus(GotFocusEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null && + ColorHelpers.ToDisplayNameExists) { - if (ColorHelpers.ToDisplayNameExists) - { - //colorNameToolTip.IsOpen = true; - } + ToolTip.SetIsOpen(_colorNameToolTip, true); } UpdatePseudoClasses(); @@ -323,12 +321,10 @@ namespace Avalonia.Controls.Primitives protected override void OnLostFocus(RoutedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null && + ColorHelpers.ToDisplayNameExists) { - if (ColorHelpers.ToDisplayNameExists) - { - //colorNameToolTip.IsOpen = false; - } + ToolTip.SetIsOpen(_colorNameToolTip, false); } UpdatePseudoClasses(); @@ -449,9 +445,9 @@ namespace Avalonia.Controls.Primitives if (ColorHelpers.ToDisplayNameExists) { - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null) { - colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); } } } @@ -832,12 +828,12 @@ namespace Avalonia.Controls.Primitives // We only want to bother with the color name tool tip if we can provide color names. if (ColorHelpers.ToDisplayNameExists) { - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null) { // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, // so toggling IsEnabled induces it to do that without incurring any visual glitches. - colorNameToolTip.IsEnabled = false; - colorNameToolTip.IsEnabled = true; + _colorNameToolTip.IsEnabled = false; + _colorNameToolTip.IsEnabled = true; } } From 9f132d4ac0d7980cd98a0a2b9cc971ed0b376a04 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 18:44:17 -0400 Subject: [PATCH 034/213] Simplify by removing unnecessary single-use methods --- .../ColorSpectrum/ColorSpectrum.cs | 238 ++++++++---------- 1 file changed, 99 insertions(+), 139 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 42e45c61eb..892fa76fc9 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -119,7 +119,10 @@ namespace Avalonia.Controls.Primitives if (_layoutRoot != null) { - _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => OnLayoutRootSizeChanged()); + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => + { + CreateBitmapsAndColorMap(); + }); } if (_inputTarget != null) @@ -335,74 +338,121 @@ namespace Avalonia.Controls.Primitives { if (change.Property == ColorProperty) { - OnColorChanged(change); + // If we're in the process of internally updating the color, + // then we don't want to respond to the Color property changing. + if (!_updatingColor) + { + Color color = Color; + + _updatingHsvColor = true; + Hsv newHsv = (new Rgb(color)).ToHsv(); + HsvColor = newHsv.ToHsvColor(color.A / 255.0); + _updatingHsvColor = false; + + UpdateEllipse(); + UpdateBitmapSources(); + } + + _oldColor = change.OldValue.GetValueOrDefault(); } else if (change.Property == HsvColorProperty) { - OnHsvColorChanged(change); + // If we're in the process of internally updating the HSV color, + // then we don't want to respond to the HsvColor property changing. + if (!_updatingHsvColor) + { + SetColor(); + } + + _oldHsvColor = change.OldValue.GetValueOrDefault(); } - else if ( - change.Property == MinHueProperty || - change.Property == MaxHueProperty) + else if (change.Property == MinHueProperty || + change.Property == MaxHueProperty) { - OnMinMaxHueChanged(); + int minHue = MinHue; + int maxHue = MaxHue; + + if (minHue < 0 || minHue > 359) + { + throw new ArgumentException("MinHue must be between 0 and 359."); + } + else if (maxHue < 0 || maxHue > 359) + { + throw new ArgumentException("MaxHue must be between 0 and 359."); + } + + ColorSpectrumChannels channels = Channels; + + // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.SaturationValue && + channels != ColorSpectrumChannels.ValueSaturation) + { + CreateBitmapsAndColorMap(); + } } - else if ( - change.Property == MinSaturationProperty || - change.Property == MaxSaturationProperty) + else if (change.Property == MinSaturationProperty || + change.Property == MaxSaturationProperty) { - OnMinMaxSaturationChanged(); + int minSaturation = MinSaturation; + int maxSaturation = MaxSaturation; + + if (minSaturation < 0 || minSaturation > 100) + { + throw new ArgumentException("MinSaturation must be between 0 and 100."); + } + else if (maxSaturation < 0 || maxSaturation > 100) + { + throw new ArgumentException("MaxSaturation must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueValue && + channels != ColorSpectrumChannels.ValueHue) + { + CreateBitmapsAndColorMap(); + } } - else if ( - change.Property == MinValueProperty || - change.Property == MaxValueProperty) + else if (change.Property == MinValueProperty || + change.Property == MaxValueProperty) { - OnMinMaxValueChanged(); + int minValue = MinValue; + int maxValue = MaxValue; + + if (minValue < 0 || minValue > 100) + { + throw new ArgumentException("MinValue must be between 0 and 100."); + } + else if (maxValue < 0 || maxValue > 100) + { + throw new ArgumentException("MaxValue must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueSaturation && + channels != ColorSpectrumChannels.SaturationHue) + { + CreateBitmapsAndColorMap(); + } } else if (change.Property == ShapeProperty) { - OnShapeChanged(); + CreateBitmapsAndColorMap(); } else if (change.Property == ChannelsProperty) { - OnComponentsChanged(); + CreateBitmapsAndColorMap(); } base.OnPropertyChanged(change); } - private void OnColorChanged(AvaloniaPropertyChangedEventArgs change) - { - // If we're in the process of internally updating the color, - // then we don't want to respond to the Color property changing. - if (!_updatingColor) - { - Color color = Color; - - _updatingHsvColor = true; - Hsv newHsv = (new Rgb(color)).ToHsv(); - HsvColor = newHsv.ToHsvColor(color.A / 255.0); - _updatingHsvColor = false; - - UpdateEllipse(); - UpdateBitmapSources(); - } - - _oldColor = change.OldValue.GetValueOrDefault(); - } - - private void OnHsvColorChanged(AvaloniaPropertyChangedEventArgs change) - { - // If we're in the process of internally updating the HSV color, - // then we don't want to respond to the HsvColor property changing. - if (!_updatingHsvColor) - { - SetColor(); - } - - _oldHsvColor = change.OldValue.GetValueOrDefault(); - } - private void SetColor() { HsvColor hsvColor = HsvColor; @@ -453,91 +503,6 @@ namespace Avalonia.Controls.Primitives } } - protected void OnMinMaxHueChanged() - { - int minHue = MinHue; - int maxHue = MaxHue; - - if (minHue < 0 || minHue > 359) - { - throw new ArgumentException("MinHue must be between 0 and 359."); - } - else if (maxHue < 0 || maxHue > 359) - { - throw new ArgumentException("MaxHue must be between 0 and 359."); - } - - ColorSpectrumChannels channels = Channels; - - // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it - // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.SaturationValue && - channels != ColorSpectrumChannels.ValueSaturation) - { - CreateBitmapsAndColorMap(); - } - } - - protected void OnMinMaxSaturationChanged() - { - int minSaturation = MinSaturation; - int maxSaturation = MaxSaturation; - - if (minSaturation < 0 || minSaturation > 100) - { - throw new ArgumentException("MinSaturation must be between 0 and 100."); - } - else if (maxSaturation < 0 || maxSaturation > 100) - { - throw new ArgumentException("MaxSaturation must be between 0 and 100."); - } - - ColorSpectrumChannels channels = Channels; - - // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it - // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueValue && - channels != ColorSpectrumChannels.ValueHue) - { - CreateBitmapsAndColorMap(); - } - } - - private void OnMinMaxValueChanged() - { - int minValue = MinValue; - int maxValue = MaxValue; - - if (minValue < 0 || minValue > 100) - { - throw new ArgumentException("MinValue must be between 0 and 100."); - } - else if (maxValue < 0 || maxValue > 100) - { - throw new ArgumentException("MaxValue must be between 0 and 100."); - } - - ColorSpectrumChannels channels = Channels; - - // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it - // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueSaturation && - channels != ColorSpectrumChannels.SaturationHue) - { - CreateBitmapsAndColorMap(); - } - } - - private void OnShapeChanged() - { - CreateBitmapsAndColorMap(); - } - - private void OnComponentsChanged() - { - CreateBitmapsAndColorMap(); - } - /// /// Updates the visual state of the control by applying latest PseudoClasses. /// @@ -840,11 +805,6 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); } - private void OnLayoutRootSizeChanged() - { - CreateBitmapsAndColorMap(); - } - private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) { _isPointerOver = true; From d4dbd92ca882f9e76dd1729cfe2b242eef7e745a Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 22 Mar 2022 18:09:20 -0400 Subject: [PATCH 035/213] Support FlowDirection and other simplifications --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 4 +++- .../ColorSpectrum/ColorSpectrum.cs | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index e95c63da33..71fc887bee 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -69,7 +69,9 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register(nameof(HsvColor), new HsvColor(1, 0, 0, 1)); + AvaloniaProperty.Register( + nameof(HsvColor), + new HsvColor(1, 0, 0, 1)); /// /// Gets or sets the maximum value of the Hue channel in the range from 0..359. diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 892fa76fc9..a9497b98f1 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -40,6 +40,7 @@ namespace Avalonia.Controls.Primitives private List _hsvValues = new List(); private IDisposable? _layoutRootDisposable; + private IDisposable? _selectionEllipsePanelDisposable; // XAML template parts private Grid? _layoutRoot; @@ -134,18 +135,18 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += OnInputTargetPointerReleased; } - if (ColorHelpers.ToDisplayNameExists) + if (ColorHelpers.ToDisplayNameExists && + _colorNameToolTip != null) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); - } + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); } if (_selectionEllipsePanel != null) { - // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 - //m_selectionEllipsePanel.RegisterPropertyChangedCallback(FrameworkElement.FlowDirectionProperty, OnSelectionEllipseFlowDirectionChanged); + _selectionEllipsePanelDisposable = _selectionEllipsePanel.GetObservable(FlowDirectionProperty).Subscribe(_ => + { + UpdateEllipse(); + }); } // If we haven't yet created our bitmaps, do so now. @@ -166,6 +167,9 @@ namespace Avalonia.Controls.Primitives _layoutRootDisposable?.Dispose(); _layoutRootDisposable = null; + _selectionEllipsePanelDisposable?.Dispose(); + _selectionEllipsePanelDisposable = null; + if (_inputTarget != null) { _inputTarget.PointerEnter -= OnInputTargetPointerEnter; @@ -862,12 +866,6 @@ namespace Avalonia.Controls.Primitives args.Handled = true; } - // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 - //private void OnSelectionEllipseFlowDirectionChanged(DependencyObject o, DependencyProperty p) - //{ - // UpdateEllipse(); - //} - private async void CreateBitmapsAndColorMap() { if (_layoutRoot == null || From 41d2b0dd1cd92e7b1528f5410560d5b6b5c031db Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 22 Mar 2022 21:37:33 -0400 Subject: [PATCH 036/213] Remove ContrastBrushConverter for now (part of ColorSlider) --- .../Converters/ContrastBrushConverter.cs | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs diff --git a/src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs deleted file mode 100644 index 79c38ea53f..0000000000 --- a/src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Globalization; -using Avalonia; -using Avalonia.Controls.Primitives; -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace FinancialManager.UWP -{ - /// - /// Gets a color, either black or white, depending on the perceived brightness of the supplied color. - /// - public class ContrastBrushConverter : IValueConverter - { - /// - /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. - /// - public byte AlphaThreshold { get; set; } = 128; - - /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - Color comparisonColor; - Color? defaultColor = null; - - // Get the changing color to compare against - if (value is Color valueColor) - { - comparisonColor = valueColor; - } - else if (value is HsvColor valueHsvColor) - { - comparisonColor = valueHsvColor.ToRgb(); - } - else if (value is SolidColorBrush valueBrush) - { - comparisonColor = valueBrush.Color; - } - else - { - // Invalid color value provided - return AvaloniaProperty.UnsetValue; - } - - // Get the default color when transparency is high - if (parameter is Color parameterColor) - { - defaultColor = parameterColor; - } - else if (parameter is HsvColor parameterHsvColor) - { - defaultColor = parameterHsvColor.ToRgb(); - } - else if (parameter is SolidColorBrush parameterBrush) - { - defaultColor = parameterBrush.Color; - } - - if (comparisonColor.A < AlphaThreshold && - defaultColor.HasValue) - { - // If the transparency is less than the threshold just use the default brush - // This can commonly be something like the TextControlForeground brush - return new SolidColorBrush(defaultColor.Value); - } - else - { - // Chose a white/black brush based on contrast to the base color - if (ColorHelpers.GetRelativeLuminance(comparisonColor) > 0.5) - { - // Bright color, use a dark for contrast - return new SolidColorBrush(Colors.Black); - } - else - { - // Dark color, use a light for contrast - return new SolidColorBrush(Colors.White); - } - } - } - - /// - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } -} From 9ad87658f194757a66b62251f2ebb02308035dd9 Mon Sep 17 00:00:00 2001 From: Andrej Bunjac Date: Thu, 24 Mar 2022 14:16:18 +0100 Subject: [PATCH 037/213] Added fixes for Margin, Padding and Thickness properties with UseLayoutRounding = true. --- src/Avalonia.Controls/Border.cs | 25 +++++++- .../Presenters/ContentPresenter.cs | 40 +++++++++++-- src/Avalonia.Layout/LayoutHelper.cs | 59 ++++++++++++++++++- src/Avalonia.Layout/Layoutable.cs | 14 ++++- 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index ee3be1d5b3..ce64570dc8 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -169,13 +169,36 @@ namespace Avalonia.Controls set => SetValue(BoxShadowProperty, value); } + private Thickness _layoutThickness = default; + + private Thickness LayoutThickness + { + get + { + if (_layoutThickness == default) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + { + var scale = LayoutHelper.GetLayoutScale(this); + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); + } + + _layoutThickness = borderThickness; + } + + return _layoutThickness; + } + } + /// /// Renders the control. /// /// The drawing context. public override void Render(DrawingContext context) { - _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, + _borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 93acd88fb1..bbb772a4ce 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -329,10 +329,33 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } + private Thickness _layoutThickness = default; + + private Thickness LayoutThickness + { + get + { + if (_layoutThickness == default) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + { + var scale = LayoutHelper.GetLayoutScale(this); + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); + } + + _layoutThickness = borderThickness; + } + + return _layoutThickness; + } + } + /// public override void Render(DrawingContext context) { - _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, + _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow); } @@ -400,13 +423,22 @@ namespace Avalonia.Controls.Presenters { if (Child == null) return finalSize; - var padding = Padding + BorderThickness; + var useLayoutRounding = UseLayoutRounding; + var scale = LayoutHelper.GetLayoutScale(this); + var padding = Padding; + var borderThickness = BorderThickness; + + if (useLayoutRounding) + { + padding = LayoutHelper.RoundLayoutThickness(padding, scale, scale); + borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale, scale); + } + + padding += borderThickness; var horizontalContentAlignment = HorizontalContentAlignment; var verticalContentAlignment = VerticalContentAlignment; - var useLayoutRounding = UseLayoutRounding; var availableSize = finalSize; var sizeForChild = availableSize; - var scale = LayoutHelper.GetLayoutScale(this); var originX = offset.X; var originY = offset.Y; diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index d4154a6d0c..d24be57d2b 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -52,16 +52,44 @@ namespace Avalonia.Layout public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding, Thickness borderThickness) { - return ArrangeChild(child, availableSize, padding + borderThickness); + if (IsParentLayoutRounded(child, out double scale)) + { + padding = RoundLayoutThickness(padding, scale, scale); + borderThickness = RoundLayoutThickness(borderThickness, scale, scale); + } + + return ArrangeChildInternal(child, availableSize, padding + borderThickness); } public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding) + { + if(IsParentLayoutRounded(child, out double scale)) + padding = RoundLayoutThickness(padding, scale, scale); + + return ArrangeChildInternal(child, availableSize, padding); + } + + private static Size ArrangeChildInternal(ILayoutable? child, Size availableSize, Thickness padding) { child?.Arrange(new Rect(availableSize).Deflate(padding)); return availableSize; } + private static bool IsParentLayoutRounded(ILayoutable? child, out double scale) + { + var layoutableParent = (ILayoutable?)child?.GetVisualParent(); + + if (layoutableParent == null || !((Layoutable)layoutableParent).UseLayoutRounding) + { + scale = 1.0; + return false; + } + + scale = GetLayoutScale(layoutableParent); + return true; + } + /// /// Invalidates measure for given control and all visual children recursively. /// @@ -126,6 +154,32 @@ namespace Avalonia.Layout return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY)); } + /// + /// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen + /// coordinates. + /// + /// Input thickness. + /// DPI along x-dimension. + /// DPI along y-dimension. + /// Value of thickness that will be rounded under screen DPI. + /// + /// This is a layout helper method. It takes DPI into account and also does not return + /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper + /// associated with the UseLayoutRounding property and should not be used as a general rounding + /// utility. + /// + public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScaleX, double dpiScaleY) + { + return new Thickness( + RoundLayoutValue(thickness.Left, dpiScaleX), + RoundLayoutValue(thickness.Top, dpiScaleY), + RoundLayoutValue(thickness.Right, dpiScaleX), + RoundLayoutValue(thickness.Bottom, dpiScaleY) + ); + } + + + /// /// Calculates the value to be used for layout rounding at high DPI. /// @@ -163,8 +217,7 @@ namespace Avalonia.Layout return newValue; } - - + /// /// Calculates the min and max height for a control. Ported from WPF. /// diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 09e0c4263a..23a76f6ee2 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -643,17 +643,27 @@ namespace Avalonia.Layout { if (IsVisible) { + var useLayoutRounding = UseLayoutRounding; + var scale = LayoutHelper.GetLayoutScale(this); + var margin = Margin; var originX = finalRect.X + margin.Left; var originY = finalRect.Y + margin.Top; + + // Margin has to be treated separately because the layout rounding function is not linear + // f(a + b) != f(a) + f(b) + // If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales. + if (UseLayoutRounding) + { + margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale); + } + var availableSizeMinusMargins = new Size( Math.Max(0, finalRect.Width - margin.Left - margin.Right), Math.Max(0, finalRect.Height - margin.Top - margin.Bottom)); var horizontalAlignment = HorizontalAlignment; var verticalAlignment = VerticalAlignment; var size = availableSizeMinusMargins; - var scale = LayoutHelper.GetLayoutScale(this); - var useLayoutRounding = UseLayoutRounding; if (horizontalAlignment != HorizontalAlignment.Stretch) { From d3551ce99fa850c840f8730fafcfab256632db01 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 11:59:33 -0400 Subject: [PATCH 038/213] Use the new TemplatePartAttribute --- .../ColorSpectrum/ColorSpectrum.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index a9497b98f1..9d04d1385b 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,6 +20,15 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// + [TemplatePart(Name = "PART_ColorNameToolTip", Type = typeof(ToolTip))] + [TemplatePart(Name = "PART_InputTarget", Type = typeof(Canvas))] + [TemplatePart(Name = "PART_LayoutRoot", Type = typeof(Grid))] + [TemplatePart(Name = "PART_SelectionEllipsePanel", Type = typeof(Panel))] + [TemplatePart(Name = "PART_SizingGrid", Type = typeof(Grid))] + [TemplatePart(Name = "PART_SpectrumEllipse", Type = typeof(Ellipse))] + [TemplatePart(Name = "PART_SpectrumRectangle", Type = typeof(Rectangle))] + [TemplatePart(Name = "PART_SpectrumOverlayEllipse", Type = typeof(Ellipse))] + [TemplatePart(Name = "PART_SpectrumOverlayRectangle", Type = typeof(Rectangle))] [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { @@ -108,15 +117,15 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); + _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); + _inputTarget = e.NameScope.Find("PART_InputTarget"); _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); + _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); _sizingGrid = e.NameScope.Find("PART_SizingGrid"); - _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); _spectrumEllipse = e.NameScope.Find("PART_SpectrumEllipse"); - _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); + _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); - _inputTarget = e.NameScope.Find("PART_InputTarget"); - _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); - _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); + _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); if (_layoutRoot != null) { From 32f8b63764fcbfeda894415e706b3686fe07997b Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:01:17 -0400 Subject: [PATCH 039/213] Remove extra variables leftover from C# conversion --- .../ColorSpectrum/ColorSpectrum.cs | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9d04d1385b..abd84cc452 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -889,41 +889,33 @@ namespace Avalonia.Controls.Primitives return; } - var layoutRoot = _layoutRoot; - var sizingGrid = _sizingGrid; - var inputTarget = _inputTarget; - var spectrumRectangle = _spectrumRectangle; - var spectrumEllipse = _spectrumEllipse; - var spectrumOverlayRectangle = _spectrumOverlayRectangle; - var spectrumOverlayEllipse = _spectrumOverlayEllipse; - // We want ColorSpectrum to always be a square, so we'll take the smaller of the dimensions // and size the sizing grid to that. - double minDimension = Math.Min(layoutRoot.Bounds.Width, layoutRoot.Bounds.Height); + double minDimension = Math.Min(_layoutRoot.Bounds.Width, _layoutRoot.Bounds.Height); if (minDimension == 0) { return; } - sizingGrid.Width = minDimension; - sizingGrid.Height = minDimension; + _sizingGrid.Width = minDimension; + _sizingGrid.Height = minDimension; - if (sizingGrid.Clip is RectangleGeometry clip) + if (_sizingGrid.Clip is RectangleGeometry clip) { clip.Rect = new Rect(0, 0, minDimension, minDimension); } - inputTarget.Width = minDimension; - inputTarget.Height = minDimension; - spectrumRectangle.Width = minDimension; - spectrumRectangle.Height = minDimension; - spectrumEllipse.Width = minDimension; - spectrumEllipse.Height = minDimension; - spectrumOverlayRectangle.Width = minDimension; - spectrumOverlayRectangle.Height = minDimension; - spectrumOverlayEllipse.Width = minDimension; - spectrumOverlayEllipse.Height = minDimension; + _inputTarget.Width = minDimension; + _inputTarget.Height = minDimension; + _spectrumRectangle.Width = minDimension; + _spectrumRectangle.Height = minDimension; + _spectrumEllipse.Width = minDimension; + _spectrumEllipse.Height = minDimension; + _spectrumOverlayRectangle.Width = minDimension; + _spectrumOverlayRectangle.Height = minDimension; + _spectrumOverlayEllipse.Width = minDimension; + _spectrumOverlayEllipse.Height = minDimension; HsvColor hsvColor = HsvColor; int minHue = MinHue; From a47d9b6487dad0303eec8728f30bc14c25d911c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:14:09 -0400 Subject: [PATCH 040/213] Improve comments ColorSpectrumChannels clarifying axis mappings --- .../ColorPicker/ColorSpectrumChannels.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs index 2b3f711634..a31586d175 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs @@ -9,38 +9,65 @@ namespace Avalonia.Controls { /// /// Defines the two HSV color channels displayed by a . - /// Order of the color channels is important. /// + /// + /// Order of the color channels is important and correspond with an X/Y axis in Box + /// shape or a degree/radius in Ring shape. + /// public enum ColorSpectrumChannels { /// /// The Hue and Value channels. /// + /// + /// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis. + /// In Ring shape, Hue is mapped to degrees and Value is mapped to radius. + /// HueValue, /// /// The Value and Hue channels. /// + /// + /// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis. + /// In Ring shape, Value is mapped to degrees and Hue is mapped to radius. + /// ValueHue, /// /// The Hue and Saturation channels. /// + /// + /// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis. + /// In Ring shape, Hue is mapped to degrees and Saturation is mapped to radius. + /// HueSaturation, /// /// The Saturation and Hue channels. /// + /// + /// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis. + /// In Ring shape, Saturation is mapped to degrees and Hue is mapped to radius. + /// SaturationHue, /// /// The Saturation and Value channels. /// + /// + /// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis. + /// In Ring shape, Saturation is mapped to degrees and Value is mapped to radius. + /// SaturationValue, /// /// The Value and Saturation channels. /// + /// + /// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis. + /// In Ring shape, Value is mapped to degrees and Saturation is mapped to radius. + /// ValueSaturation, }; } From 5904057bd200fb8b933eb8ad516d3db0c25532f6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:41:59 -0400 Subject: [PATCH 041/213] Separate border style setters for easier customization --- .../Controls/ColorSpectrum.xaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index 6a958809a0..8657a072a4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -73,8 +73,6 @@ --> + + + + From 88370ce91bbbe1d1b786c578a22a3286f6329851 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:49:10 -0400 Subject: [PATCH 043/213] Add Default theme for ColorSpectrum --- .../Controls/ColorSpectrum.xaml | 138 ++++++++++++++++++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + 2 files changed, 139 insertions(+) create mode 100644 src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml new file mode 100644 index 0000000000..128f7038eb --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 846d45b839..eea5af5932 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -11,6 +11,7 @@ + From 5a76c63c478ec21a6b2f8b960f6e6e7623904a56 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 11:36:37 -0400 Subject: [PATCH 044/213] Simplify TemplatePartAttribute usage --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index abd84cc452..62d3b4ce88 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,15 +20,15 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// - [TemplatePart(Name = "PART_ColorNameToolTip", Type = typeof(ToolTip))] - [TemplatePart(Name = "PART_InputTarget", Type = typeof(Canvas))] - [TemplatePart(Name = "PART_LayoutRoot", Type = typeof(Grid))] - [TemplatePart(Name = "PART_SelectionEllipsePanel", Type = typeof(Panel))] - [TemplatePart(Name = "PART_SizingGrid", Type = typeof(Grid))] - [TemplatePart(Name = "PART_SpectrumEllipse", Type = typeof(Ellipse))] - [TemplatePart(Name = "PART_SpectrumRectangle", Type = typeof(Rectangle))] - [TemplatePart(Name = "PART_SpectrumOverlayEllipse", Type = typeof(Ellipse))] - [TemplatePart(Name = "PART_SpectrumOverlayRectangle", Type = typeof(Rectangle))] + [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] + [TemplatePart("PART_InputTarget", typeof(Canvas))] + [TemplatePart("PART_LayoutRoot", typeof(Grid))] + [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] + [TemplatePart("PART_SizingGrid", typeof(Grid))] + [TemplatePart("PART_SpectrumEllipse", typeof(Ellipse))] + [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] + [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] + [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))] [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { From 4fe7fb71e67264d8635c2fa5383138b2eab7ff12 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 11:44:32 -0400 Subject: [PATCH 045/213] Simplify ColorChangedEventArgs --- .../ColorPicker/ColorChangedEventArgs.cs | 26 +++---------------- .../ColorSpectrum/ColorSpectrum.cs | 6 +---- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs index 93ba1a4db1..b1d15d6b17 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs @@ -17,16 +17,6 @@ namespace Avalonia.Controls /// public class ColorChangedEventArgs : EventArgs { - private Color _OldColor; - private Color _NewColor; - - /// - /// Initializes a new instance of the class. - /// - public ColorChangedEventArgs() - { - } - /// /// Initializes a new instance of the class. /// @@ -34,26 +24,18 @@ namespace Avalonia.Controls /// The new/updated color that triggered the change event. public ColorChangedEventArgs(Color oldColor, Color newColor) { - _OldColor = oldColor; - _NewColor = newColor; + OldColor = oldColor; + NewColor = newColor; } /// /// Gets the old/original color from before the change event. /// - public Color OldColor - { - get => _OldColor; - internal set => _OldColor = value; - } + public Color OldColor { get; private set; } /// /// Gets the new/updated color that triggered the change event. /// - public Color NewColor - { - get => _NewColor; - internal set => _NewColor = value; - } + public Color NewColor { get; private set; } } } diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 62d3b4ce88..11e1437f81 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -499,11 +499,7 @@ namespace Avalonia.Controls.Primitives if (colorChanged || areBothColorsBlack) { - var colorChangedEventArgs = new ColorChangedEventArgs(); - - colorChangedEventArgs.OldColor = _oldColor; - colorChangedEventArgs.NewColor = newColor; - + var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); if (ColorHelpers.ToDisplayNameExists) From 073b3e4d34706a1198fc7fea3f9d02dc747dcb50 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:05:14 -0400 Subject: [PATCH 046/213] Slightly rework events using OnAttached/DetachedToVisualTree --- .../ColorSpectrum/ColorSpectrum.cs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 11e1437f81..dfead58c6a 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.Primitives { base.OnApplyTemplate(e); - UnregisterEvents(); + UnregisterEvents(); // Failsafe _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); @@ -127,14 +127,6 @@ namespace Avalonia.Controls.Primitives _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); - if (_layoutRoot != null) - { - _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => - { - CreateBitmapsAndColorMap(); - }); - } - if (_inputTarget != null) { _inputTarget.PointerEnter += OnInputTargetPointerEnter; @@ -144,10 +136,12 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += OnInputTargetPointerReleased; } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_layoutRoot != null) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => + { + CreateBitmapsAndColorMap(); + }); } if (_selectionEllipsePanel != null) @@ -158,6 +152,12 @@ namespace Avalonia.Controls.Primitives }); } + if (ColorHelpers.ToDisplayNameExists && + _colorNameToolTip != null) + { + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + } + // If we haven't yet created our bitmaps, do so now. if (_hsvValues.Count == 0) { @@ -168,6 +168,22 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // OnAttachedToVisualTree is called after OnApplyTemplate so events cannot be connected here + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + UnregisterEvents(); + } + /// /// Explicitly unregisters all events connected in OnApplyTemplate(). /// From 7e3cf42c00f6dfe6ea1a3a16af4f2906224cbff6 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:08:05 -0400 Subject: [PATCH 047/213] Rename event handlers that are not overridable methods --- .../ColorSpectrum/ColorSpectrum.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index dfead58c6a..c63bcb225b 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -129,11 +129,11 @@ namespace Avalonia.Controls.Primitives if (_inputTarget != null) { - _inputTarget.PointerEnter += OnInputTargetPointerEnter; - _inputTarget.PointerLeave += OnInputTargetPointerLeave; - _inputTarget.PointerPressed += OnInputTargetPointerPressed; - _inputTarget.PointerMoved += OnInputTargetPointerMoved; - _inputTarget.PointerReleased += OnInputTargetPointerReleased; + _inputTarget.PointerEnter += InputTarget_PointerEnter; + _inputTarget.PointerLeave += InputTarget_PointerLeave; + _inputTarget.PointerPressed += InputTarget_PointerPressed; + _inputTarget.PointerMoved += InputTarget_PointerMoved; + _inputTarget.PointerReleased += InputTarget_PointerReleased; } if (_layoutRoot != null) @@ -197,11 +197,11 @@ namespace Avalonia.Controls.Primitives if (_inputTarget != null) { - _inputTarget.PointerEnter -= OnInputTargetPointerEnter; - _inputTarget.PointerLeave -= OnInputTargetPointerLeave; - _inputTarget.PointerPressed -= OnInputTargetPointerPressed; - _inputTarget.PointerMoved -= OnInputTargetPointerMoved; - _inputTarget.PointerReleased -= OnInputTargetPointerReleased; + _inputTarget.PointerEnter -= InputTarget_PointerEnter; + _inputTarget.PointerLeave -= InputTarget_PointerLeave; + _inputTarget.PointerPressed -= InputTarget_PointerPressed; + _inputTarget.PointerMoved -= InputTarget_PointerMoved; + _inputTarget.PointerReleased -= InputTarget_PointerReleased; } } @@ -830,21 +830,24 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); } - private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) + /// + private void InputTarget_PointerEnter(object? sender, PointerEventArgs args) { _isPointerOver = true; UpdatePseudoClasses(); args.Handled = true; } - private void OnInputTargetPointerLeave(object? sender, PointerEventArgs args) + /// + private void InputTarget_PointerLeave(object? sender, PointerEventArgs args) { _isPointerOver = false; UpdatePseudoClasses(); args.Handled = true; } - private void OnInputTargetPointerPressed(object? sender, PointerPressedEventArgs args) + /// + private void InputTarget_PointerPressed(object? sender, PointerPressedEventArgs args) { var inputTarget = _inputTarget; @@ -864,7 +867,8 @@ namespace Avalonia.Controls.Primitives args.Handled = true; } - private void OnInputTargetPointerMoved(object? sender, PointerEventArgs args) + /// + private void InputTarget_PointerMoved(object? sender, PointerEventArgs args) { if (!_isPointerPressed) { @@ -875,7 +879,8 @@ namespace Avalonia.Controls.Primitives args.Handled = true; } - private void OnInputTargetPointerReleased(object? sender, PointerReleasedEventArgs args) + /// + private void InputTarget_PointerReleased(object? sender, PointerReleasedEventArgs args) { _isPointerPressed = false; _shouldShowLargeSelection = false; From 89b6e4e9d05f95d6ef73f30a777387e72f4d3ff2 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:10:39 -0400 Subject: [PATCH 048/213] If we can't connect in OnAttachedToVisualTree we also can't disconnect --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index c63bcb225b..2ff264baf9 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -180,8 +180,6 @@ namespace Avalonia.Controls.Primitives protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - - UnregisterEvents(); } /// From 088d63b9128e153ef91c7cfb4015718eb0f309ed Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:17:15 -0400 Subject: [PATCH 049/213] Remove Clip geometry which seems to be unnecessary --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 6 ------ src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml | 6 ++---- src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml | 6 ++---- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 2ff264baf9..45b95ce981 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -915,12 +915,6 @@ namespace Avalonia.Controls.Primitives _sizingGrid.Width = minDimension; _sizingGrid.Height = minDimension; - - if (_sizingGrid.Clip is RectangleGeometry clip) - { - clip.Rect = new Rect(0, 0, minDimension, minDimension); - } - _inputTarget.Width = minDimension; _inputTarget.Height = minDimension; _spectrumRectangle.Width = minDimension; diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 128f7038eb..c5ea317ce3 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -20,10 +20,8 @@ - - - + VerticalAlignment="Center" + ClipToBounds="True"> - - - + VerticalAlignment="Center" + ClipToBounds="True"> Date: Mon, 28 Mar 2022 12:39:38 -0400 Subject: [PATCH 050/213] Switch from Grid to Panel which is more lightweight --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 18 +++++++++--------- .../Controls/ColorSpectrum.xaml | 16 +++++++++------- .../Controls/ColorSpectrum.xaml | 16 +++++++++------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 45b95ce981..1b04607ef0 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -22,9 +22,9 @@ namespace Avalonia.Controls.Primitives /// [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] [TemplatePart("PART_InputTarget", typeof(Canvas))] - [TemplatePart("PART_LayoutRoot", typeof(Grid))] + [TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] - [TemplatePart("PART_SizingGrid", typeof(Grid))] + [TemplatePart("PART_SizingPanel", typeof(Panel))] [TemplatePart("PART_SpectrumEllipse", typeof(Ellipse))] [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] @@ -52,8 +52,8 @@ namespace Avalonia.Controls.Primitives private IDisposable? _selectionEllipsePanelDisposable; // XAML template parts - private Grid? _layoutRoot; - private Grid? _sizingGrid; + private Panel? _layoutRoot; + private Panel? _sizingPanel; private Rectangle? _spectrumRectangle; private Ellipse? _spectrumEllipse; private Rectangle? _spectrumOverlayRectangle; @@ -119,9 +119,9 @@ namespace Avalonia.Controls.Primitives _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); - _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); + _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); - _sizingGrid = e.NameScope.Find("PART_SizingGrid"); + _sizingPanel = e.NameScope.Find("PART_SizingPanel"); _spectrumEllipse = e.NameScope.Find("PART_SpectrumEllipse"); _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); @@ -893,7 +893,7 @@ namespace Avalonia.Controls.Primitives private async void CreateBitmapsAndColorMap() { if (_layoutRoot == null || - _sizingGrid == null || + _sizingPanel == null || _inputTarget == null || _spectrumRectangle == null || _spectrumEllipse == null || @@ -913,8 +913,8 @@ namespace Avalonia.Controls.Primitives return; } - _sizingGrid.Width = minDimension; - _sizingGrid.Height = minDimension; + _sizingPanel.Width = minDimension; + _sizingPanel.Height = minDimension; _inputTarget.Width = minDimension; _inputTarget.Height = minDimension; _spectrumRectangle.Width = minDimension; diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index c5ea317ce3..7fa703ed18 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -17,11 +17,13 @@ - - + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index 0b68aa3744..b138a4ad63 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -17,11 +17,13 @@ - - + + - - + + From e607f8cce8ccced935eded7450751c4973ace6ea Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:55:30 -0400 Subject: [PATCH 051/213] Watch for Bounds and FlowDirection property changes on the control itself --- .../ColorSpectrum/ColorSpectrum.cs | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 1b04607ef0..1b68c686fd 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -48,9 +48,6 @@ namespace Avalonia.Controls.Primitives private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); - private IDisposable? _layoutRootDisposable; - private IDisposable? _selectionEllipsePanelDisposable; - // XAML template parts private Panel? _layoutRoot; private Panel? _sizingPanel; @@ -136,22 +133,6 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += InputTarget_PointerReleased; } - if (_layoutRoot != null) - { - _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => - { - CreateBitmapsAndColorMap(); - }); - } - - if (_selectionEllipsePanel != null) - { - _selectionEllipsePanelDisposable = _selectionEllipsePanel.GetObservable(FlowDirectionProperty).Subscribe(_ => - { - UpdateEllipse(); - }); - } - if (ColorHelpers.ToDisplayNameExists && _colorNameToolTip != null) { @@ -187,12 +168,6 @@ namespace Avalonia.Controls.Primitives /// private void UnregisterEvents() { - _layoutRootDisposable?.Dispose(); - _layoutRootDisposable = null; - - _selectionEllipsePanelDisposable?.Dispose(); - _selectionEllipsePanelDisposable = null; - if (_inputTarget != null) { _inputTarget.PointerEnter -= InputTarget_PointerEnter; @@ -363,7 +338,11 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == ColorProperty) + if (change.Property == BoundsProperty) + { + CreateBitmapsAndColorMap(); + } + else if (change.Property == ColorProperty) { // If we're in the process of internally updating the color, // then we don't want to respond to the Color property changing. @@ -382,6 +361,10 @@ namespace Avalonia.Controls.Primitives _oldColor = change.OldValue.GetValueOrDefault(); } + else if (change.Property == FlowDirectionProperty) + { + UpdateEllipse(); + } else if (change.Property == HsvColorProperty) { // If we're in the process of internally updating the HSV color, From 680d3084aa5aaf068326137f54075f8259a4f8ee Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:58:01 -0400 Subject: [PATCH 052/213] Rename border controls in template to match convention --- src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml | 8 ++++---- src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 7fa703ed18..1d127ec115 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -72,14 +72,14 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index b138a4ad63..c8267b57d9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -72,14 +72,14 @@ - - - From 847e20150c993c7431b8a145a161c82107d6297c Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Tue, 29 Mar 2022 21:08:47 +0200 Subject: [PATCH 053/213] Fix IsRegisteredAsAnchorCandidate desync, mainly affecting recycled anchors --- .../Repeater/ItemsRepeater.cs | 8 ++--- .../Repeater/ViewportManager.cs | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 09c0e58332..21b677af66 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -391,11 +391,7 @@ namespace Avalonia.Controls var newBounds = element.Bounds; virtInfo.ArrangeBounds = newBounds; - if (!virtInfo.IsRegisteredAsAnchorCandidate) - { - _viewportManager.RegisterScrollAnchorCandidate(element); - virtInfo.IsRegisteredAsAnchorCandidate = true; - } + _viewportManager.RegisterScrollAnchorCandidate(element, virtInfo); } } @@ -480,7 +476,7 @@ namespace Avalonia.Controls _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); _viewManager.ClearElement(element, isClearedDueToCollectionChange); - _viewportManager.OnElementCleared(element); + _viewportManager.OnElementCleared(element, GetVirtualizationInfo(element)); } private int GetElementIndexImpl(IControl element) diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index ec25fcb265..7f03cc575e 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -249,9 +249,10 @@ namespace Avalonia.Controls virtInfo.IsRegisteredAsAnchorCandidate = false; } - public void OnElementCleared(IControl element) + public void OnElementCleared(IControl element, VirtualizationInfo virtInfo) { _scroller?.UnregisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = false; } public void OnOwnerMeasuring() @@ -358,9 +359,12 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - if (child != targetChild) + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (child != targetChild && info.IsRegisteredAsAnchorCandidate) { _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; } } } @@ -377,9 +381,13 @@ namespace Avalonia.Controls } } - public void RegisterScrollAnchorCandidate(IControl element) + public void RegisterScrollAnchorCandidate(IControl element, VirtualizationInfo virtInfo) { - _scroller?.RegisterAnchorCandidate(element); + if (!virtInfo.IsRegisteredAsAnchorCandidate) + { + _scroller?.RegisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = true; + } } private IControl? GetImmediateChildOfRepeater(IControl descendant) @@ -405,15 +413,18 @@ namespace Avalonia.Controls _isBringIntoViewInProgress = false; _makeAnchorElement = null; + // Undo the anchor deregistrations done by OnBringIntoViewRequested. if (_scroller is object) { foreach (var child in _owner.Children) { var info = ItemsRepeater.GetVirtualizationInfo(child); - if (info.IsRealized && info.IsHeldByLayout) + // The item brought into view is still registered - don't register it more than once. + if (info.IsRealized && info.IsHeldByLayout && !info.IsRegisteredAsAnchorCandidate) { _scroller.RegisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = true; } } } @@ -430,7 +441,13 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - _scroller.UnregisterAnchorCandidate(child); + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (info.IsRegisteredAsAnchorCandidate) + { + _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; + } } _scroller = null; From c8f2548f058841e27ab49431a847e07056dbb437 Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Tue, 29 Mar 2022 22:44:32 +0200 Subject: [PATCH 054/213] Add "Scroll to Selected" and "Remove Item" to control catalog items repeater --- samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml | 2 ++ .../ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs | 10 ++++++++++ .../ViewModels/ItemsRepeaterPageViewModel.cs | 13 +++++++++++++ 3 files changed, 25 insertions(+) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 8305d72d1f..1d42b92096 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -53,10 +53,12 @@ UniformGrid - Horizontal + + ("scroller"); _scrollToLast = this.FindControl private void UnregisterEvents() { + _layoutRootDisposable?.Dispose(); + _layoutRootDisposable = null; + + _selectionEllipsePanelDisposable?.Dispose(); + _selectionEllipsePanelDisposable = null; + if (_inputTarget != null) { _inputTarget.PointerEnter -= InputTarget_PointerEnter; @@ -338,11 +363,7 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == BoundsProperty) - { - CreateBitmapsAndColorMap(); - } - else if (change.Property == ColorProperty) + if (change.Property == ColorProperty) { // If we're in the process of internally updating the color, // then we don't want to respond to the Color property changing. @@ -361,10 +382,6 @@ namespace Avalonia.Controls.Primitives _oldColor = change.OldValue.GetValueOrDefault(); } - else if (change.Property == FlowDirectionProperty) - { - UpdateEllipse(); - } else if (change.Property == HsvColorProperty) { // If we're in the process of internally updating the HSV color, From f46ed4d922faa02eaa8d658fcb88e467290d3005 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 29 Mar 2022 20:21:07 -0400 Subject: [PATCH 056/213] Switch last Grid entirely to Panel --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 14 ++++++-------- .../Controls/ColorSpectrum.xaml | 8 ++++---- .../Controls/ColorSpectrum.xaml | 8 ++++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 1b04607ef0..6779c54a1e 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -643,9 +643,7 @@ namespace Avalonia.Controls.Primitives private void UpdateEllipse() { - var selectionEllipsePanel = _selectionEllipsePanel; - - if (selectionEllipsePanel == null) + if (_selectionEllipsePanel == null) { return; } @@ -654,12 +652,12 @@ namespace Avalonia.Controls.Primitives if (_imageWidthFromLastBitmapCreation == 0 || _imageHeightFromLastBitmapCreation == 0) { - selectionEllipsePanel.IsVisible = false; + _selectionEllipsePanel.IsVisible = false; return; } else { - selectionEllipsePanel.IsVisible = true; + _selectionEllipsePanel.IsVisible = true; } double xPosition; @@ -810,8 +808,8 @@ namespace Avalonia.Controls.Primitives yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; } - Canvas.SetLeft(selectionEllipsePanel, xPosition - (selectionEllipsePanel.Width / 2)); - Canvas.SetTop(selectionEllipsePanel, yPosition - (selectionEllipsePanel.Height / 2)); + Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2)); + Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. if (ColorHelpers.ToDisplayNameExists) @@ -905,7 +903,7 @@ namespace Avalonia.Controls.Primitives } // We want ColorSpectrum to always be a square, so we'll take the smaller of the dimensions - // and size the sizing grid to that. + // and size the sizing panel to that. double minDimension = Math.Min(_layoutRoot.Bounds.Width, _layoutRoot.Bounds.Height); if (minDimension == 0) diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 1d127ec115..7065ba30f6 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -52,7 +52,7 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index c8267b57d9..4208d9665f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -52,7 +52,7 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + - - From 1a8435c04db7a8c7ae14b8949da87cad925dcf22 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 4 Apr 2022 21:35:46 -0400 Subject: [PATCH 057/213] Add CornerRadiusToDoubleConverter --- ...iusFilterKind.cs => CornerRadiusCorner.cs} | 18 +++++--- .../Converters/CornerRadiusFilterConverter.cs | 17 +++---- .../CornerRadiusToDoubleConverter.cs | 46 +++++++++++++++++++ 3 files changed, 66 insertions(+), 15 deletions(-) rename src/Avalonia.Controls/Converters/{CornerRadiusFilterKind.cs => CornerRadiusCorner.cs} (57%) create mode 100644 src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs b/src/Avalonia.Controls/Converters/CornerRadiusCorner.cs similarity index 57% rename from src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs rename to src/Avalonia.Controls/Converters/CornerRadiusCorner.cs index 6a9d0596be..205dd12f48 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusCorner.cs @@ -3,29 +3,33 @@ namespace Avalonia.Controls.Converters { /// - /// Defines constants that specify the filter type for a instance. + /// Defines constants that specify the corner of a . /// [Flags] - public enum CornerRadiusFilterKinds + public enum CornerRadiusCorner { /// - /// No filter applied. + /// No corner. /// None, + /// - /// Filters TopLeft value. + /// The TopLeft corner. /// TopLeft = 1, + /// - /// Filters TopRight value. + /// The TopRight corner. /// TopRight = 2, + /// - /// Filters BottomLeft value. + /// The BottomLeft corner. /// BottomLeft = 4, + /// - /// Filters BottomRight value. + /// The BottomRight corner. /// BottomRight = 8 } diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index 643c30178e..b298fbf2e1 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs @@ -7,17 +7,18 @@ namespace Avalonia.Controls.Converters { /// /// Converts an existing CornerRadius struct to a new CornerRadius struct, - /// with filters applied to extract only the specified fields, leaving the others set to 0. + /// with filters applied to extract only the specified corners, leaving the others set to 0. /// public class CornerRadiusFilterConverter : IValueConverter { /// - /// Gets or sets the type of the filter applied to the . + /// Gets or sets the corners to filter by. + /// Only the specified corners will be included in the converted . /// - public CornerRadiusFilterKinds Filter { get; set; } + public CornerRadiusCorner Filter { get; set; } /// - /// Gets or sets the scale multiplier applied to the . + /// Gets or sets the scale multiplier applied uniformly to each corner. /// public double Scale { get; set; } = 1; @@ -29,10 +30,10 @@ namespace Avalonia.Controls.Converters } return new CornerRadius( - Filter.HasAllFlags(CornerRadiusFilterKinds.TopLeft) ? radius.TopLeft * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.TopRight) ? radius.TopRight * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.BottomRight) ? radius.BottomRight * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.BottomLeft) ? radius.BottomLeft * Scale : 0); + Filter.HasAllFlags(CornerRadiusCorner.TopLeft) ? radius.TopLeft * Scale : 0, + Filter.HasAllFlags(CornerRadiusCorner.TopRight) ? radius.TopRight * Scale : 0, + Filter.HasAllFlags(CornerRadiusCorner.BottomRight) ? radius.BottomRight * Scale : 0, + Filter.HasAllFlags(CornerRadiusCorner.BottomLeft) ? radius.BottomLeft * Scale : 0); } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs new file mode 100644 index 0000000000..2d549504ff --- /dev/null +++ b/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts one corner of a to its double value. + /// + public class CornerRadiusToDoubleConverter : IValueConverter + { + /// + /// Gets or sets the specific corner of the to convert to double. + /// + public CornerRadiusCorner Corner { get; set; } + + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (!(value is CornerRadius cornerRadius)) + { + return AvaloniaProperty.UnsetValue; + } + + switch (Corner) + { + case CornerRadiusCorner.TopLeft: + return cornerRadius.TopLeft; + case CornerRadiusCorner.TopRight: + return cornerRadius.TopRight; + case CornerRadiusCorner.BottomRight: + return cornerRadius.BottomRight; + case CornerRadiusCorner.BottomLeft: + return cornerRadius.BottomLeft; + default: + return 0.0; + } + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} From 30f713746a399883d59446b3a2d5307aaaf014b6 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 4 Apr 2022 21:39:11 -0400 Subject: [PATCH 058/213] Support CornerRadius in ColorSpectrum --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 6 ++++++ .../Controls/ColorSpectrum.xaml | 20 ++++++++++--------- .../Controls/ColorSpectrum.xaml | 20 ++++++++++--------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index eee041e5c5..ec34193f8c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -19,5 +19,11 @@ Shape="Ring" Height="256" Width="256" /> + diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 7065ba30f6..338ea69dac 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -11,6 +11,8 @@ + + - + diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml similarity index 97% rename from src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 5de6daea11..545702ea84 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,14 +1,8 @@ - - - - - - - @@ -136,5 +130,5 @@ - + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 682f395e5b..468b723f5b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -11,7 +11,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index b7d35ad5cc..5b217e4764 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -11,7 +11,6 @@ - From cf654da43f1d4a8dae5f1e37347c724221eb4ecc Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 22:38:49 -0400 Subject: [PATCH 075/213] Add separate Avalonia.Controls.ColorPicker project --- Avalonia.sln | 26 +++++++++++++++++++ samples/ControlCatalog/App.xaml.cs | 13 +++++++++- samples/ControlCatalog/ControlCatalog.csproj | 1 + samples/ControlCatalog/MainView.xaml.cs | 4 +++ samples/Sandbox/Sandbox.csproj | 1 + src/Avalonia.Base/Properties/AssemblyInfo.cs | 1 + .../Avalonia.Controls.ColorPicker.csproj | 21 +++++++++++++++ .../Avalonia.Diagnostics.csproj | 1 + .../Avalonia.LeakTests.csproj | 1 + 9 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj diff --git a/Avalonia.sln b/Avalonia.sln index 1e2a3c6027..ea30514c3e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -169,6 +169,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{1ECC012A-8837-4AE2-9BDA-3E2857898727}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" @@ -1963,6 +1965,30 @@ Global {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 866fb8632a..6539cdaee6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -18,6 +18,16 @@ namespace ControlCatalog DataContext = new ApplicationViewModel(); } + public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + { + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml") + }; + + public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + { + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml") + }; + public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") @@ -69,7 +79,8 @@ namespace ControlCatalog public override void Initialize() { Styles.Insert(0, Fluent); - Styles.Insert(1, DataGridFluent); + Styles.Insert(1, ColorPickerFluent); + Styles.Insert(2, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c0e24357ca..7cbd8a3f9c 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -23,6 +23,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index e8ea39abbb..f2d89a1325 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -50,6 +50,7 @@ namespace ControlCatalog } Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; + Application.Current.Styles.Add(App.ColorPickerFluent); } else if (theme == CatalogTheme.FluentDark) { @@ -60,18 +61,21 @@ namespace ControlCatalog } Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; + Application.Current.Styles.Add(App.ColorPickerFluent); } else if (theme == CatalogTheme.DefaultLight) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; Application.Current.Styles[0] = App.DefaultLight; Application.Current.Styles[1] = App.DataGridDefault; + Application.Current.Styles.Add(App.ColorPickerDefault); } else if (theme == CatalogTheme.DefaultDark) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; Application.Current.Styles[0] = App.DefaultDark; Application.Current.Styles[1] = App.DataGridDefault; + Application.Current.Styles.Add(App.ColorPickerDefault); } } }; diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index 8f2812e048..f3c38cd96e 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index a0560924e7..2c40c768f5 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -19,6 +19,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Controls.ColorPicker, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj new file mode 100644 index 0000000000..21218fc771 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -0,0 +1,21 @@ + + + net6.0;netstandard2.0 + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 2fb7c07b6f..a83e8be475 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 0c663e1a8f..a308e1c3ed 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -9,6 +9,7 @@ + From 83402cb285712d5d9a5d3563f9aa21fc87bae2cc Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 22:50:28 -0400 Subject: [PATCH 076/213] Make Color.ToHsv() internal again --- src/Avalonia.Base/Media/Color.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index db18ec7c6e..cb90404f6d 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -653,9 +653,6 @@ namespace Avalonia.Media (byteToDouble * alpha)); } - // TODO: Mark the below .ToHsv(double...) method internal and make internals visible to Avalonia.Controls - // It is needed for the ColorPicker which works in normalized values directly - /// /// Converts the given RGBA color component values to their HSV color equivalent. /// @@ -668,7 +665,7 @@ namespace Avalonia.Media /// The Blue component in the RGB color model within the range 0..1. /// The Alpha component in the RGB color model within the range 0..1. /// A new equivalent to the given RGBA values. - public static HsvColor ToHsv( + internal static HsvColor ToHsv( double r, double g, double b, From 9f1833030dec741137058631dcfc1e67f63a5e23 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 23:29:46 -0400 Subject: [PATCH 077/213] Use namespaces in XAML --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 36 +++++++++---------- .../Themes/Default.xaml | 27 +++++++------- .../Themes/Fluent.xaml | 27 +++++++------- 3 files changed, 46 insertions(+), 44 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ec34193f8c..08f56be8e3 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -2,28 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" + xmlns:cpp="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls.ColorPicker" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 832daf8853..8d9b49b9b1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,7 +1,8 @@ + xmlns:converters="using:Avalonia.Controls.Converters" + xmlns:primitive="using:Avalonia.Controls.Primitives"> @@ -9,7 +10,7 @@ - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 545702ea84..9c3a74b1fa 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,7 +1,8 @@ + xmlns:converters="using:Avalonia.Controls.Converters" + xmlns:primitive="using:Avalonia.Controls.Primitives"> @@ -9,7 +10,7 @@ - - - - - - - - - - From f13398506d96b964fb96f834063053bdaef11f23 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 23:30:22 -0400 Subject: [PATCH 078/213] Fix Avalonia.Controls.ColorPicker.csproj - Disable ApiDiff tool - Enable nullable reference types --- .../Avalonia.Controls.ColorPicker.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 21218fc771..03950fb168 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -3,6 +3,9 @@ net6.0;netstandard2.0 + + + @@ -16,6 +19,7 @@ - + + From 8da297b30073c05142ad3e31fb7710553db6e824 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 23:36:09 -0400 Subject: [PATCH 079/213] Correct ColorPicker style loading in ControlCatalog --- samples/ControlCatalog/MainView.xaml.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index f2d89a1325..0326946c08 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -49,8 +49,8 @@ namespace ControlCatalog App.Fluent.Mode = FluentThemeMode.Light; } Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; - Application.Current.Styles.Add(App.ColorPickerFluent); + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; } else if (theme == CatalogTheme.FluentDark) { @@ -60,22 +60,22 @@ namespace ControlCatalog App.Fluent.Mode = FluentThemeMode.Dark; } Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; - Application.Current.Styles.Add(App.ColorPickerFluent); + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; } else if (theme == CatalogTheme.DefaultLight) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; Application.Current.Styles[0] = App.DefaultLight; - Application.Current.Styles[1] = App.DataGridDefault; - Application.Current.Styles.Add(App.ColorPickerDefault); + Application.Current.Styles[1] = App.ColorPickerDefault; + Application.Current.Styles[2] = App.DataGridDefault; } else if (theme == CatalogTheme.DefaultDark) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; Application.Current.Styles[0] = App.DefaultDark; - Application.Current.Styles[1] = App.DataGridDefault; - Application.Current.Styles.Add(App.ColorPickerDefault); + Application.Current.Styles[1] = App.ColorPickerDefault; + Application.Current.Styles[2] = App.DataGridDefault; } } }; From a389f7b0d8aa82db0d04401887f72f0bca3c2e3a Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:02:18 -0400 Subject: [PATCH 080/213] Fix ColorPicker project references --- src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj | 2 +- tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index a83e8be475..adddf3f57b 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -13,7 +13,7 @@ - + diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index a308e1c3ed..a00b24bdd7 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -9,7 +9,7 @@ - + From 60e0d12543aac9b8948f8657f0f64292cc219b50 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:04:56 -0400 Subject: [PATCH 081/213] Revert "Use namespaces in XAML" This reverts commit 9f1833030dec741137058631dcfc1e67f63a5e23. --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 36 +++++++++---------- .../Themes/Default.xaml | 27 +++++++------- .../Themes/Fluent.xaml | 27 +++++++------- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 08f56be8e3..ec34193f8c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -2,28 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:cpp="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls.ColorPicker" + xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 8d9b49b9b1..832daf8853 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,8 +1,7 @@ + xmlns:converters="using:Avalonia.Controls.Converters"> @@ -10,7 +9,7 @@ - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 9c3a74b1fa..545702ea84 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,8 +1,7 @@ + xmlns:converters="using:Avalonia.Controls.Converters"> @@ -10,7 +9,7 @@ - - - - - - - - - - From 8453b4a79ac85fd338f23f1941005ac0ad788ae1 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:11:23 -0400 Subject: [PATCH 082/213] Add AssemblyInfo for ColorPicker.csproj --- .../Properties/AssemblyInfo.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs diff --git a/src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..0135541349 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Runtime.CompilerServices; +using Avalonia.Metadata; + +[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] From a66e15e8a2a3f2b8d2f9d0ae14eaaf8509436260 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:11:41 -0400 Subject: [PATCH 083/213] Add back PackageId for ColorPicker.csproj --- .../Avalonia.Controls.ColorPicker.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 03950fb168..0952c899d4 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -1,7 +1,7 @@  net6.0;netstandard2.0 - + Avalonia.Controls.ColorPicker From 397fd5068fc26e7093e0316d1cfb0beb28aa1543 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:31:23 -0400 Subject: [PATCH 084/213] Rename 'channel' to 'component' --- .../ColorSpectrum/ColorHelpers.cs | 44 ++-- .../ColorSpectrum/ColorSpectrum.Properties.cs | 30 +-- .../ColorSpectrum/ColorSpectrum.cs | 231 +++++++++--------- .../ColorSpectrum/Hsv.cs | 18 +- .../ColorSpectrum/IncrementAmount.cs | 2 +- .../ColorSpectrum/IncrementDirection.cs | 2 +- .../ColorSpectrum/Rgb.cs | 20 +- ...Channels.cs => ColorSpectrumComponents.cs} | 18 +- .../HsvChannel.cs | 33 --- .../HsvComponent.cs | 47 ++++ 10 files changed, 229 insertions(+), 216 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrumChannels.cs => ColorSpectrumComponents.cs} (81%) delete mode 100644 src/Avalonia.Controls.ColorPicker/HsvChannel.cs create mode 100644 src/Avalonia.Controls.ColorPicker/HsvComponent.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs index cd2decf0a7..b912d39aba 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs @@ -26,9 +26,9 @@ namespace Avalonia.Controls.Primitives return string.Empty; } - public static Hsv IncrementColorChannel( + public static Hsv IncrementColorComponent( Hsv originalHsv, - HsvChannel channel, + HsvComponent component, IncrementDirection direction, IncrementAmount amount, bool shouldWrap, @@ -52,25 +52,25 @@ namespace Avalonia.Controls.Primitives // If we're adding a large increment, then we want to snap to the next // or previous major value - for hue, this is every increment of 30; // for saturation and value, this is every increment of 10. - switch (channel) + switch (component) { - case HsvChannel.Hue: + case HsvComponent.Hue: valueToIncrement = ref newHsv.H; incrementAmount = amount == IncrementAmount.Small ? 1 : 30; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: valueToIncrement = ref newHsv.S; incrementAmount = amount == IncrementAmount.Small ? 1 : 10; break; - case HsvChannel.Value: + case HsvComponent.Value: valueToIncrement = ref newHsv.V; incrementAmount = amount == IncrementAmount.Small ? 1 : 10; break; default: - throw new InvalidOperationException("Invalid HsvChannel."); + throw new InvalidOperationException("Invalid HsvComponent."); } double previousValue = valueToIncrement; @@ -99,14 +99,14 @@ namespace Avalonia.Controls.Primitives // While working with named colors, we're going to need to be working in actual HSV units, // so we'll divide the min bound and max bound by 100 in the case of saturation or value, // since we'll have received units between 0-100 and we need them within 0-1. - if (channel == HsvChannel.Saturation || - channel == HsvChannel.Value) + if (component == HsvComponent.Saturation || + component == HsvComponent.Value) { minBound /= 100; maxBound /= 100; } - newHsv = FindNextNamedColor(originalHsv, channel, direction, shouldWrap, minBound, maxBound); + newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); } return newHsv; @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Primitives public static Hsv FindNextNamedColor( Hsv originalHsv, - HsvChannel channel, + HsvComponent component, IncrementDirection direction, bool shouldWrap, double minBound, @@ -136,28 +136,28 @@ namespace Avalonia.Controls.Primitives ref double newValue = ref newHsv.H; double incrementAmount = 0.0; - switch (channel) + switch (component) { - case HsvChannel.Hue: + case HsvComponent.Hue: originalValue = originalHsv.H; newValue = ref newHsv.H; incrementAmount = 1; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: originalValue = originalHsv.S; newValue = ref newHsv.S; incrementAmount = 0.01; break; - case HsvChannel.Value: + case HsvComponent.Value: originalValue = originalHsv.V; newValue = ref newHsv.V; incrementAmount = 0.01; break; default: - throw new InvalidOperationException("Invalid HsvChannel."); + throw new InvalidOperationException("Invalid HsvComponent."); } bool shouldFindMidPoint = true; @@ -228,28 +228,28 @@ namespace Avalonia.Controls.Primitives ref double currentValue = ref currentHsv.H; double wrapIncrement = 0; - switch (channel) + switch (component) { - case HsvChannel.Hue: + case HsvComponent.Hue: startValue = ref startHsv.H; currentValue = ref currentHsv.H; wrapIncrement = 360.0; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: startValue = ref startHsv.S; currentValue = ref currentHsv.S; wrapIncrement = 1.0; break; - case HsvChannel.Value: + case HsvComponent.Value: startValue = ref startHsv.V; currentValue = ref currentHsv.V; wrapIncrement = 1.0; break; default: - throw new InvalidOperationException("Invalid HsvChannel."); + throw new InvalidOperationException("Invalid HsvComponent."); } while (newColorName == currentColorName) @@ -316,7 +316,7 @@ namespace Avalonia.Controls.Primitives return newHsv; } - public static double IncrementAlphaChannel( + public static double IncrementAlphaComponent( double originalAlpha, IncrementDirection direction, IncrementAmount amount, diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 71fc887bee..824bf9ab05 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -32,24 +32,24 @@ namespace Avalonia.Controls.Primitives Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); /// - /// Gets or sets the two HSV color channels displayed by the spectrum. + /// Gets or sets the two HSV color components displayed by the spectrum. /// /// /// Internally, the uses the HSV color model. /// - public ColorSpectrumChannels Channels + public ColorSpectrumComponents Components { - get => GetValue(ChannelsProperty); - set => SetValue(ChannelsProperty, value); + get => GetValue(ComponentsProperty); + set => SetValue(ComponentsProperty, value); } /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ChannelsProperty = - AvaloniaProperty.Register( - nameof(Channels), - ColorSpectrumChannels.HueSaturation); + public static readonly StyledProperty ComponentsProperty = + AvaloniaProperty.Register( + nameof(Components), + ColorSpectrumComponents.HueSaturation); /// /// Gets or sets the currently selected color in the HSV color model. @@ -74,7 +74,7 @@ namespace Avalonia.Controls.Primitives new HsvColor(1, 0, 0, 1)); /// - /// Gets or sets the maximum value of the Hue channel in the range from 0..359. + /// Gets or sets the maximum value of the Hue component in the range from 0..359. /// This property must be greater than . /// /// @@ -93,7 +93,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MaxHue), 359); /// - /// Gets or sets the maximum value of the Saturation channel in the range from 0..100. + /// Gets or sets the maximum value of the Saturation component in the range from 0..100. /// This property must be greater than . /// /// @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MaxSaturation), 100); /// - /// Gets or sets the maximum value of the Value channel in the range from 0..100. + /// Gets or sets the maximum value of the Value component in the range from 0..100. /// This property must be greater than . /// /// @@ -131,7 +131,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MaxValue), 100); /// - /// Gets or sets the minimum value of the Hue channel in the range from 0..359. + /// Gets or sets the minimum value of the Hue component in the range from 0..359. /// This property must be less than . /// /// @@ -150,7 +150,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MinHue), 0); /// - /// Gets or sets the minimum value of the Saturation channel in the range from 0..100. + /// Gets or sets the minimum value of the Saturation component in the range from 0..100. /// This property must be less than . /// /// @@ -169,7 +169,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MinSaturation), 0); /// - /// Gets or sets the minimum value of the Value channel in the range from 0..100. + /// Gets or sets the minimum value of the Value component in the range from 0..100. /// This property must be less than . /// /// diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6779c54a1e..aecaa88f36 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls.Primitives // in order to function properly while the asynchronous bitmap creation // is in progress. private ColorSpectrumShape _shapeFromLastBitmapCreation = ColorSpectrumShape.Box; - private ColorSpectrumChannels _componentsFromLastBitmapCreation = ColorSpectrumChannels.HueSaturation; + private ColorSpectrumComponents _componentsFromLastBitmapCreation = ColorSpectrumComponents.HueSaturation; private double _imageWidthFromLastBitmapCreation = 0.0; private double _imageHeightFromLastBitmapCreation = 0.0; private int _minHueFromLastBitmapCreation = 0; @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives public ColorSpectrum() { _shapeFromLastBitmapCreation = Shape; - _componentsFromLastBitmapCreation = Channels; + _componentsFromLastBitmapCreation = Components; _imageWidthFromLastBitmapCreation = 0; _imageHeightFromLastBitmapCreation = 0; _minHueFromLastBitmapCreation = MinHue; @@ -219,53 +219,53 @@ namespace Avalonia.Controls.Primitives bool isControlDown = e.KeyModifiers.HasFlag(KeyModifiers.Control); - HsvChannel incrementChannel = HsvChannel.Hue; + HsvComponent incrementComponent = HsvComponent.Hue; bool isSaturationValue = false; if (key == Key.Left || key == Key.Right) { - switch (Channels) + switch (Components) { - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.HueValue: - incrementChannel = HsvChannel.Hue; + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.HueValue: + incrementComponent = HsvComponent.Hue; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: isSaturationValue = true; - goto case ColorSpectrumChannels.SaturationHue; - case ColorSpectrumChannels.SaturationHue: - incrementChannel = HsvChannel.Saturation; + goto case ColorSpectrumComponents.SaturationHue; + case ColorSpectrumComponents.SaturationHue: + incrementComponent = HsvComponent.Saturation; break; - case ColorSpectrumChannels.ValueHue: - case ColorSpectrumChannels.ValueSaturation: - incrementChannel = HsvChannel.Value; + case ColorSpectrumComponents.ValueHue: + case ColorSpectrumComponents.ValueSaturation: + incrementComponent = HsvComponent.Value; break; } } else if (key == Key.Up || key == Key.Down) { - switch (Channels) + switch (Components) { - case ColorSpectrumChannels.SaturationHue: - case ColorSpectrumChannels.ValueHue: - incrementChannel = HsvChannel.Hue; + case ColorSpectrumComponents.SaturationHue: + case ColorSpectrumComponents.ValueHue: + incrementComponent = HsvComponent.Hue; break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.ValueSaturation: - incrementChannel = HsvChannel.Saturation; + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.ValueSaturation: + incrementComponent = HsvComponent.Saturation; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: isSaturationValue = true; - goto case ColorSpectrumChannels.HueValue; - case ColorSpectrumChannels.HueValue: - incrementChannel = HsvChannel.Value; + goto case ColorSpectrumComponents.HueValue; + case ColorSpectrumComponents.HueValue: + incrementComponent = HsvComponent.Value; break; } } @@ -273,19 +273,19 @@ namespace Avalonia.Controls.Primitives double minBound = 0.0; double maxBound = 0.0; - switch (incrementChannel) + switch (incrementComponent) { - case HsvChannel.Hue: + case HsvComponent.Hue: minBound = MinHue; maxBound = MaxHue; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: minBound = MinSaturation; maxBound = MaxSaturation; break; - case HsvChannel.Value: + case HsvComponent.Value: minBound = MinValue; maxBound = MaxValue; break; @@ -295,8 +295,8 @@ namespace Avalonia.Controls.Primitives // so we want left and up to be lower for hue, but higher for saturation and value. // This will ensure that the icon always moves in the direction of the key press. IncrementDirection direction = - (incrementChannel == HsvChannel.Hue && (key == Key.Left || key == Key.Up)) || - (incrementChannel != HsvChannel.Hue && (key == Key.Right || key == Key.Down)) ? + (incrementComponent == HsvComponent.Hue && (key == Key.Left || key == Key.Up)) || + (incrementComponent != HsvComponent.Hue && (key == Key.Right || key == Key.Down)) ? IncrementDirection.Lower : IncrementDirection.Higher; @@ -320,9 +320,9 @@ namespace Avalonia.Controls.Primitives IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; HsvColor hsvColor = HsvColor; - UpdateColor(ColorHelpers.IncrementColorChannel( + UpdateColor(ColorHelpers.IncrementColorComponent( new Hsv(hsvColor), - incrementChannel, + incrementComponent, direction, amount, shouldWrap: true, @@ -408,12 +408,12 @@ namespace Avalonia.Controls.Primitives throw new ArgumentException("MaxHue must be between 0 and 359."); } - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.SaturationValue && - channels != ColorSpectrumChannels.ValueSaturation) + if (components != ColorSpectrumComponents.SaturationValue && + components != ColorSpectrumComponents.ValueSaturation) { CreateBitmapsAndColorMap(); } @@ -433,12 +433,12 @@ namespace Avalonia.Controls.Primitives throw new ArgumentException("MaxSaturation must be between 0 and 100."); } - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueValue && - channels != ColorSpectrumChannels.ValueHue) + if (components != ColorSpectrumComponents.HueValue && + components != ColorSpectrumComponents.ValueHue) { CreateBitmapsAndColorMap(); } @@ -458,12 +458,12 @@ namespace Avalonia.Controls.Primitives throw new ArgumentException("MaxValue must be between 0 and 100."); } - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueSaturation && - channels != ColorSpectrumChannels.SaturationHue) + if (components != ColorSpectrumComponents.HueSaturation && + components != ColorSpectrumComponents.SaturationHue) { CreateBitmapsAndColorMap(); } @@ -472,7 +472,7 @@ namespace Avalonia.Controls.Primitives { CreateBitmapsAndColorMap(); } - else if (change.Property == ChannelsProperty) + else if (change.Property == ComponentsProperty) { CreateBitmapsAndColorMap(); } @@ -617,23 +617,22 @@ namespace Avalonia.Controls.Primitives // Note: This can sometimes cause a crash -- possibly due to differences in c# rounding. Therefore, index is now clamped. Hsv hsvAtPoint = _hsvValues[MathUtilities.Clamp((y * width + x), 0, _hsvValues.Count - 1)]; - var channels = Channels; var hsvColor = HsvColor; - switch (channels) + switch (Components) { - case ColorSpectrumChannels.HueValue: - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: hsvAtPoint.S = hsvColor.S; break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: hsvAtPoint.V = hsvColor.V; break; - case ColorSpectrumChannels.ValueSaturation: - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + case ColorSpectrumComponents.SaturationValue: hsvAtPoint.H = hsvColor.H; break; } @@ -681,8 +680,8 @@ namespace Avalonia.Controls.Primitives // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, // we inverted the direction of that axis in order to put more hue on the outside of the ring, // so we need to do similarly here when positioning the ellipse. - if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || - _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) + if (_componentsFromLastBitmapCreation == ColorSpectrumComponents.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumComponents.SaturationHue) { sPercent = 1 - sPercent; } @@ -693,32 +692,32 @@ namespace Avalonia.Controls.Primitives switch (_componentsFromLastBitmapCreation) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: xPercent = hPercent; yPercent = vPercent; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: xPercent = hPercent; yPercent = sPercent; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: xPercent = vPercent; yPercent = hPercent; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: xPercent = vPercent; yPercent = sPercent; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: xPercent = sPercent; yPercent = hPercent; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: xPercent = sPercent; yPercent = vPercent; break; @@ -757,8 +756,8 @@ namespace Avalonia.Controls.Primitives // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, // we inverted the direction of that axis in order to put more hue on the outside of the ring, // so we need to do similarly here when positioning the ellipse. - if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || - _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) + if (_componentsFromLastBitmapCreation == ColorSpectrumComponents.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumComponents.SaturationHue) { sThetaValue = 360 - sThetaValue; sRValue = -sRValue - 1; @@ -771,32 +770,32 @@ namespace Avalonia.Controls.Primitives switch (_componentsFromLastBitmapCreation) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: thetaValue = hThetaValue; rValue = vRValue; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: thetaValue = hThetaValue; rValue = sRValue; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: thetaValue = vThetaValue; rValue = hRValue; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: thetaValue = vThetaValue; rValue = sRValue; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: thetaValue = sThetaValue; rValue = hRValue; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: thetaValue = sThetaValue; rValue = vRValue; break; @@ -932,7 +931,7 @@ namespace Avalonia.Controls.Primitives int minValue = MinValue; int maxValue = MaxValue; ColorSpectrumShape shape = Shape; - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If min >= max, then by convention, min is the only number that a property can have. if (minHue >= maxHue) @@ -967,8 +966,8 @@ namespace Avalonia.Controls.Primitives bgraMinPixelData.Capacity = pixelDataSize; // We'll only save pixel data for the middle bitmaps if our third dimension is hue. - if (channels == ColorSpectrumChannels.ValueSaturation || - channels == ColorSpectrumChannels.SaturationValue) + if (components == ColorSpectrumComponents.ValueSaturation || + components == ColorSpectrumComponents.SaturationValue) { bgraMiddle1PixelData.Capacity = pixelDataSize; bgraMiddle2PixelData.Capacity = pixelDataSize; @@ -1004,7 +1003,7 @@ namespace Avalonia.Controls.Primitives for (int y = minDimensionInt - 1; y >= 0; --y) { FillPixelForBox( - x, y, hsv, minDimensionInt, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1017,7 +1016,7 @@ namespace Avalonia.Controls.Primitives for (int x = 0; x < minDimensionInt; ++x) { FillPixelForRing( - x, y, minDimensionInt / 2.0, hsv, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1030,24 +1029,24 @@ namespace Avalonia.Controls.Primitives int pixelWidth = (int)Math.Round(minDimension); int pixelHeight = (int)Math.Round(minDimension); - ColorSpectrumChannels channels2 = Channels; + ColorSpectrumComponents components2 = Components; WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); - switch (channels2) + switch (components2) { - case ColorSpectrumChannels.HueValue: - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: _saturationMinimumBitmap = minBitmap; _saturationMaximumBitmap = maxBitmap; break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: _valueBitmap = maxBitmap; break; - case ColorSpectrumChannels.ValueSaturation: - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + case ColorSpectrumComponents.SaturationValue: _hueRedBitmap = minBitmap; _hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); _hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); @@ -1058,7 +1057,7 @@ namespace Avalonia.Controls.Primitives } _shapeFromLastBitmapCreation = Shape; - _componentsFromLastBitmapCreation = Channels; + _componentsFromLastBitmapCreation = Components; _imageWidthFromLastBitmapCreation = minDimension; _imageHeightFromLastBitmapCreation = minDimension; _minHueFromLastBitmapCreation = MinHue; @@ -1080,7 +1079,7 @@ namespace Avalonia.Controls.Primitives double y, Hsv baseHsv, double minDimension, - ColorSpectrumChannels channels, + ColorSpectrumComponents components, double minHue, double maxHue, double minSaturation, @@ -1112,30 +1111,30 @@ namespace Avalonia.Controls.Primitives double xPercent = (minDimension - 1 - x) / (minDimension - 1); double yPercent = (minDimension - 1 - y) / (minDimension - 1); - switch (channels) + switch (components) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); hsvMin.H = 0; @@ -1146,14 +1145,14 @@ namespace Avalonia.Controls.Primitives hsvMax.H = 300; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); hsvMin.H = 0; @@ -1171,8 +1170,8 @@ namespace Avalonia.Controls.Primitives // so we'll invert the number before assigning the HSL value to the array. // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue // in the case of the ring configuration. - if (channels == ColorSpectrumChannels.HueSaturation || - channels == ColorSpectrumChannels.SaturationHue) + if (components == ColorSpectrumComponents.HueSaturation || + components == ColorSpectrumComponents.SaturationHue) { hsvMin.S = sMax - hsvMin.S + sMin; hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; @@ -1200,8 +1199,8 @@ namespace Avalonia.Controls.Primitives bgraMinPixelData.Add(255); // a - ignored // We'll only save pixel data for the middle bitmaps if our third dimension is hue. - if (channels == ColorSpectrumChannels.ValueSaturation || - channels == ColorSpectrumChannels.SaturationValue) + if (components == ColorSpectrumComponents.ValueSaturation || + components == ColorSpectrumComponents.SaturationValue) { Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255.0)); // b @@ -1240,7 +1239,7 @@ namespace Avalonia.Controls.Primitives double y, double radius, Hsv baseHsv, - ColorSpectrumChannels channels, + ColorSpectrumComponents components, double minHue, double maxHue, double minSaturation, @@ -1298,30 +1297,30 @@ namespace Avalonia.Controls.Primitives double thetaPercent = theta / 360; - switch (channels) + switch (components) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); hsvMin.H = 0; @@ -1332,14 +1331,14 @@ namespace Avalonia.Controls.Primitives hsvMax.H = 300; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); hsvMin.H = 0; @@ -1357,8 +1356,8 @@ namespace Avalonia.Controls.Primitives // so we'll invert the number before assigning the HSL value to the array. // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue // in the case of the ring configuration. - if (channels == ColorSpectrumChannels.HueSaturation || - channels == ColorSpectrumChannels.SaturationHue) + if (components == ColorSpectrumComponents.HueSaturation || + components == ColorSpectrumComponents.SaturationHue) { hsvMin.S = sMax - hsvMin.S + sMin; hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; @@ -1386,8 +1385,8 @@ namespace Avalonia.Controls.Primitives bgraMinPixelData.Add(255); // a // We'll only save pixel data for the middle bitmaps if our third dimension is hue. - if (channels == ColorSpectrumChannels.ValueSaturation || - channels == ColorSpectrumChannels.SaturationValue) + if (components == ColorSpectrumComponents.ValueSaturation || + components == ColorSpectrumComponents.SaturationValue) { Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255)); // b @@ -1432,7 +1431,7 @@ namespace Avalonia.Controls.Primitives } HsvColor hsvColor = HsvColor; - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // We'll set the base image and the overlay image based on which component is our third dimension. // If it's saturation or luminosity, then the base image is that dimension at its minimum value, @@ -1440,10 +1439,10 @@ namespace Avalonia.Controls.Primitives // If it's hue, then we'll figure out where in the color wheel we are, and then use the two // colors on either side of our position as our base image and overlay image. // For example, if our hue is orange, then the base image would be red and the overlay image yellow. - switch (channels) + switch (components) { - case ColorSpectrumChannels.HueValue: - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: { if (_saturationMinimumBitmap == null || _saturationMaximumBitmap == null) @@ -1463,8 +1462,8 @@ namespace Avalonia.Controls.Primitives } break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: { if (_valueBitmap == null) { @@ -1483,8 +1482,8 @@ namespace Avalonia.Controls.Primitives } break; - case ColorSpectrumChannels.ValueSaturation: - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + case ColorSpectrumComponents.SaturationValue: { if (_hueRedBitmap == null || _hueYellowBitmap == null || @@ -1554,13 +1553,13 @@ namespace Avalonia.Controls.Primitives // To find how much something contrasts with white, we use the equation // for relative luminance. // - // If the third channel is value, then we won't be updating the spectrum's displayed colors, + // If the third component is value, then we won't be updating the spectrum's displayed colors, // so in that case we should use a value of 1 when considering the backdrop // for the selection ellipse. Color displayedColor; - if (Channels == ColorSpectrumChannels.HueSaturation || - Channels == ColorSpectrumChannels.SaturationHue) + if (Components == ColorSpectrumComponents.HueSaturation || + Components == ColorSpectrumComponents.SaturationHue) { HsvColor hsvColor = HsvColor; Rgb color = (new Hsv(hsvColor.H, hsvColor.S, 1.0)).ToRgb(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs index d44e6b3a9f..8a425b9581 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs @@ -8,39 +8,39 @@ using Avalonia.Media; namespace Avalonia.Controls.Primitives { /// - /// Contains and allows modification of Hue, Saturation and Value channel values. + /// Contains and allows modification of Hue, Saturation and Value components. /// /// /// The is a specialized struct optimized for permanence and memory: /// /// This is not a read-only struct like and allows editing the fields /// Removes the alpha component unnecessary in core calculations - /// No channel value bounds checks or clamping is done. + /// No component bounds checks or clamping is done. /// /// internal struct Hsv { /// - /// The Hue channel value in the range from 0..359. + /// The Hue component in the range from 0..359. /// public double H; /// - /// The Saturation channel value in the range from 0..1. + /// The Saturation component in the range from 0..1. /// public double S; /// - /// The Value channel value in the range from 0..1. + /// The Value component in the range from 0..1. /// public double V; /// /// Initializes a new instance of the struct. /// - /// The Hue channel value in the range from 0..360. - /// The Saturation channel value in the range from 0..1. - /// The Value channel value in the range from 0..1. + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Value component in the range from 0..1. public Hsv(double h, double s, double v) { H = h; @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Primitives /// /// Converts this struct into a standard . /// - /// The Alpha channel value in the range from 0..1. + /// The Alpha component in the range from 0..1. /// A new representing this struct. public HsvColor ToHsvColor(double alpha = 1.0) { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs index fd749cd7dc..12aca593d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs @@ -6,7 +6,7 @@ namespace Avalonia.Controls.Primitives { /// - /// Defines a relative amount that a color channel should be incremented. + /// Defines a relative amount that a color component should be incremented. /// internal enum IncrementAmount { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs index 8002a44fc9..df9c1e3350 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs @@ -6,7 +6,7 @@ namespace Avalonia.Controls.Primitives { /// - /// Defines the direction a color channel should be incremented. + /// Defines the direction a color component should be incremented. /// internal enum IncrementDirection { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs index 616ab9e9ac..72e3821c2b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs @@ -9,40 +9,40 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { /// - /// Contains and allows modification of Red, Green and Blue channel values. + /// Contains and allows modification of Red, Green and Blue components. /// /// /// The is a specialized struct optimized for permanence and memory: /// /// This is not a read-only struct like and allows editing the fields /// Removes the alpha component unnecessary in core calculations - /// Normalizes RGB channel values in the range of 0..1 to simplify calculations. - /// No channel value bounds checks or clamping is done. + /// Normalizes RGB components in the range of 0..1 to simplify calculations. + /// No component bounds checks or clamping is done. /// /// internal struct Rgb { /// - /// The Red channel value in the range from 0..1. + /// The Red component in the range from 0..1. /// public double R; /// - /// The Green channel value in the range from 0..1. + /// The Green component in the range from 0..1. /// public double G; /// - /// The Blue channel value in the range from 0..1. + /// The Blue component in the range from 0..1. /// public double B; /// /// Initializes a new instance of the struct. /// - /// The Red channel value in the range from 0..1. - /// The Green channel value in the range from 0..1. - /// The Blue channel value in the range from 0..1. + /// The Red component in the range from 0..1. + /// The Green component in the range from 0..1. + /// The Blue component in the range from 0..1. public Rgb(double r, double g, double b) { R = r; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives /// /// Converts this struct into a standard . /// - /// The Alpha channel value in the range from 0..1. + /// The Alpha component in the range from 0..1. /// A new representing this struct. public Color ToColor(double alpha = 1.0) { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumChannels.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs similarity index 81% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumChannels.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs index a31586d175..164089096e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrumChannels.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs @@ -8,16 +8,16 @@ using Avalonia.Controls.Primitives; namespace Avalonia.Controls { /// - /// Defines the two HSV color channels displayed by a . + /// Defines the two HSV color components displayed by a . /// /// - /// Order of the color channels is important and correspond with an X/Y axis in Box + /// Order of the color components is important and correspond with an X/Y axis in Box /// shape or a degree/radius in Ring shape. /// - public enum ColorSpectrumChannels + public enum ColorSpectrumComponents { /// - /// The Hue and Value channels. + /// The Hue and Value components. /// /// /// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis. @@ -26,7 +26,7 @@ namespace Avalonia.Controls HueValue, /// - /// The Value and Hue channels. + /// The Value and Hue components. /// /// /// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis. @@ -35,7 +35,7 @@ namespace Avalonia.Controls ValueHue, /// - /// The Hue and Saturation channels. + /// The Hue and Saturation components. /// /// /// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis. @@ -44,7 +44,7 @@ namespace Avalonia.Controls HueSaturation, /// - /// The Saturation and Hue channels. + /// The Saturation and Hue components. /// /// /// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis. @@ -53,7 +53,7 @@ namespace Avalonia.Controls SaturationHue, /// - /// The Saturation and Value channels. + /// The Saturation and Value components. /// /// /// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis. @@ -62,7 +62,7 @@ namespace Avalonia.Controls SaturationValue, /// - /// The Value and Saturation channels. + /// The Value and Saturation components. /// /// /// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis. diff --git a/src/Avalonia.Controls.ColorPicker/HsvChannel.cs b/src/Avalonia.Controls.ColorPicker/HsvChannel.cs deleted file mode 100644 index 18f96fe70b..0000000000 --- a/src/Avalonia.Controls.ColorPicker/HsvChannel.cs +++ /dev/null @@ -1,33 +0,0 @@ -// This source file is adapted from the WinUI project. -// (https://github.com/microsoft/microsoft-ui-xaml) -// -// Licensed to The Avalonia Project under the MIT License. - -namespace Avalonia.Controls -{ - /// - /// Defines a specific HSV color model channel. - /// - public enum HsvChannel - { - /// - /// The Hue channel. - /// - Hue, - - /// - /// The Saturation channel. - /// - Saturation, - - /// - /// The Value channel. - /// - Value, - - /// - /// The Alpha channel. - /// - Alpha - }; -} diff --git a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs new file mode 100644 index 0000000000..1132bd7bbb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs @@ -0,0 +1,47 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Defines a specific component in the HSV color model. + /// + public enum HsvComponent + { + /// + /// The Hue component. + /// + /// + /// Also see: + /// + Hue, + + /// + /// The Saturation component. + /// + /// + /// Also see: + /// + Saturation, + + /// + /// The Value component. + /// + /// + /// Also see: + /// + Value, + + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha + }; +} From a39de875aa00ab18fdd2db74121b7071c9ee2a8b Mon Sep 17 00:00:00 2001 From: AndrejBunjac Date: Mon, 18 Apr 2022 17:34:31 +0200 Subject: [PATCH 085/213] Added invalidation to LayoutThickness property in Border and ContentPresenter and implemented minor review fixes. --- src/Avalonia.Controls/Border.cs | 38 +++++++++++++++---- .../Presenters/ContentPresenter.cs | 29 ++++++++++---- src/Avalonia.Layout/Layoutable.cs | 2 +- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index ce64570dc8..53de95ac41 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,8 +1,10 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Shapes; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -88,6 +90,18 @@ namespace Avalonia.Controls AffectsMeasure(BorderThicknessProperty); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + switch (change.Property.Name) + { + case nameof(UseLayoutRounding): + case nameof(BorderThickness): + _layoutThickness = null; + break; + } + } + /// /// Gets or sets a brush with which to paint the background. /// @@ -169,29 +183,39 @@ namespace Avalonia.Controls set => SetValue(BoxShadowProperty, value); } - private Thickness _layoutThickness = default; + private Thickness? _layoutThickness; + private double _scale; private Thickness LayoutThickness { get { - if (_layoutThickness == default) + VerifyScale(); + + if (_layoutThickness == null) { var borderThickness = BorderThickness; if (UseLayoutRounding) - { - var scale = LayoutHelper.GetLayoutScale(this); - borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); - } + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale); _layoutThickness = borderThickness; } - return _layoutThickness; + return _layoutThickness.Value; } } + private void VerifyScale() + { + var currentScale = LayoutHelper.GetLayoutScale(this); + if (MathUtilities.AreClose(currentScale, _scale)) + return; + + _scale = currentScale; + _layoutThickness = null; + } + /// /// Renders the control. /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index bbb772a4ce..ae08b4a452 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Utilities; namespace Avalonia.Controls.Presenters { @@ -249,6 +250,10 @@ namespace Avalonia.Controls.Presenters case nameof(TemplatedParent): TemplatedParentChanged(change); break; + case nameof(UseLayoutRounding): + case nameof(BorderThickness): + _layoutThickness = null; + break; } } @@ -329,29 +334,39 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } - private Thickness _layoutThickness = default; + private Thickness? _layoutThickness; + private double _scale; private Thickness LayoutThickness { get { - if (_layoutThickness == default) + VerifyScale(); + + if (_layoutThickness == null) { var borderThickness = BorderThickness; if (UseLayoutRounding) - { - var scale = LayoutHelper.GetLayoutScale(this); - borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); - } + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale); _layoutThickness = borderThickness; } - return _layoutThickness; + return _layoutThickness.Value; } } + private void VerifyScale() + { + var currentScale = LayoutHelper.GetLayoutScale(this); + if (MathUtilities.AreClose(currentScale, _scale)) + return; + + _scale = currentScale; + _layoutThickness = null; + } + /// public override void Render(DrawingContext context) { diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 23a76f6ee2..df7aa937a0 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -653,7 +653,7 @@ namespace Avalonia.Layout // Margin has to be treated separately because the layout rounding function is not linear // f(a + b) != f(a) + f(b) // If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales. - if (UseLayoutRounding) + if (useLayoutRounding) { margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale); } From 2902e3d24a9736484da225ec7ec15fa24b523437 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 19 Apr 2022 17:02:20 +0200 Subject: [PATCH 086/213] Rework Inlines invalidation --- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 5 ++ .../Documents/IInlineHost.cs | 11 +++++ src/Avalonia.Controls/Documents/Inline.cs | 12 ++--- .../Documents/InlineCollection.cs | 48 ++++++++++++------- .../Documents/InlineUIContainer.cs | 12 +++-- src/Avalonia.Controls/Documents/LineBreak.cs | 2 +- src/Avalonia.Controls/Documents/Run.cs | 5 +- src/Avalonia.Controls/Documents/Span.cs | 9 ++-- .../Documents/TextElement.cs | 15 ++---- src/Avalonia.Controls/TextBlock.cs | 19 ++++---- 10 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 src/Avalonia.Controls/Documents/IInlineHost.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index d18a4b2a87..2511807d9c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -238,6 +238,11 @@ namespace Avalonia.Media.TextFormatting.Unicode _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); + if (types.IsEmpty) + { + return; + } + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); diff --git a/src/Avalonia.Controls/Documents/IInlineHost.cs b/src/Avalonia.Controls/Documents/IInlineHost.cs new file mode 100644 index 0000000000..da72c207be --- /dev/null +++ b/src/Avalonia.Controls/Documents/IInlineHost.cs @@ -0,0 +1,11 @@ +using Avalonia.LogicalTree; + +namespace Avalonia.Controls.Documents +{ + internal interface IInlineHost : ILogical + { + void AddVisualChild(IControl child); + + void Invalidate(); + } +} diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 445a48ecf4..a657d754b3 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; -namespace Avalonia.Controls.Documents +namespace Avalonia.Controls.Documents { /// /// Inline element. @@ -45,7 +44,7 @@ namespace Avalonia.Controls.Documents set { SetValue(BaselineAlignmentProperty, value); } } - internal abstract void BuildTextRun(IList textRuns, IInlinesHost parent); + internal abstract void BuildTextRun(IList textRuns); internal abstract void AppendText(StringBuilder stringBuilder); @@ -63,14 +62,9 @@ namespace Avalonia.Controls.Documents { case nameof(TextDecorations): case nameof(BaselineAlignment): - Invalidate(); + InlineHost?.Invalidate(); break; } } } - - public interface IInlinesHost : ILogical - { - void AddVisualChild(IControl child); - } } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index abe8f2cd4d..a76222385e 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -12,29 +12,37 @@ namespace Avalonia.Controls.Documents [WhitespaceSignificantCollection] public class InlineCollection : AvaloniaList { + private readonly IInlineHost? _host; private string? _text = string.Empty; /// /// Initializes a new instance of the class. /// - public InlineCollection(ILogical parent) : base(0) + public InlineCollection(ILogical parent) : this(parent, null) { } + + /// + /// Initializes a new instance of the class. + /// + internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0) { + _host = host; + ResetBehavior = ResetBehavior.Remove; this.ForEachItem( x => { ((ISetLogicalParent)x).SetParent(parent); - x.Invalidated += Invalidate; - Invalidate(); + x.InlineHost = host; + host?.Invalidate(); }, x => { ((ISetLogicalParent)x).SetParent(null); - x.Invalidated -= Invalidate; - Invalidate(); + x.InlineHost = host; + host?.Invalidate(); }, - () => throw new NotSupportedException()); + () => throw new NotSupportedException()); } public bool HasComplexContent => Count > 0; @@ -98,22 +106,20 @@ namespace Avalonia.Controls.Documents public void Add(IControl child) { - if (!HasComplexContent && !string.IsNullOrEmpty(_text)) - { - base.Add(new Run(_text)); - - _text = string.Empty; - } + var implicitRun = new InlineUIContainer(child); - base.Add(new InlineUIContainer(child)); + Add(implicitRun); } public override void Add(Inline item) { if (!HasComplexContent) { - base.Add(new Run(_text)); - + if (!string.IsNullOrEmpty(_text)) + { + base.Add(new Run(_text)); + } + _text = string.Empty; } @@ -124,11 +130,19 @@ namespace Avalonia.Controls.Documents /// Raised when an inline in the collection changes. /// public event EventHandler? Invalidated; - + /// /// Raises the event. /// - protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); + protected void Invalidate() + { + if(_host != null) + { + _host.Invalidate(); + } + + Invalidated?.Invoke(this, EventArgs.Empty); + } private void Invalidate(object? sender, EventArgs e) => Invalidate(); } diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index 47851903dd..eb12092bb8 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -58,11 +57,16 @@ namespace Avalonia.Controls.Documents set => SetValue(ChildProperty, value); } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { - ((ISetLogicalParent)Child).SetParent(parent); + if(InlineHost == null) + { + return; + } + + ((ISetLogicalParent)Child).SetParent(InlineHost); - parent.AddVisualChild(Child); + InlineHost.AddVisualChild(Child); textRuns.Add(new InlineRun(Child, CreateTextRunProperties())); } diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index 00fad491d3..aeb81f7313 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.Documents { } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { textRuns.Add(new TextEndOfLine()); } diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 884718c28b..2c6482b586 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Data; -using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -51,7 +50,7 @@ namespace Avalonia.Controls.Documents set { SetValue (TextProperty, value); } } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { var text = (Text ?? "").AsMemory(); @@ -76,7 +75,7 @@ namespace Avalonia.Controls.Documents switch (change.Property.Name) { case nameof(Text): - Invalidate(); + InlineHost?.Invalidate(); break; } } diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index 32e19d4153..bd1b4fc5e1 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -27,8 +25,7 @@ namespace Avalonia.Controls.Documents public Span() { Inlines = new InlineCollection(this); - - Inlines.Invalidated += (s, e) => Invalidate(); + Inlines.Invalidated += (s, e) => InlineHost?.Invalidate(); } /// @@ -37,13 +34,13 @@ namespace Avalonia.Controls.Documents [Content] public InlineCollection Inlines { get; } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { if (Inlines.HasComplexContent) { foreach (var inline in Inlines) { - inline.BuildTextRun(textRuns, parent); + inline.BuildTextRun(textRuns); } } else diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index d8e13554b5..faf869cce6 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Media; +using Avalonia.Media; namespace Avalonia.Controls.Documents { @@ -251,10 +250,7 @@ namespace Avalonia.Controls.Documents control.SetValue(ForegroundProperty, value); } - /// - /// Raised when the visual representation of the text element changes. - /// - public event EventHandler? Invalidated; + internal IInlineHost? InlineHost { get; set; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { @@ -269,14 +265,9 @@ namespace Avalonia.Controls.Documents case nameof(FontWeight): case nameof(FontStretch): case nameof(Foreground): - Invalidate(); + InlineHost?.Invalidate(); break; } } - - /// - /// Raises the event. - /// - protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c698f0ff3b..3bcb74eee6 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// /// A control that displays a block of text. /// - public class TextBlock : Control, IInlinesHost + public class TextBlock : Control, IInlineHost { /// /// Defines the property. @@ -155,9 +155,7 @@ namespace Avalonia.Controls /// public TextBlock() { - Inlines = new InlineCollection(this); - - Inlines.Invalidated += InlinesChanged; + Inlines = new InlineCollection(this, this); } /// @@ -211,7 +209,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the inlines. + /// Gets the inlines. /// [Content] public InlineCollection Inlines { get; } @@ -569,7 +567,7 @@ namespace Avalonia.Controls foreach (var inline in Inlines) { - inline.BuildTextRun(textRuns, this); + inline.BuildTextRun(textRuns); } textSource = new InlinesTextSource(textRuns); @@ -667,8 +665,6 @@ namespace Avalonia.Controls case nameof (Padding): case nameof (LineHeight): case nameof (MaxLines): - - case nameof (InlinesProperty): case nameof (Text): case nameof (TextDecorations): @@ -685,7 +681,7 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - void IInlinesHost.AddVisualChild(IControl child) + void IInlineHost.AddVisualChild(IControl child) { if (child.VisualParent == null) { @@ -693,6 +689,11 @@ namespace Avalonia.Controls } } + void IInlineHost.Invalidate() + { + InvalidateTextLayout(); + } + private readonly struct InlinesTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From 628ae788e4ce422e4576b0c6a2d49bed0ef9977d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 19 Apr 2022 19:22:58 +0200 Subject: [PATCH 087/213] Fix PointToClient not working on macOS. --- native/Avalonia.Native/src/OSX/window.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d16c466fe6..4426e7fdff 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -457,7 +457,7 @@ public: } point = ConvertPointY(point); - NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; From 5bacd9144383ac6c43226b974f9bbeea16c6b51d Mon Sep 17 00:00:00 2001 From: Kevin Ivarsen Date: Tue, 19 Apr 2022 22:16:38 -0700 Subject: [PATCH 088/213] Fix #6603: PInvokeStackImbalance error caused by incorrect signature for WindowsDeleteString function. When PreserveSig=false, a function that natively returns an HRESULT and has no final [out] parameter should instead be marked as void. --- src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs index 5026fbaaba..89cde01ff4 100644 --- a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs +++ b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs @@ -23,7 +23,7 @@ namespace Avalonia.Win32.WinRT [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] - internal static extern unsafe IntPtr WindowsDeleteString(IntPtr hString); + internal static extern unsafe void WindowsDeleteString(IntPtr hString); [DllImport("Windows.UI.Composition", EntryPoint = "DllGetActivationFactory", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] From 050ac5fbba104fa7606cc5c3d3618f837f158731 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 20 Apr 2022 13:53:38 +0200 Subject: [PATCH 089/213] Fix line metrics for empty lines that are processed by TextWrapping --- .../Media/TextFormatting/TextFormatterImpl.cs | 26 +++++++++++++++-- .../Media/TextFormatting/TextLayout.cs | 29 ++----------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7241c62472..be07745d89 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -8,6 +8,8 @@ namespace Avalonia.Media.TextFormatting { internal class TextFormatterImpl : TextFormatter { + private static readonly char[] s_empty = { ' ' }; + /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) @@ -524,6 +526,27 @@ namespace Avalonia.Media.TextFormatting return measuredLength != 0; } + /// + /// Creates an empty text line. + /// + /// The empty text line. + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties) + { + var flowDirection = paragraphProperties.FlowDirection; + var properties = paragraphProperties.DefaultTextRunProperties; + var glyphTypeface = properties.Typeface.GlyphTypeface; + var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1); + var glyph = glyphTypeface.GetGlyph(s_empty[0]); + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + + var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, + (sbyte)flowDirection); + + var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; + + return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); + } + /// /// Performs text wrapping returns a list of text lines. /// @@ -540,8 +563,7 @@ namespace Avalonia.Media.TextFormatting { if(textRuns.Count == 0) { - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); - + return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index c6692b6203..0df608cb34 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -10,8 +10,6 @@ namespace Avalonia.Media.TextFormatting /// public class TextLayout { - private static readonly char[] s_empty = { ' ' }; - private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; @@ -408,32 +406,11 @@ namespace Avalonia.Media.TextFormatting height += textLine.Height; } - /// - /// Creates an empty text line. - /// - /// The empty text line. - private TextLine CreateEmptyTextLine(int firstTextSourceIndex) - { - var flowDirection = _paragraphProperties.FlowDirection; - var properties = _paragraphProperties.DefaultTextRunProperties; - var glyphTypeface = properties.Typeface.GlyphTypeface; - var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1); - var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; - - var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, - (sbyte)flowDirection); - - var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; - - return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine(); - } - private IReadOnlyList CreateTextLines() { if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = CreateEmptyTextLine(0); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); Bounds = new Rect(0,0,0, textLine.Height); @@ -457,7 +434,7 @@ namespace Avalonia.Media.TextFormatting { if(previousLine != null && previousLine.NewLineLength > 0) { - var emptyTextLine = CreateEmptyTextLine(_textSourceLength); + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties); textLines.Add(emptyTextLine); @@ -506,7 +483,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if(textLines.Count == 0) { - var textLine = CreateEmptyTextLine(0); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); textLines.Add(textLine); From f9dbbb3da1d42b6450ac10b6485d118fcc5fa0aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Apr 2022 16:35:53 +0200 Subject: [PATCH 090/213] Make UpdateDataValidation non-generic. --- src/Avalonia.Base/AvaloniaObject.cs | 12 ++++---- src/Avalonia.Controls/AutoCompleteBox.cs | 10 +++++-- src/Avalonia.Controls/Button.cs | 9 ++++-- .../Calendar/CalendarDatePicker.cs | 4 +-- src/Avalonia.Controls/MenuItem.cs | 9 ++++-- .../NumericUpDown/NumericUpDown.cs | 10 +++++-- .../Primitives/SelectingItemsControl.cs | 10 +++++-- src/Avalonia.Controls/Slider.cs | 7 +++-- src/Avalonia.Controls/TextBox.cs | 7 +++-- .../AvaloniaObjectTests_DataValidation.cs | 28 +++++++++---------- 10 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index bc1e95805f..1f14ddede4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -646,10 +646,12 @@ namespace Avalonia /// enabled. /// /// The property. - /// The new binding value for the property. - protected virtual void UpdateDataValidation( - AvaloniaProperty property, - BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected virtual void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { } @@ -860,7 +862,7 @@ namespace Avalonia if (metadata.EnableDataValidation == true) { - UpdateDataValidation(property, value); + UpdateDataValidation(property, value.Type, value.Error); } } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 3316c06bf5..5c95932c1f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1346,12 +1346,16 @@ namespace Avalonia.Controls /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty || property == SelectedItemProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index a4a147e0f3..0ef1ba4c8c 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -498,12 +498,15 @@ namespace Avalonia.Controls protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this); /// - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { - base.UpdateDataValidation(property, value); + base.UpdateDataValidation(property, state, error); if (property == CommandProperty) { - if (value.Type == BindingValueType.BindingError) + if (state == BindingValueType.BindingError) { if (_commandCanExecute) { diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index 0ac2056ed1..0409eb30aa 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -540,11 +540,11 @@ namespace Avalonia.Controls } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) { if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 955af8888b..619eafb71b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -501,12 +501,15 @@ namespace Avalonia.Controls return new MenuItemAutomationPeer(this); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { - base.UpdateDataValidation(property, value); + base.UpdateDataValidation(property, state, error); if (property == CommandProperty) { - _commandBindingError = value.Type == BindingValueType.BindingError; + _commandBindingError = state == BindingValueType.BindingError; if (_commandBindingError && _commandCanExecute) { _commandCanExecute = false; diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index fbbaab6182..4d86a0f17c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -403,12 +403,16 @@ namespace Avalonia.Controls /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty || property == ValueProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 6f2554bef3..bff6799792 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -501,12 +501,16 @@ namespace Avalonia.Controls.Primitives /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == SelectedItemProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index f0a0fba1af..64dfce22d4 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -361,11 +361,14 @@ namespace Avalonia.Controls Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue; } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == ValueProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1f3dbc87db..0be58e7fcc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1262,11 +1262,14 @@ namespace Avalonia.Controls return new TextBoxAutomationPeer(this); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index 65f03b3eca..d48e58136a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -52,14 +52,14 @@ namespace Avalonia.Base.UnitTests source.OnNext(BindingValue.DataValidationError(new Exception())); source.OnNext(7); - var result = target.Notifications.Cast>().ToList(); + var result = target.Notifications; Assert.Equal(4, result.Count); - Assert.Equal(BindingValueType.Value, result[0].Type); - Assert.Equal(6, result[0].Value); - Assert.Equal(BindingValueType.BindingError, result[1].Type); - Assert.Equal(BindingValueType.DataValidationError, result[2].Type); - Assert.Equal(BindingValueType.Value, result[3].Type); - Assert.Equal(7, result[3].Value); + Assert.Equal(BindingValueType.Value, result[0].type); + Assert.Equal(6, result[0].value); + Assert.Equal(BindingValueType.BindingError, result[1].type); + Assert.Equal(BindingValueType.DataValidationError, result[2].type); + Assert.Equal(BindingValueType.Value, result[3].type); + Assert.Equal(7, result[3].value); } [Fact] @@ -72,8 +72,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.NonValidatedDirectProperty, source); source.OnNext(1); - var result = target.Notifications.Cast>().ToList(); - Assert.Equal(1, result.Count); + Assert.Equal(1, target.Notifications.Count); } [Fact] @@ -154,13 +153,14 @@ namespace Avalonia.Base.UnitTests set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } - public IList Notifications { get; } = new List(); + public List<(BindingValueType type, object value)> Notifications { get; } = new(); - protected override void UpdateDataValidation( - AvaloniaProperty property, - BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception error) { - Notifications.Add(value); + Notifications.Add((state, GetValue(property))); } } From 7f469752d55097d4dcf014e23fd2f0798f199e68 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Apr 2022 16:43:20 +0200 Subject: [PATCH 091/213] Remove generic methods from IInteractive. --- .../Interactivity/IInteractive.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/Avalonia.Base/Interactivity/IInteractive.cs b/src/Avalonia.Base/Interactivity/IInteractive.cs index afda29e329..6d7dcd64f4 100644 --- a/src/Avalonia.Base/Interactivity/IInteractive.cs +++ b/src/Avalonia.Base/Interactivity/IInteractive.cs @@ -28,21 +28,6 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false); - /// - /// Adds a handler for the specified routed event. - /// - /// The type of the event's args. - /// The routed event. - /// The handler. - /// The routing strategies to listen to. - /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - void AddHandler( - RoutedEvent routedEvent, - EventHandler handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false) where TEventArgs : RoutedEventArgs; - /// /// Removes a handler for the specified routed event. /// @@ -50,15 +35,6 @@ namespace Avalonia.Interactivity /// The handler. void RemoveHandler(RoutedEvent routedEvent, Delegate handler); - /// - /// Removes a handler for the specified routed event. - /// - /// The type of the event's args. - /// The routed event. - /// The handler. - void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) - where TEventArgs : RoutedEventArgs; - /// /// Adds the object's handlers for a routed event to an event route. /// From 4ec36c97e2810c8e1bb48f85339d76e96e502b78 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Apr 2022 16:47:54 +0200 Subject: [PATCH 092/213] Remove generic methods from IDispatcher. --- src/Avalonia.Base/Threading/IDispatcher.cs | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index eccd42bd4e..713a7ac4d7 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -26,15 +26,6 @@ namespace Avalonia.Threading /// The priority with which to invoke the method. void Post(Action action, DispatcherPriority priority = default); - /// - /// Posts an action that will be invoked on the dispatcher thread. - /// - /// type of argument - /// The method to call. - /// The argument of method to call. - /// The priority with which to invoke the method. - void Post(Action action, T arg, DispatcherPriority priority = default); - /// /// Invokes a action on the dispatcher thread. /// @@ -43,14 +34,6 @@ namespace Avalonia.Threading /// A task that can be used to track the method's execution. Task InvokeAsync(Action action, DispatcherPriority priority = default); - /// - /// Invokes a method on the dispatcher thread. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - Task InvokeAsync(Func function, DispatcherPriority priority = default); - /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the /// task returned by . @@ -59,14 +42,5 @@ namespace Avalonia.Threading /// The priority with which to invoke the method. /// A task that represents a proxy for the task returned by . Task InvokeAsync(Func function, DispatcherPriority priority = default); - - /// - /// Queues the specified work to run on the dispatcher thread and returns a proxy for the - /// task returned by . - /// - /// The work to execute asynchronously. - /// The priority with which to invoke the method. - /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func> function, DispatcherPriority priority = default); } } From fa44075d26873d80b6fba5df8492d774fadec589 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Apr 2022 17:19:02 +0200 Subject: [PATCH 093/213] Make utility methods non-virtual. Not sure why these method were virtual anyway: any customization should be done by overriding the non-generic `CreateItemContainerGenerator` method. --- src/Avalonia.Controls/TreeView.cs | 2 +- src/Avalonia.Controls/TreeViewItem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 1d806913dd..b2a188a2ea 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -401,7 +401,7 @@ namespace Avalonia.Controls protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() => CreateTreeItemContainerGenerator(); - protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() + protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() { return new TreeItemContainerGenerator( this, diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index a0a3c09942..490b0b3ce3 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); /// - protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() + protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() { return new TreeItemContainerGenerator( From 6df672e4c078bae8dd0259825032af68824f3f08 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Apr 2022 22:30:45 +0200 Subject: [PATCH 094/213] Remove IAvaloniaPropertyVisitor. It was only used internally in creating `ISetterInstance`s so add a virtual method to `AvaloniaProperty` to do this explicitly without generic virtual methods. --- src/Avalonia.Base/AvaloniaProperty.cs | 11 +--- src/Avalonia.Base/DirectPropertyBase.cs | 32 +++++++-- src/Avalonia.Base/StyledPropertyBase.cs | 32 +++++++-- src/Avalonia.Base/Styling/Setter.cs | 65 +------------------ .../Utilities/IAvaloniaPropertyVisitor.cs | 32 --------- .../AvaloniaPropertyTests.cs | 11 ++-- 6 files changed, 62 insertions(+), 121 deletions(-) delete mode 100644 src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 62ca971412..fd43ced196 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -454,15 +455,6 @@ namespace Avalonia return Name; } - /// - /// Uses the visitor pattern to resolve an untyped property to a typed property. - /// - /// The type of user data passed. - /// The visitor which will accept the typed property. - /// The user data to pass. - public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) - where TData : struct; - /// /// Routes an untyped ClearValue call to a typed call. /// @@ -508,6 +500,7 @@ namespace Avalonia BindingPriority priority); internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent); + internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value); /// /// Overrides the metadata for the property on the specified type. diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 6e3baea99b..9c1ffce24c 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -120,12 +121,6 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } - /// - public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) - { - visitor.Visit(this, ref data); - } - /// internal override void RouteClearValue(AvaloniaObject o) { @@ -181,5 +176,30 @@ namespace Avalonia { throw new NotSupportedException("Direct properties do not support inheritance."); } + + internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value) + { + if (value is IBinding binding) + { + return new PropertySetterBindingInstance( + target, + this, + binding); + } + else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) + { + return new PropertySetterLazyInstance( + target, + this, + () => (TValue)template.Build()); + } + else + { + return new PropertySetterInstance( + target, + this, + (TValue)value!); + } + } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index f94723866a..dd5eb703ea 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -158,12 +159,6 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } - /// - public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) - { - visitor.Visit(this, ref data); - } - /// /// Gets the string representation of the property. /// @@ -237,6 +232,31 @@ namespace Avalonia o.InheritanceParentChanged(this, oldParent); } + internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value) + { + if (value is IBinding binding) + { + return new PropertySetterBindingInstance( + target, + this, + binding); + } + else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) + { + return new PropertySetterLazyInstance( + target, + this, + () => (TValue)template.Build()); + } + else + { + return new PropertySetterInstance( + target, + this, + (TValue)value!); + } + } + private object? GetDefaultBoxedValue(Type type) { _ = type ?? throw new ArgumentNullException(nameof(type)); diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index 168a882499..b4b3399022 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -16,7 +16,7 @@ namespace Avalonia.Styling /// A is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor + public class Setter : ISetter, IAnimationSetter { private object? _value; @@ -68,68 +68,7 @@ namespace Avalonia.Styling throw new InvalidOperationException("Setter.Property must be set."); } - var data = new SetterVisitorData - { - target = target, - value = Value, - }; - - Property.Accept(this, ref data); - return data.result!; - } - - void IAvaloniaPropertyVisitor.Visit( - StyledPropertyBase property, - ref SetterVisitorData data) - { - if (data.value is IBinding binding) - { - data.result = new PropertySetterBindingInstance( - data.target, - property, - binding); - } - else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType)) - { - data.result = new PropertySetterLazyInstance( - data.target, - property, - () => (T)template.Build()); - } - else - { - data.result = new PropertySetterInstance( - data.target, - property, - (T)data.value!); - } - } - - void IAvaloniaPropertyVisitor.Visit( - DirectPropertyBase property, - ref SetterVisitorData data) - { - if (data.value is IBinding binding) - { - data.result = new PropertySetterBindingInstance( - data.target, - property, - binding); - } - else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType)) - { - data.result = new PropertySetterLazyInstance( - data.target, - property, - () => (T)template.Build()); - } - else - { - data.result = new PropertySetterInstance( - data.target, - property, - (T)data.value!); - } + return Property.CreateSetterInstance(target, Value); } private struct SetterVisitorData diff --git a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs deleted file mode 100644 index 6a8df91b81..0000000000 --- a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Avalonia.Utilities -{ - /// - /// A visitor to resolve an untyped to a typed property. - /// - /// The type of user data passed. - /// - /// Pass an instance that implements this interface to - /// - /// in order to resolve un untyped to a typed - /// or . - /// - public interface IAvaloniaPropertyVisitor - where TData : struct - { - /// - /// Called when the property is a styled property. - /// - /// The property value type. - /// The property. - /// The user data. - void Visit(StyledPropertyBase property, ref TData data); - - /// - /// Called when the property is a direct property. - /// - /// The property value type. - /// The property. - /// The user data. - void Visit(DirectPropertyBase property, ref TData data); - } -} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index ee29c82345..f3f39b465b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.Styling; using Avalonia.Utilities; using Xunit; @@ -146,11 +147,6 @@ namespace Avalonia.Base.UnitTests OverrideMetadata(typeof(T), metadata); } - public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) - { - throw new NotImplementedException(); - } - internal override IDisposable RouteBind( AvaloniaObject o, IObservable> source, @@ -186,6 +182,11 @@ namespace Avalonia.Base.UnitTests { throw new NotImplementedException(); } + + internal override ISetterInstance CreateSetterInstance(IStyleable target, object value) + { + throw new NotImplementedException(); + } } private class Class1 : AvaloniaObject From c53e5307a02e37a5781429514ef5f0fa935ed5f8 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 20 Apr 2022 23:30:23 +0200 Subject: [PATCH 095/213] Fix WeakHashList losing one item when upgrading storage. --- src/Avalonia.Base/Utilities/WeakHashList.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs index 32668872da..51aa24dd6c 100644 --- a/src/Avalonia.Base/Utilities/WeakHashList.cs +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -104,8 +104,10 @@ internal class WeakHashList where T : class if (existing!.TryGetTarget(out var target)) Add(target); } - _arr = null; + Add(item); + + _arr = null; } public void Remove(T item) From 30531df3273f576a11ed2ca03c07a07d740a5eea Mon Sep 17 00:00:00 2001 From: Kevin Ivarsen Date: Thu, 21 Apr 2022 01:43:29 -0700 Subject: [PATCH 096/213] Disable clipping in DevTools content/padding/margin adorner so that the margin part is visible (fixes #8025) --- src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 2553ae90ce..58807b489e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -36,6 +36,7 @@ namespace Avalonia.Diagnostics.Views new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) } }, }; + AdornerLayer.SetIsClipEnabled(_adorner, false); } protected void AddAdorner(object? sender, PointerEventArgs e) From 066c81b1ac3784b36a151bcf3b90d8cba7f5254c Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 21 Apr 2022 11:35:30 +0200 Subject: [PATCH 097/213] Another fix for WeakHashList. Added basic unit tests. --- src/Avalonia.Base/Utilities/WeakHashList.cs | 13 ++-- .../Utilities/WeakHashListTests.cs | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Utilities/WeakHashListTests.cs diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs index 51aa24dd6c..df480aa062 100644 --- a/src/Avalonia.Base/Utilities/WeakHashList.cs +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -6,6 +6,8 @@ namespace Avalonia.Utilities; internal class WeakHashList where T : class { + public const int DefaultArraySize = 8; + private struct Key { public WeakReference? Weak; @@ -63,7 +65,8 @@ internal class WeakHashList where T : class WeakReference?[]? _arr; int _arrCount; - public bool IsEmpty => _dic == null || _dic.Count == 0; + public bool IsEmpty => _dic is not null ? _dic.Count == 0 : _arrCount == 0; + public bool NeedCompact { get; private set; } public void Add(T item) @@ -79,7 +82,7 @@ internal class WeakHashList where T : class } if (_arr == null) - _arr = new WeakReference[8]; + _arr = new WeakReference[DefaultArraySize]; if (_arrCount < _arr.Length) { @@ -108,6 +111,7 @@ internal class WeakHashList where T : class Add(item); _arr = null; + _arrCount = 0; } public void Remove(T item) @@ -119,7 +123,7 @@ internal class WeakHashList where T : class if (_arr[c]?.TryGetTarget(out var target) == true && target == item) { _arr[c] = null; - Compact(); + ArrCompact(); return; } } @@ -219,7 +223,6 @@ internal class WeakHashList where T : class } if (_dic != null) { - foreach (var kvp in _dic) { if (kvp.Key.Weak?.TryGetTarget(out var target) == true) @@ -235,4 +238,4 @@ internal class WeakHashList where T : class return pooled; } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Base.UnitTests/Utilities/WeakHashListTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/WeakHashListTests.cs new file mode 100644 index 0000000000..9ae82e2be0 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/WeakHashListTests.cs @@ -0,0 +1,66 @@ +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities; + +public class WeakHashListTests +{ + [Fact] + public void Is_Empty_Works() + { + var target = new WeakHashList(); + + Assert.True(target.IsEmpty); + + target.Add("1"); + + Assert.False(target.IsEmpty); + + target.Remove("1"); + + Assert.True(target.IsEmpty); + + // Fill array storage. + var arrMaxSize = WeakHashList.DefaultArraySize; + + for (int i = 0; i < arrMaxSize; i++) + { + target.Add(i.ToString()); + } + + Assert.False(target.IsEmpty); + + // This goes above array storage and upgrades to a dictionary. + target.Add(arrMaxSize.ToString()); + + Assert.False(target.IsEmpty); + + // Remove everything, this should still keep an empty dictionary. + for (int i = 0; i < arrMaxSize + 1; i++) + { + target.Remove(i.ToString()); + } + + Assert.True(target.IsEmpty); + } + + [Fact] + public void Array_Compact_After_Remove_Works() + { + var target = new WeakHashList(); + + // Use all slots in array storage. + var arrMaxSize = WeakHashList.DefaultArraySize; + + for (int i = 0; i < arrMaxSize; i++) + { + target.Add(i.ToString()); + } + + // This should compact the array. + target.Remove("3"); + + // And new value should fill empty space. + target.Add("42"); + } +} From 3e6bc0b48d4ea1e32a7b552b5c3c54c093a6592a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 21 Apr 2022 14:17:26 +0200 Subject: [PATCH 098/213] Some more hit testing fixes --- src/Avalonia.Base/Media/GlyphRun.cs | 44 +++++---- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 74 +++++++------- .../Media/TextFormatting/TextLineTests.cs | 97 +++++++++++++------ 4 files changed, 133 insertions(+), 84 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index ec270d796a..9a2645f03d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -49,7 +49,7 @@ namespace Avalonia.Media IReadOnlyList? glyphClusters = null, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + _glyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; @@ -204,7 +204,7 @@ namespace Avalonia.Media public double GetDistanceFromCharacterHit(CharacterHit characterHit) { var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - + var distance = 0.0; if (IsLeftToRight) @@ -223,7 +223,7 @@ namespace Avalonia.Media } var glyphIndex = FindGlyphIndex(characterIndex); - + if (GlyphClusters != null) { var currentCluster = GlyphClusters[glyphIndex]; @@ -249,7 +249,7 @@ namespace Avalonia.Media { //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - + if (GlyphClusters != null) { if (characterIndex > GlyphClusters[0]) @@ -284,13 +284,13 @@ namespace Avalonia.Media public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) { var characterIndex = 0; - + // Before if (distance <= 0) { isInside = false; - if(GlyphClusters != null) + if (GlyphClusters != null) { characterIndex = GlyphClusters[characterIndex]; } @@ -307,11 +307,11 @@ namespace Avalonia.Media characterIndex = GlyphIndices.Count - 1; - if(GlyphClusters != null) + if (GlyphClusters != null) { characterIndex = GlyphClusters[characterIndex]; } - + var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _); return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); @@ -327,7 +327,7 @@ namespace Avalonia.Media var advance = GetGlyphAdvance(index, out var cluster); characterIndex = cluster; - + if (distance > currentX && distance <= currentX + advance) { break; @@ -345,7 +345,7 @@ namespace Avalonia.Media var advance = GetGlyphAdvance(index, out var cluster); characterIndex = cluster; - + if (currentX - advance < distance) { break; @@ -554,6 +554,16 @@ namespace Avalonia.Media nextCluster = GlyphClusters[currentIndex]; } + if (nextCluster < Characters.Start) + { + nextCluster = Characters.Start; + } + + if (cluster < Characters.Start) + { + cluster = Characters.Start; + } + int trailingLength; if (nextCluster == cluster) @@ -577,7 +587,7 @@ namespace Avalonia.Media private double GetGlyphAdvance(int index, out int cluster) { cluster = GlyphClusters != null ? GlyphClusters[index] : index; - + if (GlyphAdvances != null) { return GlyphAdvances[index]; @@ -603,7 +613,7 @@ namespace Avalonia.Media var widthIncludingTrailingWhitespace = 0d; var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount); - + for (var index = 0; index < GlyphIndices.Count; index++) { var advance = GetGlyphAdvance(index, out _); @@ -615,7 +625,7 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = GlyphIndices.Count - glyphCount; index { new ShapedTextCharacters(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); + return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 30e3728d1f..b480774d1d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting case ShapedTextCharacters shapedRun: { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + break; } default: @@ -426,31 +427,42 @@ namespace Avalonia.Media.TextFormatting if (nextRun != null) { - if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End) + switch (nextRun) { - goto skip; - } + case ShapedTextCharacters when currentRun is ShapedTextCharacters: + { + if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End) + { + goto skip; + } - if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) - { - goto skip; - } + if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) + { + goto skip; + } - if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) - { - goto skip; - } + if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) + { + goto skip; + } - if (currentRun.Text.End < firstTextSourceCharacterIndex) - { - goto skip; - } + if (currentRun.Text.End < firstTextSourceCharacterIndex) + { + goto skip; + } - goto noop; + goto noop; + } + default: + { + goto noop; + } + } skip: { startX += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; } continue; @@ -460,7 +472,6 @@ namespace Avalonia.Media.TextFormatting } } - var endX = startX; var endOffset = 0d; @@ -520,11 +531,13 @@ namespace Avalonia.Media.TextFormatting } default: { - if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length) + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength) { endX += currentRun.Size.Width; } + currentPosition += currentRun.TextSourceLength; + break; } } @@ -538,7 +551,9 @@ namespace Avalonia.Media.TextFormatting if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) { - var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection); + currentRect = currentRect.WithWidth(currentRect.Width + width); + + var textBounds = new TextBounds(currentRect, currentDirection); result[result.Count - 1] = textBounds; } @@ -551,21 +566,9 @@ namespace Avalonia.Media.TextFormatting if (currentDirection == FlowDirection.LeftToRight) { - if (nextRun != null) - { - if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength) - { - break; - } - - currentPosition = nextRun.Text.End; - } - else + if (currentPosition >= firstTextSourceCharacterIndex + textLength) { - if (currentPosition >= firstTextSourceCharacterIndex + textLength) - { - break; - } + break; } } else @@ -575,10 +578,7 @@ namespace Avalonia.Media.TextFormatting break; } - if (currentPosition != currentRun.Text.Start) - { - endX += currentRun.Size.Width - endOffset; - } + endX += currentRun.Size.Width - endOffset; } lastDirection = currentDirection; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e9bc792be3..b58d9051f3 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + [Fact] public void Should_Get_Next_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; - + using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting shapedRun.ShapedBuffer.GlyphClusters.Reverse() : shapedRun.ShapedBuffer.GlyphClusters); } - + var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); foreach (var cluster in clusters) @@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public void Should_Get_Previous_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; - + using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } clusters.Reverse(); - + var nextCharacterHit = new CharacterHit(text.Length - 1); foreach (var cluster in clusters) { var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; - + Assert.Equal(cluster, currentCaretIndex); nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); @@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); } } - + [InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("01234567🎉\n")] [InlineData("𐐷1234")] @@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); + Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); } } @@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026"); yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026"); yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, ""); - + object[] CreateData(string text, double width, TextTrimming mode, string expected) { return new object[] @@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); - + var formatter = new TextFormatterImpl(); var textLine = @@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(4, textLine.TextRuns.Count); - var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1)); + var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); Assert.Equal(3, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); @@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting switch (textSourceIndex) { case 0: - return new CustomDrawableRun(); + return new CustomDrawableRun(); case 1: return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); case 2: - return new CustomDrawableRun(); + return new CustomDrawableRun(); case 3: return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); default: @@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + private class CustomDrawableRun : DrawableTextRun { public override Size Size => new(14, 14); public override double Baseline => 14; public override void Draw(DrawingContext drawingContext, Point origin) { - + } } @@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedTextRuns = textLine.TextRuns.Cast().ToList(); var lastCluster = -1; - + foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; var currentClusters = shapedBuffer.GlyphClusters.ToList(); - foreach (var currentCluster in currentClusters) + foreach (var currentCluster in currentClusters) { if (lastCluster == currentCluster) { continue; } - + glyphClusters.Add(currentCluster); lastCluster = currentCluster; } } - + return glyphClusters; } - + private static List BuildRects(TextLine textLine) { var rects = new List(); @@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; - + for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) { var currentCluster = shapedBuffer.GlyphClusters[index]; - + var advance = shapedBuffer.GlyphAdvances[index]; if (lastCluster != currentCluster) @@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting rects.Remove(rect); rect = rect.WithWidth(rect.Width + advance); - + rects.Add(rect); } - + currentX += advance; lastCluster = currentCluster; @@ -655,8 +655,43 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return rects; } + [Fact] - public void Should_Get_TextBounds() + public void Should_Get_TextBounds_Mixed() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var text = "0123".AsMemory(); + var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); + + var textRuns = new List + { + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption), defaultProperties), + new CustomDrawableRun(), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 1, text.Length), shaperOption), defaultProperties), + new CustomDrawableRun(), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties), + new CustomDrawableRun(), + }; + + var textSource = new FixedRunsTextSource(textRuns); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_Get_TextBounds_BiDi() { using (Start()) { @@ -673,7 +708,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 3, text.Length), ltrOptions), defaultProperties) }; - + var textSource = new FixedRunsTextSource(textRuns); var formatter = new TextFormatterImpl(); @@ -700,12 +735,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public TextRun? GetTextRun(int textSourceIndex) { + var currentPosition = 0; + foreach (var textRun in _textRuns) { - if(textRun.Text.Start == textSourceIndex) + if (currentPosition == textSourceIndex) { return textRun; } + + currentPosition += textRun.TextSourceLength; } return null; From 32d72930972ce5586caaf5259909c4b932945b2f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 21 Apr 2022 15:33:01 +0200 Subject: [PATCH 099/213] Fix GetNextCharacterHit for trailing whitespace --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 7f2dde7c1e..9ac4b71a12 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -662,7 +662,7 @@ namespace Avalonia.Controls.Presenters caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if (textLine.NewLineLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) + if (textLine.TrailingWhitespaceLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) { characterHit = new CharacterHit(caretIndex); } From aaf04a38dab6bb8d1dfc263e6fc922a3ce111b10 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 21 Apr 2022 15:39:09 +0200 Subject: [PATCH 100/213] Fix property GetValue --- src/Avalonia.Controls/Documents/InlineUIContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index eb12092bb8..5f08c23099 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -105,7 +105,7 @@ namespace Avalonia.Controls.Documents get { double baseline = Size.Height; - double baselineOffsetValue = (double)Control.GetValue(TextBlock.BaselineOffsetProperty); + double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); if (!MathUtilities.IsZero(baselineOffsetValue)) { From 982e2d5db090305b3f57fc46e226da26839061fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Apr 2022 16:15:36 +0200 Subject: [PATCH 101/213] Fix merge error due to changed API. And moved fields to live with other field. --- src/Avalonia.Controls/Border.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 53de95ac41..06368eb5c6 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -71,6 +71,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(BorderLineJoin), PenLineJoin.Miter); private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); + private Thickness? _layoutThickness; + private double _scale; /// /// Initializes static members of the class. @@ -90,7 +92,7 @@ namespace Avalonia.Controls AffectsMeasure(BorderThicknessProperty); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) @@ -183,9 +185,6 @@ namespace Avalonia.Controls set => SetValue(BoxShadowProperty, value); } - private Thickness? _layoutThickness; - private double _scale; - private Thickness LayoutThickness { get From f33fe3b708f1d6a5966b4ba71447d35165a4aeaf Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 21 Apr 2022 20:25:16 +0200 Subject: [PATCH 102/213] Avoid checking all array values. --- src/Avalonia.Base/Utilities/WeakHashList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs index df480aa062..fe582e8a78 100644 --- a/src/Avalonia.Base/Utilities/WeakHashList.cs +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -118,7 +118,7 @@ internal class WeakHashList where T : class { if (_arr != null) { - for (var c = 0; c < _arr.Length; c++) + for (var c = 0; c < _arrCount; c++) { if (_arr[c]?.TryGetTarget(out var target) == true && target == item) { From a81a2d908ab71bfca837902290affe9628e4fccb Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 21 Apr 2022 20:57:46 +0200 Subject: [PATCH 103/213] Fix double property reads. --- src/Avalonia.Controls/Border.cs | 2 +- src/Avalonia.Controls/Presenters/ContentPresenter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 06368eb5c6..bc740c133a 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -196,7 +196,7 @@ namespace Avalonia.Controls var borderThickness = BorderThickness; if (UseLayoutRounding) - borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale); + borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale); _layoutThickness = borderThickness; } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index e70a3bc1c9..996cb29534 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -514,7 +514,7 @@ namespace Avalonia.Controls.Presenters var borderThickness = BorderThickness; if (UseLayoutRounding) - borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale); + borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale); _layoutThickness = borderThickness; } From 2a5b2cf90f16418806170a18ed5692600784c7a5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Apr 2022 22:24:32 -0400 Subject: [PATCH 104/213] Use newer GetOldValue method --- .../ColorSpectrum/ColorSpectrum.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index aecaa88f36..fe9a2fac43 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -361,7 +361,7 @@ namespace Avalonia.Controls.Primitives } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ColorProperty) { @@ -380,7 +380,7 @@ namespace Avalonia.Controls.Primitives UpdateBitmapSources(); } - _oldColor = change.OldValue.GetValueOrDefault(); + _oldColor = change.GetOldValue(); } else if (change.Property == HsvColorProperty) { @@ -391,7 +391,7 @@ namespace Avalonia.Controls.Primitives SetColor(); } - _oldHsvColor = change.OldValue.GetValueOrDefault(); + _oldHsvColor = change.GetOldValue(); } else if (change.Property == MinHueProperty || change.Property == MaxHueProperty) From fd96e6483d1ec66cc3afefd5e5250f79c4f8e474 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Apr 2022 21:59:08 +0200 Subject: [PATCH 105/213] Add scaled ComboBox to ControlCatalog. To test #7147. --- samples/ControlCatalog/Pages/ComboBoxPage.xaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 64e80a8e11..9f2fbb88f3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -86,6 +86,16 @@ + + + + + + Inline Items + Inline Item 2 + Inline Item 3 + Inline Item 4 + WrapSelection From d1956d18b4ee8f3112855e59f89cdb5d82960430 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Apr 2022 21:59:57 +0200 Subject: [PATCH 106/213] Scale ComboBox popup according to RenderTransform. --- .../Primitives/IPopupHost.cs | 56 +++++-- .../Primitives/OverlayPopupHost.cs | 42 +++-- src/Avalonia.Controls/Primitives/Popup.cs | 150 ++++++++++++++++-- src/Avalonia.Controls/Primitives/PopupRoot.cs | 39 ++--- .../Controls/OverlayPopupHost.xaml | 18 ++- .../Controls/PopupRoot.xaml | 22 +-- .../Controls/ComboBox.xaml | 3 +- .../Controls/OverlayPopupHost.xaml | 16 +- .../Controls/PopupRoot.xaml | 14 +- .../Primitives/PopupTests.cs | 4 + 10 files changed, 257 insertions(+), 107 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index 36d2ae9230..8652924f90 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -17,16 +18,50 @@ namespace Avalonia.Controls.Primitives public interface IPopupHost : IDisposable, IFocusScope { /// - /// Sets the control to display in the popup. + /// Gets or sets the fixed width of the popup. /// - /// - void SetChild(IControl? control); + double Width { get; set; } + + /// + /// Gets or sets the minimum width of the popup. + /// + double MinWidth { get; set; } + + /// + /// Gets or sets the maximum width of the popup. + /// + double MaxWidth { get; set; } + + /// + /// Gets or sets the fixed height of the popup. + /// + double Height { get; set; } + + /// + /// Gets or sets the minimum height of the popup. + /// + double MinHeight { get; set; } + + /// + /// Gets or sets the maximum height of the popup. + /// + double MaxHeight { get; set; } /// /// Gets the presenter from the control's template. /// IContentPresenter? Presenter { get; } + /// + /// Gets or sets whether the popup appears on top of all other windows. + /// + bool Topmost { get; set; } + + /// + /// Gets or sets a transform that will be applied to the popup. + /// + Transform? Transform { get; set; } + /// /// Gets the root of the visual tree in the case where the popup is presented using a /// separate visual tree. @@ -57,6 +92,12 @@ namespace Avalonia.Controls.Primitives PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, Rect? rect = null); + /// + /// Sets the control to display in the popup. + /// + /// + void SetChild(IControl? control); + /// /// Shows the popup. /// @@ -66,14 +107,5 @@ namespace Avalonia.Controls.Primitives /// Hides the popup. /// void Hide(); - - /// - /// Binds the constraints of the popup host to a set of properties, usally those present on - /// . - /// - IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, - StyledProperty minWidthProperty, StyledProperty maxWidthProperty, - StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty); } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 6ac544e0fe..4765718c3b 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -11,6 +11,12 @@ namespace Avalonia.Controls.Primitives { public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup { + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + PopupRoot.TransformProperty.AddOwner(); + private readonly OverlayLayer _overlayLayer; private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); private ManagedPopupPositioner _positioner; @@ -29,10 +35,22 @@ namespace Avalonia.Controls.Primitives } public IVisual? HostedVisualTreeRoot => null; - + + public Transform? Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + /// IInteractive? IInteractive.InteractiveParent => Parent; + bool IPopupHost.Topmost + { + get => false; + set { /* Not currently supported in overlay popups */ } + } + public void Dispose() => Hide(); @@ -48,28 +66,6 @@ namespace Avalonia.Controls.Primitives _shown = false; } - public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, - StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty) - { - // Topmost property is not supported - var bindings = new List(); - - void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); - Bind(WidthProperty, widthProperty); - Bind(MinWidthProperty, minWidthProperty); - Bind(MaxWidthProperty, maxWidthProperty); - Bind(HeightProperty, heightProperty); - Bind(MinHeightProperty, minHeightProperty); - Bind(MaxHeightProperty, maxHeightProperty); - - return Disposable.Create(() => - { - foreach (var x in bindings) - x.Dispose(); - }); - } - public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None, PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index bb546107e0..7a7e41b029 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -14,6 +14,8 @@ using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.VisualTree; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { @@ -33,6 +35,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty ChildProperty = AvaloniaProperty.Register(nameof(Child)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InheritsTransformProperty = + AvaloniaProperty.Register(nameof(InheritsTransform)); + /// /// Defines the property. /// @@ -196,6 +204,16 @@ namespace Avalonia.Controls.Primitives set; } + /// + /// Gets or sets a value that determines whether the popup inherits the render transform + /// from its . Defaults to false. + /// + public bool InheritsTransform + { + get => GetValue(InheritsTransformProperty); + set => SetValue(InheritsTransformProperty, value); + } + /// /// Gets or sets a value that determines how the can be dismissed. /// @@ -395,24 +413,29 @@ namespace Avalonia.Controls.Primitives } _isOpenRequested = false; - var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); + var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); var handlerCleanup = new CompositeDisposable(7); - popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, - HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); - + UpdateHostSizing(popupHost, topLevel, placementTarget); + popupHost.Topmost = Topmost; popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); - popupHost.ConfigurePosition( - placementTarget, - PlacementMode, - new Point(HorizontalOffset, VerticalOffset), - PlacementAnchor, - PlacementGravity, - PlacementConstraintAdjustment, - PlacementRect); + if (InheritsTransform && placementTarget is Control c) + { + SubscribeToEventHandler>( + c, + PlacementTargetPropertyChanged, + (x, handler) => x.PropertyChanged += handler, + (x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup); + } + else + { + popupHost.Transform = null; + } + + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, @@ -494,7 +517,7 @@ namespace Avalonia.Controls.Primitives } } - _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); + _openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); @@ -542,7 +565,93 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); Close(); } - + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (_openState is not null) + { + if (change.Property == WidthProperty || + change.Property == MinWidthProperty || + change.Property == MaxWidthProperty || + change.Property == HeightProperty || + change.Property == MinHeightProperty || + change.Property == MaxHeightProperty) + { + UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); + } + else if (change.Property == PlacementTargetProperty || + change.Property == PlacementModeProperty || + change.Property == HorizontalOffsetProperty || + change.Property == VerticalOffsetProperty || + change.Property == PlacementAnchorProperty || + change.Property == PlacementConstraintAdjustmentProperty || + change.Property == PlacementRectProperty) + { + if (change.Property == PlacementTargetProperty) + { + var newTarget = change.GetNewValue() ?? this.FindLogicalAncestorOfType(); + + if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel) + { + Close(); + return; + } + + _openState.PlacementTarget = newTarget; + } + + UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget); + } + else if (change.Property == TopmostProperty) + { + _openState.PopupHost.Topmost = change.GetNewValue(); + } + } + } + + private void UpdateHostPosition(IPopupHost popupHost, IControl placementTarget) + { + popupHost.ConfigurePosition( + placementTarget, + PlacementMode, + new Point(HorizontalOffset, VerticalOffset), + PlacementAnchor, + PlacementGravity, + PlacementConstraintAdjustment, + PlacementRect ?? new Rect(default, placementTarget.Bounds.Size)); + } + + private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, IControl placementTarget) + { + var scaleX = 1.0; + var scaleY = 1.0; + + if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m) + { + scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); + scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); + + // Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's + // an issue with LayoutTransformControl in that it sets its LayoutTransform property + // with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform + // is null, which breaks TemplateBindings to this property. Offending commit/line: + // + // https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54 + popupHost.Transform = new ScaleTransform(scaleX, scaleY); + } + else + { + popupHost.Transform = null; + } + + popupHost.Width = Width * scaleX; + popupHost.MinWidth = MinWidth * scaleX; + popupHost.MaxWidth = MaxWidth * scaleX; + popupHost.Height = Height * scaleY; + popupHost.MinHeight = MinHeight * scaleY; + popupHost.MaxHeight = MaxHeight * scaleY; + } + private void HandlePositionChange() { if (_openState != null) @@ -824,6 +933,14 @@ namespace Avalonia.Controls.Primitives } } + private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (_openState is not null && e.Property == Visual.TransformedBoundsProperty) + { + UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); + } + } + private void WindowLostFocus() { if (IsLightDismissEnabled) @@ -862,15 +979,16 @@ namespace Avalonia.Controls.Primitives private readonly IDisposable _cleanup; private IDisposable? _presenterCleanup; - public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) + public PopupOpenState(IControl placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) { + PlacementTarget = placementTarget; TopLevel = topLevel; PopupHost = popupHost; _cleanup = cleanup; } public TopLevel TopLevel { get; } - + public IControl PlacementTarget { get; set; } public IPopupHost PopupHost { get; } public void SetPresenterSubscription(IDisposable? presenterCleanup) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index abf56e5420..f7bf7c1a27 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; @@ -8,7 +6,6 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls.Primitives { @@ -17,6 +14,12 @@ namespace Avalonia.Controls.Primitives /// public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + private PopupPositionerParameters _positionerParameters; /// @@ -54,6 +57,15 @@ namespace Avalonia.Controls.Primitives /// public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl; + /// + /// Gets or sets a transform that will be applied to the popup. + /// + public Transform? Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + /// /// Gets the parent control in the event route. /// @@ -103,27 +115,6 @@ namespace Avalonia.Controls.Primitives IVisual IPopupHost.HostedVisualTreeRoot => this; - public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, - StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty) - { - var bindings = new List(); - - void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); - Bind(WidthProperty, widthProperty); - Bind(MinWidthProperty, minWidthProperty); - Bind(MaxWidthProperty, maxWidthProperty); - Bind(HeightProperty, heightProperty); - Bind(MinHeightProperty, minHeightProperty); - Bind(MaxHeightProperty, maxHeightProperty); - Bind(TopmostProperty, topmostProperty); - return Disposable.Create(() => - { - foreach (var x in bindings) - x.Dispose(); - }); - } - protected override Size MeasureOverride(Size availableSize) { var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity; diff --git a/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml index 301e19d208..07d905ea1d 100644 --- a/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml @@ -1,4 +1,4 @@ - diff --git a/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml index 9468cc5535..5e8f3337ee 100644 --- a/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml @@ -10,16 +10,18 @@ - - - - - - + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index e35093d2f1..93ecc438eb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -119,7 +119,8 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="Background" - IsLightDismissEnabled="True"> + IsLightDismissEnabled="True" + InheritsTransform="True"> - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml index 1e573913b9..f608cf55f5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml @@ -10,16 +10,18 @@ - - - - + + + + - - + + + diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index cac8ca885d..d9cb40d1cc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -325,6 +325,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "LayoutTransformControl", "VisualLayerManager", "ContentPresenter", "ContentPresenter", @@ -337,6 +338,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "LayoutTransformControl", "Panel", "Border", "VisualLayerManager", @@ -356,6 +358,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new object[] { + popupRoot, popupRoot, popupRoot, target, @@ -372,6 +375,7 @@ namespace Avalonia.Controls.UnitTests.Primitives popupRoot, popupRoot, popupRoot, + popupRoot, target, null, }, From 6b2cedfcd1b763691373ba0b6ffb5f0cb6c9f445 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Apr 2022 10:15:46 +0200 Subject: [PATCH 107/213] Fix failing test. It was probably not a good idea to test the contents of the template explicitly in unit tests, but here we are... --- .../Primitives/PopupRootTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index f27ff3928c..6d3351d2b2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -78,9 +78,13 @@ namespace Avalonia.Controls.UnitTests.Primitives var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); - Assert.IsType(templatedChild); + Assert.IsType(templatedChild); - var visualLayerManager = templatedChild.GetVisualChildren().Skip(1).Single(); + var panel = templatedChild.GetVisualChildren().Single(); + + Assert.IsType(panel); + + var visualLayerManager = panel.GetVisualChildren().Skip(1).Single(); Assert.IsType(visualLayerManager); From 0cdbd53bc312d2711461552c88a5250c1c1d3c1a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Apr 2022 15:21:26 +0200 Subject: [PATCH 108/213] Rewrite TextBounds test --- .../Media/TextFormatting/TextLineTests.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index b58d9051f3..e3b9e5a8b1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -665,14 +665,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var text = "0123".AsMemory(); var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); + var shapedBuffer = TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption); + var firstRun = new ShapedTextCharacters(shapedBuffer, defaultProperties); + var textRuns = new List { - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 1, text.Length), shaperOption), defaultProperties), + firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties), + new ShapedTextCharacters(shapedBuffer, defaultProperties), new CustomDrawableRun(), + new ShapedTextCharacters(shapedBuffer, defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -687,6 +690,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + + textBounds = textLine.GetTextBounds(0, firstRun.Text.Length); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + + textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); } } From 7c63e1a60b9b20ee5bec85c26a21a15f801c1600 Mon Sep 17 00:00:00 2001 From: Tako Date: Mon, 25 Apr 2022 17:42:52 +0300 Subject: [PATCH 109/213] Fix ContextMenu freeze. --- src/Avalonia.Controls/ItemsControl.cs | 2 +- .../Platform/DefaultMenuInteractionHandler.cs | 2 +- .../Primitives/SelectingItemsControlTests.cs | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 256160a116..56b0014c05 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -518,7 +518,7 @@ namespace Avalonia.Controls } c = result; - } while (c != null && c != from); + } while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last); return null; } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6e9ac537f1..2f9bf0ac06 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -417,7 +417,7 @@ namespace Avalonia.Controls.Platform protected internal virtual void MenuOpened(object? sender, RoutedEventArgs e) { - if (e.Source == Menu) + if (e.Source is Menu) { Menu?.MoveSelection(NavigationDirection.First, true); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 0e0ca7cd25..3d36395c3a 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -6,6 +6,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -1614,6 +1615,50 @@ namespace Avalonia.Controls.UnitTests.Primitives target.MoveSelection(NavigationDirection.Next, true); } + [Fact(Timeout = 2000)] + public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem { Focusable = false }, + new ListBoxItem(), + } + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below. + // https://github.com/xunit/xunit/issues/2222 + await Task.Run(() => target.MoveSelection(NavigationDirection.First, true)); + Assert.Equal(-1, target.SelectedIndex); + } + + [Fact(Timeout = 2000)] + public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem(), + new ListBoxItem { Focusable = false }, + } + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below. + // https://github.com/xunit/xunit/issues/2222 + await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true)); + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void MoveSelection_Does_Select_Disabled_Controls() { From f63ec5f5a915789a0dff264585ec895bd1b3cf61 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:47:51 -0400 Subject: [PATCH 110/213] Separate color picker control styles --- .../Themes/Default.xaml | 133 +--------------- .../Themes/Default/ColorSpectrum.xaml | 134 ++++++++++++++++ .../Themes/Fluent.xaml | 146 +++--------------- .../Themes/Fluent/ColorSpectrum.xaml | 134 ++++++++++++++++ 4 files changed, 290 insertions(+), 257 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 832daf8853..528eed9969 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,134 +1,7 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml new file mode 100644 index 0000000000..9596ca9653 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 545702ea84..fb656ce964 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,134 +1,26 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml new file mode 100644 index 0000000000..b209fe75b3 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 21dd8392bf95045d404dbb67a4e5c589d691eb27 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:48:39 -0400 Subject: [PATCH 111/213] Add initial ColorPreviewer primitive --- .../ColorPreviewer/AccentColorConverter.cs | 112 ++++++++++++++ .../ColorPreviewer.Properties.cs | 69 +++++++++ .../ColorPreviewer/ColorPreviewer.cs | 138 ++++++++++++++++++ .../Themes/Fluent.xaml | 1 + .../Themes/Fluent/ColorPreviewer.xaml | 90 ++++++++++++ 5 files changed, 410 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs new file mode 100644 index 0000000000..ad8f66251a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs @@ -0,0 +1,112 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Creates an accent color for a given base color value and step parameter. + /// + public class AccentColorConverter : IValueConverter + { + /// + /// The amount to change the Value channel for each accent color step. + /// + public const double ValueDelta = 0.1; + + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + int accentStep; + Color? rgbColor = null; + HsvColor? hsvColor = null; + + // Get the current color in HSV + if (value is Color valueColor) + { + rgbColor = valueColor; + } + else if (value is HsvColor valueHsvColor) + { + hsvColor = valueHsvColor; + } + else if (value is SolidColorBrush valueBrush) + { + rgbColor = valueBrush.Color; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // Get the value component delta + try + { + accentStep = int.Parse(parameter?.ToString() ?? "", CultureInfo.InvariantCulture); + } + catch + { + // Invalid parameter provided, unable to convert to integer + return AvaloniaProperty.UnsetValue; + } + + if (hsvColor == null && + rgbColor != null) + { + hsvColor = rgbColor.Value.ToHsv(); + } + + if (hsvColor != null) + { + return new SolidColorBrush(GetAccent(hsvColor.Value, accentStep).ToRgb()); + } + else + { + return AvaloniaProperty.UnsetValue; + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + + /// + /// This does not account for perceptual differences and also does not match with + /// system accent color calculation. + /// + /// + /// Use the HSV representation as it's more perceptual. + /// In most cases only the value is changed by a fixed percentage so the algorithm is reproducible. + /// + /// The base color to calculate the accent from. + /// The number of accent color steps to move. + /// The new accent color. + public static HsvColor GetAccent(HsvColor hsvColor, int accentStep) + { + if (accentStep != 0) + { + double colorValue = hsvColor.V; + colorValue += (accentStep * AccentColorConverter.ValueDelta); + colorValue = Math.Round(colorValue, 2); + + return new HsvColor(hsvColor.A, hsvColor.H, hsvColor.S, colorValue); + } + else + { + return hsvColor; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs new file mode 100644 index 0000000000..74c0943919 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -0,0 +1,69 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorPreviewer + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White); + + /// + /// Gets or sets the currently previewed color in the RGB color model. + /// + /// + /// For control authors use instead to avoid loss + /// of precision and color drifting. + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.Transparent.ToHsv()); + + /// + /// Gets or sets the currently previewed color in the HSV color model. + /// + /// + /// This should be used in all cases instead of the property. + /// Internally, the uses the HSV color model and using + /// this property will avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => (bool)this.GetValue(ShowAccentColorsProperty); + set => SetValue(ShowAccentColorsProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs new file mode 100644 index 0000000000..35bd62601f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -0,0 +1,138 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Metadata; +using Avalonia.Input; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Presents a preview color with optional accent colors. + /// + [TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))] + [TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))] + [TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))] + [TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))] + public partial class ColorPreviewer : TemplatedControl + { + /// + /// Event for when the selected color changes within the previewer. + /// This happens when an accent color is pressed. + /// + public event EventHandler? ColorChanged; + + private bool eventsConnected = false; + + private Border? AccentDec1Border; + private Border? AccentDec2Border; + private Border? AccentInc1Border; + private Border? AccentInc2Border; + + /// + /// Initializes a new instance of the class. + /// + public ColorPreviewer() : base() + { + } + + /// + /// Connects or disconnects all control event handlers. + /// + /// True to connect event handlers, otherwise false. + private void ConnectEvents(bool connected) + { + if (connected == true && eventsConnected == false) + { + // Add all events + if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; } + if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; } + if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; } + if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; } + + eventsConnected = true; + } + else if (connected == false && eventsConnected == true) + { + // Remove all events + if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; } + if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; } + if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; } + if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; } + + eventsConnected = false; + } + + return; + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + // Remove any existing events present if the control was previously loaded then unloaded + ConnectEvents(false); + + AccentDec1Border = e.NameScope.Find(nameof(AccentDec1Border)); + AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border)); + AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border)); + AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border)); + + // Must connect after controls are found + ConnectEvents(true); + + base.OnApplyTemplate(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + HsvColor = Color.ToHsv(); + } + else if (change.Property == HsvColorProperty) + { + Color = HsvColor.ToRgb(); + } + + base.OnPropertyChanged(change); + } + + /// + /// Called before the event occurs. + /// + /// The newly selected color. + protected virtual void OnColorChanged(HsvColor newColor) + { + var oldColor = HsvColor; + HsvColor = newColor; + + ColorChanged?.Invoke(this, new ColorChangedEventArgs(oldColor.ToRgb(), newColor.ToRgb())); + + return; + } + + /// + /// Event handler for when an accent color border is pressed. + /// This will update the color to the background of the pressed panel. + /// + private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs e) + { + Border? border = sender as Border; + int accentStep = 0; + HsvColor hsvColor = HsvColor; + + // Get the value component delta + try + { + accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture); + } + catch { } + + HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + OnColorChanged(newHsvColor); + + return; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index fb656ce964..d96066e56b 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -21,6 +21,7 @@ + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml new file mode 100644 index 0000000000..a91da45578 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -0,0 +1,90 @@ + + + + + + + + + From ad5249992df8667f2785cf2504b2ddcc62a730c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:49:24 -0400 Subject: [PATCH 112/213] Add initial ColorSlider primitive --- .../ColorComponent.cs | 28 ++ .../{ColorSpectrum => }/ColorHelpers.cs | 344 +++++++++++++++++- .../ColorModel.cs | 18 + .../ColorSlider/ColorSlider.Properties.cs | 143 ++++++++ .../ColorSlider/ColorSlider.cs | 221 +++++++++++ .../Themes/Fluent.xaml | 1 + .../Themes/Fluent/ColorSlider.xaml | 172 +++++++++ 7 files changed, 925 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorComponent.cs rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => }/ColorHelpers.cs (50%) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorModel.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml diff --git a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs new file mode 100644 index 0000000000..a0385c03b4 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific component within a color model. + /// + public enum ColorComponent + { + /// + /// Represents the alpha component. + /// + Alpha, + + /// + /// Represents the first color component which is Red when RGB or Hue when HSV. + /// + Component1, + + /// + /// Represents the second color component which is Green when RGB or Saturation when HSV. + /// + Component2, + + /// + /// Represents the third color component which is Blue when RGB or Value when HSV. + /// + Component3 + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorHelpers.cs similarity index 50% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs rename to src/Avalonia.Controls.ColorPicker/ColorHelpers.cs index b912d39aba..37c6f552d6 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorHelpers.cs @@ -6,9 +6,12 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { @@ -26,6 +29,291 @@ namespace Avalonia.Controls.Primitives return string.Empty; } + /// + /// Generates a new bitmap of the specified size by changing a specific color component. + /// This will produce a gradient representing a sweep of all possible values of the color component. + /// + /// The pixel width (X, horizontal) of the resulting bitmap. + /// The pixel height (Y, vertical) of the resulting bitmap. + /// The orientation of the resulting bitmap (gradient direction). + /// The color model being used: RGBA or HSVA. + /// The specific color component to sweep. + /// The base HSV color used for components not being changed. + /// Fix the alpha component value to maximum during calculation. + /// This will remove any alpha/transparency from the other component backgrounds. + /// Fix the saturation and value components to maximum + /// during calculation with the HSVA color model. + /// This will ensure colors are always discernible regardless of saturation/value. + /// A new bitmap representing a gradient of color component values. + internal static async Task CreateComponentBitmapAsync( + int width, + int height, + Orientation orientation, + ColorModel colorModel, + ColorComponent component, + HsvColor baseHsvColor, + bool isAlphaMaxForced, + bool isSaturationValueMaxForced) + { + if (width == 0 || height == 0) + { + return new byte[0]; + } + + var bitmap = await Task.Run(() => + { + int pixelDataIndex = 0; + double componentStep; + byte[] bgraPixelData; + Color baseRgbColor = Colors.White; + Color rgbColor; + int bgraPixelDataHeight; + int bgraPixelDataWidth; + + // Allocate the buffer + // BGRA formatted color components 1 byte each (4 bytes in a pixel) + bgraPixelData = new byte[width * height * 4]; + bgraPixelDataHeight = height * 4; + bgraPixelDataWidth = width * 4; + + // Maximize alpha component value + if (isAlphaMaxForced && + component != ColorComponent.Alpha) + { + baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + } + + // Convert HSV to RGB once + if (colorModel == ColorModel.Rgba) + { + baseRgbColor = baseHsvColor.ToRgb(); + } + + // Maximize Saturation and Value components when in HSVA mode + if (isSaturationValueMaxForced && + colorModel == ColorModel.Hsva && + component != ColorComponent.Alpha) + { + switch (component) + { + case ColorComponent.Component1: + baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0); + break; + case ColorComponent.Component2: + baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0); + break; + case ColorComponent.Component3: + baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V); + break; + } + } + + // Create the color component gradient + if (orientation == Orientation.Horizontal) + { + // Determine the numerical increment of the color steps within the component + if (colorModel == ColorModel.Hsva) + { + if (component == ColorComponent.Component1) + { + componentStep = 360.0 / width; + } + else + { + componentStep = 1.0 / width; + } + } + else + { + componentStep = 255.0 / width; + } + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (y == 0) + { + rgbColor = GetColor(x * componentStep); + + // Get a new color + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = rgbColor.A; + } + else + { + // Use the color in the row above + // Remember the pixel data is 1 dimensional instead of 2 + bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth]; + } + + pixelDataIndex += 4; + } + } + } + else + { + // Determine the numerical increment of the color steps within the component + if (colorModel == ColorModel.Hsva) + { + if (component == ColorComponent.Component1) + { + componentStep = 360.0 / height; + } + else + { + componentStep = 1.0 / height; + } + } + else + { + componentStep = 255.0 / height; + } + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (x == 0) + { + // The lowest component value should be at the 'bottom' of the bitmap + rgbColor = GetColor((height - 1 - y) * componentStep); + + // Get a new color + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = rgbColor.A; + } + else + { + // Use the color in the column to the left + // Remember the pixel data is 1 dimensional instead of 2 + bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4]; + bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3]; + bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2]; + bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1]; + } + + pixelDataIndex += 4; + } + } + } + + Color GetColor(double componentValue) + { + Color newRgbColor = Colors.White; + + switch (component) + { + case ColorComponent.Component1: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep hue + newRgbColor = HsvColor.ToRgb( + MathUtilities.Clamp(componentValue, 0.0, 360.0), + baseHsvColor.S, + baseHsvColor.V, + baseHsvColor.A); + } + else + { + // Sweep red + newRgbColor = new Color( + baseRgbColor.A, + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), + baseRgbColor.G, + baseRgbColor.B); + } + + break; + } + case ColorComponent.Component2: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep saturation + newRgbColor = HsvColor.ToRgb( + baseHsvColor.H, + MathUtilities.Clamp(componentValue, 0.0, 1.0), + baseHsvColor.V, + baseHsvColor.A); + } + else + { + // Sweep green + newRgbColor = new Color( + baseRgbColor.A, + baseRgbColor.R, + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), + baseRgbColor.B); + } + + break; + } + case ColorComponent.Component3: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep value + newRgbColor = HsvColor.ToRgb( + baseHsvColor.H, + baseHsvColor.S, + MathUtilities.Clamp(componentValue, 0.0, 1.0), + baseHsvColor.A); + } + else + { + // Sweep blue + newRgbColor = new Color( + baseRgbColor.A, + baseRgbColor.R, + baseRgbColor.G, + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0))); + } + + break; + } + case ColorComponent.Alpha: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep alpha + newRgbColor = HsvColor.ToRgb( + baseHsvColor.H, + baseHsvColor.S, + baseHsvColor.V, + MathUtilities.Clamp(componentValue, 0.0, 1.0)); + } + else + { + // Sweep alpha + newRgbColor = new Color( + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), + baseRgbColor.R, + baseRgbColor.G, + baseRgbColor.B); + } + + break; + } + } + + return newRgbColor; + } + + return bgraPixelData; + }); + + return bitmap; + } + public static Hsv IncrementColorComponent( Hsv originalHsv, HsvComponent component, @@ -363,14 +651,22 @@ namespace Avalonia.Controls.Primitives return originalAlpha / 100; } + /// + /// + /// + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// + /// public static WriteableBitmap CreateBitmapFromPixelData( int pixelWidth, int pixelHeight, List bgraPixelData) { - Vector dpi = new Vector(96, 96); // Standard may need to change on some devices + // Standard may need to change on some devices + Vector dpi = new Vector(96, 96); - WriteableBitmap bitmap = new WriteableBitmap( + var bitmap = new WriteableBitmap( new PixelSize(pixelWidth, pixelHeight), dpi, PixelFormat.Bgra8888, @@ -385,6 +681,50 @@ namespace Avalonia.Controls.Primitives return bitmap; } + /// + /// Converts the given bitmap (in raw BGRA pre-multiplied alpha pixels) into an image brush + /// that can be used in the UI. + /// + /// The bitmap (in raw BGRA pre-multiplied alpha pixels) + /// to convert to a brush. + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// A new . + public static IBrush? BitmapToBrushAsync( + byte[] bgraPixelData, + int pixelWidth, + int pixelHeight) + { + if (bgraPixelData.Length == 0 || + (pixelWidth == 0 && + pixelHeight == 0)) + { + return null; + } + + // Standard may need to change on some devices + Vector dpi = new Vector(96, 96); + + var bitmap = new WriteableBitmap( + new PixelSize(pixelWidth, pixelHeight), + dpi, + PixelFormat.Bgra8888, + AlphaFormat.Premul); + + // Warning: This is highly questionable + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData, 0, frameBuffer.Address, bgraPixelData.Length); + } + + var brush = new ImageBrush(bitmap) + { + Stretch = Stretch.Fill + }; + + return brush; + } + /// /// Gets the relative (perceptual) luminance/brightness of the given color. /// 1 is closer to white while 0 is closer to black. diff --git a/src/Avalonia.Controls.ColorPicker/ColorModel.cs b/src/Avalonia.Controls.ColorPicker/ColorModel.cs new file mode 100644 index 0000000000..f11b514706 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorModel.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Controls +{ + /// + /// Defines the model used to represent colors. + /// + public enum ColorModel + { + /// + /// Color is represented by hue, saturation, value and alpha components. + /// + Hsva, + + /// + /// Color is represented by red, green, blue and alpha components. + /// + Rgba + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs new file mode 100644 index 0000000000..3aa3e3a789 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -0,0 +1,143 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorSlider + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White); + + /// + /// Gets or sets the currently selected color in the RGB color model. + /// + /// + /// Use this property instead of when in + /// to avoid loss of precision and color drifting. + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorComponentProperty = + AvaloniaProperty.Register( + nameof(ColorComponent), + ColorComponent.Component1); + + /// + /// Gets or sets the color component represented by the slider. + /// + public ColorComponent ColorComponent + { + get => GetValue(ColorComponentProperty); + set => SetValue(ColorComponentProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + + /// + /// Gets or sets the active color model used by the slider. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv()); + + /// + /// Gets or sets the currently selected color in the HSV color model. + /// + /// + /// Use this property instead of when in + /// to avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsAlphaMaxForced), + true); + + /// + /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components + /// other than . + /// This ensures that the background is always visible and never transparent regardless of the actual color. + /// + public bool IsAlphaMaxForced + { + get => GetValue(IsAlphaMaxForcedProperty); + set => SetValue(IsAlphaMaxForcedProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAutoUpdatingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAutoUpdatingEnabled), + true); + + /// + /// Gets or sets a value indicating whether automatic background and foreground updates will be + /// calculated when the set color changes. + /// + /// + /// This can be disabled for performance reasons when working with multiple sliders. + /// + public bool IsAutoUpdatingEnabled + { + get => GetValue(IsAutoUpdatingEnabledProperty); + set => SetValue(IsAutoUpdatingEnabledProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSaturationValueMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsSaturationValueMaxForced), + true); + + /// + /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values + /// when using the HSVA color model. Only component values other than will be changed. + /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color. + /// + public bool IsSaturationValueMaxForced + { + get => GetValue(IsSaturationValueMaxForcedProperty); + set => SetValue(IsSaturationValueMaxForcedProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs new file mode 100644 index 0000000000..e3f2dc3555 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -0,0 +1,221 @@ +using System; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// A slider with a background that represents a single color component. + /// + public partial class ColorSlider : Slider + { + private Size cachedSize = Size.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ColorSlider() : base() + { + } + + /// + /// Update the slider's Foreground and Background brushes based on the current slider state and color. + /// + /// + /// Manually refreshes the background gradient of the slider. + /// This is callable separately for performance reasons. + /// + public void UpdateColors() + { + HsvColor hsvColor = HsvColor; + + // Calculate and set the background + UpdateBackground(hsvColor); + + // Calculate and set the foreground ensuring contrast with the background + Color rgbColor = hsvColor.ToRgb(); + Color selectedRgbColor; + double sliderPercent = Value / (Maximum - Minimum); + + var component = ColorComponent; + + if (ColorModel == ColorModel.Hsva) + { + if (IsAlphaMaxForced && + component != ColorComponent.Alpha) + { + hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); + } + + switch (component) + { + case ColorComponent.Component1: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); + + hsvColor = new HsvColor( + hsvColor.A, + componentValue, + IsSaturationValueMaxForced ? 1.0 : hsvColor.S, + IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + + break; + } + + case ColorComponent.Component2: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + + hsvColor = new HsvColor( + hsvColor.A, + hsvColor.H, + componentValue, + IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + + break; + } + + case ColorComponent.Component3: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + + hsvColor = new HsvColor( + hsvColor.A, + hsvColor.H, + IsSaturationValueMaxForced ? 1.0 : hsvColor.S, + componentValue); + + break; + } + } + + selectedRgbColor = hsvColor.ToRgb(); + } + else + { + if (IsAlphaMaxForced && + component != ColorComponent.Alpha) + { + rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); + } + + byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); + + switch (component) + { + case ColorComponent.Component1: + rgbColor = new Color(rgbColor.A, componentValue, rgbColor.G, rgbColor.B); + break; + case ColorComponent.Component2: + rgbColor = new Color(rgbColor.A, rgbColor.R, componentValue, rgbColor.B); + break; + case ColorComponent.Component3: + rgbColor = new Color(rgbColor.A, rgbColor.R, rgbColor.G, componentValue); + break; + } + + selectedRgbColor = rgbColor; + } + + //var converter = new ContrastBrushConverter(); + //this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush; + + return; + } + + /// + /// Generates a new background image for the color slider and applies it. + /// + private async void UpdateBackground(HsvColor color) + { + // Updates may be requested when sliders are not in the visual tree. + // For first-time load this is handled by the Loaded event. + // However, after that problems may arise, consider the following case: + // + // (1) Backgrounds are drawn normally the first time on Loaded. + // Actual height/width are available. + // (2) The palette tab is selected which has no sliders + // (3) The picker flyout is closed + // (4) Externally the color is changed + // The color change will trigger slider background updates but + // with the flyout closed, actual height/width are zero. + // No zero size bitmap can be generated. + // (5) The picker flyout is re-opened by the user and the default + // last-opened tab will be viewed: palette. + // No loaded events will be fired for sliders. The color change + // event was already handled in (4). The sliders will never + // be updated. + // + // In this case the sliders become out of sync with the Color because there is no way + // to tell when they actually come into view. To work around this, force a re-render of + // the background with the last size of the slider. This last size will be when it was + // last loaded or updated. + // + // In the future additional consideration may be required for SizeChanged of the control. + // This work-around will also cause issues if display scaling changes in the special + // case where cached sizes are required. + + var width = Convert.ToInt32(Bounds.Width); + var height = Convert.ToInt32(Bounds.Height); + + if (width == 0 || height == 0) + { + // Attempt to use the last size if it was available + if (cachedSize.IsDefault == false) + { + width = Convert.ToInt32(cachedSize.Width); + height = Convert.ToInt32(cachedSize.Height); + } + } + else + { + cachedSize = new Size(width, height); + } + + var bitmap = await ColorHelpers.CreateComponentBitmapAsync( + width, + height, + Orientation, + ColorModel, + ColorComponent, + color, + IsAlphaMaxForced, + IsSaturationValueMaxForced); + + if (bitmap != null) + { + Background = ColorHelpers.BitmapToBrushAsync(bitmap, width, height); + } + + return; + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + bool update = false; + + if (change.Property == ColorProperty) + { + // Sync with HSV (which is primary) + HsvColor = Color.ToHsv(); + update = true; + } + else if (change.Property == HsvColorProperty) + { + update = true; + } + else if (change.Property == BoundsProperty) + { + update = true; + } + + if (update && IsAutoUpdatingEnabled) + { + UpdateColors(); + } + + base.OnPropertyChanged(change); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index d96066e56b..c25d79727f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml new file mode 100644 index 0000000000..1ca9b12ffe --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + From a5b3e85dfc519bee6a762e2d5c45869002e0d135 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:50:00 -0400 Subject: [PATCH 113/213] Simplify default color property values --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 824bf9ab05..ab5b83afcb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register( nameof(Color), - Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); + Colors.White); /// /// Gets or sets the two HSV color components displayed by the spectrum. @@ -71,7 +71,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty HsvColorProperty = AvaloniaProperty.Register( nameof(HsvColor), - new HsvColor(1, 0, 0, 1)); + Colors.White.ToHsv()); /// /// Gets or sets the maximum value of the Hue component in the range from 0..359. From e9cb628f820ba7226cce379a601b9867053eef76 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:50:22 -0400 Subject: [PATCH 114/213] Improve formatting --- .../Converters/CornerRadiusFilterConverter.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index b2433bfd97..a91f143019 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; - using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters @@ -22,7 +21,12 @@ namespace Avalonia.Controls.Converters /// public double Scale { get; set; } = 1; - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { if (!(value is CornerRadius radius)) { @@ -36,7 +40,12 @@ namespace Avalonia.Controls.Converters Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0); } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new NotImplementedException(); } From c068adb60944b8e6a2ba1bfc1a3666c8e0caf1fd Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:51:01 -0400 Subject: [PATCH 115/213] Update ColorPickerPage in ControlCatalog with ColorSlider and ColorPreviewer --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ec34193f8c..f343fd8f59 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -3,14 +3,18 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - + + + + + + + From b6b8e96fd97a5d77d7e26ab2e189ab9d84849e48 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:56:49 -0400 Subject: [PATCH 116/213] Move spectrum enums into ColorSpectrum directory --- .../{ => ColorSpectrum}/ColorSpectrumComponents.cs | 0 .../{ => ColorSpectrum}/ColorSpectrumShape.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ => ColorSpectrum}/ColorSpectrumComponents.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ => ColorSpectrum}/ColorSpectrumShape.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs From 1f5e3c0d9dcfa2f95ad2d217521f075d33364d5c Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 19:28:21 -0400 Subject: [PATCH 117/213] Move helpers into separate directory --- src/Avalonia.Controls.ColorPicker/{ => Helpers}/ColorHelpers.cs | 0 .../{ColorSpectrum => Helpers}/Hsv.cs | 0 .../{ColorSpectrum => Helpers}/IncrementAmount.cs | 0 .../{ColorSpectrum => Helpers}/IncrementDirection.cs | 0 .../{ColorSpectrum => Helpers}/Rgb.cs | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ => Helpers}/ColorHelpers.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/Hsv.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/IncrementAmount.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/IncrementDirection.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/Rgb.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorHelpers.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs From 65e5e580acd359c0103e7b9bb3b0397d7676f1e1 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 21:57:19 -0400 Subject: [PATCH 118/213] Make ColorSlider fully functional --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 3 +- .../ColorSlider/ColorSlider.cs | 367 ++++++++++++------ .../Helpers/ColorHelpers.cs | 2 +- 3 files changed, 241 insertions(+), 131 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index f343fd8f59..09ec15ad15 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -25,7 +25,8 @@ Width="256" /> + RowDefinitions="Auto,Auto,Auto,Auto,Auto" + Margin="0,10,0,0"> public partial class ColorSlider : Slider { - private Size cachedSize = Size.Empty; + private bool disableUpdates = false; /// /// Initializes a new instance of the class. @@ -19,200 +20,308 @@ namespace Avalonia.Controls.Primitives } /// - /// Update the slider's Foreground and Background brushes based on the current slider state and color. + /// Generates a new background image for the color slider and applies it. /// - /// - /// Manually refreshes the background gradient of the slider. - /// This is callable separately for performance reasons. - /// - public void UpdateColors() + private async void UpdateBackground() { - HsvColor hsvColor = HsvColor; + // In Avalonia, Bounds returns the actual device-independent pixel size of a control. + // However, this is not necessarily the size of the control rendered on a display. + // A desktop or application scaling factor may be applied which must be accounted for here. + // Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device- + // independent pixels of controls. - // Calculate and set the background - UpdateBackground(hsvColor); + var scale = LayoutHelper.GetLayoutScale(this); + var pixelWidth = Convert.ToInt32(Bounds.Width * scale); + var pixelHeight = Convert.ToInt32(Bounds.Height * scale); - // Calculate and set the foreground ensuring contrast with the background - Color rgbColor = hsvColor.ToRgb(); - Color selectedRgbColor; - double sliderPercent = Value / (Maximum - Minimum); + if (pixelWidth != 0 && pixelHeight != 0) + { + var bitmap = await ColorHelpers.CreateComponentBitmapAsync( + pixelWidth, + pixelHeight, + Orientation, + ColorModel, + ColorComponent, + HsvColor, + IsAlphaMaxForced, + IsSaturationValueMaxForced); + + if (bitmap != null) + { + Background = ColorHelpers.BitmapToBrushAsync(bitmap, pixelWidth, pixelHeight); + } + } + + return; + } + /// + /// Updates the slider property values by applying the current color. + /// + /// + /// Warning: This will trigger property changed updates. + /// Consider using externally. + /// + private void SetColorToSliderValues() + { + var hsvColor = HsvColor; + var rgbColor = Color; var component = ColorComponent; if (ColorModel == ColorModel.Hsva) { - if (IsAlphaMaxForced && - component != ColorComponent.Alpha) + // Note: Components converted into a usable range for the user + switch (component) { - hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 100; + Value = hsvColor.A * 100; + break; + case ColorComponent.Component1: // Hue + Minimum = 0; + Maximum = 359; + Value = hsvColor.H; + break; + case ColorComponent.Component2: // Saturation + Minimum = 0; + Maximum = 100; + Value = hsvColor.S * 100; + break; + case ColorComponent.Component3: // Value + Minimum = 0; + Maximum = 100; + Value = hsvColor.V * 100; + break; } - + } + else + { switch (component) { - case ColorComponent.Component1: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); - - hsvColor = new HsvColor( - hsvColor.A, - componentValue, - IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - IsSaturationValueMaxForced ? 1.0 : hsvColor.V); - - break; - } + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.A); + break; + case ColorComponent.Component1: // Red + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.R); + break; + case ColorComponent.Component2: // Green + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.G); + break; + case ColorComponent.Component3: // Blue + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.B); + break; + } + } - case ColorComponent.Component2: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + return; + } - hsvColor = new HsvColor( - hsvColor.A, - hsvColor.H, - componentValue, - IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + /// + /// Gets the current color determined by the slider values. + /// + private (Color, HsvColor) GetColorFromSliderValues() + { + HsvColor hsvColor = new HsvColor(); + Color rgbColor = new Color(); + double sliderPercent = Value / (Maximum - Minimum); - break; - } + var baseHsvColor = HsvColor; + var baseRgbColor = Color; + var component = ColorComponent; + if (ColorModel == ColorModel.Hsva) + { + switch (component) + { + case ColorComponent.Alpha: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(componentValue, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component1: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); + hsvColor = new HsvColor(baseHsvColor.A, componentValue, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component2: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, componentValue, baseHsvColor.V); + break; + } case ColorComponent.Component3: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor( - hsvColor.A, - hsvColor.H, - IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - componentValue); - - break; - } + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, componentValue); + break; + } } - selectedRgbColor = hsvColor.ToRgb(); + return (hsvColor.ToRgb(), hsvColor); } else { - if (IsAlphaMaxForced && - component != ColorComponent.Alpha) - { - rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); - } - byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); switch (component) { + case ColorComponent.Alpha: + rgbColor = new Color(componentValue, baseRgbColor.R, baseRgbColor.G, baseRgbColor.B); + break; case ColorComponent.Component1: - rgbColor = new Color(rgbColor.A, componentValue, rgbColor.G, rgbColor.B); + rgbColor = new Color(baseRgbColor.A, componentValue, baseRgbColor.G, baseRgbColor.B); break; case ColorComponent.Component2: - rgbColor = new Color(rgbColor.A, rgbColor.R, componentValue, rgbColor.B); + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, componentValue, baseRgbColor.B); break; case ColorComponent.Component3: - rgbColor = new Color(rgbColor.A, rgbColor.R, rgbColor.G, componentValue); + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, baseRgbColor.G, componentValue); break; } - selectedRgbColor = rgbColor; + return (rgbColor, rgbColor.ToHsv()); } - - //var converter = new ContrastBrushConverter(); - //this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush; - - return; } /// - /// Generates a new background image for the color slider and applies it. + /// Gets the actual background color displayed for the given HSV color. + /// This can differ due to the effects of certain properties intended to improve perception. /// - private async void UpdateBackground(HsvColor color) + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor) { - // Updates may be requested when sliders are not in the visual tree. - // For first-time load this is handled by the Loaded event. - // However, after that problems may arise, consider the following case: - // - // (1) Backgrounds are drawn normally the first time on Loaded. - // Actual height/width are available. - // (2) The palette tab is selected which has no sliders - // (3) The picker flyout is closed - // (4) Externally the color is changed - // The color change will trigger slider background updates but - // with the flyout closed, actual height/width are zero. - // No zero size bitmap can be generated. - // (5) The picker flyout is re-opened by the user and the default - // last-opened tab will be viewed: palette. - // No loaded events will be fired for sliders. The color change - // event was already handled in (4). The sliders will never - // be updated. - // - // In this case the sliders become out of sync with the Color because there is no way - // to tell when they actually come into view. To work around this, force a re-render of - // the background with the last size of the slider. This last size will be when it was - // last loaded or updated. - // - // In the future additional consideration may be required for SizeChanged of the control. - // This work-around will also cause issues if display scaling changes in the special - // case where cached sizes are required. - - var width = Convert.ToInt32(Bounds.Width); - var height = Convert.ToInt32(Bounds.Height); - - if (width == 0 || height == 0) + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; + var isSaturationValueMaxForced = IsSaturationValueMaxForced; + + if (isAlphaMaxForced && + component != ColorComponent.Alpha) { - // Attempt to use the last size if it was available - if (cachedSize.IsDefault == false) - { - width = Convert.ToInt32(cachedSize.Width); - height = Convert.ToInt32(cachedSize.Height); - } + hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); } - else + + switch (component) { - cachedSize = new Size(width, height); + case ColorComponent.Component1: + return new HsvColor( + hsvColor.A, + hsvColor.H, + isSaturationValueMaxForced ? 1.0 : hsvColor.S, + isSaturationValueMaxForced ? 1.0 : hsvColor.V); + case ColorComponent.Component2: + return new HsvColor( + hsvColor.A, + hsvColor.H, + hsvColor.S, + isSaturationValueMaxForced ? 1.0 : hsvColor.V); + case ColorComponent.Component3: + return new HsvColor( + hsvColor.A, + hsvColor.H, + isSaturationValueMaxForced ? 1.0 : hsvColor.S, + hsvColor.V); + default: + return hsvColor; } + } + + /// + /// Gets the actual background color displayed for the given RGB color. + /// This can differ due to the effects of certain properties intended to improve perception. + /// + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private Color GetEquivalentBackgroundColor(Color rgbColor) + { + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; - var bitmap = await ColorHelpers.CreateComponentBitmapAsync( - width, - height, - Orientation, - ColorModel, - ColorComponent, - color, - IsAlphaMaxForced, - IsSaturationValueMaxForced); - - if (bitmap != null) + if (isAlphaMaxForced && + component != ColorComponent.Alpha) { - Background = ColorHelpers.BitmapToBrushAsync(bitmap, width, height); + rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); } - return; + return rgbColor; } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - bool update = false; + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + // Always keep the two color properties in sync if (change.Property == ColorProperty) { - // Sync with HSV (which is primary) + disableUpdates = true; + HsvColor = Color.ToHsv(); - update = true; + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + disableUpdates = false; } else if (change.Property == HsvColorProperty) { - update = true; + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + disableUpdates = false; } else if (change.Property == BoundsProperty) { - update = true; + if (IsAutoUpdatingEnabled) + { + UpdateBackground(); + } } - - if (update && IsAutoUpdatingEnabled) + else if (change.Property == ValueProperty || + change.Property == MinimumProperty || + change.Property == MaximumProperty) { - UpdateColors(); + disableUpdates = true; + + (var color, var hsvColor) = GetColorFromSliderValues(); + + if (ColorModel == ColorModel.Hsva) + { + HsvColor = hsvColor; + Color = hsvColor.ToRgb(); + } + else + { + Color = color; + HsvColor = color.ToHsv(); + } + + disableUpdates = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs index 37c6f552d6..6500d10fe9 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs @@ -719,7 +719,7 @@ namespace Avalonia.Controls.Primitives var brush = new ImageBrush(bitmap) { - Stretch = Stretch.Fill + Stretch = Stretch.None }; return brush; From aae4b6708d15343f5f962e0c7a7c63d626e44064 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 21:57:55 -0400 Subject: [PATCH 119/213] Remove Color property from ColorPreviewer --- .../ColorPreviewer.Properties.cs | 27 +++---------------- .../ColorPreviewer/ColorPreviewer.cs | 18 +------------ 2 files changed, 4 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index 74c0943919..903b5fb52b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -5,27 +5,6 @@ namespace Avalonia.Controls.Primitives /// public partial class ColorPreviewer { - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorProperty = - AvaloniaProperty.Register( - nameof(Color), - Colors.White); - - /// - /// Gets or sets the currently previewed color in the RGB color model. - /// - /// - /// For control authors use instead to avoid loss - /// of precision and color drifting. - /// - public Color Color - { - get => GetValue(ColorProperty); - set => SetValue(ColorProperty, value); - } - /// /// Defines the property. /// @@ -38,9 +17,9 @@ namespace Avalonia.Controls.Primitives /// Gets or sets the currently previewed color in the HSV color model. /// /// - /// This should be used in all cases instead of the property. - /// Internally, the uses the HSV color model and using - /// this property will avoid loss of precision and color drifting. + /// Only an HSV color is supported in this control to ensure there is never any + /// loss of precision or color information. Accent colors, like the color spectrum, + /// only operate with the HSV color model. /// public HsvColor HsvColor { diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 35bd62601f..1c0dd2154a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives { /// /// Event for when the selected color changes within the previewer. - /// This happens when an accent color is pressed. + /// This occurs when an accent color is pressed. /// public event EventHandler? ColorChanged; @@ -82,22 +82,6 @@ namespace Avalonia.Controls.Primitives base.OnApplyTemplate(e); } - /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - // Always keep the two color properties in sync - if (change.Property == ColorProperty) - { - HsvColor = Color.ToHsv(); - } - else if (change.Property == HsvColorProperty) - { - Color = HsvColor.ToRgb(); - } - - base.OnPropertyChanged(change); - } - /// /// Called before the event occurs. /// From 357eddf5e933f4797e21d2235e19936c66878b21 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 22:52:00 -0400 Subject: [PATCH 120/213] Implement ColorSlider PseudoClasses --- .../ColorSlider/ColorSlider.cs | 66 ++++++++++++++++--- .../Themes/Fluent/ColorSlider.xaml | 20 ++++-- .../Themes/Fluent/ColorSpectrum.xaml | 3 + 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 61df36c806..9d9e9f393e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; @@ -8,8 +9,13 @@ namespace Avalonia.Controls.Primitives /// /// A slider with a background that represents a single color component. /// + [PseudoClasses(pcDarkSelector, pcLightSelector)] public partial class ColorSlider : Slider { + protected const string pcDarkSelector = ":dark-selector"; + protected const string pcLightSelector = ":light-selector"; + + private const double MaxHue = 359.99999999999999999; private bool disableUpdates = false; /// @@ -19,6 +25,49 @@ namespace Avalonia.Controls.Primitives { } + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + private void UpdatePseudoClasses() + { + // The slider itself can be transparent for certain color values. + // This causes an issue where a white selector thumb over a light window background or + // a black selector thumb over a dark window background is not visible. + // This means under a certain alpha threshold, neither a white or black selector thumb + // should be shown and instead the default slider thumb color should be used instead. + if (Color.A < 128 && + (IsAlphaMaxForced == false || + ColorComponent == ColorComponent.Alpha)) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, false); + } + else + { + Color perceivedColor; + + if (ColorModel == ColorModel.Hsva) + { + perceivedColor = GetEquivalentBackgroundColor(HsvColor).ToRgb(); + } + else + { + perceivedColor = GetEquivalentBackgroundColor(Color); + } + + if (ColorHelpers.GetRelativeLuminance(perceivedColor) <= 0.5) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, true); + } + else + { + PseudoClasses.Set(pcDarkSelector, true); + PseudoClasses.Set(pcLightSelector, false); + } + } + } + /// /// Generates a new background image for the color slider and applies it. /// @@ -80,7 +129,7 @@ namespace Avalonia.Controls.Primitives break; case ColorComponent.Component1: // Hue Minimum = 0; - Maximum = 359; + Maximum = MaxHue; Value = hsvColor.H; break; case ColorComponent.Component2: // Saturation @@ -144,26 +193,22 @@ namespace Avalonia.Controls.Primitives { case ColorComponent.Alpha: { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - hsvColor = new HsvColor(componentValue, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + hsvColor = new HsvColor(sliderPercent, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); break; } case ColorComponent.Component1: { - var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); - hsvColor = new HsvColor(baseHsvColor.A, componentValue, baseHsvColor.S, baseHsvColor.V); + hsvColor = new HsvColor(baseHsvColor.A, sliderPercent * MaxHue, baseHsvColor.S, baseHsvColor.V); break; } case ColorComponent.Component2: { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, componentValue, baseHsvColor.V); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, sliderPercent, baseHsvColor.V); break; } case ColorComponent.Component3: { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, componentValue); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, sliderPercent); break; } } @@ -279,6 +324,7 @@ namespace Avalonia.Controls.Primitives UpdateBackground(); } + UpdatePseudoClasses(); disableUpdates = false; } else if (change.Property == HsvColorProperty) @@ -293,6 +339,7 @@ namespace Avalonia.Controls.Primitives UpdateBackground(); } + UpdatePseudoClasses(); disableUpdates = false; } else if (change.Property == BoundsProperty) @@ -321,6 +368,7 @@ namespace Avalonia.Controls.Primitives HsvColor = color.ToHsv(); } + UpdatePseudoClasses(); disableUpdates = false; } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index 1ca9b12ffe..620e9f658d 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -159,14 +159,26 @@ - + + + + + - - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index b209fe75b3..8e5139975a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -118,6 +118,9 @@ + From 005907a93bf5f594694007de8b1fd91fda0cc466 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 27 Apr 2022 16:53:18 +0200 Subject: [PATCH 121/213] More hit testing fixes for embedded content runs --- src/Avalonia.Base/Media/GlyphRun.cs | 61 ++++++++++--------- .../Media/TextFormatting/TextFormatterImpl.cs | 24 ++++---- .../Media/TextFormatting/TextLineImpl.cs | 44 +++++++------ src/Skia/Avalonia.Skia/TextShaperImpl.cs | 3 +- .../Media/TextFormatting/TextLineTests.cs | 21 +++++-- 5 files changed, 81 insertions(+), 72 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 9a2645f03d..22be8d8865 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -28,6 +28,8 @@ namespace Avalonia.Media private IReadOnlyList? _glyphOffsets; private IReadOnlyList? _glyphClusters; + private int _offsetToFirstCharacter; + /// /// Initializes a new instance of the class by specifying properties of the class. /// @@ -203,7 +205,7 @@ namespace Avalonia.Media /// public double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter; var distance = 0.0; @@ -552,30 +554,20 @@ namespace Avalonia.Media } nextCluster = GlyphClusters[currentIndex]; - } - - if (nextCluster < Characters.Start) - { - nextCluster = Characters.Start; - } - - if (cluster < Characters.Start) - { - cluster = Characters.Start; - } + } int trailingLength; if (nextCluster == cluster) { - trailingLength = Characters.Start + Characters.Length - cluster; + trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster; } else { trailingLength = nextCluster - cluster; } - return new CharacterHit(cluster, trailingLength); + return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength); } /// @@ -609,6 +601,13 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { + if (GlyphClusters != null && GlyphClusters.Count > 0) + { + var firstCluster = GlyphClusters[0]; + + _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); + } + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var widthIncludingTrailingWhitespace = 0d; @@ -680,34 +679,40 @@ namespace Avalonia.Media { for (var i = GlyphClusters.Count - 1; i >= 0; i--) { - var cluster = GlyphClusters[i]; - - var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster; + var currentCluster = GlyphClusters[i]; + var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); - if (codepointIndex < 0) + if (!codepoint.IsWhiteSpace) { - trailingWhitespaceLength = _characters.Length; - - glyphCount = GlyphClusters.Count; - break; } - var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _); + var clusterLength = 1; - if (!codepoint.IsWhiteSpace) + while(i - 1 >= 0) { + var nextCluster = GlyphClusters[i - 1]; + + if(currentCluster == nextCluster) + { + clusterLength++; + i--; + + continue; + } + break; } if (codepoint.IsBreakChar) { - newLineLength++; + newLineLength += clusterLength; } - trailingWhitespaceLength++; - - glyphCount++; + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 67fba00ee8..7f0f204886 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[i]; - if (currentLength + currentRun.Text.Length < length) + if (currentLength + currentRun.TextSourceLength < length) { currentLength += currentRun.TextSourceLength; continue; } - var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i; + var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i; var first = new List(firstCount); @@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.Text.Length == length) + if (currentLength + currentRun.TextSourceLength == length) { var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { - var offset = currentRun.Text.Length >= 1 ? 1 : 0; + var offset = currentRun.TextSourceLength >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -124,16 +124,14 @@ namespace Avalonia.Media.TextFormatting var second = new List(secondCount); - if (currentRun is not ShapedTextCharacters shapedTextCharacters) + if (currentRun is ShapedTextCharacters shapedTextCharacters) { - throw new NotSupportedException("Only shaped runs can be split in between."); - } - - var split = shapedTextCharacters.Split(length - currentLength); + var split = shapedTextCharacters.Split(length - currentLength); - first.Add(split.First); + first.Add(split.First); - second.Add(split.Second!); + second.Add(split.Second!); + } for (var j = 1; j < secondCount; j++) { @@ -483,7 +481,7 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedTextCharacters: { - var firstCluster = shapedTextCharacters.Text.Start; + var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0]; var lastCluster = firstCluster; for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) @@ -492,7 +490,7 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { - measuredLength += Math.Max(0, lastCluster - firstCluster + 1); + measuredLength += Math.Max(0, lastCluster - firstCluster); goto found; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b480774d1d..6a704f6f3e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -404,7 +404,7 @@ namespace Avalonia.Media.TextFormatting var result = new List(TextRuns.Count); var lastDirection = _flowDirection; var currentDirection = lastDirection; - var currentPosition = 0; + var currentPosition = FirstTextSourceIndex; var currentRect = Rect.Empty; var startX = Start; @@ -418,6 +418,11 @@ namespace Avalonia.Media.TextFormatting continue; } + if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex) + { + continue; + } + TextRun? nextRun = null; if (index + 1 < TextRuns.Count) @@ -1018,31 +1023,21 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { - var start = 0d; - var height = 0d; + var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; + var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; + var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; + var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; var newLineLength = 0; - var ascent = 0d; - var descent = 0d; - var lineGap = 0d; - var fontRenderingEmSize = 0d; + var ascent = glyphTypeface.Ascent * scale; + var descent = glyphTypeface.Descent * scale; + var lineGap = glyphTypeface.LineGap * scale; - var lineHeight = _paragraphProperties.LineHeight; - - if (_textRuns.Count == 0) - { - var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; - fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; - var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; - ascent = glyphTypeface.Ascent * scale; - height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ? - descent - ascent + lineGap : - lineHeight; - - return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0); - } + var height = descent - ascent + lineGap; + + var lineHeight = _paragraphProperties.LineHeight; for (var index = 0; index < _textRuns.Count; index++) { @@ -1166,12 +1161,15 @@ namespace Avalonia.Media.TextFormatting } } - start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth, + var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth, _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { - height = lineHeight; + if(lineHeight > height) + { + height = lineHeight; + } } return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index a0890262e7..ebaa247da8 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -59,7 +58,7 @@ namespace Avalonia.Skia var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e3b9e5a8b1..a47638d2ec 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -665,17 +665,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var text = "0123".AsMemory(); var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); - var shapedBuffer = TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption); - var firstRun = new ShapedTextCharacters(shapedBuffer, defaultProperties); + var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, 1, text.Length), shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(shapedBuffer, defaultProperties), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 2, text.Length), shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(shapedBuffer, defaultProperties) + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -691,15 +690,25 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); - textBounds = textLine.GetTextBounds(0, firstRun.Text.Length); + textBounds = textLine.GetTextBounds(0, 1); Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + Assert.Equal(14, textBounds[0].Rectangle.Width); textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + + textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + + textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); } } From a34e0f50e2367a45da815ea8dd36b0de4dca167c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 27 Apr 2022 17:55:00 +0200 Subject: [PATCH 122/213] Bump --- .../Media/TextFormatting/TextLayoutTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index 6ed4ba0d4a..b668f4d39e 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -1,11 +1,8 @@ using Avalonia.Media; -using Avalonia.Platform; using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Media.TextFormatting; From 25a34efd9a01cfb86698038cc54c4292ec1947ff Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:08:33 -0400 Subject: [PATCH 123/213] Implement color display name in ColorSpectrum --- .../ColorPreviewer.Properties.cs | 2 +- .../ColorSpectrum/ColorSpectrum.cs | 60 +++++---- .../Helpers/ColorHelpers.cs | 24 +--- .../Helpers/ColorNameHelpers.cs | 116 ++++++++++++++++++ .../Themes/Default/ColorSpectrum.xaml | 16 +-- .../Themes/Fluent/ColorSpectrum.xaml | 16 +-- 6 files changed, 174 insertions(+), 60 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index 903b5fb52b..f90f02551d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -41,7 +41,7 @@ namespace Avalonia.Controls.Primitives /// public bool ShowAccentColors { - get => (bool)this.GetValue(ShowAccentColorsProperty); + get => GetValue(ShowAccentColorsProperty); set => SetValue(ShowAccentColorsProperty, value); } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index fe9a2fac43..9b2459265d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,7 +20,6 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// - [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] [TemplatePart("PART_InputTarget", typeof(Canvas))] [TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] @@ -60,7 +59,6 @@ namespace Avalonia.Controls.Primitives private Ellipse? _spectrumOverlayEllipse; private Canvas? _inputTarget; private Panel? _selectionEllipsePanel; - private ToolTip? _colorNameToolTip; // Put the spectrum images in a bitmap, which is then given to an ImageBrush. private WriteableBitmap? _hueRedBitmap; @@ -117,7 +115,6 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); // Failsafe - _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); @@ -152,10 +149,10 @@ namespace Avalonia.Controls.Primitives }); } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color)); } // If we haven't yet created our bitmaps, do so now. @@ -338,26 +335,45 @@ namespace Avalonia.Controls.Primitives protected override void OnGotFocus(GotFocusEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, true); + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); + + base.OnGotFocus(e); } /// protected override void OnLostFocus(RoutedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) + { + ToolTip.SetIsOpen(_selectionEllipsePanel, false); + } + + UpdatePseudoClasses(); + + base.OnLostFocus(e); + } + + /// + protected override void OnPointerLeave(PointerEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, false); + ToolTip.SetIsOpen(_selectionEllipsePanel, false); } UpdatePseudoClasses(); + + base.OnPointerLeave(e); } /// @@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); - if (ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); - } + ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color)); } } } @@ -811,15 +825,11 @@ namespace Avalonia.Controls.Primitives Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. - if (ColorHelpers.ToDisplayNameExists) + if (IsFocused && + _selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, - // so toggling IsEnabled induces it to do that without incurring any visual glitches. - _colorNameToolTip.IsEnabled = false; - _colorNameToolTip.IsEnabled = true; - } + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs index 6500d10fe9..36ee478c7b 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs @@ -17,18 +17,6 @@ namespace Avalonia.Controls.Primitives { internal static class ColorHelpers { - public const int CheckerSize = 4; - - public static bool ToDisplayNameExists - { - get => false; - } - - public static string ToDisplayName(Color color) - { - return string.Empty; - } - /// /// Generates a new bitmap of the specified size by changing a specific color component. /// This will produce a gradient representing a sweep of all possible values of the color component. @@ -325,7 +313,7 @@ namespace Avalonia.Controls.Primitives { Hsv newHsv = originalHsv; - if (amount == IncrementAmount.Small || !ToDisplayNameExists) + if (amount == IncrementAmount.Small || !ColorNameHelpers.ToDisplayNameExists) { // In order to avoid working with small values that can incur rounding issues, // we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1. @@ -416,7 +404,7 @@ namespace Avalonia.Controls.Primitives // in the middle of that color's bounds. Hsv newHsv = originalHsv; - string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); + string originalColorName = ColorNameHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); string newColorName = originalColorName; // Note: *newValue replaced with ref local variable for C#, must be initialized @@ -471,7 +459,7 @@ namespace Avalonia.Controls.Primitives { newValue = maxBound; shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); break; } } @@ -486,7 +474,7 @@ namespace Avalonia.Controls.Primitives { newValue = minBound; shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); break; } } @@ -501,7 +489,7 @@ namespace Avalonia.Controls.Primitives break; } - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); } if (shouldFindMidPoint) @@ -574,7 +562,7 @@ namespace Avalonia.Controls.Primitives } } - currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); + currentColorName = ColorNameHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); } newValue = (startValue + currentValue + startEndOffset) / 2; diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs new file mode 100644 index 0000000000..303a52f00b --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using Avalonia.Media; +using System.Text; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains helpers useful when working with color names. + /// + public static class ColorNameHelpers + { + private static readonly Dictionary cachedDisplayNames = new Dictionary(); + private static readonly object cacheMutex = new object(); + + /// + /// Determines if color display names are supported based on the current thread culture. + /// + /// + /// Only English names are currently supported following known color names. + /// In the future known color names could be localized. + /// + public static bool ToDisplayNameExists + { + get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines an approximate display name for the given color. + /// + /// The color to get the display name for. + /// The approximate color display name. + public static string ToDisplayName(Color color) + { + // Without rounding, there are 16,777,216 possible RGB colors (without alpha). + // This is too many to cache and search through for performance reasons. + // It is also needlessly large as there are only ~140 known/named colors. + // Therefore, rounding of the input color's component values is done to + // reduce the color space into something more useful. + double rounding = 5; + var roundedColor = new Color( + 0xFF, + Convert.ToByte(Math.Round(color.R / rounding) * rounding), + Convert.ToByte(Math.Round(color.G / rounding) * rounding), + Convert.ToByte(Math.Round(color.B / rounding) * rounding)); + + // Attempt to use a previously cached display name + lock (cacheMutex) + { + if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) + { + return displayName; + } + } + + // Find the closest known color by measuring 3D Euclidean distance (ignore alpha) + var closestKnownColor = KnownColor.None; + var closestKnownColorDistance = double.PositiveInfinity; + var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); + + for (int i = 1; i < knownColors.Length; i++) // Skip 'None' + { + // Transparent is skipped since alpha is ignored making it equivalent to White + if (knownColors[i] != KnownColor.Transparent) + { + Color knownColor = KnownColors.ToColor(knownColors[i]); + + double distance = Math.Sqrt( + Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + + Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + + Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); + + if (distance < closestKnownColorDistance) + { + closestKnownColor = knownColors[i]; + closestKnownColorDistance = distance; + } + } + } + + // Return the closest known color as the display name + // Cache results for next time as well + if (closestKnownColor != KnownColor.None) + { + StringBuilder sb = new StringBuilder(); + string name = closestKnownColor.ToString(); + + // Add spaces converting PascalCase to human-readable names + for (int i = 0; i < name.Length; i++) + { + if (i != 0 && + char.IsUpper(name[i])) + { + sb.Append(' '); + } + + sb.Append(name[i]); + } + + string displayName = sb.ToString(); + + lock (cacheMutex) + { + cachedDisplayNames.Add(roundedColor, displayName); + } + + return displayName; + } + else + { + return string.Empty; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 9596ca9653..78e6da8aa3 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + - + + - - - - + VerticalAlignment="Stretch" /> + + + Date: Wed, 27 Apr 2022 22:37:36 -0400 Subject: [PATCH 124/213] Follow Avalonia convention --- .../ColorPreviewer/ColorPreviewer.cs | 6 ------ .../ColorSlider/ColorSlider.cs | 4 ---- .../ColorSpectrum/ColorSpectrum.cs | 2 -- 3 files changed, 12 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 1c0dd2154a..35072d6a42 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -61,8 +61,6 @@ namespace Avalonia.Controls.Primitives eventsConnected = false; } - - return; } /// @@ -92,8 +90,6 @@ namespace Avalonia.Controls.Primitives HsvColor = newColor; ColorChanged?.Invoke(this, new ColorChangedEventArgs(oldColor.ToRgb(), newColor.ToRgb())); - - return; } /// @@ -115,8 +111,6 @@ namespace Avalonia.Controls.Primitives HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); OnColorChanged(newHsvColor); - - return; } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 9d9e9f393e..c73f2b1cea 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -100,8 +100,6 @@ namespace Avalonia.Controls.Primitives Background = ColorHelpers.BitmapToBrushAsync(bitmap, pixelWidth, pixelHeight); } } - - return; } /// @@ -170,8 +168,6 @@ namespace Avalonia.Controls.Primitives break; } } - - return; } /// diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9b2459265d..7b68068d46 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -327,8 +327,6 @@ namespace Avalonia.Controls.Primitives maxBound)); e.Handled = true; - - return; } /// From d9ef01acfcd9d7d504558ca7ac92941351a94cb3 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:40:26 -0400 Subject: [PATCH 125/213] Render the ColorSpectrum to physical device pixel resolution --- .../ColorSpectrum/ColorSpectrum.cs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 7b68068d46..4cabb5c5b7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; @@ -587,8 +588,10 @@ namespace Avalonia.Controls.Primitives return; } - double xPosition = point.Position.X; - double yPosition = point.Position.Y; + // Remember the bitmap size follows physical device pixels + var scale = LayoutHelper.GetLayoutScale(this); + double xPosition = point.Position.X * scale; + double yPosition = point.Position.Y * scale; double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2)); @@ -819,8 +822,10 @@ namespace Avalonia.Controls.Primitives yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; } - Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2)); - Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); + // Remember the bitmap size follows physical device pixels + var scale = LayoutHelper.GetLayoutScale(this); + Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2)); + Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. if (IsFocused && @@ -969,7 +974,14 @@ namespace Avalonia.Controls.Primitives List bgraMaxPixelData = new List(); List newHsvValues = new List(); - var pixelCount = (int)(Math.Round(minDimension) * Math.Round(minDimension)); + // In Avalonia, Bounds returns the actual device-independent pixel size of a control. + // However, this is not necessarily the size of the control rendered on a display. + // A desktop or application scaling factor may be applied which must be accounted for here. + // Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device- + // independent pixels of controls. + var scale = LayoutHelper.GetLayoutScale(this); + int pixelDimension = (int)Math.Round(minDimension * scale); + var pixelCount = pixelDimension * pixelDimension; var pixelDataSize = pixelCount * 4; bgraMinPixelData.Capacity = pixelDataSize; @@ -986,8 +998,6 @@ namespace Avalonia.Controls.Primitives bgraMaxPixelData.Capacity = pixelDataSize; newHsvValues.Capacity = pixelCount; - int minDimensionInt = (int)Math.Round(minDimension); - await Task.Run(() => { // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes, @@ -1006,12 +1016,12 @@ namespace Avalonia.Controls.Primitives // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap. if (shape == ColorSpectrumShape.Box) { - for (int x = minDimensionInt - 1; x >= 0; --x) + for (int x = pixelDimension - 1; x >= 0; --x) { - for (int y = minDimensionInt - 1; y >= 0; --y) + for (int y = pixelDimension - 1; y >= 0; --y) { FillPixelForBox( - x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, hsv, pixelDimension, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1019,12 +1029,12 @@ namespace Avalonia.Controls.Primitives } else { - for (int y = 0; y < minDimensionInt; ++y) + for (int y = 0; y < pixelDimension; ++y) { - for (int x = 0; x < minDimensionInt; ++x) + for (int x = 0; x < pixelDimension; ++x) { FillPixelForRing( - x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, pixelDimension / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1034,8 +1044,8 @@ namespace Avalonia.Controls.Primitives Dispatcher.UIThread.Post(() => { - int pixelWidth = (int)Math.Round(minDimension); - int pixelHeight = (int)Math.Round(minDimension); + int pixelWidth = pixelDimension; + int pixelHeight = pixelDimension; ColorSpectrumComponents components2 = Components; @@ -1066,8 +1076,8 @@ namespace Avalonia.Controls.Primitives _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; - _imageWidthFromLastBitmapCreation = minDimension; - _imageHeightFromLastBitmapCreation = minDimension; + _imageWidthFromLastBitmapCreation = pixelDimension; + _imageHeightFromLastBitmapCreation = pixelDimension; _minHueFromLastBitmapCreation = MinHue; _maxHueFromLastBitmapCreation = MaxHue; _minSaturationFromLastBitmapCreation = MinSaturation; @@ -1086,7 +1096,7 @@ namespace Avalonia.Controls.Primitives double x, double y, Hsv baseHsv, - double minDimension, + int minDimension, ColorSpectrumComponents components, double minHue, double maxHue, From aef0d012258da94978d96ff38ce19af1c6554090 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:47:29 -0400 Subject: [PATCH 126/213] Reorder properties following Avalonia convention --- .../ColorPreviewer.Properties.cs | 16 +-- .../ColorSlider/ColorSlider.Properties.cs | 96 ++++++------- .../ColorSpectrum/ColorSpectrum.Properties.cs | 136 +++++++++--------- 3 files changed, 124 insertions(+), 124 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index f90f02551d..c545f25298 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -13,6 +13,14 @@ namespace Avalonia.Controls.Primitives nameof(HsvColor), Colors.Transparent.ToHsv()); + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + /// /// Gets or sets the currently previewed color in the HSV color model. /// @@ -27,14 +35,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ShowAccentColorsProperty = - AvaloniaProperty.Register( - nameof(ShowAccentColors), - true); - /// /// Gets or sets a value indicating whether accent colors are shown along /// with the preview color. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index 3aa3e3a789..12dce0b03e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -13,6 +13,54 @@ namespace Avalonia.Controls.Primitives nameof(Color), Colors.White); + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorComponentProperty = + AvaloniaProperty.Register( + nameof(ColorComponent), + ColorComponent.Component1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv()); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsAlphaMaxForced), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAutoUpdatingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAutoUpdatingEnabled), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSaturationValueMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsSaturationValueMaxForced), + true); + /// /// Gets or sets the currently selected color in the RGB color model. /// @@ -26,14 +74,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorComponentProperty = - AvaloniaProperty.Register( - nameof(ColorComponent), - ColorComponent.Component1); - /// /// Gets or sets the color component represented by the slider. /// @@ -43,14 +83,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorComponentProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorModelProperty = - AvaloniaProperty.Register( - nameof(ColorModel), - ColorModel.Rgba); - /// /// Gets or sets the active color model used by the slider. /// @@ -60,14 +92,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorModelProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register( - nameof(HsvColor), - Colors.White.ToHsv()); - /// /// Gets or sets the currently selected color in the HSV color model. /// @@ -81,14 +105,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAlphaMaxForcedProperty = - AvaloniaProperty.Register( - nameof(IsAlphaMaxForced), - true); - /// /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components /// other than . @@ -100,14 +116,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAutoUpdatingEnabledProperty = - AvaloniaProperty.Register( - nameof(IsAutoUpdatingEnabled), - true); - /// /// Gets or sets a value indicating whether automatic background and foreground updates will be /// calculated when the set color changes. @@ -121,14 +129,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAutoUpdatingEnabledProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty IsSaturationValueMaxForcedProperty = - AvaloniaProperty.Register( - nameof(IsSaturationValueMaxForced), - true); - /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index ab5b83afcb..a1cb43a95a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -10,6 +10,74 @@ namespace Avalonia.Controls.Primitives /// public partial class ColorSpectrum { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ComponentsProperty = + AvaloniaProperty.Register( + nameof(Components), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv()); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register(nameof(MaxHue), 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register(nameof(MaxSaturation), 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register(nameof(MaxValue), 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register(nameof(MinHue), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register(nameof(MinSaturation), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register(nameof(MinValue), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShapeProperty = + AvaloniaProperty.Register( + nameof(Shape), + ColorSpectrumShape.Box); + /// /// Gets or sets the currently selected color in the RGB color model. /// @@ -23,14 +91,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorProperty = - AvaloniaProperty.Register( - nameof(Color), - Colors.White); - /// /// Gets or sets the two HSV color components displayed by the spectrum. /// @@ -43,14 +103,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ComponentsProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ComponentsProperty = - AvaloniaProperty.Register( - nameof(Components), - ColorSpectrumComponents.HueSaturation); - /// /// Gets or sets the currently selected color in the HSV color model. /// @@ -65,14 +117,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register( - nameof(HsvColor), - Colors.White.ToHsv()); - /// /// Gets or sets the maximum value of the Hue component in the range from 0..359. /// This property must be greater than . @@ -86,12 +130,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxHueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxHueProperty = - AvaloniaProperty.Register(nameof(MaxHue), 359); - /// /// Gets or sets the maximum value of the Saturation component in the range from 0..100. /// This property must be greater than . @@ -105,12 +143,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxSaturationProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxSaturationProperty = - AvaloniaProperty.Register(nameof(MaxSaturation), 100); - /// /// Gets or sets the maximum value of the Value component in the range from 0..100. /// This property must be greater than . @@ -124,12 +156,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxValueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxValueProperty = - AvaloniaProperty.Register(nameof(MaxValue), 100); - /// /// Gets or sets the minimum value of the Hue component in the range from 0..359. /// This property must be less than . @@ -143,12 +169,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinHueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinHueProperty = - AvaloniaProperty.Register(nameof(MinHue), 0); - /// /// Gets or sets the minimum value of the Saturation component in the range from 0..100. /// This property must be less than . @@ -162,12 +182,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinSaturationProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinSaturationProperty = - AvaloniaProperty.Register(nameof(MinSaturation), 0); - /// /// Gets or sets the minimum value of the Value component in the range from 0..100. /// This property must be less than . @@ -181,12 +195,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinValueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinValueProperty = - AvaloniaProperty.Register(nameof(MinValue), 0); - /// /// Gets or sets the displayed shape of the spectrum. /// @@ -195,13 +203,5 @@ namespace Avalonia.Controls.Primitives get => GetValue(ShapeProperty); set => SetValue(ShapeProperty, value); } - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShapeProperty = - AvaloniaProperty.Register( - nameof(Shape), - ColorSpectrumShape.Box); } } From 21190ff39bba3fcb242cf3225247b36d74b3d082 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:53:16 -0400 Subject: [PATCH 127/213] Add :dark-selector PseudoClass to ColorSpectrum This standardizes with ColorSlider (which requires three states) but so far isn't needed in the templates. --- .../ColorSpectrum/ColorSpectrum.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 4cabb5c5b7..563fa24c08 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -29,10 +29,11 @@ namespace Avalonia.Controls.Primitives [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))] - [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] + [PseudoClasses(pcPressed, pcLargeSelector, pcDarkSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { protected const string pcPressed = ":pressed"; + protected const string pcDarkSelector = ":dark-selector"; protected const string pcLargeSelector = ":large-selector"; protected const string pcLightSelector = ":light-selector"; @@ -556,7 +557,16 @@ namespace Avalonia.Controls.Primitives PseudoClasses.Set(pcLargeSelector, false); } - PseudoClasses.Set(pcLightSelector, SelectionEllipseShouldBeLight()); + if (SelectionEllipseShouldBeLight()) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, true); + } + else + { + PseudoClasses.Set(pcDarkSelector, true); + PseudoClasses.Set(pcLightSelector, false); + } } private void UpdateColor(Hsv newHsv) From c3cdf856a3345f74413aed31febb61d4f214eb21 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:14:29 -0400 Subject: [PATCH 128/213] Add default themes for ColorSlider and ColorPreviewer --- .../Themes/Default.xaml | 21 ++ .../Themes/Default/ColorPreviewer.xaml | 90 ++++++++ .../Themes/Default/ColorSlider.xaml | 198 ++++++++++++++++++ .../Themes/Fluent/ColorSlider.xaml | 32 ++- 4 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 528eed9969..6d2f979f6e 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,7 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml new file mode 100644 index 0000000000..fe35dbd587 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -0,0 +1,90 @@ + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml new file mode 100644 index 0000000000..c0b78d628a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index 620e9f658d..54f58d2a8f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -8,6 +8,20 @@ + + - - - - - - - From a86e0cc64e18787cae2ebb7b531966ee8b3ec0dd Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:19:02 -0400 Subject: [PATCH 129/213] Move ColorPicker theme definitions into theme folders --- samples/ControlCatalog/App.xaml.cs | 4 ++-- .../Themes/{ => Default}/Default.xaml | 0 .../Themes/{ => Fluent}/Fluent.xaml | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/Avalonia.Controls.ColorPicker/Themes/{ => Default}/Default.xaml (100%) rename src/Avalonia.Controls.ColorPicker/Themes/{ => Fluent}/Fluent.xaml (100%) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6539cdaee6..7ebb87094a 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -20,12 +20,12 @@ namespace ControlCatalog public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml") }; public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml") }; public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml similarity index 100% rename from src/Avalonia.Controls.ColorPicker/Themes/Default.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml similarity index 100% rename from src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml From e0bc2e35c50e96563e9dd0f24cc33160cf8765be Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:25:11 -0400 Subject: [PATCH 130/213] Add RgbComponent enum and support direct casting with all component enums --- .../ColorComponent.cs | 8 ++-- .../HsvComponent.cs | 22 +++++----- .../RgbComponent.cs | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/RgbComponent.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs index a0385c03b4..71725056cf 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs @@ -8,21 +8,21 @@ /// /// Represents the alpha component. /// - Alpha, + Alpha = 0, /// /// Represents the first color component which is Red when RGB or Hue when HSV. /// - Component1, + Component1 = 1, /// /// Represents the second color component which is Green when RGB or Saturation when HSV. /// - Component2, + Component2 = 2, /// /// Represents the third color component which is Blue when RGB or Value when HSV. /// - Component3 + Component3 = 3 } } diff --git a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs index 1132bd7bbb..1a7a13166a 100644 --- a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs +++ b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs @@ -12,13 +12,21 @@ namespace Avalonia.Controls /// public enum HsvComponent { + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha = 0, + /// /// The Hue component. /// /// /// Also see: /// - Hue, + Hue = 1, /// /// The Saturation component. @@ -26,7 +34,7 @@ namespace Avalonia.Controls /// /// Also see: /// - Saturation, + Saturation = 2, /// /// The Value component. @@ -34,14 +42,6 @@ namespace Avalonia.Controls /// /// Also see: /// - Value, - - /// - /// The Alpha component. - /// - /// - /// Also see: - /// - Alpha + Value = 3 }; } diff --git a/src/Avalonia.Controls.ColorPicker/RgbComponent.cs b/src/Avalonia.Controls.ColorPicker/RgbComponent.cs new file mode 100644 index 0000000000..c3591573bb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/RgbComponent.cs @@ -0,0 +1,42 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Defines a specific component in the RGB color model. + /// + public enum RgbComponent + { + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha = 0, + + /// + /// The Red component. + /// + /// + /// Also see: + /// + Red = 1, + + /// + /// The Green component. + /// + /// + /// Also see: + /// + Green = 2, + + /// + /// The Blue component. + /// + /// + /// Also see: + /// + Blue = 3 + }; +} From c3ce137bda4483c51d2cca1b79c761ca326f6be8 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:27:05 -0400 Subject: [PATCH 131/213] Move AccentColorConverter in Converters directory --- .../{ColorPreviewer => Converters}/AccentColorConverter.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorPreviewer => Converters}/AccentColorConverter.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs rename to src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs From f550b8f9e82fa0f02490316a2e0d07840d73b0b4 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:30:22 -0400 Subject: [PATCH 132/213] Move AccentColorConverter in Avalonia.Controls.Primitives.Converters namespace This better hides these special-purpose converters --- .../ColorPreviewer/ColorPreviewer.cs | 1 + .../Converters/AccentColorConverter.cs | 2 +- .../Themes/Default/ColorPreviewer.xaml | 4 ++-- .../Themes/Fluent/ColorPreviewer.xaml | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 35072d6a42..3c429783d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives.Converters; using Avalonia.Input; using Avalonia.Media; diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs index ad8f66251a..07ebc899db 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -3,7 +3,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; -namespace Avalonia.Controls.Primitives +namespace Avalonia.Controls.Primitives.Converters { /// /// Creates an accent color for a given base color value and step parameter. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index fe35dbd587..9100bf0440 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,10 +1,10 @@  - + - - - - diff --git a/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..9b7fcecf45 --- /dev/null +++ b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Default +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 35603fe216..ede0791438 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,6 +10,15 @@ + + + + + + + MSBuild:Compile + + diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 7860e08ef5..6251c86720 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -4,12 +4,13 @@ x:CompileBindings="True" Selector="NativeMenuBar"> - + + diff --git a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..34670882f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Fluent +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} From 8772a46bbaab12d3d77b76fd97eed4cffd11e365 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Apr 2022 14:26:28 +0200 Subject: [PATCH 150/213] Fix scaled text measurements --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 4 +++- src/Avalonia.Controls/TextBlock.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index db1bbdbc6c..c3912077ee 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -532,7 +532,9 @@ namespace Avalonia.Controls.Presenters return finalSize; } - _constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); + var textSize = PixelSize.FromSize(finalSize, 1); + + _constraint = new Size(textSize.Width, textSize.Height); _textLayout = null; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c04a62008b..7427f21134 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -626,9 +626,9 @@ namespace Avalonia.Controls var padding = Padding; - var textSize = finalSize.Deflate(padding); + var textSize = PixelSize.FromSize(finalSize.Deflate(padding), 1); - _constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height)); + _constraint = new Size(textSize.Width, textSize.Height); _textLayout = null; From 2425cbf7aa2504b96b6fe441ef9522de92bbc98b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Apr 2022 17:03:05 +0200 Subject: [PATCH 151/213] Use RenderScaling in the ArrangeOverride --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index c3912077ee..eecf0d9101 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -532,7 +532,7 @@ namespace Avalonia.Controls.Presenters return finalSize; } - var textSize = PixelSize.FromSize(finalSize, 1); + var textSize = PixelSize.FromSize(finalSize, VisualRoot?.RenderScaling ?? 1); _constraint = new Size(textSize.Width, textSize.Height); diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7427f21134..1087310841 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -626,7 +626,7 @@ namespace Avalonia.Controls var padding = Padding; - var textSize = PixelSize.FromSize(finalSize.Deflate(padding), 1); + var textSize = PixelSize.FromSize(finalSize.Deflate(padding), VisualRoot?.RenderScaling ?? 1); _constraint = new Size(textSize.Width, textSize.Height); From bf61dd9a1ed4237ac611c2d986b12a7e64f33216 Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Mon, 2 May 2022 10:17:00 +0200 Subject: [PATCH 152/213] macos - disable native menus completely --- native/Avalonia.Native/src/OSX/window.mm | 4 ---- 1 file changed, 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d16c466fe6..6adb177ae9 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -2233,10 +2233,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [NSApp setMenu:nativeAppMenu->GetNative()]; } - else - { - [NSApp setMenu:nullptr]; - } } -(void) applyMenu:(AvnMenu *)menu From a056f0b654ae220ddbb58a2648fd286676769fef Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 2 May 2022 11:39:08 +0200 Subject: [PATCH 153/213] Second attempt to fix text measurements with scaling --- .../Presenters/TextPresenter.cs | 27 ++++++++++++------- src/Avalonia.Controls/TextBlock.cs | 19 +++++++------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index eecf0d9101..0785149a73 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -511,30 +511,39 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } - + + protected override Size MeasureOverride(Size availableSize) { + if (string.IsNullOrEmpty(Text)) + { + return new Size(); + } + _constraint = availableSize; - + _textLayout = null; - + InvalidateArrange(); - var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); - - return new Size(measuredSize.Width, measuredSize.Height); + var measuredSize = TextLayout.Bounds.Size; + + return measuredSize; } protected override Size ArrangeOverride(Size finalSize) { + if (finalSize.Width < TextLayout.Bounds.Width) + { + finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); + } + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { return finalSize; } - var textSize = PixelSize.FromSize(finalSize, VisualRoot?.RenderScaling ?? 1); - - _constraint = new Size(textSize.Width, textSize.Height); + _constraint = new Size(finalSize.Width, double.PositiveInfinity); _textLayout = null; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1087310841..bbe6aeb7ee 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -604,7 +604,9 @@ namespace Avalonia.Controls return new Size(); } - var padding = Padding; + var scale = LayoutHelper.GetLayoutScale(this); + + var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); _constraint = availableSize.Deflate(padding); @@ -612,23 +614,24 @@ namespace Avalonia.Controls InvalidateArrange(); - var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); + var measuredSize = TextLayout.Bounds.Size.Inflate(padding); - return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); + return measuredSize; } protected override Size ArrangeOverride(Size finalSize) { + if(finalSize.Width < TextLayout.Bounds.Width) + { + finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); + } + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { return finalSize; } - var padding = Padding; - - var textSize = PixelSize.FromSize(finalSize.Deflate(padding), VisualRoot?.RenderScaling ?? 1); - - _constraint = new Size(textSize.Width, textSize.Height); + _constraint = new Size(finalSize.Width, double.PositiveInfinity); _textLayout = null; From bb8aaee1e0da5116d06febbe8aa512add3be0ce0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 2 May 2022 23:09:13 +0200 Subject: [PATCH 154/213] Optimizing resource related code. --- .../Controls/ResourceNodeExtensions.cs | 11 ++++------- src/Avalonia.Base/Styling/IStyle.cs | 2 +- src/Avalonia.Base/Styling/Styles.cs | 2 +- .../Controls/DataValidationErrors.xaml | 10 +++++++--- .../MarkupExtensions/StaticResourceExtension.cs | 17 +++++++---------- .../Styling/StyleInclude.cs | 6 +++--- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 513b3f2424..1758c45650 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -40,19 +40,16 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceHost? current = control; + IResourceNode? current = control; while (current != null) { - if (current is IResourceHost host) + if (current.TryGetResource(key, out value)) { - if (host.TryGetResource(key, out value)) - { - return true; - } + return true; } - current = (current as IStyledElement)?.StylingParent as IResourceHost; + current = (current as IStyledElement)?.StylingParent as IResourceNode; } value = null; diff --git a/src/Avalonia.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs index 78fbe0f2b5..738a69cb88 100644 --- a/src/Avalonia.Base/Styling/IStyle.cs +++ b/src/Avalonia.Base/Styling/IStyle.cs @@ -8,7 +8,7 @@ namespace Avalonia.Styling /// /// Defines the interface for styles. /// - public interface IStyle + public interface IStyle : IResourceNode { /// /// Gets a collection of child styles. diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 81502f1570..d79081152e 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -160,7 +160,7 @@ namespace Avalonia.Styling for (var i = Count - 1; i >= 0; --i) { - if (this[i] is IResourceProvider p && p.TryGetResource(key, out value)) + if (this[i].TryGetResource(key, out value)) { return true; } diff --git a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml index a3a4cf4662..d7bf4bbbf1 100644 --- a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml @@ -1,9 +1,13 @@ - + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index db33b88cc3..f865f87220 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions targetType = setter.Property.PropertyType; } - // Look upwards though the ambient context for IResourceHosts and IResourceProviders + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. - foreach (var e in stack.Parents) + foreach (var parent in stack.Parents) { - object value; - - if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) - { - return ColorToBrushConverter.Convert(value, targetType); - } - else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) + if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value)) { return ColorToBrushConverter.Convert(value, targetType); } @@ -58,7 +52,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, x => GetValue(x, targetType)); + var localTargetType = targetType; + var localInstance = this; + + DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType)); return AvaloniaProperty.UnsetValue; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 607b552c28..fa4a27fc50 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; + bool IResourceNode.HasResources => Loaded?.HasResources ?? false; IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); @@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object? value) { - if (!_isLoading && Loaded is IResourceProvider p) + if (!_isLoading) { - return p.TryGetResource(key, out value); + return Loaded.TryGetResource(key, out value); } value = null; From 4aa0f878c2890dca2ade446832192dafae5d0675 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 2 May 2022 23:25:18 +0200 Subject: [PATCH 155/213] Add an explanation why certain locals are copied. --- .../MarkupExtensions/StaticResourceExtension.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index f865f87220..add97a660b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -52,6 +52,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { + // This is stored locally to avoid allocating closure in the outer scope. var localTargetType = targetType; var localInstance = this; From 0a8d679ab29e3364cce0b716d7c9bc9f55380441 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 3 May 2022 11:54:35 +0200 Subject: [PATCH 156/213] Register StretchProperty for Viewbox instead of Image --- src/Avalonia.Controls/Viewbox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index dd74d549bd..33a05f126d 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty StretchProperty = - AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); /// /// Defines the property. From bdadb6a35111899747c353aa01a1ab334805a384 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 3 May 2022 12:04:26 +0200 Subject: [PATCH 157/213] Fix GetTextBounds for mixed runs --- .../Media/TextFormatting/TextLineImpl.cs | 47 ++++-------------- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 7 +-- .../Media/TextShaperImpl.cs | 33 +++++------- ...estrictedHeight_VerticalAlign.expected.png | Bin 768 -> 752 bytes ...estrictedHeight_VerticalAlign.expected.png | Bin 532 -> 557 bytes 5 files changed, 24 insertions(+), 63 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 35ada3c7ee..a518e2ffb8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -536,6 +536,11 @@ namespace Avalonia.Media.TextFormatting endX += currentRun.Size.Width; } + if(currentPosition < firstTextSourceCharacterIndex) + { + startX += currentRun.Size.Width; + } + currentPosition += currentRun.TextSourceLength; break; @@ -590,10 +595,10 @@ namespace Avalonia.Media.TextFormatting public TextLineImpl FinalizeLine() { - BidiReorder(); - _textLineMetrics = CreateLineMetrics(); + BidiReorder(); + return this; } @@ -1068,41 +1073,11 @@ namespace Avalonia.Media.TextFormatting } } - switch (_paragraphProperties.FlowDirection) + if (index == _textRuns.Count - 1) { - case FlowDirection.LeftToRight: - { - if (index == _textRuns.Count - 1) - { - width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; - trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewlineLength; - } - - break; - } - - case FlowDirection.RightToLeft: - { - if (index == _textRuns.Count - 1) - { - var firstRun = _textRuns[0]; - - if (firstRun is ShapedTextCharacters shapedTextCharacters) - { - var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - - shapedTextCharacters.GlyphRun.Metrics.Width; - - width = widthIncludingWhitespace + - textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset; - - trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength; - } - } - - break; - } + width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; + trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; + newLineLength = textRun.GlyphRun.Metrics.NewlineLength; } widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index ebaa247da8..908b0ffa47 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -27,7 +27,7 @@ namespace Avalonia.Skia buffer.GuessSegmentProperties(); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); @@ -35,11 +35,6 @@ namespace Avalonia.Skia font.Shape(buffer); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 59027a663f..f4e4b00147 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,8 +10,7 @@ using GlyphInfo = HarfBuzzSharp.GlyphInfo; namespace Avalonia.Direct2D1.Media { - -internal class TextShaperImpl : ITextShaperImpl + internal class TextShaperImpl : ITextShaperImpl { public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { @@ -23,25 +21,20 @@ internal class TextShaperImpl : ITextShaperImpl using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; font.Shape(buffer); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; @@ -60,13 +53,13 @@ internal class TextShaperImpl : ITextShaperImpl var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (glyphIndex == 0 && text[glyphCluster] == '\t') + if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); @@ -75,9 +68,7 @@ internal class TextShaperImpl : ITextShaperImpl 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = - new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, - glyphOffset); + var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; } @@ -91,7 +82,7 @@ internal class TextShaperImpl : ITextShaperImpl var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint((int)second.Codepoint).IsBreakChar) @@ -102,7 +93,7 @@ internal class TextShaperImpl : ITextShaperImpl if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') { var first = glyphInfos[length - 2]; - + first.Codepoint = '\u200C'; second.Codepoint = '\u200C'; second.Cluster = first.Cluster; @@ -113,7 +104,7 @@ internal class TextShaperImpl : ITextShaperImpl { *p = first; } - + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; @@ -148,7 +139,7 @@ internal class TextShaperImpl : ITextShaperImpl private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) { // Depends on direction of layout - // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } } diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png index edd4dfd263d0aeddca97eb4f38b4a5310ef6441f..e8624fa457f37565fdc483c474424991e7b696d3 100644 GIT binary patch delta 663 zcmV;I0%-k!2Ji)tK~kDYL_t(|UhUgIa??N*$8nhhgpv-9Kst&PbZ~-{bWo>HI%eP$ zbliY0bTFi#WQHbHCZVECE@0mCYC&Ev)~@tc(Vy>U=9gVr&L&sF5? z2a=zgt*ZJWIdk3Ow4gWZj_kMw*zr#C&2{Yze1z^>+>Eo|l*ZA6u^yCDYTA#}F7b|b z1-+DUQlBp7XTR-n+9lr6_JMtu-Y0j%e~v?mF0?|+c*2;U(Dpd(9@K4;4-y?YmRFK4 z^g(@eT|_WHMd&L`zBFy;-KXYy|(KU{b8zDnNKxsvFW zH-0;ezHL90^u_#aD^}3IBtx9|jEVjQPyh0jnrD(F8Df4|Lt|DbXw1sJ6tu3b_2LwN zfr_3W`0X%Otk(2a znEu5s`c3DXRyVzJM1r%nkRJ0aMM(FjDeD5@R(^af#<|TF^PCdjI796AKN{2Wx1| x3I&Z>NnUc)osL*?lhFbeli&gb8GzoZ>JL{KmA-v_Vov}7002ovPDHLkV1g47Q*QtO delta 680 zcmV;Z0$2U;1%L*SK~kzoL_t(|UhUg4a??N*#&MYggpv-903AgNIyeCxbfl&a9W!tW zI&MH0Iv6M@nW3S|1S(3nfO#)Z%WR}I+O^(-y!!uUzG!9H8)yBZ?8;6DlfeQOe*gdg zXxpaWPS|ey#tfUx?*;5HyP&tZcTL!I`ujjZqdp26OL9YU+nOU6lCRd>je^FKJd`}O zW_MF^B>8f=*LCkC {7WCyrdau|9DDpz`!J7I0!x`G6xaq6RA@-#wV?8P7*0lB2 zcR0njpx4+}$;-od?LS_b&&a;ve=BL7*bO6(B(JSGl6vx=r1i#b7`Y?S$yCbPULeM6 zTHMz){gUg6L?5a-l^jR=yNWr7KJht^UmM12HvJ8fFHxkRw^Gof+f%VkP`9s=*e0mk z7vps`wh8=x#-cMc>d`3$^6eTg3VIo*Bf3vR=Kl|;! z(DHaSQP7@?)mjVRC24uQm~x;g)e2hQi1%9P=%2M8);uWsoOPb3`6l@l?GNK=&Er+A zpubDJ=7suZ&Sz^Ll=qV7ea`gjQ**ChgxBkvx2!pv=VH~fuM&*cZ0&rgMIUtbr>&cyZeJy_O~80f#WsdHG}g%XEHOVp z>R4%l)Unb8sbi%HQpZXYq>i0Rjt95J(bw-R-tWct3GwW!G(|iMC>jgkxg4sswAxF~ z1qq=Aoq5-UtN(5T)WaMaO`)LClwW{S>gC-`V7Q6^ O0000Wy2)Tif!}f*BA9YQBr<>Yv<0+ed$LhGhl$6qoFtRV$DAqv>tEFoHy;= zjj8AUHLmvgqjP}4f0`Ym-l@|ncRSzK*B<`-V%Iyxa-Mt3j*FJP@}0!LwOcIxMdrlE z`EBc_9=&?AZmz%QzU4W~PG3(r%l<3q)s%g)(bchAU-i5`|6g~~!uOv(cvj{7-f@I; zZO^|R^Na=Arn~kV?eHzSTz#h~bwk)M4ct2k#G&)%(W*Dsx)C6j$|eyBU2 z#3_f(+TH&xR=QqWGxcS(?x8Q~dmJTy>788NygzpSjqgWi)>y6GoVM2Iepc=R`^UZi z3We58xyrynyb(IzS%pj{N(kv(qntpW!+KP9r*m+Cc8U!>XmlI yb8njNTQ2ZS)p@fv+pJj4eL2tLs~9ojqI);H&U7`F+XGKFP=N&RV07srr_IdAW5%)1;Q!uDXlX#W9ggKvom4qEyL4~4P0&0{{ck2_jY zz?fNE$h%Z>(W=lXQ?j31ZCk(J|C5mA@slS#KgDbNF)^S4hwFN;%CwgrT&UI0`gre( zyBn{Z`^PAr`Jt|XYkILgL&iym%_{3BPpMbF-QIRwbLpF=FDusivZW`ln|z07Z}8Ve zAHMMX-Jp@P^FYYz`%&?e|M>j%d7pLG=tA@w(@X6Ni@OMP+s%{|8UZRef2 zOxJB?{S~_W*Ou5%Gj2?N7|#)W?H1GDRgZPpV+}8q9Cgq?Fkj{U;Tz2DaF{%vno$(gm^S9kO54e|f1`v3Z(cq0>#J*i*%C*Mn*p3gV8zGmi= zcRgFP)>dv@6<_N1-{NIR@src0`e9#J_nmMI=P`-C@@dwg Date: Tue, 3 May 2022 14:38:34 +0200 Subject: [PATCH 158/213] Fix crash when property getter throws --- .../ViewModels/AvaloniaPropertyViewModel.cs | 64 +++++++++++++------ .../ViewModels/ClrPropertyViewModel.cs | 41 ++++++++---- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index aa03330cc5..0a2a6cac25 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,13 +1,17 @@ +using System; +using Avalonia.Data; +using Avalonia.Media; + namespace Avalonia.Diagnostics.ViewModels { internal class AvaloniaPropertyViewModel : PropertyViewModel { private readonly AvaloniaObject _target; - private System.Type _assignedType; + private Type _assignedType; private object? _value; private string _priority; private string _group; - private readonly System.Type _propertyType; + private readonly Type _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -28,13 +32,9 @@ namespace Avalonia.Diagnostics.ViewModels public AvaloniaProperty Property { get; } public override object Key => Property; public override string Name { get; } - public override bool? IsAttached => - Property.IsAttached; - - public override string Priority => - _priority; - - public override System.Type AssignedType => _assignedType; + public override bool? IsAttached => Property.IsAttached; + public override string Priority => _priority; + public override Type AssignedType => _assignedType; public override string? Value { @@ -53,30 +53,58 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; - public override System.Type? DeclaringType { get; } - public override System.Type PropertyType => _propertyType; + public override Type? DeclaringType { get; } + public override Type PropertyType => _propertyType; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { if (Property.IsDirect) { - RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType,_value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + object? value; + Type? valueType = null; + + try + { + value = _target.GetValue(Property); + valueType = value?.GetType(); + } + catch (Exception e) + { + value = e; + } + + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; } else { - var val = _target.GetDiagnostic(Property); + object? value; + Type? valueType = null; + BindingPriority? priority = null; + + try + { + var diag = _target.GetDiagnostic(Property); + + value = diag.Value; + valueType = value?.GetType(); + priority = diag.Priority; + } + catch (Exception e) + { + value = e; + } - RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); - if (val != null) + if (priority != null) { - RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); + RaiseAndSetIfChanged(ref _priority, priority.ToString()!, nameof(Priority)); RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); } else diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index e2d8a30c8a..296413de78 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,13 +1,15 @@ -using System.Reflection; +using System; +using System.Reflection; +using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { internal class ClrPropertyViewModel : PropertyViewModel { private readonly object _target; - private System.Type _assignedType; + private Type _assignedType; private object? _value; - private readonly System.Type _propertyType; + private readonly Type _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -25,6 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels { Name = property.DeclaringType.Name + '.' + property.Name; } + DeclaringType = property.DeclaringType; _propertyType = property.PropertyType; @@ -36,10 +39,10 @@ namespace Avalonia.Diagnostics.ViewModels public override string Name { get; } public override string Group => "CLR Properties"; - public override System.Type AssignedType => _assignedType; - public override System.Type PropertyType => _propertyType; + public override Type AssignedType => _assignedType; + public override Type PropertyType => _propertyType; - public override string? Value + public override string? Value { get => ConvertToString(_value); set @@ -54,20 +57,30 @@ namespace Avalonia.Diagnostics.ViewModels } } - public override string Priority => - string.Empty; + public override string Priority => string.Empty; - public override bool? IsAttached => - default; + public override bool? IsAttached => default; - public override System.Type? DeclaringType { get; } + public override Type? DeclaringType { get; } // [MemberNotNull(nameof(_type))] public override void Update() { - var val = Property.GetValue(_target); - RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + object? value; + Type? valueType = null; + + try + { + value = Property.GetValue(_target); + valueType = value?.GetType(); + } + catch (Exception e) + { + value = e; + } + + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaisePropertyChanged(nameof(Type)); } } From b5391f9419bd32f6410dc8c378ee3db3a9ad2e54 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 3 May 2022 16:34:15 +0200 Subject: [PATCH 159/213] Fix GetTextBounds for some Bidi scenarios --- .../Media/TextFormatting/TextLineImpl.cs | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a518e2ffb8..73ec055bbe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -252,7 +252,7 @@ namespace Avalonia.Media.TextFormatting //Look at the left and right edge of the current run if (currentRun.IsLeftToRight) { - if (lastRun == null || lastRun.IsLeftToRight) + if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) { if (characterIndex <= textRun.Text.Start) { @@ -455,7 +455,7 @@ namespace Avalonia.Media.TextFormatting } default: { - goto noop; + goto noop; } } @@ -536,7 +536,7 @@ namespace Avalonia.Media.TextFormatting endX += currentRun.Size.Width; } - if(currentPosition < firstTextSourceCharacterIndex) + if (currentPosition < firstTextSourceCharacterIndex) { startX += currentRun.Size.Width; } @@ -554,24 +554,29 @@ namespace Avalonia.Media.TextFormatting var width = endX - startX; - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + if (!MathUtilities.IsZero(width)) { - currentRect = currentRect.WithWidth(currentRect.Width + width); + if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + { + currentRect = currentRect.WithWidth(currentRect.Width + width); - var textBounds = new TextBounds(currentRect, currentDirection); + var textBounds = new TextBounds(currentRect, currentDirection); - result[result.Count - 1] = textBounds; - } - else - { - currentRect = new Rect(startX, 0, width, Height); + result[result.Count - 1] = textBounds; + } + else + { + + currentRect = new Rect(startX, 0, width, Height); - result.Add(new TextBounds(currentRect, currentDirection)); + result.Add(new TextBounds(currentRect, currentDirection)); + + } } if (currentDirection == FlowDirection.LeftToRight) { - if (currentPosition >= firstTextSourceCharacterIndex + textLength) + if (currentPosition > firstTextSourceCharacterIndex + textLength) { break; } @@ -1026,7 +1031,7 @@ namespace Avalonia.Media.TextFormatting var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; - + var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; @@ -1036,8 +1041,8 @@ namespace Avalonia.Media.TextFormatting var lineGap = glyphTypeface.LineGap * scale; var height = descent - ascent + lineGap; - - var lineHeight = _paragraphProperties.LineHeight; + + var lineHeight = _paragraphProperties.LineHeight; for (var index = 0; index < _textRuns.Count; index++) { @@ -1136,10 +1141,10 @@ namespace Avalonia.Media.TextFormatting if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { - if(lineHeight > height) + if (lineHeight > height) { height = lineHeight; - } + } } return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, From 6a8eb5a1cf551503c2090e3a0b8dfdc4eec8497c Mon Sep 17 00:00:00 2001 From: peter kuhn Date: Wed, 4 May 2022 06:58:39 +0200 Subject: [PATCH 160/213] Fix Typo --- src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj | 9 --------- src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index ede0791438..35603fe216 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,15 +10,6 @@ - - - - - - - MSBuild:Compile - - diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 6251c86720..d40ba0cc1d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -5,7 +5,7 @@ Selector="NativeMenuBar"> - + @@ -21,7 +21,7 @@ - + From c34b7f2d31531c0f1210d2ffd8adf132e574e3dd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 May 2022 13:35:34 +0200 Subject: [PATCH 161/213] Remove generic methods from ILogSink. Part of the push to remove generic virtual methods from Avalonia for performance reasons. Generic methods were added to this interface in #3055 to prevent boxing before we added `Logger.TryGet` (#4135). Given that since we added `Logger.TryGet`, only enabled logging levels will result in a call to the logger, we can move back to using `params object[]` and boxing; removing the generic interface methods. --- src/Avalonia.Base/Logging/ILogSink.cs | 51 ----------------------- src/Avalonia.Base/Logging/TraceLogSink.cs | 38 ++--------------- tests/Avalonia.UnitTests/TestLogSink.cs | 17 -------- 3 files changed, 3 insertions(+), 103 deletions(-) diff --git a/src/Avalonia.Base/Logging/ILogSink.cs b/src/Avalonia.Base/Logging/ILogSink.cs index 27558ba0ee..60709776c6 100644 --- a/src/Avalonia.Base/Logging/ILogSink.cs +++ b/src/Avalonia.Base/Logging/ILogSink.cs @@ -26,57 +26,6 @@ namespace Avalonia.Logging object? source, string messageTemplate); - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - void Log( - LogEventLevel level, - string area, - object? source, - string messageTemplate, - T0 propertyValue0); - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - /// Message property value. - void Log( - LogEventLevel level, - string area, - object? source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1); - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - /// Message property value. - /// Message property value. - void Log( - LogEventLevel level, - string area, - object? source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1, - T2 propertyValue2); - /// /// Logs a new event. /// diff --git a/src/Avalonia.Base/Logging/TraceLogSink.cs b/src/Avalonia.Base/Logging/TraceLogSink.cs index 02ed191d2c..05e4b8bc5a 100644 --- a/src/Avalonia.Base/Logging/TraceLogSink.cs +++ b/src/Avalonia.Base/Logging/TraceLogSink.cs @@ -28,31 +28,7 @@ namespace Avalonia.Logging { if (IsEnabled(level, area)) { - Trace.WriteLine(Format(area, messageTemplate, source)); - } - } - - public void Log(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0) - { - if (IsEnabled(level, area)) - { - Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0)); - } - } - - public void Log(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - if (IsEnabled(level, area)) - { - Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1)); - } - } - - public void Log(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - if (IsEnabled(level, area)) - { - Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2)); + Trace.WriteLine(Format(area, messageTemplate, source, null)); } } @@ -68,9 +44,7 @@ namespace Avalonia.Logging string area, string template, object? source, - T0? v0 = default, - T1? v1 = default, - T2? v2 = default) + object?[]? values) { var result = new StringBuilder(template.Length); var r = new CharacterReader(template.AsSpan()); @@ -93,13 +67,7 @@ namespace Avalonia.Logging if (r.Peek != '{') { result.Append('\''); - result.Append(i++ switch - { - 0 => v0, - 1 => v1, - 2 => v2, - _ => null - }); + result.Append(values?[i++]); result.Append('\''); r.TakeUntil('}'); r.Take(); diff --git a/tests/Avalonia.UnitTests/TestLogSink.cs b/tests/Avalonia.UnitTests/TestLogSink.cs index e10292a59b..8b8084ca17 100644 --- a/tests/Avalonia.UnitTests/TestLogSink.cs +++ b/tests/Avalonia.UnitTests/TestLogSink.cs @@ -37,23 +37,6 @@ namespace Avalonia.UnitTests _callback(level, area, source, messageTemplate); } - public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) - { - _callback(level, area, source, messageTemplate, propertyValue0); - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, - T0 propertyValue0, T1 propertyValue1) - { - _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1); - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, - T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); - } - public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) { From a3112b49e5c310a685d49876352257d85cac22f5 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 4 May 2022 14:02:21 +0200 Subject: [PATCH 162/213] Unwrap exceptions --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 4 ++-- .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 0a2a6cac25..7384daae30 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -71,7 +71,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch (Exception e) { - value = e; + value = e.GetBaseException(); } RaiseAndSetIfChanged(ref _value, value, nameof(Value)); @@ -96,7 +96,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch (Exception e) { - value = e; + value = e.GetBaseException(); } RaiseAndSetIfChanged(ref _value, value, nameof(Value)); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 296413de78..73fb615b32 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -76,7 +76,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch (Exception e) { - value = e; + value = e.GetBaseException(); } RaiseAndSetIfChanged(ref _value, value, nameof(Value)); From b3af7f0c6a012512520a09ac06a39fa5fc1b24c0 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 4 May 2022 14:45:02 +0200 Subject: [PATCH 163/213] Proper Canvas positioning for adorner layer --- src/Avalonia.Controls/Canvas.cs | 75 +++++++++++-------- .../Primitives/AdornerLayer.cs | 2 +- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index fabf8978c7..adee7d4d90 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Concurrency; using Avalonia.Input; using Avalonia.Layout; @@ -159,47 +160,57 @@ namespace Avalonia.Controls } /// - /// Arranges the control's children. + /// Arranges a single child. /// - /// The size allocated to the control. - /// The space taken. - protected override Size ArrangeOverride(Size finalSize) + /// The child to arrange. + /// The size allocated to the canvas. + protected virtual void ArrangeChild(Control child, Size finalSize) { - foreach (Control child in Children) - { - double x = 0.0; - double y = 0.0; - double elementLeft = GetLeft(child); + double x = 0.0; + double y = 0.0; + double elementLeft = GetLeft(child); - if (!double.IsNaN(elementLeft)) - { - x = elementLeft; - } - else + if (!double.IsNaN(elementLeft)) + { + x = elementLeft; + } + else + { + // Arrange with right. + double elementRight = GetRight(child); + if (!double.IsNaN(elementRight)) { - // Arrange with right. - double elementRight = GetRight(child); - if (!double.IsNaN(elementRight)) - { - x = finalSize.Width - child.DesiredSize.Width - elementRight; - } + x = finalSize.Width - child.DesiredSize.Width - elementRight; } + } - double elementTop = GetTop(child); - if (!double.IsNaN(elementTop) ) - { - y = elementTop; - } - else + double elementTop = GetTop(child); + if (!double.IsNaN(elementTop)) + { + y = elementTop; + } + else + { + double elementBottom = GetBottom(child); + if (!double.IsNaN(elementBottom)) { - double elementBottom = GetBottom(child); - if (!double.IsNaN(elementBottom)) - { - y = finalSize.Height - child.DesiredSize.Height - elementBottom; - } + y = finalSize.Height - child.DesiredSize.Height - elementBottom; } + } - child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); + child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); + } + + /// + /// Arranges the control's children. + /// + /// The size allocated to the control. + /// The space taken. + protected override Size ArrangeOverride(Size finalSize) + { + foreach (Control child in Children) + { + ArrangeChild(child, finalSize); } return finalSize; diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index fb047d93df..5ad4e39baf 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -105,7 +105,7 @@ namespace Avalonia.Controls.Primitives } else { - child.Arrange(new Rect(finalSize)); + ArrangeChild((Control) child, finalSize); } } } From 2cf78ac11c7badc0f9ddb015809266d052a810f6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 4 May 2022 17:38:03 +0100 Subject: [PATCH 164/213] move IWindowBaseImpl to its own file. --- .../Avalonia.Native/src/OSX/INSWindowHolder.h | 15 + .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 121 ++++ .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 511 ++++++++++++++ native/Avalonia.Native/src/OSX/window.h | 7 +- native/Avalonia.Native/src/OSX/window.mm | 621 +----------------- 5 files changed, 650 insertions(+), 625 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/INSWindowHolder.h create mode 100644 native/Avalonia.Native/src/OSX/WindowBaseImpl.h create mode 100644 native/Avalonia.Native/src/OSX/WindowBaseImpl.mm diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h new file mode 100644 index 0000000000..aa8c34ef00 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/INSWindowHolder.h @@ -0,0 +1,15 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H +#define AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H + +struct INSWindowHolder +{ + virtual AvnWindow* _Nonnull GetNSWindow () = 0; + virtual AvnView* _Nonnull GetNSView () = 0; +}; + +#endif //AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h new file mode 100644 index 0000000000..2f9b05988f --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -0,0 +1,121 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H +#define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H + +#include "INSWindowHolder.h" + +class WindowBaseImpl : public virtual ComObject, + public virtual IAvnWindowBase, + public INSWindowHolder { +private: + NSCursor *cursor; + +public: + FORWARD_IUNKNOWN() + +BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) + END_INTERFACE_MAP() + + virtual ~WindowBaseImpl() { + View = NULL; + Window = NULL; + } + + AutoFitContentView *StandardContainer; + AvnView *View; + AvnWindow *Window; + ComPtr BaseEvents; + ComPtr _glContext; + NSObject *renderTarget; + AvnPoint lastPositionSet; + NSString *_lastTitle; + IAvnMenu *_mainMenu; + + bool _shown; + bool _inResize; + + WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl); + + virtual HRESULT ObtainNSWindowHandle(void **ret) override; + + virtual HRESULT ObtainNSWindowHandleRetained(void **ret) override; + + virtual HRESULT ObtainNSViewHandle(void **ret) override; + + virtual HRESULT ObtainNSViewHandleRetained(void **ret) override; + + virtual AvnWindow *GetNSWindow() override; + + virtual AvnView *GetNSView() override; + + virtual HRESULT Show(bool activate, bool isDialog) override; + + virtual bool ShouldTakeFocusOnShow(); + + virtual HRESULT Hide() override; + + virtual HRESULT Activate() override; + + virtual HRESULT SetTopMost(bool value) override; + + virtual HRESULT Close() override; + + virtual HRESULT GetClientSize(AvnSize *ret) override; + + virtual HRESULT GetFrameSize(AvnSize *ret) override; + + virtual HRESULT GetScaling(double *ret) override; + + virtual HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize) override; + + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override; + + virtual HRESULT Invalidate(AvnRect rect) override; + + virtual HRESULT SetMainMenu(IAvnMenu *menu) override; + + virtual HRESULT BeginMoveDrag() override; + + virtual HRESULT BeginResizeDrag(AvnWindowEdge edge) override; + + virtual HRESULT GetPosition(AvnPoint *ret) override; + + virtual HRESULT SetPosition(AvnPoint point) override; + + virtual HRESULT PointToClient(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) override; + + virtual HRESULT SetCursor(IAvnCursor *cursor) override; + + virtual void UpdateCursor(); + + virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) override; + + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override; + + virtual HRESULT SetBlurEnabled(bool enable) override; + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard *clipboard, IAvnDndResultCallback *cb, + void *sourceHandle) override; + + virtual bool IsDialog(); + +protected: + virtual NSWindowStyleMask GetStyle(); + + void UpdateStyle(); + +public: + virtual void OnResized(); +}; + +#endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm new file mode 100644 index 0000000000..482b6172af --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -0,0 +1,511 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "common.h" +#import "window.h" +#include "menu.h" +#include "rendertarget.h" +#include "automation.h" +#import "WindowBaseImpl.h" +#import "cursor.h" + +WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { + _shown = false; + _inResize = false; + _mainMenu = nullptr; + BaseEvents = events; + _glContext = gl; + renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl]; + View = [[AvnView alloc] initWithParent:this]; + StandardContainer = [[AutoFitContentView new] initWithContent:View]; + + Window = [[AvnWindow alloc] initWithParent:this]; + [Window setContentView:StandardContainer]; + + lastPositionSet.X = 100; + lastPositionSet.Y = 100; + _lastTitle = @""; + + [Window setStyleMask:NSWindowStyleMaskBorderless]; + [Window setBackingType:NSBackingStoreBuffered]; + + [Window setOpaque:false]; +} + +HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) View; + + return S_OK; +} + +HRESULT WindowBaseImpl::ObtainNSViewHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) View; + + return S_OK; +} + +AvnWindow *WindowBaseImpl::GetNSWindow() { + return Window; +} + +AvnView *WindowBaseImpl::GetNSView() { + return View; +} + +HRESULT WindowBaseImpl::ObtainNSWindowHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) Window; + + return S_OK; +} + +HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { + START_COM_CALL; + + @autoreleasepool { + SetPosition(lastPositionSet); + UpdateStyle(); + + [Window setTitle:_lastTitle]; + + if (ShouldTakeFocusOnShow() && activate) { + [Window orderFront:Window]; + [Window makeKeyAndOrderFront:Window]; + [Window makeFirstResponder:View]; + [NSApp activateIgnoringOtherApps:YES]; + } else { + [Window orderFront:Window]; + } + + _shown = true; + + return S_OK; + } +} + +bool WindowBaseImpl::ShouldTakeFocusOnShow() { + return true; +} + +HRESULT WindowBaseImpl::ObtainNSWindowHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) Window; + + return S_OK; +} + +HRESULT WindowBaseImpl::Hide() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + [Window orderOut:Window]; + [Window restoreParentWindow]; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Activate() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + [Window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + } + } + + return S_OK; +} + +HRESULT WindowBaseImpl::SetTopMost(bool value) { + START_COM_CALL; + + @autoreleasepool { + [Window setLevel:value ? NSFloatingWindowLevel : NSNormalWindowLevel]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Close() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + auto window = Window; + Window = nullptr; + + try { + // Seems to throw sometimes on application exit. + [window close]; + } + catch (NSException *) {} + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetClientSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + auto frame = [View frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + auto frame = [Window frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetScaling(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + if (Window == nullptr) { + *ret = 1; + return S_OK; + } + + *ret = [Window backingScaleFactor]; + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) { + START_COM_CALL; + + @autoreleasepool { + [Window setMinSize:ToNSSize(minSize)]; + [Window setMaxSize:ToNSSize(maxSize)]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reason) { + if (_inResize) { + return S_OK; + } + + _inResize = true; + + START_COM_CALL; + auto resizeBlock = ResizeScope(View, reason); + + @autoreleasepool { + auto maxSize = [Window maxSize]; + auto minSize = [Window minSize]; + + if (x < minSize.width) { + x = minSize.width; + } + + if (y < minSize.height) { + y = minSize.height; + } + + if (x > maxSize.width) { + x = maxSize.width; + } + + if (y > maxSize.height) { + y = maxSize.height; + } + + @try { + if (!_shown) { + BaseEvents->Resized(AvnSize{x, y}, reason); + } + + [Window setContentSize:NSSize{x, y}]; + [Window invalidateShadow]; + } + @finally { + _inResize = false; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Invalidate(AvnRect rect) { + START_COM_CALL; + + @autoreleasepool { + [View setNeedsDisplayInRect:[View frame]]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { + START_COM_CALL; + + _mainMenu = menu; + + auto nativeMenu = dynamic_cast(menu); + + auto nsmenu = nativeMenu->GetNative(); + + [Window applyMenu:nsmenu]; + + if ([Window isKeyWindow]) { + [Window showWindowMenuWithAppMenu]; + } + + return S_OK; +} + +HRESULT WindowBaseImpl::BeginMoveDrag() { + START_COM_CALL; + + @autoreleasepool { + auto lastEvent = [View lastMouseDownEvent]; + + if (lastEvent == nullptr) { + return S_OK; + } + + [Window performWindowDragWithEvent:lastEvent]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::BeginResizeDrag(AvnWindowEdge edge) { + START_COM_CALL; + + return S_OK; +} + +HRESULT WindowBaseImpl::GetPosition(AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto frame = [Window frame]; + + ret->X = frame.origin.x; + ret->Y = frame.origin.y + frame.size.height; + + *ret = ConvertPointY(*ret); + + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetPosition(AvnPoint point) { + START_COM_CALL; + + @autoreleasepool { + lastPositionSet = point; + [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::PointToClient(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + point = ConvertPointY(point); + NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); + + *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::PointToScreen(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)]; + auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y)); + *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); + + return S_OK; + } +} + +HRESULT WindowBaseImpl::ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) { + START_COM_CALL; + + [View setSwRenderedFrame:fb dispose:dispose]; + return S_OK; +} + +HRESULT WindowBaseImpl::SetCursor(IAvnCursor *cursor) { + START_COM_CALL; + + @autoreleasepool { + Cursor *avnCursor = dynamic_cast(cursor); + this->cursor = avnCursor->GetNative(); + UpdateCursor(); + + if (avnCursor->IsHidden()) { + [NSCursor hide]; + } else { + [NSCursor unhide]; + } + + return S_OK; + } +} + +void WindowBaseImpl::UpdateCursor() { + if (cursor != nil) { + [cursor set]; + } +} + +HRESULT WindowBaseImpl::CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) { + START_COM_CALL; + + if (View == NULL) + return E_FAIL; + *ppv = [renderTarget createSurfaceRenderTarget]; + return *ppv == nil ? E_FAIL : S_OK; +} + +HRESULT WindowBaseImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut) { + START_COM_CALL; + + if (View == NULL) + return E_FAIL; + *retOut = ::CreateNativeControlHost(View); + return S_OK; +} + +HRESULT WindowBaseImpl::SetBlurEnabled(bool enable) { + START_COM_CALL; + + [StandardContainer ShowBlur:enable]; + + return S_OK; +} + +HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) { + START_COM_CALL; + + auto item = TryGetPasteboardItem(clipboard); + [item setString:@"" forType:GetAvnCustomDataType()]; + if (item == nil) + return E_INVALIDARG; + if (View == NULL) + return E_FAIL; + + auto nsevent = [NSApp currentEvent]; + auto nseventType = [nsevent type]; + + // If current event isn't a mouse one (probably due to malfunctioning user app) + // attempt to forge a new one + if (!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) + || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) { + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto nspoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); + CGPoint cgpoint = NSPointToCGPoint(nspoint); + auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); + nsevent = [NSEvent eventWithCGEvent:cgevent]; + CFRelease(cgevent); + } + + auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:item]; + + auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; + NSRect dragItemRect = {(float) point.X, (float) point.Y, [dragItemImage size].width, [dragItemImage size].height}; + [dragItem setDraggingFrame:dragItemRect contents:dragItemImage]; + + int op = 0; + int ieffects = (int) effects; + if ((ieffects & (int) AvnDragDropEffects::Copy) != 0) + op |= NSDragOperationCopy; + if ((ieffects & (int) AvnDragDropEffects::Link) != 0) + op |= NSDragOperationLink; + if ((ieffects & (int) AvnDragDropEffects::Move) != 0) + op |= NSDragOperationMove; + [View beginDraggingSessionWithItems:@[dragItem] event:nsevent + source:CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; + return S_OK; +} + +bool WindowBaseImpl::IsDialog() { + return false; +} + +NSWindowStyleMask WindowBaseImpl::GetStyle() { + return NSWindowStyleMaskBorderless; +} + +void WindowBaseImpl::UpdateStyle() { + [Window setStyleMask:GetStyle()]; +} + +void WindowBaseImpl::OnResized() { + +} diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 1369ceaea0..68dc673917 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -1,6 +1,7 @@ #ifndef window_h #define window_h + class WindowBaseImpl; @interface AvnView : NSView @@ -40,12 +41,6 @@ class WindowBaseImpl; -(bool) isDialog; @end -struct INSWindowHolder -{ - virtual AvnWindow* _Nonnull GetNSWindow () = 0; - virtual AvnView* _Nonnull GetNSView () = 0; -}; - struct IWindowStateChanged { virtual void WindowStateChanged () = 0; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4426e7fdff..9676515b16 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1,628 +1,11 @@ #include "common.h" -#include "window.h" +#import "window.h" #include "KeyTransform.h" -#include "cursor.h" #include "menu.h" -#include #include "rendertarget.h" -#include "AvnString.h" #include "automation.h" +#import "WindowBaseImpl.h" -class WindowBaseImpl : public virtual ComObject, - public virtual IAvnWindowBase, - public INSWindowHolder -{ -private: - NSCursor* cursor; - -public: - FORWARD_IUNKNOWN() - BEGIN_INTERFACE_MAP() - INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) - END_INTERFACE_MAP() - - virtual ~WindowBaseImpl() - { - View = NULL; - Window = NULL; - } - AutoFitContentView* StandardContainer; - AvnView* View; - AvnWindow* Window; - ComPtr BaseEvents; - ComPtr _glContext; - NSObject* renderTarget; - AvnPoint lastPositionSet; - NSString* _lastTitle; - IAvnMenu* _mainMenu; - - bool _shown; - bool _inResize; - - WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) - { - _shown = false; - _inResize = false; - _mainMenu = nullptr; - BaseEvents = events; - _glContext = gl; - renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; - View = [[AvnView alloc] initWithParent:this]; - StandardContainer = [[AutoFitContentView new] initWithContent:View]; - - Window = [[AvnWindow alloc] initWithParent:this]; - [Window setContentView: StandardContainer]; - - lastPositionSet.X = 100; - lastPositionSet.Y = 100; - _lastTitle = @""; - - [Window setStyleMask:NSWindowStyleMaskBorderless]; - [Window setBackingType:NSBackingStoreBuffered]; - - [Window setOpaque:false]; - } - - virtual HRESULT ObtainNSWindowHandle(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge void*)Window; - - return S_OK; - } - - virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge_retained void*)Window; - - return S_OK; - } - - virtual HRESULT ObtainNSViewHandle(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge void*)View; - - return S_OK; - } - - virtual HRESULT ObtainNSViewHandleRetained(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge_retained void*)View; - - return S_OK; - } - - virtual AvnWindow* GetNSWindow() override - { - return Window; - } - - virtual AvnView* GetNSView() override - { - return View; - } - - virtual HRESULT Show(bool activate, bool isDialog) override - { - START_COM_CALL; - - @autoreleasepool - { - SetPosition(lastPositionSet); - UpdateStyle(); - - [Window setTitle:_lastTitle]; - - if(ShouldTakeFocusOnShow() && activate) - { - [Window orderFront: Window]; - [Window makeKeyAndOrderFront:Window]; - [Window makeFirstResponder:View]; - [NSApp activateIgnoringOtherApps:YES]; - } - else - { - [Window orderFront: Window]; - } - - _shown = true; - - return S_OK; - } - } - - virtual bool ShouldTakeFocusOnShow() - { - return true; - } - - virtual HRESULT Hide () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window != nullptr) - { - [Window orderOut:Window]; - [Window restoreParentWindow]; - } - - return S_OK; - } - } - - virtual HRESULT Activate () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window != nullptr) - { - [Window makeKeyAndOrderFront:nil]; - [NSApp activateIgnoringOtherApps:YES]; - } - } - - return S_OK; - } - - virtual HRESULT SetTopMost (bool value) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; - - return S_OK; - } - } - - virtual HRESULT Close() override - { - START_COM_CALL; - - @autoreleasepool - { - if (Window != nullptr) - { - auto window = Window; - Window = nullptr; - - try{ - // Seems to throw sometimes on application exit. - [window close]; - } - catch(NSException*){} - } - - return S_OK; - } - } - - virtual HRESULT GetClientSize(AvnSize* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - auto frame = [View frame]; - ret->Width = frame.size.width; - ret->Height = frame.size.height; - - return S_OK; - } - } - - virtual HRESULT GetFrameSize(AvnSize* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - auto frame = [Window frame]; - ret->Width = frame.size.width; - ret->Height = frame.size.height; - - return S_OK; - } - } - - virtual HRESULT GetScaling (double* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - if(Window == nullptr) - { - *ret = 1; - return S_OK; - } - - *ret = [Window backingScaleFactor]; - return S_OK; - } - } - - virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setMinSize: ToNSSize(minSize)]; - [Window setMaxSize: ToNSSize(maxSize)]; - - return S_OK; - } - } - - virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override - { - if(_inResize) - { - return S_OK; - } - - _inResize = true; - - START_COM_CALL; - auto resizeBlock = ResizeScope(View, reason); - - @autoreleasepool - { - auto maxSize = [Window maxSize]; - auto minSize = [Window minSize]; - - if (x < minSize.width) - { - x = minSize.width; - } - - if (y < minSize.height) - { - y = minSize.height; - } - - if (x > maxSize.width) - { - x = maxSize.width; - } - - if (y > maxSize.height) - { - y = maxSize.height; - } - - @try - { - if(!_shown) - { - BaseEvents->Resized(AvnSize{x,y}, reason); - } - - [Window setContentSize:NSSize{x,y}]; - [Window invalidateShadow]; - } - @finally - { - _inResize = false; - } - - return S_OK; - } - } - - virtual HRESULT Invalidate (AvnRect rect) override - { - START_COM_CALL; - - @autoreleasepool - { - [View setNeedsDisplayInRect:[View frame]]; - - return S_OK; - } - } - - virtual HRESULT SetMainMenu(IAvnMenu* menu) override - { - START_COM_CALL; - - _mainMenu = menu; - - auto nativeMenu = dynamic_cast(menu); - - auto nsmenu = nativeMenu->GetNative(); - - [Window applyMenu:nsmenu]; - - if ([Window isKeyWindow]) - { - [Window showWindowMenuWithAppMenu]; - } - - return S_OK; - } - - virtual HRESULT BeginMoveDrag () override - { - START_COM_CALL; - - @autoreleasepool - { - auto lastEvent = [View lastMouseDownEvent]; - - if(lastEvent == nullptr) - { - return S_OK; - } - - [Window performWindowDragWithEvent:lastEvent]; - - return S_OK; - } - } - - virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) override - { - START_COM_CALL; - - return S_OK; - } - - virtual HRESULT GetPosition (AvnPoint* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - auto frame = [Window frame]; - - ret->X = frame.origin.x; - ret->Y = frame.origin.y + frame.size.height; - - *ret = ConvertPointY(*ret); - - return S_OK; - } - } - - virtual HRESULT SetPosition (AvnPoint point) override - { - START_COM_CALL; - - @autoreleasepool - { - lastPositionSet = point; - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))]; - - return S_OK; - } - } - - virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - point = ConvertPointY(point); - NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; - auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); - - *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; - - return S_OK; - } - } - - virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); - NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)]; - auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y)); - *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); - - return S_OK; - } - } - - virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) override - { - START_COM_CALL; - - [View setSwRenderedFrame: fb dispose: dispose]; - return S_OK; - } - - virtual HRESULT SetCursor(IAvnCursor* cursor) override - { - START_COM_CALL; - - @autoreleasepool - { - Cursor* avnCursor = dynamic_cast(cursor); - this->cursor = avnCursor->GetNative(); - UpdateCursor(); - - if(avnCursor->IsHidden()) - { - [NSCursor hide]; - } - else - { - [NSCursor unhide]; - } - - return S_OK; - } - } - - virtual void UpdateCursor() - { - if (cursor != nil) - { - [cursor set]; - } - } - - virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override - { - START_COM_CALL; - - if(View == NULL) - return E_FAIL; - *ppv = [renderTarget createSurfaceRenderTarget]; - return *ppv == nil ? E_FAIL : S_OK; - } - - virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override - { - START_COM_CALL; - - if(View == NULL) - return E_FAIL; - *retOut = ::CreateNativeControlHost(View); - return S_OK; - } - - virtual HRESULT SetBlurEnabled (bool enable) override - { - START_COM_CALL; - - [StandardContainer ShowBlur:enable]; - - return S_OK; - } - - virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, - IAvnClipboard* clipboard, IAvnDndResultCallback* cb, - void* sourceHandle) override - { - START_COM_CALL; - - auto item = TryGetPasteboardItem(clipboard); - [item setString:@"" forType:GetAvnCustomDataType()]; - if(item == nil) - return E_INVALIDARG; - if(View == NULL) - return E_FAIL; - - auto nsevent = [NSApp currentEvent]; - auto nseventType = [nsevent type]; - - // If current event isn't a mouse one (probably due to malfunctioning user app) - // attempt to forge a new one - if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) - || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) - { - NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; - auto nspoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); - CGPoint cgpoint = NSPointToCGPoint(nspoint); - auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); - nsevent = [NSEvent eventWithCGEvent: cgevent]; - CFRelease(cgevent); - } - - auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item]; - - auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; - NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height}; - [dragItem setDraggingFrame: dragItemRect contents: dragItemImage]; - - int op = 0; int ieffects = (int)effects; - if((ieffects & (int)AvnDragDropEffects::Copy) != 0) - op |= NSDragOperationCopy; - if((ieffects & (int)AvnDragDropEffects::Link) != 0) - op |= NSDragOperationLink; - if((ieffects & (int)AvnDragDropEffects::Move) != 0) - op |= NSDragOperationMove; - [View beginDraggingSessionWithItems: @[dragItem] event: nsevent - source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; - return S_OK; - } - - virtual bool IsDialog() - { - return false; - } - -protected: - virtual NSWindowStyleMask GetStyle() - { - return NSWindowStyleMaskBorderless; - } - - void UpdateStyle() - { - [Window setStyleMask: GetStyle()]; - } - -public: - virtual void OnResized () - { - - } -}; class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged { From 6eb40ac09d1b38fd80b2ca4be7799f9d048dc42c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 4 May 2022 17:40:33 +0100 Subject: [PATCH 165/213] make avalonia-native compile again with import instead of include. --- native/Avalonia.Native/src/OSX/SystemDialogs.mm | 1 + native/Avalonia.Native/src/OSX/automation.h | 2 +- native/Avalonia.Native/src/OSX/automation.mm | 4 ++-- native/Avalonia.Native/src/OSX/menu.mm | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index a47221056b..21ad9cfa7c 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -1,5 +1,6 @@ #include "common.h" #include "window.h" +#include "INSWindowHolder.h" class SystemDialogs : public ComSingleObject { diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 4a12a965fd..79ccfd0eaa 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,5 +1,5 @@ #import -#include "window.h" +#import "window.h" NS_ASSUME_NONNULL_BEGIN diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 7d697140c2..226e8810c7 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,7 +1,7 @@ #include "common.h" -#include "automation.h" +#import "automation.h" #include "AvnString.h" -#include "window.h" +#import "INSWindowHolder.h" @interface AvnAccessibilityElement (Events) - (void) raiseChildrenChanged; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 2dbe76bc6d..726e58478b 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,7 +1,7 @@ #include "common.h" #include "menu.h" -#include "window.h" +#import "window.h" #include "KeyTransform.h" #include #include /* For kVK_ constants, and TIS functions. */ From e2b76313b76fb261a7b92542660561ccfebd983d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 4 May 2022 18:01:31 +0100 Subject: [PATCH 166/213] further seperate out files. --- .../project.pbxproj | 32 + .../src/OSX/IWindowStateChanged.h | 18 + native/Avalonia.Native/src/OSX/ResizeScope.h | 23 + native/Avalonia.Native/src/OSX/ResizeScope.mm | 17 + .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 1 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 1 + native/Avalonia.Native/src/OSX/WindowImpl.h | 99 +++ native/Avalonia.Native/src/OSX/WindowImpl.mm | 546 ++++++++++++++ native/Avalonia.Native/src/OSX/automation.h | 1 - native/Avalonia.Native/src/OSX/automation.mm | 1 + native/Avalonia.Native/src/OSX/window.h | 30 +- native/Avalonia.Native/src/OSX/window.mm | 667 +----------------- 12 files changed, 741 insertions(+), 695 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/IWindowStateChanged.h create mode 100644 native/Avalonia.Native/src/OSX/ResizeScope.h create mode 100644 native/Avalonia.Native/src/OSX/ResizeScope.mm create mode 100644 native/Avalonia.Native/src/OSX/WindowImpl.h create mode 100644 native/Avalonia.Native/src/OSX/WindowImpl.mm diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 7571d51c9f..2a206b0692 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391E45702740FE9DD69695 /* ResizeScope.mm */; }; + 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 183919BF108EB72A029F7671 /* WindowImpl.mm */; }; + 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */; }; + 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; }; + 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; }; + 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; + 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; + 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; @@ -35,6 +43,14 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; + 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; + 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = ""; }; + 1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = ""; }; + 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; + 18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = ""; }; + 18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = ""; }; + 18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = ""; }; 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; }; @@ -130,6 +146,14 @@ 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, AB661C1C2148230E00291242 /* Frameworks */, + 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */, + 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */, + 18391BBB7782C296D424071F /* INSWindowHolder.h */, + 183919BF108EB72A029F7671 /* WindowImpl.mm */, + 18391CD090AA776E7E841AC9 /* WindowImpl.h */, + 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */, + 18391E45702740FE9DD69695 /* ResizeScope.mm */, + 1839171D898F9BFC1373631A /* ResizeScope.h */, ); sourceTree = ""; }; @@ -150,6 +174,11 @@ files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, + 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */, + 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, + 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, + 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, + 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,6 +258,9 @@ AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, + 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, + 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, + 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/IWindowStateChanged.h b/native/Avalonia.Native/src/OSX/IWindowStateChanged.h new file mode 100644 index 0000000000..f0905da3ac --- /dev/null +++ b/native/Avalonia.Native/src/OSX/IWindowStateChanged.h @@ -0,0 +1,18 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H +#define AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H + +struct IWindowStateChanged +{ + virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; + virtual SystemDecorations Decorations () = 0; + virtual AvnWindowState WindowState () = 0; +}; + +#endif //AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.h b/native/Avalonia.Native/src/OSX/ResizeScope.h new file mode 100644 index 0000000000..c57dc96690 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/ResizeScope.h @@ -0,0 +1,23 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_RESIZESCOPE_H +#define AVALONIA_NATIVE_OSX_RESIZESCOPE_H + +#include "window.h" +#include "avalonia-native.h" + +class ResizeScope +{ +public: + ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason); + + ~ResizeScope(); +private: + AvnView* _Nonnull _view; + AvnPlatformResizeReason _restore; +}; + +#endif //AVALONIA_NATIVE_OSX_RESIZESCOPE_H diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.mm b/native/Avalonia.Native/src/OSX/ResizeScope.mm new file mode 100644 index 0000000000..8644b41fba --- /dev/null +++ b/native/Avalonia.Native/src/OSX/ResizeScope.mm @@ -0,0 +1,17 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "ResizeScope.h" + +ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) { + _view = view; + _restore = [view getResizeReason]; + [view setResizeReason:reason]; +} + +ResizeScope::~ResizeScope() { + [_view setResizeReason:_restore]; +} diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 2f9b05988f..ec013657dc 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -6,6 +6,7 @@ #ifndef AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H #define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H +#import "rendertarget.h" #include "INSWindowHolder.h" class WindowBaseImpl : public virtual ComObject, diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 482b6172af..bb40cd2a7e 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -11,6 +11,7 @@ #include "automation.h" #import "WindowBaseImpl.h" #import "cursor.h" +#include "ResizeScope.h" WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { _shown = false; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h new file mode 100644 index 0000000000..b16e72fb32 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -0,0 +1,99 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_WINDOWIMPL_H +#define AVALONIA_NATIVE_OSX_WINDOWIMPL_H + + +#import "WindowBaseImpl.h" +#include "IWindowStateChanged.h" + +class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged +{ +private: + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; + AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; + bool _isClientAreaExtended; + bool _isDialog; + AvnExtendClientAreaChromeHints _extendClientHints; + + FORWARD_IUNKNOWN() +BEGIN_INTERFACE_MAP() + INHERIT_INTERFACE_MAP(WindowBaseImpl) + INTERFACE_MAP_ENTRY(IAvnWindow, IID_IAvnWindow) + END_INTERFACE_MAP() + virtual ~WindowImpl() + { + } + + ComPtr WindowEvents; + + WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); + + void HideOrShowTrafficLights (); + + virtual HRESULT Show (bool activate, bool isDialog) override; + + virtual HRESULT SetEnabled (bool enable) override; + + virtual HRESULT SetParent (IAvnWindow* parent) override; + + void StartStateTransition (); + + void EndStateTransition (); + + SystemDecorations Decorations (); + + AvnWindowState WindowState (); + + void WindowStateChanged (); + + bool UndecoratedIsMaximized (); + + bool IsZoomed (); + + void DoZoom(); + + virtual HRESULT SetCanResize(bool value) override; + + virtual HRESULT SetDecorations(SystemDecorations value) override; + + virtual HRESULT SetTitle (char* utf8title) override; + + virtual HRESULT SetTitleBarColor(AvnColor color) override; + + virtual HRESULT GetWindowState (AvnWindowState*ret) override; + + virtual HRESULT TakeFocusFromChildren () override; + + virtual HRESULT SetExtendClientArea (bool enable) override; + + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override; + + virtual HRESULT GetExtendTitleBarHeight (double*ret) override; + + virtual HRESULT SetExtendTitleBarHeight (double value) override; + + void EnterFullScreenMode (); + + void ExitFullScreenMode (); + + virtual HRESULT SetWindowState (AvnWindowState state) override; + + virtual void OnResized () override; + + virtual bool IsDialog() override; + +protected: + virtual NSWindowStyleMask GetStyle() override; +}; + +#endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm new file mode 100644 index 0000000000..0b7f9e0a13 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -0,0 +1,546 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import "window.h" +#include "automation.h" +#include "menu.h" +#import "WindowImpl.h" + + +WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; + _fullScreenActive = false; + _canResize = true; + _decorations = SystemDecorationsFull; + _transitioningWindowState = false; + _inSetWindowState = false; + _lastWindowState = Normal; + _actualWindowState = Normal; + WindowEvents = events; + [Window setCanBecomeKeyAndMain]; + [Window disableCursorRects]; + [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; +} + +void WindowImpl::HideOrShowTrafficLights() { + if (Window == nil) { + return; + } + + for (id subview in Window.contentView.superview.subviews) { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { + NSView *titlebarView = [subview subviews][0]; + for (id button in titlebarView.subviews) { + if ([button isKindOfClass:[NSButton class]]) { + if (_isClientAreaExtended) { + auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + [button setHidden:!wantsChrome]; + } else { + [button setHidden:(_decorations != SystemDecorationsFull)]; + } + + [button setWantsLayer:true]; + } + } + } + } +} + +HRESULT WindowImpl::Show(bool activate, bool isDialog) { + START_COM_CALL; + + @autoreleasepool { + _isDialog = isDialog; + WindowBaseImpl::Show(activate, isDialog); + + HideOrShowTrafficLights(); + + return SetWindowState(_lastWindowState); + } +} + +HRESULT WindowImpl::SetEnabled(bool enable) { + START_COM_CALL; + + @autoreleasepool { + [Window setEnabled:enable]; + return S_OK; + } +} + +HRESULT WindowImpl::SetParent(IAvnWindow *parent) { + START_COM_CALL; + + @autoreleasepool { + if (parent == nullptr) + return E_POINTER; + + auto cparent = dynamic_cast(parent); + if (cparent == nullptr) + return E_INVALIDARG; + + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; + + UpdateStyle(); + + return S_OK; + } +} + +void WindowImpl::StartStateTransition() { + _transitioningWindowState = true; +} + +void WindowImpl::EndStateTransition() { + _transitioningWindowState = false; +} + +SystemDecorations WindowImpl::Decorations() { + return _decorations; +} + +AvnWindowState WindowImpl::WindowState() { + return _lastWindowState; +} + +void WindowImpl::WindowStateChanged() { + if (_shown && !_inSetWindowState && !_transitioningWindowState) { + AvnWindowState state; + GetWindowState(&state); + + if (_lastWindowState != state) { + if (_isClientAreaExtended) { + if (_lastWindowState == FullScreen) { + // we exited fs. + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + + [Window setTitlebarAppearsTransparent:true]; + + [StandardContainer setFrameSize:StandardContainer.frame.size]; + } else if (state == FullScreen) { + // we entered fs. + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = nullptr; + } + + [Window setTitlebarAppearsTransparent:false]; + + [StandardContainer setFrameSize:StandardContainer.frame.size]; + } + } + + _lastWindowState = state; + _actualWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } +} + +bool WindowImpl::UndecoratedIsMaximized() { + auto windowSize = [Window frame]; + auto available = [Window screen].visibleFrame; + return CGRectEqualToRect(windowSize, available); +} + +bool WindowImpl::IsZoomed() { + return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); +} + +void WindowImpl::DoZoom() { + switch (_decorations) { + case SystemDecorationsNone: + case SystemDecorationsBorderOnly: + [Window setFrame:[Window screen].visibleFrame display:true]; + break; + + + case SystemDecorationsFull: + [Window performZoom:Window]; + break; + } +} + +HRESULT WindowImpl::SetCanResize(bool value) { + START_COM_CALL; + + @autoreleasepool { + _canResize = value; + UpdateStyle(); + return S_OK; + } +} + +HRESULT WindowImpl::SetDecorations(SystemDecorations value) { + START_COM_CALL; + + @autoreleasepool { + auto currentWindowState = _lastWindowState; + _decorations = value; + + if (_fullScreenActive) { + return S_OK; + } + + UpdateStyle(); + + HideOrShowTrafficLights(); + + switch (_decorations) { + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if (currentWindowState == Maximized) { + if (!UndecoratedIsMaximized()) { + DoZoom(); + } + } + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if (currentWindowState == Maximized) { + if (!UndecoratedIsMaximized()) { + DoZoom(); + } + } + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + if (currentWindowState == Maximized) { + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + break; + } + + return S_OK; + } +} + +HRESULT WindowImpl::SetTitle(char *utf8title) { + START_COM_CALL; + + @autoreleasepool { + _lastTitle = [NSString stringWithUTF8String:(const char *) utf8title]; + [Window setTitle:_lastTitle]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetTitleBarColor(AvnColor color) { + START_COM_CALL; + + @autoreleasepool { + float a = (float) color.Alpha / 255.0f; + float r = (float) color.Red / 255.0f; + float g = (float) color.Green / 255.0f; + float b = (float) color.Blue / 255.0f; + + auto nscolor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:a]; + + // Based on the titlebar color we have to choose either light or dark + // OSX doesnt let you set a foreground color for titlebar. + if ((r * 0.299 + g * 0.587 + b * 0.114) > 186.0f / 255.0f) { + [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]]; + } else { + [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]]; + } + + [Window setTitlebarAppearsTransparent:true]; + [Window setBackgroundColor:nscolor]; + } + + return S_OK; +} + +HRESULT WindowImpl::GetWindowState(AvnWindowState *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + if (([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { + *ret = FullScreen; + return S_OK; + } + + if ([Window isMiniaturized]) { + *ret = Minimized; + return S_OK; + } + + if (IsZoomed()) { + *ret = Maximized; + return S_OK; + } + + *ret = Normal; + + return S_OK; + } +} + +HRESULT WindowImpl::TakeFocusFromChildren() { + START_COM_CALL; + + @autoreleasepool { + if (Window == nil) + return S_OK; + if ([Window isKeyWindow]) + [Window makeFirstResponder:View]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendClientArea(bool enable) { + START_COM_CALL; + + @autoreleasepool { + _isClientAreaExtended = enable; + + if (enable) { + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) { + [StandardContainer ShowTitleBar:true]; + } else { + [StandardContainer ShowTitleBar:false]; + } + + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } else { + Window.toolbar = nullptr; + } + } else { + Window.titleVisibility = NSWindowTitleVisible; + Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; + } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) { + START_COM_CALL; + + @autoreleasepool { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } +} + +HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendTitleBarHeight(double value) { + START_COM_CALL; + + @autoreleasepool { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } +} + +void WindowImpl::EnterFullScreenMode() { + _fullScreenActive = true; + + [Window setTitle:_lastTitle]; + [Window toggleFullScreen:nullptr]; +} + +void WindowImpl::ExitFullScreenMode() { + [Window toggleFullScreen:nullptr]; + + _fullScreenActive = false; + + SetDecorations(_decorations); +} + +HRESULT WindowImpl::SetWindowState(AvnWindowState state) { + START_COM_CALL; + + @autoreleasepool { + if (Window == nullptr) { + return S_OK; + } + + if (_actualWindowState == state) { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _actualWindowState; + _lastWindowState = state; + + if (currentState == Normal) { + _preZoomSize = [Window frame]; + } + + if (_shown) { + switch (state) { + case Maximized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } + + lastPositionSet.X = 0; + lastPositionSet.Y = 0; + + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + if (!IsZoomed()) { + DoZoom(); + } + break; + + case Minimized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } else { + [Window miniaturize:Window]; + } + break; + + case FullScreen: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + EnterFullScreenMode(); + break; + + case Normal: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + if (currentState == FullScreen) { + ExitFullScreenMode(); + } + + if (IsZoomed()) { + if (_decorations == SystemDecorationsFull) { + DoZoom(); + } else { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + + } + break; + } + + _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); + } + + + _inSetWindowState = false; + + return S_OK; + } +} + +void WindowImpl::OnResized() { + if (_shown && !_inSetWindowState && !_transitioningWindowState) { + WindowStateChanged(); + } +} + +bool WindowImpl::IsDialog() { + return _isDialog; +} + +NSWindowStyleMask WindowImpl::GetStyle() { + unsigned long s = NSWindowStyleMaskBorderless; + + switch (_decorations) { + case SystemDecorationsNone: + s = s | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; + + if (_canResize) { + s = s | NSWindowStyleMaskResizable; + } + break; + } + + if ([Window parentWindow] == nullptr) { + s |= NSWindowStyleMaskMiniaturizable; + } + + if (_isClientAreaExtended) { + s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; + } + return s; +} diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 79ccfd0eaa..1727d21f27 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,5 +1,4 @@ #import -#import "window.h" NS_ASSUME_NONNULL_BEGIN diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 226e8810c7..087d15a248 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,5 +1,6 @@ #include "common.h" #import "automation.h" +#import "window.h" #include "AvnString.h" #import "INSWindowHolder.h" diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 68dc673917..365172622f 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -1,7 +1,7 @@ #ifndef window_h #define window_h - +#import "avalonia-native.h" class WindowBaseImpl; @interface AvnView : NSView @@ -41,32 +41,4 @@ class WindowBaseImpl; -(bool) isDialog; @end -struct IWindowStateChanged -{ - virtual void WindowStateChanged () = 0; - virtual void StartStateTransition () = 0; - virtual void EndStateTransition () = 0; - virtual SystemDecorations Decorations () = 0; - virtual AvnWindowState WindowState () = 0; -}; - -class ResizeScope -{ -public: - ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason) - { - _view = view; - _restore = [view getResizeReason]; - [view setResizeReason:reason]; - } - - ~ResizeScope() - { - [_view setResizeReason:_restore]; - } -private: - AvnView* _Nonnull _view; - AvnPlatformResizeReason _restore; -}; - #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9676515b16..a1d231f30a 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,673 +5,10 @@ #include "rendertarget.h" #include "automation.h" #import "WindowBaseImpl.h" +#include "WindowImpl.h" +#include "IWindowStateChanged.h" -class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged -{ -private: - bool _canResize; - bool _fullScreenActive; - SystemDecorations _decorations; - AvnWindowState _lastWindowState; - AvnWindowState _actualWindowState; - bool _inSetWindowState; - NSRect _preZoomSize; - bool _transitioningWindowState; - bool _isClientAreaExtended; - bool _isDialog; - AvnExtendClientAreaChromeHints _extendClientHints; - - FORWARD_IUNKNOWN() - BEGIN_INTERFACE_MAP() - INHERIT_INTERFACE_MAP(WindowBaseImpl) - INTERFACE_MAP_ENTRY(IAvnWindow, IID_IAvnWindow) - END_INTERFACE_MAP() - virtual ~WindowImpl() - { - } - - ComPtr WindowEvents; - WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) - { - _isClientAreaExtended = false; - _extendClientHints = AvnDefaultChrome; - _fullScreenActive = false; - _canResize = true; - _decorations = SystemDecorationsFull; - _transitioningWindowState = false; - _inSetWindowState = false; - _lastWindowState = Normal; - _actualWindowState = Normal; - WindowEvents = events; - [Window setCanBecomeKeyAndMain]; - [Window disableCursorRects]; - [Window setTabbingMode:NSWindowTabbingModeDisallowed]; - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - } - - void HideOrShowTrafficLights () - { - if (Window == nil) - { - return; - } - - for (id subview in Window.contentView.superview.subviews) { - if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { - NSView *titlebarView = [subview subviews][0]; - for (id button in titlebarView.subviews) { - if ([button isKindOfClass:[NSButton class]]) - { - if(_isClientAreaExtended) - { - auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - [button setHidden: !wantsChrome]; - } - else - { - [button setHidden: (_decorations != SystemDecorationsFull)]; - } - - [button setWantsLayer:true]; - } - } - } - } - } - - virtual HRESULT Show (bool activate, bool isDialog) override - { - START_COM_CALL; - - @autoreleasepool - { - _isDialog = isDialog; - WindowBaseImpl::Show(activate, isDialog); - - HideOrShowTrafficLights(); - - return SetWindowState(_lastWindowState); - } - } - - virtual HRESULT SetEnabled (bool enable) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setEnabled:enable]; - return S_OK; - } - } - - virtual HRESULT SetParent (IAvnWindow* parent) override - { - START_COM_CALL; - - @autoreleasepool - { - if(parent == nullptr) - return E_POINTER; - - auto cparent = dynamic_cast(parent); - if(cparent == nullptr) - return E_INVALIDARG; - - // If one tries to show a child window with a minimized parent window, then the parent window will be - // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive - // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. - if (cparent->WindowState() == Minimized) - cparent->SetWindowState(Normal); - - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; - [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - - UpdateStyle(); - - return S_OK; - } - } - - void StartStateTransition () override - { - _transitioningWindowState = true; - } - - void EndStateTransition () override - { - _transitioningWindowState = false; - } - - SystemDecorations Decorations () override - { - return _decorations; - } - - AvnWindowState WindowState () override - { - return _lastWindowState; - } - - void WindowStateChanged () override - { - if(_shown && !_inSetWindowState && !_transitioningWindowState) - { - AvnWindowState state; - GetWindowState(&state); - - if(_lastWindowState != state) - { - if(_isClientAreaExtended) - { - if(_lastWindowState == FullScreen) - { - // we exited fs. - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - - [Window setTitlebarAppearsTransparent:true]; - - [StandardContainer setFrameSize: StandardContainer.frame.size]; - } - else if(state == FullScreen) - { - // we entered fs. - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = nullptr; - } - - [Window setTitlebarAppearsTransparent:false]; - - [StandardContainer setFrameSize: StandardContainer.frame.size]; - } - } - - _lastWindowState = state; - _actualWindowState = state; - WindowEvents->WindowStateChanged(state); - } - } - } - - bool UndecoratedIsMaximized () - { - auto windowSize = [Window frame]; - auto available = [Window screen].visibleFrame; - return CGRectEqualToRect(windowSize, available); - } - - bool IsZoomed () - { - return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); - } - - void DoZoom() - { - switch (_decorations) - { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; - } - } - - virtual HRESULT SetCanResize(bool value) override - { - START_COM_CALL; - - @autoreleasepool - { - _canResize = value; - UpdateStyle(); - return S_OK; - } - } - - virtual HRESULT SetDecorations(SystemDecorations value) override - { - START_COM_CALL; - - @autoreleasepool - { - auto currentWindowState = _lastWindowState; - _decorations = value; - - if(_fullScreenActive) - { - return S_OK; - } - - UpdateStyle(); - - HideOrShowTrafficLights(); - - switch (_decorations) - { - case SystemDecorationsNone: - [Window setHasShadow:NO]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - - if(currentWindowState == Maximized) - { - if(!UndecoratedIsMaximized()) - { - DoZoom(); - } - } - break; - - case SystemDecorationsBorderOnly: - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - - if(currentWindowState == Maximized) - { - if(!UndecoratedIsMaximized()) - { - DoZoom(); - } - } - break; - - case SystemDecorationsFull: - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; - [Window setTitle:_lastTitle]; - - if(currentWindowState == Maximized) - { - auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; - - [View setFrameSize:newFrame]; - } - break; - } - - return S_OK; - } - } - - virtual HRESULT SetTitle (char* utf8title) override - { - START_COM_CALL; - - @autoreleasepool - { - _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; - [Window setTitle:_lastTitle]; - - return S_OK; - } - } - - virtual HRESULT SetTitleBarColor(AvnColor color) override - { - START_COM_CALL; - - @autoreleasepool - { - float a = (float)color.Alpha / 255.0f; - float r = (float)color.Red / 255.0f; - float g = (float)color.Green / 255.0f; - float b = (float)color.Blue / 255.0f; - - auto nscolor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:a]; - - // Based on the titlebar color we have to choose either light or dark - // OSX doesnt let you set a foreground color for titlebar. - if ((r*0.299 + g*0.587 + b*0.114) > 186.0f / 255.0f) - { - [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]]; - } - else - { - [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]]; - } - - [Window setTitlebarAppearsTransparent:true]; - [Window setBackgroundColor:nscolor]; - } - - return S_OK; - } - - virtual HRESULT GetWindowState (AvnWindowState*ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) - { - *ret = FullScreen; - return S_OK; - } - - if([Window isMiniaturized]) - { - *ret = Minimized; - return S_OK; - } - - if(IsZoomed()) - { - *ret = Maximized; - return S_OK; - } - - *ret = Normal; - - return S_OK; - } - } - - virtual HRESULT TakeFocusFromChildren () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window == nil) - return S_OK; - if([Window isKeyWindow]) - [Window makeFirstResponder: View]; - - return S_OK; - } - } - - virtual HRESULT SetExtendClientArea (bool enable) override - { - START_COM_CALL; - - @autoreleasepool - { - _isClientAreaExtended = enable; - - if(enable) - { - Window.titleVisibility = NSWindowTitleHidden; - - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) - { - [StandardContainer ShowTitleBar:true]; - } - else - { - [StandardContainer ShowTitleBar:false]; - } - - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - else - { - Window.toolbar = nullptr; - } - } - else - { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - - [Window setIsExtended:enable]; - - HideOrShowTrafficLights(); - - UpdateStyle(); - - return S_OK; - } - } - - virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override - { - START_COM_CALL; - - @autoreleasepool - { - _extendClientHints = hints; - - SetExtendClientArea(_isClientAreaExtended); - return S_OK; - } - } - - virtual HRESULT GetExtendTitleBarHeight (double*ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - *ret = [Window getExtendedTitleBarHeight]; - - return S_OK; - } - } - - virtual HRESULT SetExtendTitleBarHeight (double value) override - { - START_COM_CALL; - - @autoreleasepool - { - [StandardContainer SetTitleBarHeightHint:value]; - return S_OK; - } - } - - void EnterFullScreenMode () - { - _fullScreenActive = true; - - [Window setTitle:_lastTitle]; - [Window toggleFullScreen:nullptr]; - } - - void ExitFullScreenMode () - { - [Window toggleFullScreen:nullptr]; - - _fullScreenActive = false; - - SetDecorations(_decorations); - } - - virtual HRESULT SetWindowState (AvnWindowState state) override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window == nullptr) - { - return S_OK; - } - - if(_actualWindowState == state) - { - return S_OK; - } - - _inSetWindowState = true; - - auto currentState = _actualWindowState; - _lastWindowState = state; - - if(currentState == Normal) - { - _preZoomSize = [Window frame]; - } - - if(_shown) - { - switch (state) { - case Maximized: - if(currentState == FullScreen) - { - ExitFullScreenMode(); - } - - lastPositionSet.X = 0; - lastPositionSet.Y = 0; - - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - if(!IsZoomed()) - { - DoZoom(); - } - break; - - case Minimized: - if(currentState == FullScreen) - { - ExitFullScreenMode(); - } - else - { - [Window miniaturize:Window]; - } - break; - - case FullScreen: - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - EnterFullScreenMode(); - break; - - case Normal: - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - if(currentState == FullScreen) - { - ExitFullScreenMode(); - } - - if(IsZoomed()) - { - if(_decorations == SystemDecorationsFull) - { - DoZoom(); - } - else - { - [Window setFrame:_preZoomSize display:true]; - auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; - - [View setFrameSize:newFrame]; - } - - } - break; - } - - _actualWindowState = _lastWindowState; - WindowEvents->WindowStateChanged(_actualWindowState); - } - - - _inSetWindowState = false; - - return S_OK; - } - } - - virtual void OnResized () override - { - if(_shown && !_inSetWindowState && !_transitioningWindowState) - { - WindowStateChanged(); - } - } - - virtual bool IsDialog() override - { - return _isDialog; - } - -protected: - virtual NSWindowStyleMask GetStyle() override - { - unsigned long s = NSWindowStyleMaskBorderless; - - switch (_decorations) - { - case SystemDecorationsNone: - s = s | NSWindowStyleMaskFullSizeContentView; - break; - - case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - break; - - case SystemDecorationsFull: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; - - if(_canResize) - { - s = s | NSWindowStyleMaskResizable; - } - break; - } - - if([Window parentWindow] == nullptr) - { - s |= NSWindowStyleMaskMiniaturizable; - } - - if(_isClientAreaExtended) - { - s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; - } - return s; - } -}; - NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; @implementation AutoFitContentView From 947590d453ee9a3490088d09f4cbca3edebee74f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 4 May 2022 18:10:07 +0100 Subject: [PATCH 167/213] fix warnings --- native/Avalonia.Native/src/OSX/WindowImpl.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index b16e72fb32..fae53acc22 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -46,15 +46,15 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetParent (IAvnWindow* parent) override; - void StartStateTransition (); + void StartStateTransition () override ; - void EndStateTransition (); + void EndStateTransition () override ; - SystemDecorations Decorations (); + SystemDecorations Decorations () override ; - AvnWindowState WindowState (); + AvnWindowState WindowState () override ; - void WindowStateChanged (); + void WindowStateChanged () override ; bool UndecoratedIsMaximized (); From 68b4173743353b9901fbca06d4a41add3adbb5ae Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 4 May 2022 18:39:30 +0100 Subject: [PATCH 168/213] remove unused window code. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 7 +- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 15 ++-- native/Avalonia.Native/src/OSX/WindowImpl.h | 2 - native/Avalonia.Native/src/OSX/WindowImpl.mm | 6 -- native/Avalonia.Native/src/OSX/window.h | 12 ++-- native/Avalonia.Native/src/OSX/window.mm | 68 ++++++------------- 6 files changed, 32 insertions(+), 78 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index ec013657dc..94175b8187 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -35,7 +35,6 @@ BEGIN_INTERFACE_MAP() NSObject *renderTarget; AvnPoint lastPositionSet; NSString *_lastTitle; - IAvnMenu *_mainMenu; bool _shown; bool _inResize; @@ -76,13 +75,13 @@ BEGIN_INTERFACE_MAP() virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override; - virtual HRESULT Invalidate(AvnRect rect) override; + virtual HRESULT Invalidate(__attribute__((unused)) AvnRect rect) override; virtual HRESULT SetMainMenu(IAvnMenu *menu) override; virtual HRESULT BeginMoveDrag() override; - virtual HRESULT BeginResizeDrag(AvnWindowEdge edge) override; + virtual HRESULT BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) override; virtual HRESULT GetPosition(AvnPoint *ret) override; @@ -115,8 +114,6 @@ protected: void UpdateStyle(); -public: - virtual void OnResized(); }; #endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index bb40cd2a7e..9959d6a034 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -16,7 +16,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { _shown = false; _inResize = false; - _mainMenu = nullptr; BaseEvents = events; _glContext = gl; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl]; @@ -279,7 +278,7 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } } -HRESULT WindowBaseImpl::Invalidate(AvnRect rect) { +HRESULT WindowBaseImpl::Invalidate(__attribute__((unused)) AvnRect rect) { START_COM_CALL; @autoreleasepool { @@ -292,8 +291,6 @@ HRESULT WindowBaseImpl::Invalidate(AvnRect rect) { HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { START_COM_CALL; - _mainMenu = menu; - auto nativeMenu = dynamic_cast(menu); auto nsmenu = nativeMenu->GetNative(); @@ -323,7 +320,7 @@ HRESULT WindowBaseImpl::BeginMoveDrag() { } } -HRESULT WindowBaseImpl::BeginResizeDrag(AvnWindowEdge edge) { +HRESULT WindowBaseImpl::BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) { START_COM_CALL; return S_OK; @@ -431,7 +428,7 @@ HRESULT WindowBaseImpl::CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) { if (View == NULL) return E_FAIL; *ppv = [renderTarget createSurfaceRenderTarget]; - return *ppv == nil ? E_FAIL : S_OK; + return static_cast(*ppv == nil ? E_FAIL : S_OK); } HRESULT WindowBaseImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut) { @@ -505,8 +502,4 @@ NSWindowStyleMask WindowBaseImpl::GetStyle() { void WindowBaseImpl::UpdateStyle() { [Window setStyleMask:GetStyle()]; -} - -void WindowBaseImpl::OnResized() { - -} +} \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index fae53acc22..b229921baa 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -88,8 +88,6 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetWindowState (AvnWindowState state) override; - virtual void OnResized () override; - virtual bool IsDialog() override; protected: diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 0b7f9e0a13..9cf5160c97 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -504,12 +504,6 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) { } } -void WindowImpl::OnResized() { - if (_shown && !_inSetWindowState && !_transitioningWindowState) { - WindowStateChanged(); - } -} - bool WindowImpl::IsDialog() { return _isDialog; } diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 365172622f..271dd2534e 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -2,6 +2,9 @@ #define window_h #import "avalonia-native.h" + +@class AvnMenu; + class WindowBaseImpl; @interface AvnView : NSView @@ -10,7 +13,7 @@ class WindowBaseImpl; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; --(AvnPixelSize) getPixelSize; + -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; + (AvnPoint)toAvnPoint:(CGPoint)p; @@ -20,12 +23,11 @@ class WindowBaseImpl; -(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; -(void) ShowTitleBar: (bool) show; -(void) SetTitleBarHeightHint: (double) height; --(void) SetContent: (NSView* _Nonnull) content; + -(void) ShowBlur: (bool) show; @end @interface AvnWindow : NSWindow -+(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(void) setCanBecomeKeyAndMain; -(void) pollModalSession: (NSModalSession _Nonnull) session; @@ -34,8 +36,8 @@ class WindowBaseImpl; -(void) setEnabled: (bool) enable; -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; --(void) applyMenu:(NSMenu* _Nullable)menu; --(double) getScaling; +-(void) applyMenu:(AvnMenu* _Nullable)menu; + -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; -(bool) isDialog; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index a1d231f30a..fe80142f1a 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1,3 +1,4 @@ +#import #include "common.h" #import "window.h" #include "KeyTransform.h" @@ -6,10 +7,6 @@ #include "automation.h" #import "WindowBaseImpl.h" #include "WindowImpl.h" -#include "IWindowStateChanged.h" - - -NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; @implementation AutoFitContentView { @@ -106,26 +103,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _settingSize = false; } - --(void) SetContent: (NSView* _Nonnull) content -{ - if(content != nullptr) - { - [content removeFromSuperview]; - [self addSubview:content]; - _content = content; - } -} @end @implementation AvnView { ComPtr _parent; - ComPtr _swRenderedFrame; - AvnFramebuffer _swRenderedFrameBuffer; - bool _queuedDisplayFromThread; NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; + bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; @@ -143,11 +127,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } --(AvnPixelSize) getPixelSize -{ - return _lastPixelSize; -} - - (NSEvent*) lastMouseDownEvent { return _lastMouseDownEvent; @@ -155,7 +134,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) updateRenderTarget { - [_renderTarget resize:_lastPixelSize withScale: [[self window] backingScaleFactor]]; + [_renderTarget resize:_lastPixelSize withScale:static_cast([[self window] backingScaleFactor])]; [self setNeedsDisplayInRect:[self frame]]; } @@ -345,7 +324,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; + AvnVector delta = { 0, 0}; if(type == Wheel) { @@ -378,7 +357,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent delta.Y = [event deltaY]; } - auto timestamp = [event timestamp] * 1000; + uint32 timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(type != AvnRawMouseEventType::Move || @@ -437,6 +416,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _isXButton2Pressed = true; [self mouseEvent:event withType:XButton2Down]; break; + + default: + break; } } @@ -470,6 +452,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _isXButton2Pressed = false; [self mouseEvent:event withType:XButton2Up]; break; + + default: + break; } } @@ -523,13 +508,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)mouseEntered:(NSEvent *)event { - _isMouseOver = true; [super mouseEntered:event]; } - (void)mouseExited:(NSEvent *)event { - _isMouseOver = false; [self mouseEvent:event withType:LeaveWindow]; [super mouseExited:event]; } @@ -543,7 +526,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto key = s_KeyMap[[event keyCode]]; - auto timestamp = [event timestamp] * 1000; + uint32_t timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(_parent != nullptr) @@ -711,7 +694,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - CGRect result; + CGRect result = { 0 }; return result; } @@ -730,10 +713,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent CreateClipboard([info draggingPasteboard], nil), GetAvnDataObjectHandleFromDraggingInfo(info)); - NSDragOperation ret = 0; + NSDragOperation ret = static_cast(0); // Ensure that the managed part didn't add any new effects - reffects = (int)effects & (int)reffects; + reffects = (int)effects & reffects; // OSX requires exactly one operation if((reffects & (int)AvnDragDropEffects::Copy) != 0) @@ -829,9 +812,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _isEnabled; bool _isExtended; AvnMenu* _menu; - double _lastScaling; - IAvnAutomationPeer* _automationPeer; - NSMutableArray* _automationChildren; } -(void) setIsExtended:(bool)value; @@ -844,11 +824,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return _parent->IsDialog(); } --(double) getScaling -{ - return _lastScaling; -} - -(double) getExtendedTitleBarHeight { if(_isExtended) @@ -871,11 +846,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -+(void)closeAll -{ - [[NSApplication sharedApplication] terminate:self]; -} - - (void)performClose:(id)sender { if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) @@ -983,7 +953,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _closed = false; _isEnabled = true; - _lastScaling = [self backingScaleFactor]; + [self backingScaleFactor]; [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; _isExtended = false; @@ -1004,7 +974,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidChangeBackingProperties:(NSNotification *)notification { - _lastScaling = [self backingScaleFactor]; + [self backingScaleFactor]; } - (void)windowWillClose:(NSNotification *)notification @@ -1221,9 +1191,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { auto avnPoint = [AvnView toAvnPoint:windowPoint]; auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; + AvnVector delta = { 0, 0 }; - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } } break; From 68af00ef0e7f7eee34ee1f5dfc0e113e04a4128e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 4 May 2022 18:40:08 +0100 Subject: [PATCH 169/213] remove unused code in other classes. --- native/Avalonia.Native/src/OSX/app.mm | 12 ------------ native/Avalonia.Native/src/OSX/common.h | 3 +-- native/Avalonia.Native/src/OSX/cursor.mm | 1 - native/Avalonia.Native/src/OSX/main.mm | 6 +----- native/Avalonia.Native/src/OSX/menu.h | 1 - native/Avalonia.Native/src/OSX/menu.mm | 5 ++--- native/Avalonia.Native/src/OSX/rendertarget.mm | 4 ---- 7 files changed, 4 insertions(+), 28 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 05b129baca..14f1f6888c 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -82,18 +82,6 @@ ComPtr _events; _isHandlingSendEvent = oldHandling; } } - -// This is needed for certain embedded controls -- (BOOL) isHandlingSendEvent -{ - return _isHandlingSendEvent; -} - -- (void)setHandlingSendEvent:(BOOL)handlingSendEvent -{ - _isHandlingSendEvent = handlingSendEvent; -} - @end extern void InitializeAvnApp(IAvnApplicationEvents* events) diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 9186d9e15a..a90a235b9d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -27,7 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); -extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); +extern void SetAppMenu(IAvnMenu *menu); extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); @@ -38,7 +38,6 @@ extern NSPoint ToNSPoint (AvnPoint p); extern NSRect ToNSRect (AvnRect r); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); -extern CGFloat PrimaryDisplayHeight(); extern NSSize ToNSSize (AvnSize s); #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index dc38294a18..8638a03531 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -1,6 +1,5 @@ #include "common.h" #include "cursor.h" -#include class CursorFactory : public ComSingleObject { diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index ea79c494d7..011f881e94 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -343,7 +343,7 @@ public: @autoreleasepool { - ::SetAppMenu(s_appTitle, appMenu); + ::SetAppMenu(appMenu); return S_OK; } } @@ -428,7 +428,3 @@ AvnPoint ConvertPointY (AvnPoint p) return p; } -CGFloat PrimaryDisplayHeight() -{ - return NSMaxY([[[NSScreen screens] firstObject] frame]); -} diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 186fcf255b..ce46ac11e0 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -31,7 +31,6 @@ private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; - bool _isSeparator; bool _isCheckable; public: diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 726e58478b..fd74edd772 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -74,8 +74,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeparator) { _isCheckable = false; - _isSeparator = isSeparator; - + if(isSeparator) { _native = [NSMenuItem separatorItem]; @@ -460,7 +459,7 @@ extern IAvnMenuItem* CreateAppMenuItemSeparator() static IAvnMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (NSString* appName, IAvnMenu* menu) +extern void SetAppMenu(IAvnMenu *menu) { s_appMenu = menu; diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index dc5c24e41e..266d0345d1 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -1,14 +1,10 @@ #include "common.h" #include "rendertarget.h" -#import #import #import -#include -#include #include #include -#include @interface IOSurfaceHolder : NSObject @end From 849754e424b6ac10cb60a486c7812e28247574ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 May 2022 16:51:44 +0200 Subject: [PATCH 170/213] Expose CurrentOpacity on ISkiaDrawingContextImpl. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 1 + src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2548b9f5aa..b45968ec27 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -126,6 +126,7 @@ namespace Avalonia.Skia SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; GRContext ISkiaDrawingContextImpl.GrContext => _grContext; + double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity; /// public void Clear(Color color) diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs index 38fa5a5253..87c89beb7e 100644 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs @@ -8,5 +8,6 @@ namespace Avalonia.Skia SKCanvas SkCanvas { get; } GRContext GrContext { get; } SKSurface SkSurface { get; } + double CurrentOpacity { get; } } } From e0ec6a4ba148c9016d4f438da1de43b0b30604a3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 5 May 2022 12:16:02 -0400 Subject: [PATCH 171/213] Use top level to inform about pointer input --- src/iOS/Avalonia.iOS/TouchHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iOS/Avalonia.iOS/TouchHandler.cs b/src/iOS/Avalonia.iOS/TouchHandler.cs index 43b19c85af..959a660d8a 100644 --- a/src/iOS/Avalonia.iOS/TouchHandler.cs +++ b/src/iOS/Avalonia.iOS/TouchHandler.cs @@ -41,7 +41,7 @@ namespace Avalonia.iOS _ => RawPointerEventType.TouchUpdate }, pt, RawInputModifiers.None, id); - _device.ProcessRawEvent(ev); + _tl.Input?.Invoke(ev); if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) _knownTouches.Remove(t); @@ -49,4 +49,4 @@ namespace Avalonia.iOS } } -} \ No newline at end of file +} From c029ddfb32d253094478ac07c308a54f1afec67b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 5 May 2022 18:13:39 +0100 Subject: [PATCH 172/213] fix project config. --- .../xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 5d20a135b9..87a8312c38 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -56,10 +56,14 @@ + + Date: Thu, 5 May 2022 18:14:06 +0100 Subject: [PATCH 173/213] move avnview to its own file. --- .../project.pbxproj | 8 + native/Avalonia.Native/src/OSX/AvnView.h | 29 + native/Avalonia.Native/src/OSX/AvnView.mm | 706 ++++++++++++++++++ .../Avalonia.Native/src/OSX/INSWindowHolder.h | 2 + native/Avalonia.Native/src/OSX/ResizeScope.h | 2 + native/Avalonia.Native/src/OSX/ResizeScope.mm | 1 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 1 + native/Avalonia.Native/src/OSX/WindowImpl.mm | 1 + native/Avalonia.Native/src/OSX/automation.mm | 1 + native/Avalonia.Native/src/OSX/window.h | 12 - native/Avalonia.Native/src/OSX/window.mm | 699 +---------------- 11 files changed, 752 insertions(+), 710 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AvnView.h create mode 100644 native/Avalonia.Native/src/OSX/AvnView.mm diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 2a206b0692..2d9dcbf9c8 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; + 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; + 18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; @@ -43,6 +45,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = ""; }; @@ -50,6 +53,7 @@ 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; 18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = ""; }; 18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = ""; }; + 18391D1669284AD2EC9E866A /* AvnView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnView.h; sourceTree = ""; }; 18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = ""; }; 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; @@ -154,6 +158,8 @@ 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */, 18391E45702740FE9DD69695 /* ResizeScope.mm */, 1839171D898F9BFC1373631A /* ResizeScope.h */, + 1839132D0E2454D911F1D1F9 /* AvnView.mm */, + 18391D1669284AD2EC9E866A /* AvnView.h */, ); sourceTree = ""; }; @@ -179,6 +185,7 @@ 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, + 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -261,6 +268,7 @@ 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, + 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h new file mode 100644 index 0000000000..c6dd90150f --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -0,0 +1,29 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import + + +#import +#import +#include "window.h" +#import "comimpl.h" +#import "common.h" +#import "WindowImpl.h" +#import "KeyTransform.h" + +@class AvnAccessibilityElement; + +@interface AvnView : NSView +-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; +-(NSEvent* _Nonnull) lastMouseDownEvent; +-(AvnPoint) translateLocalPoint:(AvnPoint)pt; +-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; +-(void) onClosed; + +-(AvnPlatformResizeReason) getResizeReason; +-(void) setResizeReason:(AvnPlatformResizeReason)reason; ++ (AvnPoint)toAvnPoint:(CGPoint)p; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm new file mode 100644 index 0000000000..24cbc25502 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -0,0 +1,706 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import "AvnView.h" +#include "automation.h" + +@implementation AvnView +{ + ComPtr _parent; + NSTrackingArea* _area; + bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; + AvnInputModifiers _modifierState; + NSEvent* _lastMouseDownEvent; + bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; + NSObject* _renderTarget; + AvnPlatformResizeReason _resizeReason; + AvnAccessibilityElement* _accessibilityChild; +} + +- (void)onClosed +{ + @synchronized (self) + { + _parent = nullptr; + } +} + +- (NSEvent*) lastMouseDownEvent +{ + return _lastMouseDownEvent; +} + +- (void) updateRenderTarget +{ + [_renderTarget resize:_lastPixelSize withScale:static_cast([[self window] backingScaleFactor])]; + [self setNeedsDisplayInRect:[self frame]]; +} + +-(AvnView*) initWithParent: (WindowBaseImpl*) parent +{ + self = [super init]; + _renderTarget = parent->renderTarget; + [self setWantsLayer:YES]; + [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; + + _parent = parent; + _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; + + _modifierState = AvnInputModifiersNone; + return self; +} + +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +- (void)setLayer:(CALayer *)layer +{ + [_renderTarget setNewLayer: layer]; + [super setLayer: layer]; +} + +- (BOOL)isOpaque +{ + return YES; +} + +- (BOOL)acceptsFirstResponder +{ + return true; +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + return true; +} + +- (BOOL)canBecomeKeyView +{ + return true; +} + +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + if(_area != nullptr) + { + [self removeTrackingArea:_area]; + _area = nullptr; + } + + if (_parent == nullptr) + { + return; + } + + NSRect rect = NSZeroRect; + rect.size = newSize; + + NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; + _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; + [self addTrackingArea:_area]; + + _parent->UpdateCursor(); + + auto fsize = [self convertSizeToBacking: [self frame].size]; + + if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) + { + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); + } +} + +- (void)updateLayer +{ + AvnInsidePotentialDeadlock deadlock; + if (_parent == nullptr) + { + return; + } + + _parent->BaseEvents->RunRenderPriorityJobs(); + + if (_parent == nullptr) + { + return; + } + + _parent->BaseEvents->Paint(); +} + +- (void)drawRect:(NSRect)dirtyRect +{ + return; +} + +-(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose +{ + @autoreleasepool { + [_renderTarget setSwFrame:fb]; + dispose->Release(); + } +} + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self bounds].size.height - pt.Y; + return pt; +} + ++ (AvnPoint)toAvnPoint:(CGPoint)p +{ + AvnPoint result; + + result.X = p.x; + result.Y = p.y; + + return result; +} + +- (void) viewDidChangeBackingProperties +{ + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + if(_parent != nullptr) + { + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + } + + [super viewDidChangeBackingProperties]; +} + +- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled +{ + auto parentWindow = objc_cast([self window]); + + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + if(trigerInputWhenDisabled) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } + } + + return TRUE; + } + + return FALSE; +} + +- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type +{ + bool triggerInputWhenDisabled = type != Move; + + if([self ignoreUserInput: triggerInputWhenDisabled]) + { + return; + } + + auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta = { 0, 0}; + + if(type == Wheel) + { + auto speed = 5; + + if([event hasPreciseScrollingDeltas]) + { + speed = 50; + } + + delta.X = [event scrollingDeltaX] / speed; + delta.Y = [event scrollingDeltaY] / speed; + + if(delta.X == 0 && delta.Y == 0) + { + return; + } + } + else if (type == Magnify) + { + delta.X = delta.Y = [event magnification]; + } + else if (type == Rotate) + { + delta.X = delta.Y = [event rotation]; + } + else if (type == Swipe) + { + delta.X = [event deltaX]; + delta.Y = [event deltaY]; + } + + uint32 timestamp = static_cast([event timestamp] * 1000); + auto modifiers = [self getModifiers:[event modifierFlags]]; + + if(type != Move || + ( + [self window] != nil && + ( + [[self window] firstResponder] == nil + || ![[[self window] firstResponder] isKindOfClass: [NSView class]] + ) + ) + ) + [self becomeFirstResponder]; + + if(_parent != nullptr) + { + _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); + } + + [super mouseMoved:event]; +} + +- (BOOL) resignFirstResponder +{ + _parent->BaseEvents->LostFocus(); + return YES; +} + +- (void)mouseMoved:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; +} + +- (void)mouseDown:(NSEvent *)event +{ + _isLeftPressed = true; + _lastMouseDownEvent = event; + [self mouseEvent:event withType:LeftButtonDown]; +} + +- (void)otherMouseDown:(NSEvent *)event +{ + _lastMouseDownEvent = event; + + switch(event.buttonNumber) + { + case 2: + case 3: + _isMiddlePressed = true; + [self mouseEvent:event withType:MiddleButtonDown]; + break; + case 4: + _isXButton1Pressed = true; + [self mouseEvent:event withType:XButton1Down]; + break; + case 5: + _isXButton2Pressed = true; + [self mouseEvent:event withType:XButton2Down]; + break; + + default: + break; + } +} + +- (void)rightMouseDown:(NSEvent *)event +{ + _isRightPressed = true; + _lastMouseDownEvent = event; + [self mouseEvent:event withType:RightButtonDown]; +} + +- (void)mouseUp:(NSEvent *)event +{ + _isLeftPressed = false; + [self mouseEvent:event withType:LeftButtonUp]; +} + +- (void)otherMouseUp:(NSEvent *)event +{ + switch(event.buttonNumber) + { + case 2: + case 3: + _isMiddlePressed = false; + [self mouseEvent:event withType:MiddleButtonUp]; + break; + case 4: + _isXButton1Pressed = false; + [self mouseEvent:event withType:XButton1Up]; + break; + case 5: + _isXButton2Pressed = false; + [self mouseEvent:event withType:XButton2Up]; + break; + + default: + break; + } +} + +- (void)rightMouseUp:(NSEvent *)event +{ + _isRightPressed = false; + [self mouseEvent:event withType:RightButtonUp]; +} + +- (void)mouseDragged:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; + [super mouseDragged:event]; +} + +- (void)otherMouseDragged:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; + [super otherMouseDragged:event]; +} + +- (void)rightMouseDragged:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; + [super rightMouseDragged:event]; +} + +- (void)scrollWheel:(NSEvent *)event +{ + [self mouseEvent:event withType:Wheel]; + [super scrollWheel:event]; +} + +- (void)magnifyWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Magnify]; + [super magnifyWithEvent:event]; +} + +- (void)rotateWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Rotate]; + [super rotateWithEvent:event]; +} + +- (void)swipeWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Swipe]; + [super swipeWithEvent:event]; +} + +- (void)mouseEntered:(NSEvent *)event +{ + [super mouseEntered:event]; +} + +- (void)mouseExited:(NSEvent *)event +{ + [self mouseEvent:event withType:LeaveWindow]; + [super mouseExited:event]; +} + +- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type +{ + if([self ignoreUserInput: false]) + { + return; + } + + auto key = s_KeyMap[[event keyCode]]; + + uint32_t timestamp = static_cast([event timestamp] * 1000); + auto modifiers = [self getModifiers:[event modifierFlags]]; + + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + } +} + +- (BOOL)performKeyEquivalent:(NSEvent *)event +{ + bool result = _lastKeyHandled; + + _lastKeyHandled = false; + + return result; +} + +- (void)flagsChanged:(NSEvent *)event +{ + auto newModifierState = [self getModifiers:[event modifierFlags]]; + + bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; + bool isControlCurrentlyPressed = (_modifierState & Control) == Control; + bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; + bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; + + bool isAltPressed = (newModifierState & Alt) == Alt; + bool isControlPressed = (newModifierState & Control) == Control; + bool isShiftPressed = (newModifierState & Shift) == Shift; + bool isCommandPressed = (newModifierState & Windows) == Windows; + + + if (isAltPressed && !isAltCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isAltCurrentlyPressed && !isAltPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isControlPressed && !isControlCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isControlCurrentlyPressed && !isControlPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isShiftPressed && !isShiftCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isShiftCurrentlyPressed && !isShiftPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if(isCommandPressed && !isCommandCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isCommandCurrentlyPressed && ! isCommandPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + _modifierState = newModifierState; + + [[self inputContext] handleEvent:event]; + [super flagsChanged:event]; +} + +- (void)keyDown:(NSEvent *)event +{ + [self keyboardEvent:event withType:KeyDown]; + [[self inputContext] handleEvent:event]; + [super keyDown:event]; +} + +- (void)keyUp:(NSEvent *)event +{ + [self keyboardEvent:event withType:KeyUp]; + [super keyUp:event]; +} + +- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod +{ + unsigned int rv = 0; + + if (mod & NSEventModifierFlagControl) + rv |= Control; + if (mod & NSEventModifierFlagShift) + rv |= Shift; + if (mod & NSEventModifierFlagOption) + rv |= Alt; + if (mod & NSEventModifierFlagCommand) + rv |= Windows; + + if (_isLeftPressed) + rv |= LeftMouseButton; + if (_isMiddlePressed) + rv |= MiddleMouseButton; + if (_isRightPressed) + rv |= RightMouseButton; + if (_isXButton1Pressed) + rv |= XButton1MouseButton; + if (_isXButton2Pressed) + rv |= XButton2MouseButton; + + return (AvnInputModifiers)rv; +} + +- (BOOL)hasMarkedText +{ + return _lastKeyHandled; +} + +- (NSRange)markedRange +{ + return NSMakeRange(NSNotFound, 0); +} + +- (NSRange)selectedRange +{ + return NSMakeRange(NSNotFound, 0); +} + +- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange +{ + +} + +- (void)unmarkText +{ + +} + +- (NSArray *)validAttributesForMarkedText +{ + return [NSArray new]; +} + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange +{ + return [NSAttributedString new]; +} + +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange +{ + if(!_lastKeyHandled) + { + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + } + } +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)point +{ + return 0; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange +{ + CGRect result = { 0 }; + + return result; +} + +- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info +{ + auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; + NSDragOperation nsop = [info draggingSourceOperationMask]; + + auto effects = ConvertDragDropEffects(nsop); + int reffects = (int)_parent->BaseEvents + ->DragEvent(type, point, modifiers, effects, + CreateClipboard([info draggingPasteboard], nil), + GetAvnDataObjectHandleFromDraggingInfo(info)); + + NSDragOperation ret = static_cast(0); + + // Ensure that the managed part didn't add any new effects + reffects = (int)effects & reffects; + + // OSX requires exactly one operation + if((reffects & (int)AvnDragDropEffects::Copy) != 0) + ret = NSDragOperationCopy; + else if((reffects & (int)AvnDragDropEffects::Move) != 0) + ret = NSDragOperationMove; + else if((reffects & (int)AvnDragDropEffects::Link) != 0) + ret = NSDragOperationLink; + if(ret == 0) + ret = NSDragOperationNone; + return ret; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )sender +{ + +} + +- (AvnPlatformResizeReason)getResizeReason +{ + return _resizeReason; +} + +- (void)setResizeReason:(AvnPlatformResizeReason)reason +{ + _resizeReason = reason; +} + +- (AvnAccessibilityElement *) accessibilityChild +{ + if (_accessibilityChild == nil) + { + auto peer = _parent->BaseEvents->GetAutomationPeer(); + + if (peer == nil) + return nil; + + _accessibilityChild = [AvnAccessibilityElement acquire:peer]; + } + + return _accessibilityChild; +} + +- (NSArray *)accessibilityChildren +{ + auto child = [self accessibilityChild]; + return NSAccessibilityUnignoredChildrenForOnlyChild(child); +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + return [[self accessibilityChild] accessibilityHitTest:point]; +} + +- (id)accessibilityFocusedUIElement +{ + return [[self accessibilityChild] accessibilityFocusedUIElement]; +} + +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h index aa8c34ef00..adc0dd1990 100644 --- a/native/Avalonia.Native/src/OSX/INSWindowHolder.h +++ b/native/Avalonia.Native/src/OSX/INSWindowHolder.h @@ -6,6 +6,8 @@ #ifndef AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H #define AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H +@class AvnView; + struct INSWindowHolder { virtual AvnWindow* _Nonnull GetNSWindow () = 0; diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.h b/native/Avalonia.Native/src/OSX/ResizeScope.h index c57dc96690..7509f93c01 100644 --- a/native/Avalonia.Native/src/OSX/ResizeScope.h +++ b/native/Avalonia.Native/src/OSX/ResizeScope.h @@ -9,6 +9,8 @@ #include "window.h" #include "avalonia-native.h" +@class AvnView; + class ResizeScope { public: diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.mm b/native/Avalonia.Native/src/OSX/ResizeScope.mm index 8644b41fba..90e7f5cf15 100644 --- a/native/Avalonia.Native/src/OSX/ResizeScope.mm +++ b/native/Avalonia.Native/src/OSX/ResizeScope.mm @@ -5,6 +5,7 @@ #import #include "ResizeScope.h" +#import "AvnView.h" ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) { _view = view; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 9959d6a034..8f409fa84b 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -6,6 +6,7 @@ #import #include "common.h" #import "window.h" +#import "AvnView.h" #include "menu.h" #include "rendertarget.h" #include "automation.h" diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 9cf5160c97..ef8be4be9c 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -5,6 +5,7 @@ #import #import "window.h" +#import "AvnView.h" #include "automation.h" #include "menu.h" #import "WindowImpl.h" diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 087d15a248..7a0b3b8127 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -3,6 +3,7 @@ #import "window.h" #include "AvnString.h" #import "INSWindowHolder.h" +#import "AvnView.h" @interface AvnAccessibilityElement (Events) - (void) raiseChildrenChanged; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 271dd2534e..8caceb5bc1 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -7,18 +7,6 @@ class WindowBaseImpl; -@interface AvnView : NSView --(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; --(NSEvent* _Nonnull) lastMouseDownEvent; --(AvnPoint) translateLocalPoint:(AvnPoint)pt; --(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; --(void) onClosed; - --(AvnPlatformResizeReason) getResizeReason; --(void) setResizeReason:(AvnPlatformResizeReason)reason; -+ (AvnPoint)toAvnPoint:(CGPoint)p; -@end - @interface AutoFitContentView : NSView -(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; -(void) ShowTitleBar: (bool) show; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index fe80142f1a..a66ff94c8a 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -7,6 +7,7 @@ #include "automation.h" #import "WindowBaseImpl.h" #include "WindowImpl.h" +#include "AvnView.h" @implementation AutoFitContentView { @@ -105,704 +106,6 @@ } @end -@implementation AvnView -{ - ComPtr _parent; - NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; - AvnInputModifiers _modifierState; - NSEvent* _lastMouseDownEvent; - bool _lastKeyHandled; - AvnPixelSize _lastPixelSize; - NSObject* _renderTarget; - AvnPlatformResizeReason _resizeReason; - AvnAccessibilityElement* _accessibilityChild; -} - -- (void)onClosed -{ - @synchronized (self) - { - _parent = nullptr; - } -} - -- (NSEvent*) lastMouseDownEvent -{ - return _lastMouseDownEvent; -} - -- (void) updateRenderTarget -{ - [_renderTarget resize:_lastPixelSize withScale:static_cast([[self window] backingScaleFactor])]; - [self setNeedsDisplayInRect:[self frame]]; -} - --(AvnView*) initWithParent: (WindowBaseImpl*) parent -{ - self = [super init]; - _renderTarget = parent->renderTarget; - [self setWantsLayer:YES]; - [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; - - _parent = parent; - _area = nullptr; - _lastPixelSize.Height = 100; - _lastPixelSize.Width = 100; - [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; - - _modifierState = AvnInputModifiersNone; - return self; -} - -- (BOOL)isFlipped -{ - return YES; -} - -- (BOOL)wantsUpdateLayer -{ - return YES; -} - -- (void)setLayer:(CALayer *)layer -{ - [_renderTarget setNewLayer: layer]; - [super setLayer: layer]; -} - -- (BOOL)isOpaque -{ - return YES; -} - -- (BOOL)acceptsFirstResponder -{ - return true; -} - -- (BOOL)acceptsFirstMouse:(NSEvent *)event -{ - return true; -} - -- (BOOL)canBecomeKeyView -{ - return true; -} - --(void)setFrameSize:(NSSize)newSize -{ - [super setFrameSize:newSize]; - - if(_area != nullptr) - { - [self removeTrackingArea:_area]; - _area = nullptr; - } - - if (_parent == nullptr) - { - return; - } - - NSRect rect = NSZeroRect; - rect.size = newSize; - - NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; - _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; - [self addTrackingArea:_area]; - - _parent->UpdateCursor(); - - auto fsize = [self convertSizeToBacking: [self frame].size]; - - if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) - { - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - - auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); - } -} - -- (void)updateLayer -{ - AvnInsidePotentialDeadlock deadlock; - if (_parent == nullptr) - { - return; - } - - _parent->BaseEvents->RunRenderPriorityJobs(); - - if (_parent == nullptr) - { - return; - } - - _parent->BaseEvents->Paint(); -} - -- (void)drawRect:(NSRect)dirtyRect -{ - return; -} - --(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose -{ - @autoreleasepool { - [_renderTarget setSwFrame:fb]; - dispose->Release(); - } -} - -- (AvnPoint) translateLocalPoint:(AvnPoint)pt -{ - pt.Y = [self bounds].size.height - pt.Y; - return pt; -} - -+ (AvnPoint)toAvnPoint:(CGPoint)p -{ - AvnPoint result; - - result.X = p.x; - result.Y = p.y; - - return result; -} - -- (void) viewDidChangeBackingProperties -{ - auto fsize = [self convertSizeToBacking: [self frame].size]; - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - - if(_parent != nullptr) - { - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); - } - - [super viewDidChangeBackingProperties]; -} - -- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled -{ - auto parentWindow = objc_cast([self window]); - - if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) - { - if(trigerInputWhenDisabled) - { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) - { - window->WindowEvents->GotInputWhenDisabled(); - } - } - - return TRUE; - } - - return FALSE; -} - -- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type -{ - bool triggerInputWhenDisabled = type != Move; - - if([self ignoreUserInput: triggerInputWhenDisabled]) - { - return; - } - - auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; - auto avnPoint = [AvnView toAvnPoint:localPoint]; - auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta = { 0, 0}; - - if(type == Wheel) - { - auto speed = 5; - - if([event hasPreciseScrollingDeltas]) - { - speed = 50; - } - - delta.X = [event scrollingDeltaX] / speed; - delta.Y = [event scrollingDeltaY] / speed; - - if(delta.X == 0 && delta.Y == 0) - { - return; - } - } - else if (type == Magnify) - { - delta.X = delta.Y = [event magnification]; - } - else if (type == Rotate) - { - delta.X = delta.Y = [event rotation]; - } - else if (type == Swipe) - { - delta.X = [event deltaX]; - delta.Y = [event deltaY]; - } - - uint32 timestamp = static_cast([event timestamp] * 1000); - auto modifiers = [self getModifiers:[event modifierFlags]]; - - if(type != AvnRawMouseEventType::Move || - ( - [self window] != nil && - ( - [[self window] firstResponder] == nil - || ![[[self window] firstResponder] isKindOfClass: [NSView class]] - ) - ) - ) - [self becomeFirstResponder]; - - if(_parent != nullptr) - { - _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); - } - - [super mouseMoved:event]; -} - -- (BOOL) resignFirstResponder -{ - _parent->BaseEvents->LostFocus(); - return YES; -} - -- (void)mouseMoved:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; -} - -- (void)mouseDown:(NSEvent *)event -{ - _isLeftPressed = true; - _lastMouseDownEvent = event; - [self mouseEvent:event withType:LeftButtonDown]; -} - -- (void)otherMouseDown:(NSEvent *)event -{ - _lastMouseDownEvent = event; - - switch(event.buttonNumber) - { - case 2: - case 3: - _isMiddlePressed = true; - [self mouseEvent:event withType:MiddleButtonDown]; - break; - case 4: - _isXButton1Pressed = true; - [self mouseEvent:event withType:XButton1Down]; - break; - case 5: - _isXButton2Pressed = true; - [self mouseEvent:event withType:XButton2Down]; - break; - - default: - break; - } -} - -- (void)rightMouseDown:(NSEvent *)event -{ - _isRightPressed = true; - _lastMouseDownEvent = event; - [self mouseEvent:event withType:RightButtonDown]; -} - -- (void)mouseUp:(NSEvent *)event -{ - _isLeftPressed = false; - [self mouseEvent:event withType:LeftButtonUp]; -} - -- (void)otherMouseUp:(NSEvent *)event -{ - switch(event.buttonNumber) - { - case 2: - case 3: - _isMiddlePressed = false; - [self mouseEvent:event withType:MiddleButtonUp]; - break; - case 4: - _isXButton1Pressed = false; - [self mouseEvent:event withType:XButton1Up]; - break; - case 5: - _isXButton2Pressed = false; - [self mouseEvent:event withType:XButton2Up]; - break; - - default: - break; - } -} - -- (void)rightMouseUp:(NSEvent *)event -{ - _isRightPressed = false; - [self mouseEvent:event withType:RightButtonUp]; -} - -- (void)mouseDragged:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; - [super mouseDragged:event]; -} - -- (void)otherMouseDragged:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; - [super otherMouseDragged:event]; -} - -- (void)rightMouseDragged:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; - [super rightMouseDragged:event]; -} - -- (void)scrollWheel:(NSEvent *)event -{ - [self mouseEvent:event withType:Wheel]; - [super scrollWheel:event]; -} - -- (void)magnifyWithEvent:(NSEvent *)event -{ - [self mouseEvent:event withType:Magnify]; - [super magnifyWithEvent:event]; -} - -- (void)rotateWithEvent:(NSEvent *)event -{ - [self mouseEvent:event withType:Rotate]; - [super rotateWithEvent:event]; -} - -- (void)swipeWithEvent:(NSEvent *)event -{ - [self mouseEvent:event withType:Swipe]; - [super swipeWithEvent:event]; -} - -- (void)mouseEntered:(NSEvent *)event -{ - [super mouseEntered:event]; -} - -- (void)mouseExited:(NSEvent *)event -{ - [self mouseEvent:event withType:LeaveWindow]; - [super mouseExited:event]; -} - -- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type -{ - if([self ignoreUserInput: false]) - { - return; - } - - auto key = s_KeyMap[[event keyCode]]; - - uint32_t timestamp = static_cast([event timestamp] * 1000); - auto modifiers = [self getModifiers:[event modifierFlags]]; - - if(_parent != nullptr) - { - _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); - } -} - -- (BOOL)performKeyEquivalent:(NSEvent *)event -{ - bool result = _lastKeyHandled; - - _lastKeyHandled = false; - - return result; -} - -- (void)flagsChanged:(NSEvent *)event -{ - auto newModifierState = [self getModifiers:[event modifierFlags]]; - - bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; - bool isControlCurrentlyPressed = (_modifierState & Control) == Control; - bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; - bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; - - bool isAltPressed = (newModifierState & Alt) == Alt; - bool isControlPressed = (newModifierState & Control) == Control; - bool isShiftPressed = (newModifierState & Shift) == Shift; - bool isCommandPressed = (newModifierState & Windows) == Windows; - - - if (isAltPressed && !isAltCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if (isAltCurrentlyPressed && !isAltPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - if (isControlPressed && !isControlCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if (isControlCurrentlyPressed && !isControlPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - if (isShiftPressed && !isShiftCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if(isShiftCurrentlyPressed && !isShiftPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - if(isCommandPressed && !isCommandCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if(isCommandCurrentlyPressed && ! isCommandPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - _modifierState = newModifierState; - - [[self inputContext] handleEvent:event]; - [super flagsChanged:event]; -} - -- (void)keyDown:(NSEvent *)event -{ - [self keyboardEvent:event withType:KeyDown]; - [[self inputContext] handleEvent:event]; - [super keyDown:event]; -} - -- (void)keyUp:(NSEvent *)event -{ - [self keyboardEvent:event withType:KeyUp]; - [super keyUp:event]; -} - -- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod -{ - unsigned int rv = 0; - - if (mod & NSEventModifierFlagControl) - rv |= Control; - if (mod & NSEventModifierFlagShift) - rv |= Shift; - if (mod & NSEventModifierFlagOption) - rv |= Alt; - if (mod & NSEventModifierFlagCommand) - rv |= Windows; - - if (_isLeftPressed) - rv |= LeftMouseButton; - if (_isMiddlePressed) - rv |= MiddleMouseButton; - if (_isRightPressed) - rv |= RightMouseButton; - if (_isXButton1Pressed) - rv |= XButton1MouseButton; - if (_isXButton2Pressed) - rv |= XButton2MouseButton; - - return (AvnInputModifiers)rv; -} - -- (BOOL)hasMarkedText -{ - return _lastKeyHandled; -} - -- (NSRange)markedRange -{ - return NSMakeRange(NSNotFound, 0); -} - -- (NSRange)selectedRange -{ - return NSMakeRange(NSNotFound, 0); -} - -- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange -{ - -} - -- (void)unmarkText -{ - -} - -- (NSArray *)validAttributesForMarkedText -{ - return [NSArray new]; -} - -- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange -{ - return [NSAttributedString new]; -} - -- (void)insertText:(id)string replacementRange:(NSRange)replacementRange -{ - if(!_lastKeyHandled) - { - if(_parent != nullptr) - { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); - } - } -} - -- (NSUInteger)characterIndexForPoint:(NSPoint)point -{ - return 0; -} - -- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange -{ - CGRect result = { 0 }; - - return result; -} - -- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info -{ - auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; - auto avnPoint = [AvnView toAvnPoint:localPoint]; - auto point = [self translateLocalPoint:avnPoint]; - auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; - NSDragOperation nsop = [info draggingSourceOperationMask]; - - auto effects = ConvertDragDropEffects(nsop); - int reffects = (int)_parent->BaseEvents - ->DragEvent(type, point, modifiers, effects, - CreateClipboard([info draggingPasteboard], nil), - GetAvnDataObjectHandleFromDraggingInfo(info)); - - NSDragOperation ret = static_cast(0); - - // Ensure that the managed part didn't add any new effects - reffects = (int)effects & reffects; - - // OSX requires exactly one operation - if((reffects & (int)AvnDragDropEffects::Copy) != 0) - ret = NSDragOperationCopy; - else if((reffects & (int)AvnDragDropEffects::Move) != 0) - ret = NSDragOperationMove; - else if((reffects & (int)AvnDragDropEffects::Link) != 0) - ret = NSDragOperationLink; - if(ret == 0) - ret = NSDragOperationNone; - return ret; -} - -- (NSDragOperation)draggingEntered:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; -} - -- (NSDragOperation)draggingUpdated:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; -} - -- (void)draggingExited:(id )sender -{ - [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; -} - -- (BOOL)prepareForDragOperation:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; -} - -- (BOOL)performDragOperation:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; -} - -- (void)concludeDragOperation:(nullable id )sender -{ - -} - -- (AvnPlatformResizeReason)getResizeReason -{ - return _resizeReason; -} - -- (void)setResizeReason:(AvnPlatformResizeReason)reason -{ - _resizeReason = reason; -} - -- (AvnAccessibilityElement *) accessibilityChild -{ - if (_accessibilityChild == nil) - { - auto peer = _parent->BaseEvents->GetAutomationPeer(); - - if (peer == nil) - return nil; - - _accessibilityChild = [AvnAccessibilityElement acquire:peer]; - } - - return _accessibilityChild; -} - -- (NSArray *)accessibilityChildren -{ - auto child = [self accessibilityChild]; - return NSAccessibilityUnignoredChildrenForOnlyChild(child); -} - -- (id)accessibilityHitTest:(NSPoint)point -{ - return [[self accessibilityChild] accessibilityHitTest:point]; -} - -- (id)accessibilityFocusedUIElement -{ - return [[self accessibilityChild] accessibilityFocusedUIElement]; -} - -@end - @implementation AvnWindow { From f19639684d4fd7ac1b497a27f874475c6888447d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 5 May 2022 18:17:58 +0100 Subject: [PATCH 174/213] move AutoFitContentView to its own file. --- .../src/OSX/AutoFitContentView.h | 15 +++ .../src/OSX/AutoFitContentView.mm | 104 ++++++++++++++++++ .../project.pbxproj | 8 ++ .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 1 + native/Avalonia.Native/src/OSX/WindowImpl.mm | 3 +- native/Avalonia.Native/src/OSX/window.h | 8 -- native/Avalonia.Native/src/OSX/window.mm | 98 +---------------- 8 files changed, 132 insertions(+), 107 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AutoFitContentView.h create mode 100644 native/Avalonia.Native/src/OSX/AutoFitContentView.mm diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.h b/native/Avalonia.Native/src/OSX/AutoFitContentView.h new file mode 100644 index 0000000000..68c9fb8dc8 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.h @@ -0,0 +1,15 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import "avalonia-native.h" + +@interface AutoFitContentView : NSView +-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; +-(void) ShowTitleBar: (bool) show; +-(void) SetTitleBarHeightHint: (double) height; + +-(void) ShowBlur: (bool) show; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm new file mode 100644 index 0000000000..92d6f67a91 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm @@ -0,0 +1,104 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#include "AvnView.h" +#import "AutoFitContentView.h" + +@implementation AutoFitContentView +{ + NSVisualEffectView* _titleBarMaterial; + NSBox* _titleBarUnderline; + NSView* _content; + NSVisualEffectView* _blurBehind; + double _titleBarHeightHint; + bool _settingSize; +} + +-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content +{ + _titleBarHeightHint = -1; + _content = content; + _settingSize = false; + + [self setAutoresizesSubviews:true]; + [self setWantsLayer:true]; + + _titleBarMaterial = [NSVisualEffectView new]; + [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; + [_titleBarMaterial setWantsLayer:true]; + _titleBarMaterial.hidden = true; + + _titleBarUnderline = [NSBox new]; + _titleBarUnderline.boxType = NSBoxSeparator; + _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; + _titleBarUnderline.hidden = true; + + [self addSubview:_titleBarMaterial]; + [self addSubview:_titleBarUnderline]; + + _blurBehind = [NSVisualEffectView new]; + [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [_blurBehind setMaterial:NSVisualEffectMaterialLight]; + [_blurBehind setWantsLayer:true]; + _blurBehind.hidden = true; + + [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + [self addSubview:_blurBehind]; + [self addSubview:_content]; + + [self setWantsLayer:true]; + return self; +} + +-(void) ShowBlur:(bool)show +{ + _blurBehind.hidden = !show; +} + +-(void) ShowTitleBar: (bool) show +{ + _titleBarMaterial.hidden = !show; + _titleBarUnderline.hidden = !show; +} + +-(void) SetTitleBarHeightHint: (double) height +{ + _titleBarHeightHint = height; + + [self setFrameSize:self.frame.size]; +} + +-(void)setFrameSize:(NSSize)newSize +{ + if(_settingSize) + { + return; + } + + _settingSize = true; + [super setFrameSize:newSize]; + + auto window = objc_cast([self window]); + + // TODO get actual titlebar size + + double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; + + NSRect tbar; + tbar.origin.x = 0; + tbar.origin.y = newSize.height - height; + tbar.size.width = newSize.width; + tbar.size.height = height; + + [_titleBarMaterial setFrame:tbar]; + tbar.size.height = height < 1 ? 0 : 1; + [_titleBarUnderline setFrame:tbar]; + + _settingSize = false; +} +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 2d9dcbf9c8..88e4c0c682 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -13,9 +13,11 @@ 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; }; 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; }; 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; + 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; }; 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; + 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */; }; 18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; @@ -48,6 +50,8 @@ 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; + 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoFitContentView.h; sourceTree = ""; }; + 1839166350F32661F3ABD70F /* AutoFitContentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AutoFitContentView.mm; sourceTree = ""; }; 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = ""; }; 1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = ""; }; 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; @@ -160,6 +164,8 @@ 1839171D898F9BFC1373631A /* ResizeScope.h */, 1839132D0E2454D911F1D1F9 /* AvnView.mm */, 18391D1669284AD2EC9E866A /* AvnView.h */, + 1839166350F32661F3ABD70F /* AutoFitContentView.mm */, + 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */, ); sourceTree = ""; }; @@ -186,6 +192,7 @@ 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, + 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -269,6 +276,7 @@ 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, + 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 94175b8187..d2690cee41 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -9,6 +9,8 @@ #import "rendertarget.h" #include "INSWindowHolder.h" +@class AutoFitContentView; + class WindowBaseImpl : public virtual ComObject, public virtual IAvnWindowBase, public INSWindowHolder { diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 8f409fa84b..ca610d3ce6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -13,6 +13,7 @@ #import "WindowBaseImpl.h" #import "cursor.h" #include "ResizeScope.h" +#import "AutoFitContentView.h" WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { _shown = false; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ef8be4be9c..954d27ab98 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -5,10 +5,9 @@ #import #import "window.h" +#import "AutoFitContentView.h" #import "AvnView.h" #include "automation.h" -#include "menu.h" -#import "WindowImpl.h" WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 8caceb5bc1..e9b98ce9ae 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -7,14 +7,6 @@ class WindowBaseImpl; -@interface AutoFitContentView : NSView --(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; --(void) ShowTitleBar: (bool) show; --(void) SetTitleBarHeightHint: (double) height; - --(void) ShowBlur: (bool) show; -@end - @interface AvnWindow : NSWindow -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(void) setCanBecomeKeyAndMain; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index a66ff94c8a..5bd3d8ddac 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -8,103 +8,7 @@ #import "WindowBaseImpl.h" #include "WindowImpl.h" #include "AvnView.h" - -@implementation AutoFitContentView -{ - NSVisualEffectView* _titleBarMaterial; - NSBox* _titleBarUnderline; - NSView* _content; - NSVisualEffectView* _blurBehind; - double _titleBarHeightHint; - bool _settingSize; -} - --(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content -{ - _titleBarHeightHint = -1; - _content = content; - _settingSize = false; - - [self setAutoresizesSubviews:true]; - [self setWantsLayer:true]; - - _titleBarMaterial = [NSVisualEffectView new]; - [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; - [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; - [_titleBarMaterial setWantsLayer:true]; - _titleBarMaterial.hidden = true; - - _titleBarUnderline = [NSBox new]; - _titleBarUnderline.boxType = NSBoxSeparator; - _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; - _titleBarUnderline.hidden = true; - - [self addSubview:_titleBarMaterial]; - [self addSubview:_titleBarUnderline]; - - _blurBehind = [NSVisualEffectView new]; - [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; - [_blurBehind setMaterial:NSVisualEffectMaterialLight]; - [_blurBehind setWantsLayer:true]; - _blurBehind.hidden = true; - - [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - - [self addSubview:_blurBehind]; - [self addSubview:_content]; - - [self setWantsLayer:true]; - return self; -} - --(void) ShowBlur:(bool)show -{ - _blurBehind.hidden = !show; -} - --(void) ShowTitleBar: (bool) show -{ - _titleBarMaterial.hidden = !show; - _titleBarUnderline.hidden = !show; -} - --(void) SetTitleBarHeightHint: (double) height -{ - _titleBarHeightHint = height; - - [self setFrameSize:self.frame.size]; -} - --(void)setFrameSize:(NSSize)newSize -{ - if(_settingSize) - { - return; - } - - _settingSize = true; - [super setFrameSize:newSize]; - - auto window = objc_cast([self window]); - - // TODO get actual titlebar size - - double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; - - NSRect tbar; - tbar.origin.x = 0; - tbar.origin.y = newSize.height - height; - tbar.size.width = newSize.width; - tbar.size.height = height; - - [_titleBarMaterial setFrame:tbar]; - tbar.size.height = height < 1 ? 0 : 1; - [_titleBarUnderline setFrame:tbar]; - - _settingSize = false; -} -@end +#include "AutoFitContentView.h" @implementation AvnWindow From 3aab84d8daca2575897ca435af49f073f0a8aeb2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 5 May 2022 18:21:58 +0100 Subject: [PATCH 175/213] remove some imports no longer needed. --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 2 -- native/Avalonia.Native/src/OSX/WindowImpl.mm | 1 - native/Avalonia.Native/src/OSX/window.mm | 4 ---- 3 files changed, 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index ca610d3ce6..fb67addce1 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -8,9 +8,7 @@ #import "window.h" #import "AvnView.h" #include "menu.h" -#include "rendertarget.h" #include "automation.h" -#import "WindowBaseImpl.h" #import "cursor.h" #include "ResizeScope.h" #import "AutoFitContentView.h" diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 954d27ab98..0d325c4490 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -9,7 +9,6 @@ #import "AvnView.h" #include "automation.h" - WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { _isClientAreaExtended = false; _extendClientHints = AvnDefaultChrome; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index bf2cbdbf19..68a459d088 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1,15 +1,11 @@ #import #include "common.h" #import "window.h" -#include "KeyTransform.h" #include "menu.h" -#include "rendertarget.h" #include "automation.h" #import "WindowBaseImpl.h" #include "WindowImpl.h" #include "AvnView.h" -#include "AutoFitContentView.h" - @implementation AvnWindow { From 174b94f3b4c822018dcc7daaccc9f1420ef8c172 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 6 May 2022 09:28:45 +0200 Subject: [PATCH 176/213] Replace ImageMagick with ImageSharp for render test comparisons. --- Avalonia.sln | 2 +- ....NET-Q16-AnyCPU.props => ImageSharp.props} | 2 +- .../Avalonia.Direct2D1.RenderTests.csproj | 2 +- tests/Avalonia.RenderTests/TestBase.cs | 68 ++++++++++++++++--- .../Avalonia.Skia.RenderTests.csproj | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) rename build/{Magick.NET-Q16-AnyCPU.props => ImageSharp.props} (63%) diff --git a/Avalonia.sln b/Avalonia.sln index ea30514c3e..c8e513f94c 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -99,7 +99,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props - build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props build\Microsoft.CSharp.props = build\Microsoft.CSharp.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props @@ -118,6 +117,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\System.Memory.props = build\System.Memory.props build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\XUnit.props = build\XUnit.props + build\ImageSharp.props = build\ImageSharp.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" diff --git a/build/Magick.NET-Q16-AnyCPU.props b/build/ImageSharp.props similarity index 63% rename from build/Magick.NET-Q16-AnyCPU.props rename to build/ImageSharp.props index 21d9cdcb1f..178c274ac9 100644 --- a/build/Magick.NET-Q16-AnyCPU.props +++ b/build/ImageSharp.props @@ -1,5 +1,5 @@ - + diff --git a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index e7f1552370..52acc78db1 100644 --- a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -19,6 +19,6 @@ - + diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index b70c721085..523876500f 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -1,10 +1,9 @@ using System.IO; using System.Runtime.CompilerServices; -using ImageMagick; using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Rendering; - +using SixLabors.ImageSharp; using Xunit; using Avalonia.Platform; using System.Threading.Tasks; @@ -12,6 +11,8 @@ using System; using System.Threading; using Avalonia.Media; using Avalonia.Threading; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; #if AVALONIA_SKIA using Avalonia.Skia; #else @@ -119,12 +120,12 @@ namespace Avalonia.Direct2D1.RenderTests var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); - using (var expected = new MagickImage(expectedPath)) - using (var immediate = new MagickImage(immediatePath)) - using (var deferred = new MagickImage(deferredPath)) + using (var expected = Image.Load(expectedPath)) + using (var immediate = Image.Load(immediatePath)) + using (var deferred = Image.Load(deferredPath)) { - double immediateError = expected.Compare(immediate, ErrorMetric.RootMeanSquared); - double deferredError = expected.Compare(deferred, ErrorMetric.RootMeanSquared); + var immediateError = CompareImages(immediate, expected); + var deferredError = CompareImages(deferred, expected); if (immediateError > 0.022) { @@ -143,10 +144,10 @@ namespace Avalonia.Direct2D1.RenderTests var expectedPath = Path.Combine(OutputPath, testName + ".expected.png"); var actualPath = Path.Combine(OutputPath, testName + ".out.png"); - using (var expected = new MagickImage(expectedPath)) - using (var actual = new MagickImage(actualPath)) + using (var expected = Image.Load(expectedPath)) + using (var actual = Image.Load(actualPath)) { - double immediateError = expected.Compare(actual, ErrorMetric.RootMeanSquared); + double immediateError = CompareImages(actual, expected); if (immediateError > 0.022) { @@ -154,6 +155,53 @@ namespace Avalonia.Direct2D1.RenderTests } } } + + /// + /// Calculates root mean square error for given two images. + /// Based roughly on ImageMagick implementation to ensure consistency. + /// + private static double CompareImages(Image actual, Image expected) + { + if (actual.Width != expected.Width || actual.Height != expected.Height) + { + throw new ArgumentException("Images have different resolutions"); + } + + var quantity = actual.Width * actual.Height; + double squaresError = 0; + + const double scale = 1 / 255d; + + for (var x = 0; x < actual.Width; x++) + { + double localError = 0; + + for (var y = 0; y < actual.Height; y++) + { + var expectedAlpha = expected[x, y].A * scale; + var actualAlpha = actual[x, y].A * scale; + + var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R); + var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G); + var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B); + var a = expectedAlpha - actualAlpha; + + var error = r * r + g * g + b * b + a * a; + + localError += error; + } + + squaresError += localError; + } + + var meanSquaresError = squaresError / quantity; + + const int channelCount = 4; + + meanSquaresError = meanSquaresError / channelCount; + + return Math.Sqrt(meanSquaresError); + } private string GetTestsDirectory() { diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index ffbcdd9e7c..d3f2b44968 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -20,7 +20,7 @@ - + From 66d7ffe2e99d8809bf1801bd7df20397db4260e0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 11:23:52 +0100 Subject: [PATCH 177/213] use content min/max size for minsize. --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index fb67addce1..43a27a34b2 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -225,8 +225,8 @@ HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) { START_COM_CALL; @autoreleasepool { - [Window setMinSize:ToNSSize(minSize)]; - [Window setMaxSize:ToNSSize(maxSize)]; + [Window setContentMinSize:ToNSSize(minSize)]; + [Window setContentMaxSize:ToNSSize(maxSize)]; return S_OK; } @@ -243,8 +243,8 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso auto resizeBlock = ResizeScope(View, reason); @autoreleasepool { - auto maxSize = [Window maxSize]; - auto minSize = [Window minSize]; + auto maxSize = [Window contentMaxSize]; + auto minSize = [Window contentMinSize]; if (x < minSize.width) { x = minSize.width; From 90f6143c58d9641fca2a19734fbeb9462c30639c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 6 May 2022 12:40:33 +0200 Subject: [PATCH 178/213] Don't use managed dialogs by default in ControlCatalog. It means that system dialogs never get properly tested. --- samples/ControlCatalog.NetCore/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4b81935452..4464413e63 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -117,7 +117,6 @@ namespace ControlCatalog.NetCore EnableMultitouch = true }) .UseSkia() - .UseManagedSystemDialogs() .AfterSetup(builder => { builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() From 4cba4519f3e76d368384a40a9e655a76eedcf7f9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 11:45:47 +0100 Subject: [PATCH 179/213] only create the NSWindow when show is called. Cache sizes until needed. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 11 ++-- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 53 ++++++++++++++----- native/Avalonia.Native/src/OSX/window.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 6 ++- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index d2690cee41..19c443806c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -24,10 +24,7 @@ BEGIN_INTERFACE_MAP() INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) END_INTERFACE_MAP() - virtual ~WindowBaseImpl() { - View = NULL; - Window = NULL; - } + virtual ~WindowBaseImpl(); AutoFitContentView *StandardContainer; AvnView *View; @@ -36,6 +33,9 @@ BEGIN_INTERFACE_MAP() ComPtr _glContext; NSObject *renderTarget; AvnPoint lastPositionSet; + NSSize lastSize; + NSSize lastMinSize; + NSSize lastMaxSize; NSString *_lastTitle; bool _shown; @@ -116,6 +116,9 @@ protected: void UpdateStyle(); +private: + void InitialiseNSWindow (); + }; #endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index fb67addce1..fe3a209d3e 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -12,6 +12,13 @@ #import "cursor.h" #include "ResizeScope.h" #import "AutoFitContentView.h" +#include "WindowBaseImpl.h" + + +WindowBaseImpl::~WindowBaseImpl() { + View = nullptr; + Window = nullptr; +} WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { _shown = false; @@ -22,17 +29,11 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) View = [[AvnView alloc] initWithParent:this]; StandardContainer = [[AutoFitContentView new] initWithContent:View]; - Window = [[AvnWindow alloc] initWithParent:this]; - [Window setContentView:StandardContainer]; - lastPositionSet.X = 100; lastPositionSet.Y = 100; _lastTitle = @""; - [Window setStyleMask:NSWindowStyleMaskBorderless]; - [Window setBackingType:NSBackingStoreBuffered]; - - [Window setOpaque:false]; + Window = nullptr; } HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { @@ -83,6 +84,8 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { START_COM_CALL; @autoreleasepool { + InitialiseNSWindow(); + SetPosition(lastPositionSet); UpdateStyle(); @@ -97,6 +100,10 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { [Window orderFront:Window]; } + [Window setContentMinSize:lastMinSize]; + [Window setContentMaxSize:lastMaxSize]; + [Window setContentSize: lastSize]; + _shown = true; return S_OK; @@ -225,8 +232,13 @@ HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) { START_COM_CALL; @autoreleasepool { - [Window setMinSize:ToNSSize(minSize)]; - [Window setMaxSize:ToNSSize(maxSize)]; + lastMinSize = ToNSSize(minSize); + lastMaxSize = ToNSSize(maxSize); + + if(Window != nullptr) { + [Window setContentMinSize:lastMinSize]; + [Window setContentMaxSize:lastMaxSize]; + } return S_OK; } @@ -243,8 +255,8 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso auto resizeBlock = ResizeScope(View, reason); @autoreleasepool { - auto maxSize = [Window maxSize]; - auto minSize = [Window minSize]; + auto maxSize = lastMaxSize; + auto minSize = lastMinSize; if (x < minSize.width) { x = minSize.width; @@ -267,8 +279,12 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso BaseEvents->Resized(AvnSize{x, y}, reason); } - [Window setContentSize:NSSize{x, y}]; - [Window invalidateShadow]; + lastSize = NSSize {x, y}; + + if(Window != nullptr) { + [Window setContentSize:lastSize]; + [Window invalidateShadow]; + } } @finally { _inResize = false; @@ -502,4 +518,13 @@ NSWindowStyleMask WindowBaseImpl::GetStyle() { void WindowBaseImpl::UpdateStyle() { [Window setStyleMask:GetStyle()]; -} \ No newline at end of file +} + +void WindowBaseImpl::InitialiseNSWindow() { + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{ 0, 0, lastSize } styleMask:GetStyle()]; + [Window setContentView:StandardContainer]; + [Window setStyleMask:NSWindowStyleMaskBorderless]; + [Window setBackingType:NSBackingStoreBuffered]; + + [Window setOpaque:false]; +} diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index e9b98ce9ae..a8c31e7b60 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -8,7 +8,7 @@ class WindowBaseImpl; @interface AvnWindow : NSWindow --(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; +-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; -(void) setCanBecomeKeyAndMain; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 68a459d088..965b4a849b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -143,9 +143,10 @@ _canBecomeKeyAndMain = true; } --(AvnWindow*) initWithParent: (WindowBaseImpl*) parent +-(AvnWindow*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; { - self = [super init]; + self = [super initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:false]; + [self setReleasedWhenClosed:false]; _parent = parent; [self setDelegate:self]; @@ -155,6 +156,7 @@ [self backingScaleFactor]; [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; + _isExtended = false; return self; } From 0ee1a7e3910525a069230d713c93ed47d3528b4d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 11:50:42 +0100 Subject: [PATCH 180/213] add explanation for init with content size. --- native/Avalonia.Native/src/OSX/window.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 965b4a849b..68f8f76d75 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -145,6 +145,9 @@ -(AvnWindow*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; { + // https://jameshfisher.com/2020/07/10/why-is-the-contentrect-of-my-nswindow-ignored/ + // create nswindow with specific contentRect, otherwise we wont be able to resize the window + // until several ms after the window is physically on the screen. self = [super initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:false]; [self setReleasedWhenClosed:false]; From 2372155995ec6680c5ec27570da95a23b9e09cc4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 13:05:40 +0100 Subject: [PATCH 181/213] only create the window if reference is null --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index fe3a209d3e..0c85dcc303 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -521,10 +521,12 @@ void WindowBaseImpl::UpdateStyle() { } void WindowBaseImpl::InitialiseNSWindow() { - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{ 0, 0, lastSize } styleMask:GetStyle()]; - [Window setContentView:StandardContainer]; - [Window setStyleMask:NSWindowStyleMaskBorderless]; - [Window setBackingType:NSBackingStoreBuffered]; + if(Window == nullptr) { + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; + [Window setContentView:StandardContainer]; + [Window setStyleMask:NSWindowStyleMaskBorderless]; + [Window setBackingType:NSBackingStoreBuffered]; - [Window setOpaque:false]; + [Window setOpaque:false]; + } } From e76887fe25830404a0d6afd97782e0f8629c22b0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 14:14:08 +0100 Subject: [PATCH 182/213] INSWindowHolder uses NSView and NSWindow types. --- native/Avalonia.Native/src/OSX/INSWindowHolder.h | 4 ++-- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 4 ++-- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h index adc0dd1990..ae64a53e7d 100644 --- a/native/Avalonia.Native/src/OSX/INSWindowHolder.h +++ b/native/Avalonia.Native/src/OSX/INSWindowHolder.h @@ -10,8 +10,8 @@ struct INSWindowHolder { - virtual AvnWindow* _Nonnull GetNSWindow () = 0; - virtual AvnView* _Nonnull GetNSView () = 0; + virtual NSWindow* _Nonnull GetNSWindow () = 0; + virtual NSView* _Nonnull GetNSView () = 0; }; #endif //AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 19c443806c..fd079b1427 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -51,9 +51,9 @@ BEGIN_INTERFACE_MAP() virtual HRESULT ObtainNSViewHandleRetained(void **ret) override; - virtual AvnWindow *GetNSWindow() override; + virtual NSWindow *GetNSWindow() override; - virtual AvnView *GetNSView() override; + virtual NSView *GetNSView() override; virtual HRESULT Show(bool activate, bool isDialog) override; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 0c85dcc303..6ec026ed10 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -60,11 +60,11 @@ HRESULT WindowBaseImpl::ObtainNSViewHandleRetained(void **ret) { return S_OK; } -AvnWindow *WindowBaseImpl::GetNSWindow() { +NSWindow *WindowBaseImpl::GetNSWindow() { return Window; } -AvnView *WindowBaseImpl::GetNSView() { +NSView *WindowBaseImpl::GetNSView() { return View; } From 0ac5a1c808f192b17afcbb9acc8a9d387432fab8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 14:16:11 +0100 Subject: [PATCH 183/213] set min and max when creating window. --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 6ec026ed10..826486ae31 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -527,6 +527,10 @@ void WindowBaseImpl::InitialiseNSWindow() { [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; + [Window setContentSize: lastSize]; + [Window setContentMinSize:lastMinSize]; + [Window setContentMaxSize:lastMaxSize]; + [Window setOpaque:false]; } } From 8be332ab1d1504ff12950821ce8136e1a8647603 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 14:20:49 +0100 Subject: [PATCH 184/213] make AvnWindowProtocol --- .../project.pbxproj | 12 ++++++++++ .../Avalonia.Native/src/OSX/AvnPanelWindow.h | 9 ++++++++ .../Avalonia.Native/src/OSX/AvnPanelWindow.mm | 8 +++++++ .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 6 +++-- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 18 +++++++++------ native/Avalonia.Native/src/OSX/WindowImpl.mm | 8 +++---- .../Avalonia.Native/src/OSX/WindowProtocol.h | 23 +++++++++++++++++++ native/Avalonia.Native/src/OSX/window.h | 15 ++---------- native/Avalonia.Native/src/OSX/window.mm | 10 ++++---- 9 files changed, 78 insertions(+), 31 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AvnPanelWindow.h create mode 100644 native/Avalonia.Native/src/OSX/AvnPanelWindow.mm create mode 100644 native/Avalonia.Native/src/OSX/WindowProtocol.h diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 88e4c0c682..8ee2a61741 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -9,9 +9,11 @@ /* Begin PBXBuildFile section */ 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391E45702740FE9DD69695 /* ResizeScope.mm */; }; 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 183919BF108EB72A029F7671 /* WindowImpl.mm */; }; + 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */; }; 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */; }; 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; }; 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; }; + 1839196EC57BBC5C8BE1C735 /* AvnPanelWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391DC046BC700D56DF0B82 /* AvnPanelWindow.h */; }; 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; }; 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; @@ -19,6 +21,7 @@ 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */; }; 18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; }; + 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839122E037567BDD1D09DEB /* WindowProtocol.h */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; @@ -47,6 +50,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1839122E037567BDD1D09DEB /* WindowProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowProtocol.h; sourceTree = ""; }; 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; @@ -54,10 +58,12 @@ 1839166350F32661F3ABD70F /* AutoFitContentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AutoFitContentView.mm; sourceTree = ""; }; 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = ""; }; 1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = ""; }; + 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnPanelWindow.mm; sourceTree = ""; }; 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; 18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = ""; }; 18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = ""; }; 18391D1669284AD2EC9E866A /* AvnView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnView.h; sourceTree = ""; }; + 18391DC046BC700D56DF0B82 /* AvnPanelWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnPanelWindow.h; sourceTree = ""; }; 18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = ""; }; 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; @@ -166,6 +172,9 @@ 18391D1669284AD2EC9E866A /* AvnView.h */, 1839166350F32661F3ABD70F /* AutoFitContentView.mm */, 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */, + 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */, + 18391DC046BC700D56DF0B82 /* AvnPanelWindow.h */, + 1839122E037567BDD1D09DEB /* WindowProtocol.h */, ); sourceTree = ""; }; @@ -193,6 +202,8 @@ 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, + 1839196EC57BBC5C8BE1C735 /* AvnPanelWindow.h in Headers */, + 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -277,6 +288,7 @@ 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, + 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnPanelWindow.h b/native/Avalonia.Native/src/OSX/AvnPanelWindow.h new file mode 100644 index 0000000000..0fe95d044d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnPanelWindow.h @@ -0,0 +1,9 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#define NSWindow NSPanel +#define AvnWindow AvnPanelWindow + +//#include "window.h" \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm b/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm new file mode 100644 index 0000000000..8b79e28ca1 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm @@ -0,0 +1,8 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#define AvnWindow AvnPanelWindow + +//#include "window.mm" \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index fd079b1427..19065097fb 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -7,6 +7,7 @@ #define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H #import "rendertarget.h" +#import "WindowProtocol.h" #include "INSWindowHolder.h" @class AutoFitContentView; @@ -28,7 +29,7 @@ BEGIN_INTERFACE_MAP() AutoFitContentView *StandardContainer; AvnView *View; - AvnWindow *Window; + NSWindow * Window; ComPtr BaseEvents; ComPtr _glContext; NSObject *renderTarget; @@ -116,9 +117,10 @@ protected: void UpdateStyle(); + id GetWindowProtocol (); + private: void InitialiseNSWindow (); - }; #endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 826486ae31..82c634dc4f 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -100,10 +100,6 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { [Window orderFront:Window]; } - [Window setContentMinSize:lastMinSize]; - [Window setContentMaxSize:lastMaxSize]; - [Window setContentSize: lastSize]; - _shown = true; return S_OK; @@ -132,7 +128,8 @@ HRESULT WindowBaseImpl::Hide() { @autoreleasepool { if (Window != nullptr) { [Window orderOut:Window]; - [Window restoreParentWindow]; + + [GetWindowProtocol() restoreParentWindow]; } return S_OK; @@ -311,10 +308,10 @@ HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { auto nsmenu = nativeMenu->GetNative(); - [Window applyMenu:nsmenu]; + [GetWindowProtocol() applyMenu:nsmenu]; if ([Window isKeyWindow]) { - [Window showWindowMenuWithAppMenu]; + [GetWindowProtocol() showWindowMenuWithAppMenu]; } return S_OK; @@ -532,5 +529,12 @@ void WindowBaseImpl::InitialiseNSWindow() { [Window setContentMaxSize:lastMaxSize]; [Window setOpaque:false]; + + [Window setContentMinSize: lastMinSize]; + [Window setContentMaxSize: lastMaxSize]; } } + +id WindowBaseImpl::GetWindowProtocol() { + return static_cast>(Window); +} diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 0d325c4490..3de4f5d5a8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -20,7 +20,7 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase _lastWindowState = Normal; _actualWindowState = Normal; WindowEvents = events; - [Window setCanBecomeKeyAndMain]; + [GetWindowProtocol() setCanBecomeKeyAndMain]; [Window disableCursorRects]; [Window setTabbingMode:NSWindowTabbingModeDisallowed]; [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; @@ -68,7 +68,7 @@ HRESULT WindowImpl::SetEnabled(bool enable) { START_COM_CALL; @autoreleasepool { - [Window setEnabled:enable]; + [GetWindowProtocol() setEnabled:enable]; return S_OK; } } @@ -354,7 +354,7 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { View.layer.zPosition = 0; } - [Window setIsExtended:enable]; + [GetWindowProtocol() setIsExtended:enable]; HideOrShowTrafficLights(); @@ -383,7 +383,7 @@ HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { return E_POINTER; } - *ret = [Window getExtendedTitleBarHeight]; + *ret = [GetWindowProtocol() getExtendedTitleBarHeight]; return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h new file mode 100644 index 0000000000..ac20fe8eb1 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -0,0 +1,23 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import + +@class AvnMenu; + +@protocol AvnWindowProtocol +-(void) setCanBecomeKeyAndMain; +-(void) pollModalSession: (NSModalSession _Nonnull) session; +-(void) restoreParentWindow; +-(bool) shouldTryToHandleEvents; +-(void) setEnabled: (bool) enable; +-(void) showAppMenuOnly; +-(void) showWindowMenuWithAppMenu; +-(void) applyMenu:(AvnMenu* _Nullable)menu; + +-(double) getExtendedTitleBarHeight; +-(void) setIsExtended:(bool)value; +-(bool) isDialog; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index a8c31e7b60..1a001dd6e4 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -2,25 +2,14 @@ #define window_h #import "avalonia-native.h" +#import "WindowProtocol.h" @class AvnMenu; class WindowBaseImpl; -@interface AvnWindow : NSWindow +@interface AvnWindow : NSWindow -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; --(void) setCanBecomeKeyAndMain; --(void) pollModalSession: (NSModalSession _Nonnull) session; --(void) restoreParentWindow; --(bool) shouldTryToHandleEvents; --(void) setEnabled: (bool) enable; --(void) showAppMenuOnly; --(void) showWindowMenuWithAppMenu; --(void) applyMenu:(AvnMenu* _Nullable)menu; - --(double) getExtendedTitleBarHeight; --(void) setIsExtended:(bool)value; --(bool) isDialog; @end #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 68f8f76d75..e4bfedaba4 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1,11 +1,11 @@ #import -#include "common.h" +#import "common.h" #import "window.h" -#include "menu.h" -#include "automation.h" +#import "menu.h" +#import "automation.h" #import "WindowBaseImpl.h" -#include "WindowImpl.h" -#include "AvnView.h" +#import "WindowImpl.h" +#import "AvnView.h" @implementation AvnWindow { From cd9be07ced12909309b68a68934ba4371855a3c3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 14:30:36 +0100 Subject: [PATCH 185/213] ensure menu gets applied. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 1 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 36 ++++++++++++++----- native/Avalonia.Native/src/OSX/WindowImpl.mm | 1 - 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 19065097fb..d52518820c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -37,6 +37,7 @@ BEGIN_INTERFACE_MAP() NSSize lastSize; NSSize lastMinSize; NSSize lastMaxSize; + AvnMenu* lastMenu; NSString *_lastTitle; bool _shown; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 82c634dc4f..bd763ce2fd 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -34,6 +34,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) _lastTitle = @""; Window = nullptr; + lastMenu = nullptr; } HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { @@ -91,6 +92,10 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { [Window setTitle:_lastTitle]; + if(!isDialog) { + [GetWindowProtocol() setCanBecomeKeyAndMain]; + } + if (ShouldTakeFocusOnShow() && activate) { [Window orderFront:Window]; [Window makeKeyAndOrderFront:Window]; @@ -306,12 +311,14 @@ HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { auto nativeMenu = dynamic_cast(menu); - auto nsmenu = nativeMenu->GetNative(); + lastMenu = nativeMenu->GetNative(); - [GetWindowProtocol() applyMenu:nsmenu]; + if(Window != nullptr) { + [GetWindowProtocol() applyMenu:lastMenu]; - if ([Window isKeyWindow]) { - [GetWindowProtocol() showWindowMenuWithAppMenu]; + if ([Window isKeyWindow]) { + [GetWindowProtocol() showWindowMenuWithAppMenu]; + } } return S_OK; @@ -524,17 +531,30 @@ void WindowBaseImpl::InitialiseNSWindow() { [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - [Window setContentSize: lastSize]; + [Window setContentSize:lastSize]; [Window setContentMinSize:lastMinSize]; [Window setContentMaxSize:lastMaxSize]; [Window setOpaque:false]; - [Window setContentMinSize: lastMinSize]; - [Window setContentMaxSize: lastMaxSize]; + [Window setContentMinSize:lastMinSize]; + [Window setContentMaxSize:lastMaxSize]; + + if (lastMenu != nullptr) { + [GetWindowProtocol() applyMenu:lastMenu]; + + if ([Window isKeyWindow]) { + [GetWindowProtocol() showWindowMenuWithAppMenu]; + } + } } } id WindowBaseImpl::GetWindowProtocol() { - return static_cast>(Window); + id instance; + if ([Window conformsToProtocol:@protocol(AvnWindowProtocol)]) { + instance = Window; + } + + return instance; } diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 3de4f5d5a8..2a25cc69da 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -20,7 +20,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase _lastWindowState = Normal; _actualWindowState = Normal; WindowEvents = events; - [GetWindowProtocol() setCanBecomeKeyAndMain]; [Window disableCursorRects]; [Window setTabbingMode:NSWindowTabbingModeDisallowed]; [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; From f008e403cf1e1ef18fa1d6fba631eb3048a18059 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 16:02:50 +0100 Subject: [PATCH 186/213] make it compile 2 versions is AvnWindow (NSWindow / NSPanel version) fix include mess, and pragma once. --- native/Avalonia.Native/inc/rendertarget.h | 5 + .../src/OSX/AutoFitContentView.h | 4 +- .../src/OSX/AutoFitContentView.mm | 3 +- .../project.pbxproj | 26 ++- .../Avalonia.Native/src/OSX/AvnPanelWindow.h | 9 - .../Avalonia.Native/src/OSX/AvnPanelWindow.mm | 7 +- native/Avalonia.Native/src/OSX/AvnView.h | 10 +- native/Avalonia.Native/src/OSX/AvnView.mm | 3 +- .../src/OSX/{window.mm => AvnWindow.mm} | 189 +++++++----------- native/Avalonia.Native/src/OSX/PopupImpl.h | 9 + native/Avalonia.Native/src/OSX/PopupImpl.mm | 68 +++++++ native/Avalonia.Native/src/OSX/ResizeScope.h | 1 - native/Avalonia.Native/src/OSX/ResizeScope.mm | 2 +- .../Avalonia.Native/src/OSX/SystemDialogs.mm | 1 - .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 5 +- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 19 +- native/Avalonia.Native/src/OSX/WindowImpl.h | 1 - native/Avalonia.Native/src/OSX/WindowImpl.mm | 6 +- .../src/OSX/WindowInterfaces.h | 17 ++ .../Avalonia.Native/src/OSX/WindowProtocol.h | 2 + native/Avalonia.Native/src/OSX/automation.h | 3 +- native/Avalonia.Native/src/OSX/automation.mm | 7 +- native/Avalonia.Native/src/OSX/main.mm | 1 - native/Avalonia.Native/src/OSX/menu.mm | 1 - native/Avalonia.Native/src/OSX/window.h | 15 -- src/Avalonia.Native/avn.idl | 1 + 26 files changed, 235 insertions(+), 180 deletions(-) delete mode 100644 native/Avalonia.Native/src/OSX/AvnPanelWindow.h rename native/Avalonia.Native/src/OSX/{window.mm => AvnWindow.mm} (79%) create mode 100644 native/Avalonia.Native/src/OSX/PopupImpl.h create mode 100644 native/Avalonia.Native/src/OSX/PopupImpl.mm create mode 100644 native/Avalonia.Native/src/OSX/WindowInterfaces.h delete mode 100644 native/Avalonia.Native/src/OSX/window.h diff --git a/native/Avalonia.Native/inc/rendertarget.h b/native/Avalonia.Native/inc/rendertarget.h index 2b0338d099..a59915777f 100644 --- a/native/Avalonia.Native/inc/rendertarget.h +++ b/native/Avalonia.Native/inc/rendertarget.h @@ -1,3 +1,8 @@ +#pragma once + +#include "com.h" +#include "comimpl.h" +#include "avalonia-native.h" @protocol IRenderTarget -(void) setNewLayer: (CALayer*) layer; diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.h b/native/Avalonia.Native/src/OSX/AutoFitContentView.h index 68c9fb8dc8..7f1f764141 100644 --- a/native/Avalonia.Native/src/OSX/AutoFitContentView.h +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.h @@ -3,8 +3,10 @@ // Copyright (c) 2022 Avalonia. All rights reserved. // +#pragma once + #import -#import "avalonia-native.h" +#include "avalonia-native.h" @interface AutoFitContentView : NSView -(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm index 92d6f67a91..4eaa08cbe2 100644 --- a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm @@ -4,7 +4,8 @@ // #include "AvnView.h" -#import "AutoFitContentView.h" +#include "AutoFitContentView.h" +#import "WindowInterfaces.h" @implementation AutoFitContentView { diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 8ee2a61741..6fc3977d4e 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -9,16 +9,19 @@ /* Begin PBXBuildFile section */ 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391E45702740FE9DD69695 /* ResizeScope.mm */; }; 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 183919BF108EB72A029F7671 /* WindowImpl.mm */; }; + 183914E50CF6D2EFC1667F7C /* WindowInterfaces.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391DB45C7D892E61BF388C /* WindowInterfaces.h */; }; 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */; }; 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */; }; 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; }; 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; }; - 1839196EC57BBC5C8BE1C735 /* AvnPanelWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391DC046BC700D56DF0B82 /* AvnPanelWindow.h */; }; 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; }; + 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839155B28B20FFB672D29C6 /* AvnWindow.mm */; }; + 18391AC65ADD7DDD33FBE737 /* PopupImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183910513F396141938832B5 /* PopupImpl.h */; }; 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; + 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391BB698579F40F1783F31 /* PopupImpl.mm */; }; 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */; }; 18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; }; 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839122E037567BDD1D09DEB /* WindowProtocol.h */; }; @@ -43,16 +46,17 @@ AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; - AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; }; AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 183910513F396141938832B5 /* PopupImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopupImpl.h; sourceTree = ""; }; 1839122E037567BDD1D09DEB /* WindowProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowProtocol.h; sourceTree = ""; }; 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; + 1839155B28B20FFB672D29C6 /* AvnWindow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnWindow.mm; sourceTree = ""; }; 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoFitContentView.h; sourceTree = ""; }; 1839166350F32661F3ABD70F /* AutoFitContentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AutoFitContentView.mm; sourceTree = ""; }; @@ -60,10 +64,11 @@ 1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = ""; }; 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnPanelWindow.mm; sourceTree = ""; }; 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; + 18391BB698579F40F1783F31 /* PopupImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PopupImpl.mm; sourceTree = ""; }; 18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = ""; }; 18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = ""; }; 18391D1669284AD2EC9E866A /* AvnView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnView.h; sourceTree = ""; }; - 18391DC046BC700D56DF0B82 /* AvnPanelWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnPanelWindow.h; sourceTree = ""; }; + 18391DB45C7D892E61BF388C /* WindowInterfaces.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowInterfaces.h; sourceTree = ""; }; 18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = ""; }; 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; @@ -78,7 +83,6 @@ 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; 37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = ""; }; - 37C09D8A21581EF2006A6758 /* window.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = window.h; sourceTree = ""; }; 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; @@ -92,7 +96,6 @@ AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; - AB661C1F2148286E00291242 /* window.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = window.mm; sourceTree = ""; }; AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; AB7A61EF2147C815003C5833 /* libAvalonia.Native.OSX.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libAvalonia.Native.OSX.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; @@ -148,8 +151,6 @@ AB661C212148288600291242 /* common.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */, 37E2330E21583241000CB7E2 /* KeyTransform.mm */, - AB661C1F2148286E00291242 /* window.mm */, - 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, @@ -173,8 +174,11 @@ 1839166350F32661F3ABD70F /* AutoFitContentView.mm */, 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */, 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */, - 18391DC046BC700D56DF0B82 /* AvnPanelWindow.h */, 1839122E037567BDD1D09DEB /* WindowProtocol.h */, + 1839155B28B20FFB672D29C6 /* AvnWindow.mm */, + 18391DB45C7D892E61BF388C /* WindowInterfaces.h */, + 18391BB698579F40F1783F31 /* PopupImpl.mm */, + 183910513F396141938832B5 /* PopupImpl.h */, ); sourceTree = ""; }; @@ -202,8 +206,9 @@ 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, - 1839196EC57BBC5C8BE1C735 /* AvnPanelWindow.h in Headers */, 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */, + 183914E50CF6D2EFC1667F7C /* WindowInterfaces.h in Headers */, + 18391AC65ADD7DDD33FBE737 /* PopupImpl.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -282,13 +287,14 @@ 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, - AB661C202148286E00291242 /* window.mm in Sources */, 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, + 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, + 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnPanelWindow.h b/native/Avalonia.Native/src/OSX/AvnPanelWindow.h deleted file mode 100644 index 0fe95d044d..0000000000 --- a/native/Avalonia.Native/src/OSX/AvnPanelWindow.h +++ /dev/null @@ -1,9 +0,0 @@ -// -// Created by Dan Walmsley on 06/05/2022. -// Copyright (c) 2022 Avalonia. All rights reserved. -// - -#define NSWindow NSPanel -#define AvnWindow AvnPanelWindow - -//#include "window.h" \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm b/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm index 8b79e28ca1..2365189010 100644 --- a/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm @@ -3,6 +3,9 @@ // Copyright (c) 2022 Avalonia. All rights reserved. // -#define AvnWindow AvnPanelWindow +#pragma once + +#define IS_NSPANEL + +#include "AvnWindow.mm" -//#include "window.mm" \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h index c6dd90150f..86a68d34c5 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.h +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -2,17 +2,15 @@ // Created by Dan Walmsley on 05/05/2022. // Copyright (c) 2022 Avalonia. All rights reserved. // - +#pragma once #import #import #import -#include "window.h" -#import "comimpl.h" -#import "common.h" -#import "WindowImpl.h" -#import "KeyTransform.h" +#include "common.h" +#include "WindowImpl.h" +#include "KeyTransform.h" @class AvnAccessibilityElement; diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 24cbc25502..e6cf73755b 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -4,8 +4,9 @@ // #import -#import "AvnView.h" +#include "AvnView.h" #include "automation.h" +#import "WindowInterfaces.h" @implementation AvnView { diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm similarity index 79% rename from native/Avalonia.Native/src/OSX/window.mm rename to native/Avalonia.Native/src/OSX/AvnWindow.mm index e4bfedaba4..09534a0a4b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -1,13 +1,32 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + + #import -#import "common.h" -#import "window.h" -#import "menu.h" -#import "automation.h" +#import "WindowProtocol.h" #import "WindowBaseImpl.h" -#import "WindowImpl.h" -#import "AvnView.h" -@implementation AvnWindow +#ifdef IS_NSPANEL +#define BASE_CLASS NSPanel +#define CLASS_NAME AvnPanel +#else +#define BASE_CLASS NSWindow +#define CLASS_NAME AvnWindow +#endif + +#import +#include "common.h" +#include "menu.h" +#include "automation.h" +#include "WindowBaseImpl.h" +#include "WindowImpl.h" +#include "AvnView.h" +#include "WindowInterfaces.h" +#include "PopupImpl.h" + +@implementation CLASS_NAME { ComPtr _parent; bool _canBecomeKeyAndMain; @@ -66,7 +85,7 @@ - (void)pollModalSession:(nonnull NSModalSession)session { auto response = [NSApp runModalSession:session]; - + if(response == NSModalResponseContinue) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -85,18 +104,18 @@ if(_menu != nullptr) { auto appMenuItem = ::GetAppMenuItem(); - + if(appMenuItem != nullptr) { auto appMenu = [appMenuItem menu]; - + [appMenu removeItem:appMenuItem]; - + [_menu insertItem:appMenuItem atIndex:0]; - + [_menu setHasGlobalMenuItem:true]; } - + [NSApp setMenu:_menu]; } else @@ -108,22 +127,22 @@ -(void) showAppMenuOnly { auto appMenuItem = ::GetAppMenuItem(); - + if(appMenuItem != nullptr) { auto appMenu = ::GetAppMenu(); - + auto nativeAppMenu = dynamic_cast(appMenu); - + [[appMenuItem menu] removeItem:appMenuItem]; - + if(_menu != nullptr) { [_menu setHasGlobalMenuItem:false]; } - + [nativeAppMenu->GetNative() addItem:appMenuItem]; - + [NSApp setMenu:nativeAppMenu->GetNative()]; } } @@ -134,7 +153,7 @@ { menu = [AvnMenu new]; } - + _menu = menu; } @@ -143,19 +162,19 @@ _canBecomeKeyAndMain = true; } --(AvnWindow*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +-(CLASS_NAME*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; { // https://jameshfisher.com/2020/07/10/why-is-the-contentrect-of-my-nswindow-ignored/ // create nswindow with specific contentRect, otherwise we wont be able to resize the window // until several ms after the window is physically on the screen. self = [super initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:false]; - + [self setReleasedWhenClosed:false]; _parent = parent; [self setDelegate:self]; _closed = false; _isEnabled = true; - + [self backingScaleFactor]; [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; @@ -167,12 +186,12 @@ - (BOOL)windowShouldClose:(NSWindow *)sender { auto window = dynamic_cast(_parent.getRaw()); - + if(window != nullptr) { return !window->WindowEvents->Closing(); } - + return true; } @@ -201,16 +220,17 @@ // If the window has a child window being shown as a dialog then don't allow it to become the key window. for(NSWindow* uch in [self childWindows]) { - auto ch = objc_cast(uch); + // TODO protocol + auto ch = objc_cast(uch); if(ch == nil) continue; if (ch.isDialog) return false; } - + return true; } - + return false; } @@ -232,7 +252,7 @@ -(void)becomeKeyWindow { [self showWindowMenuWithAppMenu]; - + if(_parent != nullptr) { _parent->BaseEvents->Activated(); @@ -243,7 +263,8 @@ -(void) restoreParentWindow; { - auto parent = objc_cast([self parentWindow]); + // TODO protocol + auto parent = objc_cast([self parentWindow]); if(parent != nil) { [parent removeChildWindow:self]; @@ -253,7 +274,7 @@ - (void)windowDidMiniaturize:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->WindowStateChanged(); @@ -263,7 +284,7 @@ - (void)windowDidDeminiaturize:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->WindowStateChanged(); @@ -273,7 +294,7 @@ - (void)windowDidResize:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->WindowStateChanged(); @@ -283,7 +304,7 @@ - (void)windowWillExitFullScreen:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->StartStateTransition(); @@ -293,22 +314,22 @@ - (void)windowDidExitFullScreen:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->EndStateTransition(); - + if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) { NSRect screenRect = [[self screen] visibleFrame]; [self setFrame:screenRect display:YES]; } - + if(parent->WindowState() == Minimized) { [self miniaturize:nullptr]; } - + parent->WindowStateChanged(); } } @@ -316,7 +337,7 @@ - (void)windowWillEnterFullScreen:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->StartStateTransition(); @@ -326,7 +347,7 @@ - (void)windowDidEnterFullScreen:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); - + if(parent != nullptr) { parent->EndStateTransition(); @@ -343,20 +364,20 @@ { if(_parent) _parent->BaseEvents->Deactivated(); - + [self showAppMenuOnly]; - + [super resignKeyWindow]; } - (void)windowDidMove:(NSNotification *)notification { AvnPoint position; - + if(_parent != nullptr) { auto cparent = dynamic_cast(_parent.getRaw()); - + if(cparent != nullptr) { if(cparent->WindowState() == Maximized) @@ -364,7 +385,7 @@ cparent->SetWindowState(Normal); } } - + _parent->GetPosition(&position); _parent->BaseEvents->PositionChanged(position); } @@ -379,7 +400,7 @@ - (void)sendEvent:(NSEvent *)event { [super sendEvent:event]; - + /// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast. if(_parent != nullptr && dynamic_cast(_parent.getRaw()) != nullptr) { @@ -390,30 +411,30 @@ AvnView* view = _parent->View; NSPoint windowPoint = [event locationInWindow]; NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; - + if (!NSPointInRect(viewPoint, view.bounds)) { auto avnPoint = [AvnView toAvnPoint:windowPoint]; auto point = [self translateLocalPoint:avnPoint]; AvnVector delta = { 0, 0 }; - + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } } - break; - + break; + case NSEventTypeMouseEntered: { _parent->UpdateCursor(); } - break; - + break; + case NSEventTypeMouseExited: { [[NSCursor arrowCursor] set]; } - break; - + break; + default: break; } @@ -422,63 +443,3 @@ @end -class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup -{ -private: - BEGIN_INTERFACE_MAP() - INHERIT_INTERFACE_MAP(WindowBaseImpl) - INTERFACE_MAP_ENTRY(IAvnPopup, IID_IAvnPopup) - END_INTERFACE_MAP() - virtual ~PopupImpl(){} - ComPtr WindowEvents; - PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) - { - WindowEvents = events; - [Window setLevel:NSPopUpMenuWindowLevel]; - } -protected: - virtual NSWindowStyleMask GetStyle() override - { - return NSWindowStyleMaskBorderless; - } - - virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override - { - START_COM_CALL; - - @autoreleasepool - { - if (Window != nullptr) - { - [Window setContentSize:NSSize{x, y}]; - - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; - } - - return S_OK; - } - } -public: - virtual bool ShouldTakeFocusOnShow() override - { - return false; - } -}; - -extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) -{ - @autoreleasepool - { - IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); - return ptr; - } -} - -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) -{ - @autoreleasepool - { - IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); - return ptr; - } -} diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.h b/native/Avalonia.Native/src/OSX/PopupImpl.h new file mode 100644 index 0000000000..451019a6a4 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PopupImpl.h @@ -0,0 +1,9 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_POPUPIMPL_H +#define AVALONIA_NATIVE_OSX_POPUPIMPL_H + +#endif //AVALONIA_NATIVE_OSX_POPUPIMPL_H diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm new file mode 100644 index 0000000000..64a8780158 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -0,0 +1,68 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#include "WindowInterfaces.h" +#include "AvnView.h" +#include "WindowImpl.h" +#include "automation.h" +#include "menu.h" +#include "common.h" +#import "WindowBaseImpl.h" +#import "WindowProtocol.h" +#import +#include "PopupImpl.h" + +class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup +{ +private: + BEGIN_INTERFACE_MAP() + INHERIT_INTERFACE_MAP(WindowBaseImpl) + INTERFACE_MAP_ENTRY(IAvnPopup, IID_IAvnPopup) + END_INTERFACE_MAP() + virtual ~PopupImpl(){} + ComPtr WindowEvents; + PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) + { + WindowEvents = events; + [Window setLevel:NSPopUpMenuWindowLevel]; + } +protected: + virtual NSWindowStyleMask GetStyle() override + { + return NSWindowStyleMaskBorderless; + } + + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override + { + START_COM_CALL; + + @autoreleasepool + { + if (Window != nullptr) + { + [Window setContentSize:NSSize{x, y}]; + + [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; + } + + return S_OK; + } + } +public: + virtual bool ShouldTakeFocusOnShow() override + { + return false; + } +}; + + +extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) +{ + @autoreleasepool + { + IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); + return ptr; + } +} \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.h b/native/Avalonia.Native/src/OSX/ResizeScope.h index 7509f93c01..9a43c158fe 100644 --- a/native/Avalonia.Native/src/OSX/ResizeScope.h +++ b/native/Avalonia.Native/src/OSX/ResizeScope.h @@ -6,7 +6,6 @@ #ifndef AVALONIA_NATIVE_OSX_RESIZESCOPE_H #define AVALONIA_NATIVE_OSX_RESIZESCOPE_H -#include "window.h" #include "avalonia-native.h" @class AvnView; diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.mm b/native/Avalonia.Native/src/OSX/ResizeScope.mm index 90e7f5cf15..9f1177af8b 100644 --- a/native/Avalonia.Native/src/OSX/ResizeScope.mm +++ b/native/Avalonia.Native/src/OSX/ResizeScope.mm @@ -5,7 +5,7 @@ #import #include "ResizeScope.h" -#import "AvnView.h" +#include "AvnView.h" ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) { _view = view; diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index 21ad9cfa7c..535b6c3b66 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -1,5 +1,4 @@ #include "common.h" -#include "window.h" #include "INSWindowHolder.h" class SystemDialogs : public ComSingleObject diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index d52518820c..a8f549b3c6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -6,11 +6,12 @@ #ifndef AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H #define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H -#import "rendertarget.h" -#import "WindowProtocol.h" +#include "rendertarget.h" #include "INSWindowHolder.h" @class AutoFitContentView; +@class AvnMenu; +@protocol AvnWindowProtocol; class WindowBaseImpl : public virtual ComObject, public virtual IAvnWindowBase, diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index bd763ce2fd..a58d0bb8be 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -5,14 +5,14 @@ #import #include "common.h" -#import "window.h" -#import "AvnView.h" +#include "AvnView.h" #include "menu.h" #include "automation.h" -#import "cursor.h" +#include "cursor.h" #include "ResizeScope.h" -#import "AutoFitContentView.h" -#include "WindowBaseImpl.h" +#include "AutoFitContentView.h" +#import "WindowProtocol.h" +#import "WindowInterfaces.h" WindowBaseImpl::~WindowBaseImpl() { @@ -558,3 +558,12 @@ id WindowBaseImpl::GetWindowProtocol() { return instance; } + +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) +{ + @autoreleasepool + { + IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); + return ptr; + } +} diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index b229921baa..a4ee4f447c 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -6,7 +6,6 @@ #ifndef AVALONIA_NATIVE_OSX_WINDOWIMPL_H #define AVALONIA_NATIVE_OSX_WINDOWIMPL_H - #import "WindowBaseImpl.h" #include "IWindowStateChanged.h" diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 2a25cc69da..9992d64b47 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -4,10 +4,10 @@ // #import -#import "window.h" -#import "AutoFitContentView.h" -#import "AvnView.h" +#include "AutoFitContentView.h" +#include "AvnView.h" #include "automation.h" +#include "WindowProtocol.h" WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { _isClientAreaExtended = false; diff --git a/native/Avalonia.Native/src/OSX/WindowInterfaces.h b/native/Avalonia.Native/src/OSX/WindowInterfaces.h new file mode 100644 index 0000000000..6e6d62e85e --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowInterfaces.h @@ -0,0 +1,17 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import +#include "WindowProtocol.h" +#include "WindowBaseImpl.h" + +@interface AvnWindow : NSWindow +-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +@end + +@interface AvnPanel : NSPanel +-(AvnPanel* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index ac20fe8eb1..d81d2f1ed1 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -3,6 +3,8 @@ // Copyright (c) 2022 Avalonia. All rights reserved. // +#pragma once + #import @class AvnMenu; diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 1727d21f27..367df3619d 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,5 +1,6 @@ -#import +#pragma once +#import NS_ASSUME_NONNULL_BEGIN class IAvnAutomationPeer; diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 7a0b3b8127..d0c8d7a9db 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,9 +1,8 @@ #include "common.h" -#import "automation.h" -#import "window.h" +#include "automation.h" #include "AvnString.h" -#import "INSWindowHolder.h" -#import "AvnView.h" +#include "INSWindowHolder.h" +#include "AvnView.h" @interface AvnAccessibilityElement (Events) - (void) raiseChildrenChanged; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 011f881e94..6ee86b21ae 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -1,7 +1,6 @@ //This file will contain actual IID structures #define COM_GUIDS_MATERIALIZE #include "common.h" -#include "window.h" static NSString* s_appTitle = @"Avalonia"; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index fd74edd772..b05588a441 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,7 +1,6 @@ #include "common.h" #include "menu.h" -#import "window.h" #include "KeyTransform.h" #include #include /* For kVK_ constants, and TIS functions. */ diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h deleted file mode 100644 index 1a001dd6e4..0000000000 --- a/native/Avalonia.Native/src/OSX/window.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef window_h -#define window_h - -#import "avalonia-native.h" -#import "WindowProtocol.h" - -@class AvnMenu; - -class WindowBaseImpl; - -@interface AvnWindow : NSWindow --(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; -@end - -#endif /* window_h */ diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 8092004989..d6ef0f8918 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -2,6 +2,7 @@ @clr-access internal @clr-map bool int @cpp-preamble @@ +#pragma once #include "com.h" #include "stddef.h" @@ From 1cca34f56ede6b1c4b2169b64a74a9832331e536 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 17:30:03 +0100 Subject: [PATCH 187/213] actually create nspanels for dialogs. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 35 ++++++++----------- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 1 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 28 ++++++++++----- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- .../Avalonia.Native/src/OSX/WindowProtocol.h | 1 - 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 09534a0a4b..d0b23540f9 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -29,7 +29,6 @@ @implementation CLASS_NAME { ComPtr _parent; - bool _canBecomeKeyAndMain; bool _closed; bool _isEnabled; bool _isExtended; @@ -157,11 +156,6 @@ _menu = menu; } --(void) setCanBecomeKeyAndMain -{ - _canBecomeKeyAndMain = true; -} - -(CLASS_NAME*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; { // https://jameshfisher.com/2020/07/10/why-is-the-contentrect-of-my-nswindow-ignored/ @@ -215,28 +209,27 @@ -(BOOL)canBecomeKeyWindow { - if (_canBecomeKeyAndMain) + // If the window has a child window being shown as a dialog then don't allow it to become the key window. + for(NSWindow* uch in [self childWindows]) { - // If the window has a child window being shown as a dialog then don't allow it to become the key window. - for(NSWindow* uch in [self childWindows]) - { - // TODO protocol - auto ch = objc_cast(uch); - if(ch == nil) - continue; - if (ch.isDialog) - return false; - } - - return true; + // TODO protocol + auto ch = objc_cast(uch); + if(ch == nil) + continue; + if (ch.isDialog) + return false; } - return false; + return true; } -(BOOL)canBecomeMainWindow { - return _canBecomeKeyAndMain; +#ifdef IS_NSPANEL + return false; +#else + return true; +#endif } -(bool)shouldTryToHandleEvents diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index a8f549b3c6..ae1e6a7016 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -122,6 +122,7 @@ protected: id GetWindowProtocol (); private: + void CreateNSWindow (bool isDialog); void InitialiseNSWindow (); }; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index a58d0bb8be..414632770f 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -13,6 +13,7 @@ #include "AutoFitContentView.h" #import "WindowProtocol.h" #import "WindowInterfaces.h" +#include "WindowBaseImpl.h" WindowBaseImpl::~WindowBaseImpl() { @@ -31,6 +32,9 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) lastPositionSet.X = 100; lastPositionSet.Y = 100; + lastSize = NSSize { 100, 100 }; + lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; + lastMinSize = NSSize { 0, 0 }; _lastTitle = @""; Window = nullptr; @@ -85,6 +89,7 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { START_COM_CALL; @autoreleasepool { + CreateNSWindow(isDialog); InitialiseNSWindow(); SetPosition(lastPositionSet); @@ -92,10 +97,6 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { [Window setTitle:_lastTitle]; - if(!isDialog) { - [GetWindowProtocol() setCanBecomeKeyAndMain]; - } - if (ShouldTakeFocusOnShow() && activate) { [Window orderFront:Window]; [Window makeKeyAndOrderFront:Window]; @@ -524,9 +525,21 @@ void WindowBaseImpl::UpdateStyle() { [Window setStyleMask:GetStyle()]; } -void WindowBaseImpl::InitialiseNSWindow() { +void WindowBaseImpl::CreateNSWindow(bool isDialog) { if(Window == nullptr) { - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; + if(isDialog) + { + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; + } + else + { + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; + } + } +} + +void WindowBaseImpl::InitialiseNSWindow() { + if(Window != nullptr) { [Window setContentView:StandardContainer]; [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; @@ -537,9 +550,6 @@ void WindowBaseImpl::InitialiseNSWindow() { [Window setOpaque:false]; - [Window setContentMinSize:lastMinSize]; - [Window setContentMaxSize:lastMaxSize]; - if (lastMenu != nullptr) { [GetWindowProtocol() applyMenu:lastMenu]; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 9992d64b47..63a38f0c22 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -507,7 +507,7 @@ bool WindowImpl::IsDialog() { } NSWindowStyleMask WindowImpl::GetStyle() { - unsigned long s = NSWindowStyleMaskBorderless; + unsigned long s = this->_isDialog ? NSWindowStyleMaskUtilityWindow : NSWindowStyleMaskBorderless; switch (_decorations) { case SystemDecorationsNone: diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index d81d2f1ed1..1c97d89f39 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -10,7 +10,6 @@ @class AvnMenu; @protocol AvnWindowProtocol --(void) setCanBecomeKeyAndMain; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; From d6a4a6c901fec82f9675c3f70e72a7e01c008fb5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 18:04:57 +0100 Subject: [PATCH 188/213] ensure cast to protocol instead of concrete types. --- native/Avalonia.Native/src/OSX/AutoFitContentView.mm | 5 +++-- native/Avalonia.Native/src/OSX/AvnView.mm | 7 ++++++- native/Avalonia.Native/src/OSX/AvnWindow.mm | 6 ++---- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 4 ++-- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 8 ++++---- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm index 4eaa08cbe2..314c579b76 100644 --- a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm @@ -5,7 +5,8 @@ #include "AvnView.h" #include "AutoFitContentView.h" -#import "WindowInterfaces.h" +#include "WindowInterfaces.h" +#include "WindowProtocol.h" @implementation AutoFitContentView { @@ -84,7 +85,7 @@ _settingSize = true; [super setFrameSize:newSize]; - auto window = objc_cast([self window]); + auto window = static_cast>([self window]); // TODO get actual titlebar size diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index e6cf73755b..02526afbcb 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -195,7 +195,12 @@ - (bool) ignoreUserInput:(bool)trigerInputWhenDisabled { - auto parentWindow = objc_cast([self window]); + if(_parent == nullptr) + { + return TRUE; + } + + auto parentWindow = _parent->GetWindowProtocol(); if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) { diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index d0b23540f9..ef4dcc3df4 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -212,8 +212,7 @@ // If the window has a child window being shown as a dialog then don't allow it to become the key window. for(NSWindow* uch in [self childWindows]) { - // TODO protocol - auto ch = objc_cast(uch); + auto ch = static_cast>(uch); if(ch == nil) continue; if (ch.isDialog) @@ -256,8 +255,7 @@ -(void) restoreParentWindow; { - // TODO protocol - auto parent = objc_cast([self parentWindow]); + auto parent = static_cast>([self parentWindow]); if(parent != nil) { [parent removeChildWindow:self]; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index ae1e6a7016..8c82bba98c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -114,13 +114,13 @@ BEGIN_INTERFACE_MAP() virtual bool IsDialog(); + id GetWindowProtocol (); + protected: virtual NSWindowStyleMask GetStyle(); void UpdateStyle(); - id GetWindowProtocol (); - private: void CreateNSWindow (bool isDialog); void InitialiseNSWindow (); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 414632770f..227f348333 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -561,12 +561,12 @@ void WindowBaseImpl::InitialiseNSWindow() { } id WindowBaseImpl::GetWindowProtocol() { - id instance; - if ([Window conformsToProtocol:@protocol(AvnWindowProtocol)]) { - instance = Window; + if(Window == nullptr) + { + return nullptr; } - return instance; + return static_cast>(Window); } extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) From 99e07e68d592771273aa4b795682603d2507d973 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 May 2022 18:07:01 +0100 Subject: [PATCH 189/213] remove unnecessary cast. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index ef4dcc3df4..33f7f75314 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -255,7 +255,8 @@ -(void) restoreParentWindow; { - auto parent = static_cast>([self parentWindow]); + auto parent = [self parentWindow]; + if(parent != nil) { [parent removeChildWindow:self]; From 3600639d39482ef60efbb573f4586d8019da745a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 8 May 2022 19:17:08 +0100 Subject: [PATCH 190/213] re-create NSWindow if we call show and need to swap out for a dialog. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 4 ++++ .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 1 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 21 ++++++++++++++----- .../Avalonia.Native/src/OSX/WindowProtocol.h | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 33f7f75314..6ff19ead68 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -433,5 +433,9 @@ } } +- (void)disconnectParent { + _parent = nullptr; +} + @end diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 8c82bba98c..eff13bcb23 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -123,6 +123,7 @@ protected: private: void CreateNSWindow (bool isDialog); + void CleanNSWindow (); void InitialiseNSWindow (); }; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 227f348333..a7c38bf652 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -525,14 +525,25 @@ void WindowBaseImpl::UpdateStyle() { [Window setStyleMask:GetStyle()]; } +void WindowBaseImpl::CleanNSWindow() { + if(Window != nullptr) { + [GetWindowProtocol() disconnectParent]; + [Window close]; + Window = nullptr; + } +} + void WindowBaseImpl::CreateNSWindow(bool isDialog) { - if(Window == nullptr) { - if(isDialog) - { + if (isDialog) { + if (![Window isKindOfClass:[AvnPanel class]]) { + CleanNSWindow(); + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; } - else - { + } else { + if (![Window isKindOfClass:[AvnWindow class]]) { + CleanNSWindow(); + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; } } diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index 1c97d89f39..92194706de 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -20,5 +20,6 @@ -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; +-(void) disconnectParent; -(bool) isDialog; @end \ No newline at end of file From c756f812ec887cbdf77fd2d3d420a7164f25ee0f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 May 2022 11:19:20 +0100 Subject: [PATCH 191/213] remove shadow invalidation hack. --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index a7c38bf652..db5eb54e3f 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -286,7 +286,6 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso if(Window != nullptr) { [Window setContentSize:lastSize]; - [Window invalidateShadow]; } } @finally { From 5a027c585ab9f827bea077cc0a23f9f0579f9bce Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 May 2022 12:44:07 +0100 Subject: [PATCH 192/213] Merge pull request #8103 from AvaloniaUI/fixes/deterministic-builds Fixes/deterministic builds --- .../CompilerExtensions/XamlIlClrPropertyInfoHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 871a2a2045..e76e2cd46e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -55,7 +55,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return cached.get; } - var name = lst.Count == 0 ? key : key + "_" + Guid.NewGuid().ToString("N"); + var name = lst.Count == 0 ? key : key + "_" + context.Configuration.IdentifierGenerator.GenerateIdentifierPart(); var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); From 2f1ffbd81e6d573a6d5dfa6b06647a0d071d3903 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:52:15 +0300 Subject: [PATCH 193/213] Make ThreadSafeObjectPool actually thread safe (#8106) * Make ThreadSafeObjectPool actually thread safe --- src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index c6845485dc..827a02334a 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -5,12 +5,11 @@ namespace Avalonia.Threading public class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); - private object _lock = new object(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); public T Get() { - lock (_lock) + lock (_stack) { if(_stack.Count == 0) return new T(); From 08487446d98311daa2d6d2304fc4647f984ea8b9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 10 May 2022 14:19:35 +0100 Subject: [PATCH 194/213] [OSX] cache IsClientAreaExtendedToDecorations, and apply it when NSPanel / NSWindow is created and Shown. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 56 ++++++++++++------- .../ViewModels/MainWindowViewModel.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 7 +++ 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 63a38f0c22..7ab2b2b5fc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -55,8 +55,20 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { @autoreleasepool { _isDialog = isDialog; + + bool created = Window == nullptr; + WindowBaseImpl::Show(activate, isDialog); + if(created) + { + if(_isClientAreaExtended) + { + [GetWindowProtocol() setIsExtended:true]; + SetExtendClientArea(true); + } + } + HideOrShowTrafficLights(); return SetWindowState(_lastWindowState); @@ -327,37 +339,39 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { @autoreleasepool { _isClientAreaExtended = enable; - if (enable) { - Window.titleVisibility = NSWindowTitleHidden; + if(Window != nullptr) { + if (enable) { + Window.titleVisibility = NSWindowTitleHidden; - [Window setTitlebarAppearsTransparent:true]; + [Window setTitlebarAppearsTransparent:true]; - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - if (wantsTitleBar) { - [StandardContainer ShowTitleBar:true]; - } else { - [StandardContainer ShowTitleBar:false]; - } + if (wantsTitleBar) { + [StandardContainer ShowTitleBar:true]; + } else { + [StandardContainer ShowTitleBar:false]; + } - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } else { + Window.toolbar = nullptr; + } } else { + Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; } - } else { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - [GetWindowProtocol() setIsExtended:enable]; + [GetWindowProtocol() setIsExtended:enable]; - HideOrShowTrafficLights(); + HideOrShowTrafficLights(); - UpdateStyle(); + UpdateStyle(); + } return S_OK; } diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 2b0c30f311..c44024d952 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.ViewModels private WindowState _windowState; private WindowState[] _windowStates; private int _transparencyLevel; - private ExtendClientAreaChromeHints _chromeHints; + private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; private bool _extendClientAreaEnabled; private bool _systemTitleBarEnabled; private bool _preferSystemChromeEnabled; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c082bdb1b8..b5af927ea0 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -107,6 +107,13 @@ namespace Avalonia.Native private bool _isExtended; public bool IsClientAreaExtendedToDecorations => _isExtended; + public override void Show(bool activate, bool isDialog) + { + base.Show(activate, isDialog); + + InvalidateExtendedMargins(); + } + protected override bool ChromeHitTest (RawPointerEventArgs e) { if(_isExtended) From 65ed75970dd54e9c68d37c2bf67440c754018d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:25:15 +0200 Subject: [PATCH 195/213] Fix TextBox property name registrations --- src/Avalonia.Controls/TextBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0be58e7fcc..45e66828c1 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -55,13 +55,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionBrush)); public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionForegroundBrush)); public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); + AvaloniaProperty.Register(nameof(CaretBrush)); public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( From 665b0fa3b6db07ef85463453a5e0a0da2a99d065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:25:26 +0200 Subject: [PATCH 196/213] Fix TextPresenter property name registrations --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 0785149a73..07061940b5 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -26,13 +26,13 @@ namespace Avalonia.Controls.Presenters AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionBrush)); public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionForegroundBrush)); public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); + AvaloniaProperty.Register(nameof(CaretBrush)); public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( From d8e01f0e1a67f72f71a81a271c000042934a6196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:25:35 +0200 Subject: [PATCH 197/213] Fix DockPanel property name registrations --- src/Avalonia.Controls/DockPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index 8e23555c2d..3e3ed509b5 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls /// public static readonly StyledProperty LastChildFillProperty = AvaloniaProperty.Register( - nameof(LastChildFillProperty), + nameof(LastChildFill), defaultValue: true); /// From d2475dd53b4830ef5332a9de79f8ac9214bd8c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:28:44 +0200 Subject: [PATCH 198/213] Fix owner of IsTextSearchEnabled property --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index bff6799792..164aab32db 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); /// /// Event that should be raised by items that implement to From 3880f1abf41e4494c84af8fc52492f5adfbb4127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:30:19 +0200 Subject: [PATCH 199/213] Fix PasswordCharProperty owner --- src/Avalonia.Controls/MaskedTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 933788f9ea..080326606e 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Mask), string.Empty); public static new readonly StyledProperty PasswordCharProperty = - AvaloniaProperty.Register(nameof(PasswordChar), '\0'); + AvaloniaProperty.Register(nameof(PasswordChar), '\0'); public static readonly StyledProperty PromptCharProperty = AvaloniaProperty.Register(nameof(PromptChar), '_'); From ee33319be119129cef8ca1fe0a243b4d976ee109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:31:25 +0200 Subject: [PATCH 200/213] Fix TickPlacementProperty property owner --- src/Avalonia.Controls/Slider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 64dfce22d4..b6a6f3d02c 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -76,7 +76,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty TickPlacementProperty = - AvaloniaProperty.Register(nameof(TickPlacement), 0d); + AvaloniaProperty.Register(nameof(TickPlacement), 0d); /// /// Defines the property. From da640adb0cec38e95d2910bf54371ac6f973fa9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:40:00 +0200 Subject: [PATCH 201/213] Fix PaneTemplateProperty property owner --- src/Avalonia.Controls/SplitView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index ae1605a985..532cb1d329 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -138,7 +138,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty PaneTemplateProperty = - AvaloniaProperty.Register(nameof(PaneTemplate)); + AvaloniaProperty.Register(nameof(PaneTemplate)); /// /// Defines the property From c6eb4138447fd2e9758a8d893fff6b60150268df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:42:10 +0200 Subject: [PATCH 202/213] Use AddOwner for PlacementRectProperty --- src/Avalonia.Controls/ContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index ee2378101a..2b122d4174 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty PlacementRectProperty = - AvaloniaProperty.Register(nameof(PlacementRect)); + Popup.PlacementRectProperty.AddOwner(); /// /// Defines the property. From 05fe5fb4ebfc164723ddbe476049499fae675098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:43:15 +0200 Subject: [PATCH 203/213] Fix owner for WindowManagerAddShadowHintProperty property --- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 7a7e41b029..95e5e25c42 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.Primitives #pragma warning restore CS0612 // Type or member is obsolete { public static readonly StyledProperty WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. From 2d6bd60f691434e667cbbbdff933dfda9d4c0f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 19:46:41 +0200 Subject: [PATCH 204/213] Rename --- src/Avalonia.Controls/Primitives/Track.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 49f0cda982..85343b8fbb 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(IsDirectionReversed)); public static readonly StyledProperty IgnoreThumbDragProperty = - AvaloniaProperty.Register(nameof(IsThumbDragHandled)); + AvaloniaProperty.Register(nameof(IgnoreThumbDrag)); private double _minimum; private double _maximum = 100.0; @@ -118,7 +118,7 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsDirectionReversedProperty, value); } } - public bool IsThumbDragHandled + public bool IgnoreThumbDrag { get { return GetValue(IgnoreThumbDragProperty); } set { SetValue(IgnoreThumbDragProperty, value); } From da7eecec0788d086fe6a02138755c770509ffe47 Mon Sep 17 00:00:00 2001 From: Kaktusbot Date: Tue, 10 May 2022 22:38:03 +0400 Subject: [PATCH 205/213] Fix missing NotifyCountChanged in AvaloniaList.AddRange --- src/Avalonia.Base/Collections/AvaloniaList.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 9972e72dd4..a05c9c5da3 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -394,7 +394,13 @@ namespace Avalonia.Collections } while (en.MoveNext()); if (notificationItems is not null) + { NotifyAdd(notificationItems, index); + } + else + { + NotifyCountChanged(); + } } } } From 5eca6cc83e3cbb9cbef2e5b0c48959eee32e78b7 Mon Sep 17 00:00:00 2001 From: Kaktusbot Date: Tue, 10 May 2022 22:57:55 +0400 Subject: [PATCH 206/213] Add test to AvaloniaList.AddRange to notify Count property changed --- .../Collections/AvaloniaListTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index f5ae4cc1e0..b59f272b0c 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -146,6 +146,23 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void AddRange_IEnumerable_Should_Raise_Count_PropertyChanged() + { + var target = new AvaloniaList(new[] { 1, 2, 3, 4, 5 }); + var raised = false; + + target.PropertyChanged += (s, e) => { + Assert.Equal(e.PropertyName, nameof(target.Count)); + Assert.Equal(target.Count, 7); + raised = true; + }; + + target.AddRange(Enumerable.Range(6, 2)); + + Assert.True(raised); + } + [Fact] public void AddRange_Items_Should_Raise_Correct_CollectionChanged() { From a90c4d4f22d737ee4231bacaf9f1763019822687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 22:47:24 +0200 Subject: [PATCH 207/213] Use correct property name --- src/Avalonia.Controls/Primitives/Track.cs | 2 +- src/Avalonia.Controls/Slider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 85343b8fbb..14ec7a2849 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -442,7 +442,7 @@ namespace Avalonia.Controls.Primitives private void ThumbDragged(object? sender, VectorEventArgs e) { - if (IsThumbDragHandled) + if (IgnoreThumbDrag) return; Value = MathUtilities.Clamp( diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index b6a6f3d02c..be87705b54 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -197,7 +197,7 @@ namespace Avalonia.Controls if (_track != null) { - _track.IsThumbDragHandled = true; + _track.IgnoreThumbDrag = true; } if (_decreaseButton != null) From 409d673215ba683559d062b406ea928303d4bf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 22:48:54 +0200 Subject: [PATCH 208/213] Fix MaximumRowsOrColumnsProperty registration name --- src/Avalonia.Base/Layout/UniformGridLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Layout/UniformGridLayout.cs b/src/Avalonia.Base/Layout/UniformGridLayout.cs index 418cd55e41..47c994a350 100644 --- a/src/Avalonia.Base/Layout/UniformGridLayout.cs +++ b/src/Avalonia.Base/Layout/UniformGridLayout.cs @@ -116,7 +116,7 @@ namespace Avalonia.Layout /// Defines the property. /// public static readonly StyledProperty MaximumRowsOrColumnsProperty = - AvaloniaProperty.Register(nameof(MinItemWidth)); + AvaloniaProperty.Register(nameof(MaximumRowsOrColumns)); /// /// Defines the property. From 1e3c5642a3a014fc255d0e11ef1ca7842a7d9f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Tue, 10 May 2022 22:52:58 +0200 Subject: [PATCH 209/213] Fix ConicGradientBrush property registrations --- src/Avalonia.Base/Media/ConicGradientBrush.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/ConicGradientBrush.cs b/src/Avalonia.Base/Media/ConicGradientBrush.cs index 7c1266fa17..4b50019ddc 100644 --- a/src/Avalonia.Base/Media/ConicGradientBrush.cs +++ b/src/Avalonia.Base/Media/ConicGradientBrush.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// Defines the property. /// public static readonly StyledProperty CenterProperty = - AvaloniaProperty.Register( + AvaloniaProperty.Register( nameof(Center), RelativePoint.Center); @@ -19,7 +19,7 @@ namespace Avalonia.Media /// Defines the property. /// public static readonly StyledProperty AngleProperty = - AvaloniaProperty.Register( + AvaloniaProperty.Register( nameof(Angle), 0); From a5b2eb08d4000d7bb4c8c4d1f21afe51bb2031b9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 21:12:25 +0300 Subject: [PATCH 210/213] Added support for IReflectableType in InpcPropertyAccessorPlugin --- .../Plugins/InpcPropertyAccessorPlugin.cs | 19 +- .../Data/BindingTests.cs | 19 ++ .../Data/DynamicReflectableType.cs | 221 ++++++++++++++++++ 3 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 tests/Avalonia.Markup.UnitTests/Data/DynamicReflectableType.cs diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 33cecd10a7..b93bf87fdf 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -17,7 +17,7 @@ namespace Avalonia.Data.Core.Plugins new Dictionary<(Type, string), PropertyInfo?>(); /// - public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null; + public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj, propertyName) != null; /// /// Starts monitoring the value of a property on an object. @@ -36,7 +36,7 @@ namespace Avalonia.Data.Core.Plugins if (!reference.TryGetTarget(out var instance) || instance is null) return null; - var p = GetFirstPropertyWithName(instance.GetType(), propertyName); + var p = GetFirstPropertyWithName(instance, propertyName); if (p != null) { @@ -50,8 +50,16 @@ namespace Avalonia.Data.Core.Plugins } } - private PropertyInfo? GetFirstPropertyWithName(Type type, string propertyName) + private const BindingFlags PropertyBindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) { + if (instance is IReflectableType reflectableType) + return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags); + + var type = instance.GetType(); + var key = (type, propertyName); if (!_propertyLookup.TryGetValue(key, out var propertyInfo)) @@ -66,10 +74,7 @@ namespace Avalonia.Data.Core.Plugins { PropertyInfo? found = null; - const BindingFlags bindingFlags = - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; - - var properties = type.GetProperties(bindingFlags); + var properties = type.GetProperties(PropertyBindingFlags); foreach (PropertyInfo propertyInfo in properties) { diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 055de999e2..11a22f0dec 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -617,6 +617,25 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, source.SubscriberCount); } + + [Fact] + public void Binding_Can_Resolve_Property_From_IReflectableType_Type() + { + var source = new DynamicReflectableType { ["Foo"] = "foo" }; + var target = new TwoWayBindingTest { DataContext = source }; + var binding = new Binding + { + Path = "Foo", + }; + + target.Bind(TwoWayBindingTest.TwoWayProperty, binding); + + Assert.Equal("foo", target.TwoWay); + source["Foo"] = "bar"; + Assert.Equal("bar", target.TwoWay); + target.TwoWay = "baz"; + Assert.Equal("baz", source["Foo"]); + } private class StyledPropertyClass : AvaloniaObject { diff --git a/tests/Avalonia.Markup.UnitTests/Data/DynamicReflectableType.cs b/tests/Avalonia.Markup.UnitTests/Data/DynamicReflectableType.cs new file mode 100644 index 0000000000..3c57bd38cf --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/DynamicReflectableType.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Moq; + +namespace Avalonia.Markup.UnitTests.Data; + +class DynamicReflectableType : IReflectableType, INotifyPropertyChanged, IEnumerable> +{ + private Dictionary _dic = new(); + + public TypeInfo GetTypeInfo() + { + return new FakeTypeInfo(); + } + + public void Add(string key, object value) + { + _dic.Add(key, value); + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key)); + } + + public object this[string key] + { + get => _dic[key]; + set + { + _dic[key] = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key)); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + public IEnumerator> GetEnumerator() + { + return _dic.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_dic).GetEnumerator(); + } + + + class FakeTypeInfo : TypeInfo + { + protected override PropertyInfo GetPropertyImpl(string name, BindingFlags bindingAttr, Binder binder, Type returnType, Type[] types, + ParameterModifier[] modifiers) + { + var propInfo = new Mock(); + propInfo.SetupGet(x => x.Name).Returns(name); + propInfo.SetupGet(x => x.PropertyType).Returns(typeof(object)); + propInfo.SetupGet(x => x.CanWrite).Returns(true); + propInfo.Setup(x => x.GetValue(It.IsAny(), It.IsAny())) + .Returns((object target, object [] _) => ((DynamicReflectableType)target)._dic.GetValueOrDefault(name)); + propInfo.Setup(x => x.SetValue(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((object target, object value, object [] _) => + { + ((DynamicReflectableType)target)._dic[name] = value; + }); + return propInfo.Object; + } + + #region NotSupported + + + public override object[] GetCustomAttributes(bool inherit) + { + throw new NotSupportedException(); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + throw new NotSupportedException(); + } + + public override Module Module { get; } + public override string Namespace { get; } + public override string Name { get; } + protected override TypeAttributes GetAttributeFlagsImpl() + { + throw new NotSupportedException(); + } + + protected override ConstructorInfo GetConstructorImpl(BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, + Type[] types, ParameterModifier[] modifiers) + { + throw new NotSupportedException(); + } + + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override Type GetElementType() + { + throw new NotSupportedException(); + } + + public override EventInfo GetEvent(string name, BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override FieldInfo GetField(string name, BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + protected override MethodInfo GetMethodImpl(string name, BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, + Type[] types, ParameterModifier[] modifiers) + { + throw new NotSupportedException(); + } + + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, + ParameterModifier[] modifiers, CultureInfo culture, string[] namedParameters) + { + throw new NotSupportedException(); + } + + public override Type UnderlyingSystemType { get; } + + protected override bool IsArrayImpl() + { + throw new NotSupportedException(); + } + + protected override bool IsByRefImpl() + { + throw new NotSupportedException(); + } + + protected override bool IsCOMObjectImpl() + { + throw new NotSupportedException(); + } + + protected override bool IsPointerImpl() + { + throw new NotSupportedException(); + } + + protected override bool IsPrimitiveImpl() + { + throw new NotSupportedException(); + } + + public override Assembly Assembly { get; } + public override string AssemblyQualifiedName { get; } + public override Type BaseType { get; } + public override string FullName { get; } + public override Guid GUID { get; } + + + + protected override bool HasElementTypeImpl() + { + throw new NotSupportedException(); + } + + public override Type GetNestedType(string name, BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + { + throw new NotSupportedException(); + } + + public override Type GetInterface(string name, bool ignoreCase) + { + throw new NotSupportedException(); + } + + public override Type[] GetInterfaces() + { + throw new NotSupportedException(); + } + + + #endregion + + } +} \ No newline at end of file From ccce304c69385fc3f5c41a3646084792c0f37f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 13 May 2022 14:13:20 +0200 Subject: [PATCH 211/213] Fix TimePicker property registrations --- src/Avalonia.Controls/DateTimePickers/TimePicker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index f04c79505e..047667567d 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -38,13 +38,13 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly StyledProperty HeaderProperty = - AvaloniaProperty.Register(nameof(Header)); + AvaloniaProperty.Register(nameof(Header)); /// /// Defines the property /// public static readonly StyledProperty HeaderTemplateProperty = - AvaloniaProperty.Register(nameof(HeaderTemplate)); + AvaloniaProperty.Register(nameof(HeaderTemplate)); /// /// Defines the property From 2ee94118305364b72aee6865d427f1330314bc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 13 May 2022 14:16:05 +0200 Subject: [PATCH 212/213] Fix SelectingItemsControl WrapSelection property owner --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 164aab32db..a730659330 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -118,7 +118,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty WrapSelectionProperty = - AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); + AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; From 08cbec6f23848dab0680531a059eb8c92babb290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 13 May 2022 14:28:42 +0200 Subject: [PATCH 213/213] Fix ContentPresenter property field types --- .../Presenters/ContentPresenter.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 996cb29534..12a8dd747d 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -52,67 +52,67 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly AttachedProperty ForegroundProperty = + public static readonly StyledProperty ForegroundProperty = TextElement.ForegroundProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontFamilyProperty = + public static readonly StyledProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontSizeProperty = + public static readonly StyledProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontStyleProperty = + public static readonly StyledProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontWeightProperty = + public static readonly StyledProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontStretchProperty = + public static readonly StyledProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner(); /// /// Defines the property /// - public static readonly AttachedProperty TextAlignmentProperty = + public static readonly StyledProperty TextAlignmentProperty = TextBlock.TextAlignmentProperty.AddOwner(); /// /// Defines the property /// - public static readonly AttachedProperty TextWrappingProperty = + public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); /// /// Defines the property /// - public static readonly AttachedProperty TextTrimmingProperty = + public static readonly StyledProperty TextTrimmingProperty = TextBlock.TextTrimmingProperty.AddOwner(); /// /// Defines the property /// - public static readonly AttachedProperty LineHeightProperty = + public static readonly StyledProperty LineHeightProperty = TextBlock.LineHeightProperty.AddOwner(); /// /// Defines the property /// - public static readonly AttachedProperty MaxLinesProperty = + public static readonly StyledProperty MaxLinesProperty = TextBlock.MaxLinesProperty.AddOwner(); ///